From b4f4839f9995c249aded0beb04e4a7eb589bb74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Tue, 20 May 2025 10:18:23 +0900 Subject: [PATCH 01/64] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/src/entities/site/@x/posting.ts | 1 + .../features/src/entities/site/index.ts | 2 + .../src/entities/site/ui/site-select.tsx | 48 +++ apps/cs/app/(with-base-layout)/code/page.tsx | 2 + .../src/entities/code/api/lwpocode-service.ts | 61 ++++ .../src/entities/code/api/upcode-service.ts | 48 +++ apps/cs/src/entities/code/index.ts | 11 + .../entities/code/model/lwpocode-interface.ts | 61 ++++ .../entities/code/model/upcode-interface.ts | 53 ++++ apps/cs/src/entities/site/@x/posting.ts | 2 + apps/cs/src/entities/site/index.ts | 2 + apps/cs/src/entities/site/ui/site-select.tsx | 48 +++ apps/cs/src/pages/bbs/model/table-columns.tsx | 7 +- apps/cs/src/pages/code/index.ts | 1 + .../cs/src/pages/code/model/table-columns.tsx | 123 ++++++++ apps/cs/src/pages/code/ui/code-page.tsx | 248 +++++++++++++++ apps/cs/src/pages/code/ui/lowcode-dialog.tsx | 292 ++++++++++++++++++ apps/cs/src/pages/code/ui/upcode-dialog.tsx | 256 +++++++++++++++ apps/cs/src/pages/site/ui/site-page.tsx | 2 +- apps/cs/src/shared/config/api.ts | 2 + apps/cs/tsconfig.json | 2 +- 21 files changed, 1269 insertions(+), 3 deletions(-) create mode 100644 apps/common/features/src/entities/site/ui/site-select.tsx create mode 100644 apps/cs/app/(with-base-layout)/code/page.tsx create mode 100644 apps/cs/src/entities/code/api/lwpocode-service.ts create mode 100644 apps/cs/src/entities/code/api/upcode-service.ts create mode 100644 apps/cs/src/entities/code/model/lwpocode-interface.ts create mode 100644 apps/cs/src/entities/code/model/upcode-interface.ts create mode 100644 apps/cs/src/entities/site/ui/site-select.tsx create mode 100644 apps/cs/src/pages/code/index.ts create mode 100644 apps/cs/src/pages/code/model/table-columns.tsx create mode 100644 apps/cs/src/pages/code/ui/code-page.tsx create mode 100644 apps/cs/src/pages/code/ui/lowcode-dialog.tsx create mode 100644 apps/cs/src/pages/code/ui/upcode-dialog.tsx diff --git a/apps/common/features/src/entities/site/@x/posting.ts b/apps/common/features/src/entities/site/@x/posting.ts index 6470b3cb2..355f41e4d 100644 --- a/apps/common/features/src/entities/site/@x/posting.ts +++ b/apps/common/features/src/entities/site/@x/posting.ts @@ -1,2 +1,3 @@ export { SiteApi } from "../api/site-service"; export type { Site } from "../model/site-interface"; +export { SiteSelect } from "../ui/site-select"; \ No newline at end of file diff --git a/apps/common/features/src/entities/site/index.ts b/apps/common/features/src/entities/site/index.ts index c7b437a64..998c13625 100644 --- a/apps/common/features/src/entities/site/index.ts +++ b/apps/common/features/src/entities/site/index.ts @@ -5,3 +5,5 @@ export { type Site, type SiteFormData, } from "./model/site-interface"; + +export { SiteSelect } from "./ui/site-select"; \ No newline at end of file diff --git a/apps/common/features/src/entities/site/ui/site-select.tsx b/apps/common/features/src/entities/site/ui/site-select.tsx new file mode 100644 index 000000000..b7500d0e7 --- /dev/null +++ b/apps/common/features/src/entities/site/ui/site-select.tsx @@ -0,0 +1,48 @@ +import { useQuery } from "@tanstack/react-query"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { SiteApi } from "../api/site-service"; + +type SiteSelectProps = { + siteId: string; + placeholder?: string; +} & React.ComponentPropsWithoutRef; + +function SiteSelect({ + siteId, + defaultValue, + placeholder = "선택", + ...props +}: SiteSelectProps) { + const { data: sites } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SITES], + queryFn: () => SiteApi.getSiteList(), + enabled: !!siteId, + }); + + const defaultValueText = + sites?.content.find((item) => item.siteId === defaultValue)?.siteNm ?? placeholder; + + return ( + + ); +} + +export { SiteSelect }; diff --git a/apps/cs/app/(with-base-layout)/code/page.tsx b/apps/cs/app/(with-base-layout)/code/page.tsx new file mode 100644 index 000000000..3ac19ec1c --- /dev/null +++ b/apps/cs/app/(with-base-layout)/code/page.tsx @@ -0,0 +1,2 @@ +import { CodePage } from "@/pages/code"; +export default CodePage; diff --git a/apps/cs/src/entities/code/api/lwpocode-service.ts b/apps/cs/src/entities/code/api/lwpocode-service.ts new file mode 100644 index 000000000..045a6ee6d --- /dev/null +++ b/apps/cs/src/entities/code/api/lwpocode-service.ts @@ -0,0 +1,61 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + LwpoCodeFormSchema, + type LwpoCodeService, + type LwpoCodeList, + type LwpoCodeDetail, + type LwpoCodeParams, + type LwpoCodeFormData, +} from "../model/lwpocode-interface"; + +export class LwpoCodeApiService implements LwpoCodeService { + public async getLwpoCodeList(params?: LwpoCodeParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}`, + { params }, + ); + return response.data; + } + + public async getUpCodeLwpoCodeList(upCd: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/up-cd/${upCd}`, + ); + return response.data; + } + + public async LwpoCodeDetail(upCd: string, LwpoCd: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${LwpoCd}/up-cd/${upCd}`, + ); + return response.data; + } + + public async createLwpoCode(data: LwpoCodeFormData): Promise { + const validatedData = LwpoCodeFormSchema.parse(data); + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}`, + validatedData, + ); + return response.data; + } + + public async updateLwpoCode(upCd: string, LwpoCd: string, data: LwpoCodeFormData): Promise { + const validatedData = LwpoCodeFormSchema.parse(data); + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${LwpoCd}/up-cd/${upCd}`, + validatedData, + ); + return response.data; + } + + public async deleteLwpoCode(upCd: string, LwpoCd: string): Promise { + await client.delete( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${LwpoCd}/up-cd/${upCd}`, + ); + } + +} + +export const lwpoCodeApi = new LwpoCodeApiService(); diff --git a/apps/cs/src/entities/code/api/upcode-service.ts b/apps/cs/src/entities/code/api/upcode-service.ts new file mode 100644 index 000000000..86c936a1f --- /dev/null +++ b/apps/cs/src/entities/code/api/upcode-service.ts @@ -0,0 +1,48 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + upCodeFormSchema, + type UpCodeService, + type UpCodeList, + type UpCodeDetail, + type UpCodeParams, + type UpCodeFormData, +} from "../model/upcode-interface"; + +export class UpCodeApiService implements UpCodeService { + public async getUpCodeList(params?: UpCodeParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.UP_CODE}`, + { params }, + ); + return response.data; + } + + public async upCodeDetail(upCd: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`, + ); + return response.data; + } + + public async createUpCode(data: UpCodeFormData): Promise { + const validatedData = upCodeFormSchema.parse(data); + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.UP_CODE}`, + validatedData, + ); + return response.data; + } + + public async updateUpCode(upCd: string, data: UpCodeFormData): Promise { + const validatedData = upCodeFormSchema.parse(data); + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`, + validatedData, + ); + return response.data; + } + +} + +export const upCodeApi = new UpCodeApiService(); diff --git a/apps/cs/src/entities/code/index.ts b/apps/cs/src/entities/code/index.ts index dc74bbc47..4cb1c1547 100644 --- a/apps/cs/src/entities/code/index.ts +++ b/apps/cs/src/entities/code/index.ts @@ -1,3 +1,14 @@ export { CodeApi } from "./api/code-service"; export type { ICode } from "./model/code-interface"; export { CodeSelect } from "./ui/code-select"; + + +export { upCodeApi } from "./api/upcode-service"; +export type { UpCode } from "./model/upcode-interface"; +export { upCodeFormSchema, type UpCodeFormData } from "./model/upcode-interface"; + +export { lwpoCodeApi} from "./api/lwpocode-service"; +export type { LwpoCode } from "./model/lwpocode-interface"; +export { LwpoCodeFormSchema, type LwpoCodeFormData } from "./model/lwpocode-interface"; + + diff --git a/apps/cs/src/entities/code/model/lwpocode-interface.ts b/apps/cs/src/entities/code/model/lwpocode-interface.ts new file mode 100644 index 000000000..eec2b82d2 --- /dev/null +++ b/apps/cs/src/entities/code/model/lwpocode-interface.ts @@ -0,0 +1,61 @@ +import { z } from "zod"; +import type { PageableParams, PageableList } from "@/shared/model"; + +export interface LwpoCodeParams extends PageableParams { + upCd?: string; + lwpoCd?: string; + lwpoCdNm?: string; + useYn?: string; +} + +export interface LwpoCode { + lwpoCd: string; + upCd: string; + lwpoCdNm: string; + lwpoCdExpln: string; + unit: string; + prntLwpoCd: string; + useYn: string; + sortSeq: number; + regDt: string; + rgTr: string; + rgTrNm: string; + mdfcnDt: string; + mdFr: string; +} + +export interface LwpoCodeDetail extends LwpoCodeFormData { + lwpoCd: string; + upCd: string; + lwpoCdNm: string; + lwpoCdExpln: string; + unit: string; + prntLwpoCd: string; + useYn: string; + sortSeq: 0; +} + +export const LwpoCodeFormSchema = z.object({ + lwpoCd: z.string().max(10).nonempty(), + upCd: z.string().max(10).nonempty(), + lwpoCdNm: z.string().max(100).nonempty(), + lwpoCdExpln: z.string().max(1000).nullish(), + unit: z.string().max(20).nullish(), + prntLwpoCd: z.string().max(10).nullish(), + useYn: z.string(), + sortSeq: z.number(), + delYn: z.string(), +}); + +export type LwpoCodeFormData = z.infer; +export type LwpoCodeList = PageableList; + +export interface LwpoCodeService { + getLwpoCodeList(params: LwpoCodeParams): Promise; + getUpCodeLwpoCodeList(upCd: string): Promise; + LwpoCodeDetail(upCd: string, LwpoCode: string): Promise; + updateLwpoCode(upCd: string, LwpoCode: string, data: LwpoCodeFormData): Promise; + createLwpoCode(data: LwpoCodeFormData): Promise; + deleteLwpoCode(upCd: string, LwpoCode: string, data: LwpoCodeFormData): Promise; +} + \ No newline at end of file diff --git a/apps/cs/src/entities/code/model/upcode-interface.ts b/apps/cs/src/entities/code/model/upcode-interface.ts new file mode 100644 index 000000000..d3e9680c1 --- /dev/null +++ b/apps/cs/src/entities/code/model/upcode-interface.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; +import type { PageableParams, PageableList } from "@/shared/model"; + +export interface UpCodeParams extends PageableParams { + keyword?: string; + upCd?: string; + cdSe?: string; + useYn?: string; + siteId?: string; +} + +export interface UpCode { + upCd: string; + upCdNm: string; + upCdExpln: string; + cdSe: string; + siteId: string; + useYn: string; + regDt: string; +} + +export interface UpCodeDetail extends UpCodeFormData { + upCd: string; + upCdNm: string; + upCdExpln: string; + cdSe: string; + siteId: string; + useYn: string; + delYn: string; + regDt: string; + rgTrNm: string; +} + +export const upCodeFormSchema = z.object({ + upCd: z.string().max(10).nonempty(), + upCdNm: z.string().max(100).nonempty(), + upCdExpln: z.string().max(1000).nullish(), + cdSe: z.string().max(10).nonempty(), + siteId: z.string().max(2).nullish(), + useYn: z.string(), + delYn: z.string(), +}); + +export type UpCodeFormData = z.infer; +export type UpCodeList = PageableList; + +export interface UpCodeService { + getUpCodeList(params: UpCodeParams): Promise; + upCodeDetail(upCodeId: string): Promise; + updateUpCode(upCodeId: string, data: UpCodeFormData): Promise; + createUpCode(data: UpCodeFormData): Promise; +} + \ No newline at end of file diff --git a/apps/cs/src/entities/site/@x/posting.ts b/apps/cs/src/entities/site/@x/posting.ts index 6470b3cb2..b44c10445 100644 --- a/apps/cs/src/entities/site/@x/posting.ts +++ b/apps/cs/src/entities/site/@x/posting.ts @@ -1,2 +1,4 @@ export { SiteApi } from "../api/site-service"; export type { Site } from "../model/site-interface"; + +export { SiteSelect } from "../ui/site-select"; diff --git a/apps/cs/src/entities/site/index.ts b/apps/cs/src/entities/site/index.ts index c7b437a64..998c13625 100644 --- a/apps/cs/src/entities/site/index.ts +++ b/apps/cs/src/entities/site/index.ts @@ -5,3 +5,5 @@ export { type Site, type SiteFormData, } from "./model/site-interface"; + +export { SiteSelect } from "./ui/site-select"; \ No newline at end of file diff --git a/apps/cs/src/entities/site/ui/site-select.tsx b/apps/cs/src/entities/site/ui/site-select.tsx new file mode 100644 index 000000000..af2773958 --- /dev/null +++ b/apps/cs/src/entities/site/ui/site-select.tsx @@ -0,0 +1,48 @@ +import { useQuery } from "@tanstack/react-query"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { SiteApi } from "../api/site-service"; + +type SiteSelectProps = { + siteId: string; + placeholder?: string; +} & React.ComponentPropsWithoutRef; + +function SiteSelect({ + siteId, + defaultValue, + placeholder = "선택", + ...props +}: SiteSelectProps) { + const { data: sites } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SITES], + queryFn: () => SiteApi.getSiteList(), + enabled: true, + }); + + const defaultValueText = + sites?.content.find((item) => item.siteId === defaultValue)?.siteNm ?? placeholder; + + return ( + + ); +} + +export { SiteSelect }; diff --git a/apps/cs/src/pages/bbs/model/table-columns.tsx b/apps/cs/src/pages/bbs/model/table-columns.tsx index f3975aca2..5a6cb62f8 100644 --- a/apps/cs/src/pages/bbs/model/table-columns.tsx +++ b/apps/cs/src/pages/bbs/model/table-columns.tsx @@ -10,7 +10,12 @@ export const columns: ColumnDef[] = [ { id: "no", header: "NO", - cell: ({ row }) => row.index + 1, + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const currentPage = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return totalRows - (currentPage * pageSize + row.index); + }, }, { accessorKey: "bbsNm", diff --git a/apps/cs/src/pages/code/index.ts b/apps/cs/src/pages/code/index.ts new file mode 100644 index 000000000..37c2c776a --- /dev/null +++ b/apps/cs/src/pages/code/index.ts @@ -0,0 +1 @@ +export { CodePage } from "./ui/code-page"; \ No newline at end of file diff --git a/apps/cs/src/pages/code/model/table-columns.tsx b/apps/cs/src/pages/code/model/table-columns.tsx new file mode 100644 index 000000000..1a486bd5a --- /dev/null +++ b/apps/cs/src/pages/code/model/table-columns.tsx @@ -0,0 +1,123 @@ +import dayjs from "dayjs"; +import { type ColumnDef} from "@tanstack/react-table"; +import { Button } from "@common/components"; +import type { UpCode } from "@/entities/code/model/upcode-interface"; +import type { LwpoCode } from "@/entities/code/model/lwpocode-interface"; + +type UpCodeTableMeta = { + onManagementClick?: (row: UpCode) => void; +}; + +type LwpoCodeTableMeta = { + onManagementClick?: (row: LwpoCode) => void; +}; + +export const upCodecolumns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const isSelected = row.getIsSelected(); + if (isSelected) { + return "▶"; + } + const totalRows = table.getRowCount(); + const currentPage = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return totalRows - (currentPage * pageSize + row.index); + }, + }, + { + accessorKey: "upCd", + header: "상위코드", + }, + { + accessorKey: "upCdNm", + header: "상위코드명", + }, + { + accessorKey: "cdSe", + header: "코드구분", + }, + { + accessorKey: "useYn", + header: "사용여부", + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => { + const date = dayjs(row.original.regDt); + return date.format("YYYY-MM-DD"); + }, + }, + { + id: "management", + header: "관리", + cell: ({ row, table }) => { + const onManagementClick = (table.options.meta as UpCodeTableMeta)?.onManagementClick; + return ( + + ); + }, + }, +]; + +export const lowCodecolumns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const currentPage = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return totalRows - (currentPage * pageSize + row.index); + }, + }, + { + accessorKey: "lwpoCd", + header: "하위코드", + }, + { + accessorKey: "lwpoCdNm", + header: "하위코드명", + }, + { + accessorKey: "useYn", + header: "사용 여부", + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => { + const date = dayjs(row.original.regDt); + return date.format("YYYY-MM-DD"); + }, + }, + { + id: "management", + header: "관리", + cell: ({ row, table }) => { + const onManagementClick = (table.options.meta as LwpoCodeTableMeta)?.onManagementClick; + return ( + + ); + }, + }, +]; diff --git a/apps/cs/src/pages/code/ui/code-page.tsx b/apps/cs/src/pages/code/ui/code-page.tsx new file mode 100644 index 000000000..4a4fa74da --- /dev/null +++ b/apps/cs/src/pages/code/ui/code-page.tsx @@ -0,0 +1,248 @@ +"use client"; + +import "@common/assets/styles/grid.css"; + +import { useEffect, useState } from "react"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, +} from "@tanstack/react-table"; +import { Button, DataTable, Input, Label } from "@common/components"; +import { upCodeApi } from "@/entities/code/api/upcode-service"; +import { type UpCode, type UpCodeParams } from "@/entities/code/model/upcode-interface"; +import { type LwpoCode, type LwpoCodeParams } from "@/entities/code/model/lwpocode-interface"; +import { ENDPOINT } from "@/shared/config"; +import { upCodecolumns, lowCodecolumns } from "../model/table-columns"; +import { lwpoCodeApi } from "@/entities/code/api/lwpocode-service"; +import { ManagementUpCodeDialog } from "./upcode-dialog"; +import { ManagementLwpoCodeDialog } from "./lowcode-dialog"; + +function CodePage() { + const [selectedUpCode, setSelectedUpCode] = useState(null); + const [isManagementDialogOpen, setIsManagementDialogOpen] = useState(false); + + const [isRegistrationMode, setIsRegistrationMode] = useState(false); + const [isLwpoCodeRegistrationMode, setIsLwpoCodeRegistrationMode] = useState(false); + + const [selectedLwpoCode, setSelectedLwpoCode] = useState(null); + const [isLwpoCodeManagementDialogOpen, setIsLwpoCodeManagementDialogOpen] = useState(false); + + const [upCodeQuery, setUpCodeQuery] = useState({ + upCd: "", + }); + const [lwpoCodeQuery, setLwpoCodeQuery] = useState({ + upCd: "", + lwpoCd: "", + }); + const [upCodePagination, setUpCodePagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const [lwpoCodePagination, setLwpoCodePagination] = useState({ + pageIndex: 0, + pageSize: 1000, + }); + + const { data: upCodeList, refetch: refetchUpCode } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE], + queryFn: () => + upCodeApi.getUpCodeList({ + ...upCodeQuery, + page: upCodePagination.pageIndex, + size: upCodePagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const { data: lwpoCodeList, refetch: refetchLwpoCode } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, lwpoCodeQuery], + queryFn: () => + lwpoCodeApi.getLwpoCodeList({ + ...lwpoCodeQuery, + page: lwpoCodePagination.pageIndex, + size: lwpoCodePagination.pageSize, + }), + enabled: !!lwpoCodeQuery.upCd, + }); + + + const upCodehandleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetchUpCode(); + }; + + const upCodehandleRowClick = (row: UpCode) => { + setSelectedUpCode(row); + setLwpoCodeQuery({ ...lwpoCodeQuery, upCd: row.upCd }); + refetchLwpoCode(); + }; + + const handleManagementClick = (row: UpCode) => { + setSelectedUpCode(row); + setIsRegistrationMode(false); + setIsManagementDialogOpen(true); + }; + + const handleRegistrationClick = () => { + setSelectedUpCode(null); + setIsRegistrationMode(true); + setIsManagementDialogOpen(true); + }; + + const handleLwpoCodeManagementClick = (row: LwpoCode) => { + setSelectedLwpoCode(row); + setIsLwpoCodeRegistrationMode(false); + setIsLwpoCodeManagementDialogOpen(true); + }; + + const handleLwpoCodeRegistrationClick = () => { + if (!selectedUpCode?.upCd) { + alert("상위코드를 선택해주세요."); + return; + } + setSelectedLwpoCode(null); + setIsLwpoCodeRegistrationMode(true); + setIsLwpoCodeManagementDialogOpen(true); + }; + + + const upCodeTable = useReactTable({ + data: upCodeList?.content ?? [], + columns: upCodecolumns, + rowCount: upCodeList?.totalElements ?? 0, + state: { + pagination: upCodePagination, + rowSelection: selectedUpCode ? { [selectedUpCode.upCd]: true } : {}, + }, + onPaginationChange: setUpCodePagination, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + meta: { + onManagementClick: handleManagementClick, + }, + getRowId: (row) => row.upCd, + }); + + const lwpoCodeTable = useReactTable({ + data: lwpoCodeList?.content ?? [], + columns: lowCodecolumns, + rowCount: lwpoCodeList?.totalElements ?? 0, + state: { + pagination: lwpoCodePagination, + rowSelection: selectedLwpoCode ? { [selectedLwpoCode.lwpoCd]: true } : {}, + }, + onPaginationChange: setLwpoCodePagination, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + meta: { + onManagementClick: handleLwpoCodeManagementClick, + }, + getRowId: (row) => row.lwpoCd, + }); + + const lwpoCodehandleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetchLwpoCode(); + }; + + useEffect(() => { + refetchUpCode(); + }, [upCodePagination]); + + return ( +
+
+
+
+
+ + + setUpCodeQuery({ + ...upCodeQuery, + keyword: e.target.value, + }) + } + /> + +
+
+
+
+

상위코드 목록

+ +
+
+ +
+
+
+
+
+
+ + setLwpoCodeQuery({ + ...lwpoCodeQuery, + lwpoCdNm: e.target.value + }) + } + /> + +
+
+
+
+

하위코드 목록

+ +
+
+ +
+
+
+
+ {isManagementDialogOpen && ( + + )} + {isLwpoCodeManagementDialogOpen && ( + + )} +
+ ); + } + + export { CodePage }; + \ No newline at end of file diff --git a/apps/cs/src/pages/code/ui/lowcode-dialog.tsx b/apps/cs/src/pages/code/ui/lowcode-dialog.tsx new file mode 100644 index 000000000..1c64c8ade --- /dev/null +++ b/apps/cs/src/pages/code/ui/lowcode-dialog.tsx @@ -0,0 +1,292 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Input, + RadioGroup, + RadioGroupItem, + Label, +} from "@common/components"; +import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { lwpoCodeApi } from "@/entities/code/api/lwpocode-service"; +import { LwpoCodeFormSchema, type LwpoCodeFormData } from "@/entities/code/model/lwpocode-interface"; +import { ENDPOINT } from "@/shared/config"; +import { upCodeApi } from "@/entities/code/api/upcode-service"; + +type ManagementLwpoCodeDialogProps = { + upCd?: string; + lwpoCd?: string; + open?: boolean; + onOpenChange?: (open: boolean) => void; + isRegistration?: boolean; +} & Omit, 'open' | 'onOpenChange'>; + +function ManagementLwpoCodeDialog({ + lwpoCd, + upCd, + open, + onOpenChange, + isRegistration = false, + ...props +}: ManagementLwpoCodeDialogProps) { + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + + const { data: detailData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, lwpoCd], + queryFn: () => lwpoCodeApi.LwpoCodeDetail(upCd as string, lwpoCd as string), + enabled: !!upCd && !!lwpoCd && !isRegistration, + }); + + const { data: upCodeData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd], + queryFn: () => upCodeApi.upCodeDetail(upCd as string), + enabled: !!upCd, + }); + + const { mutateAsync: saveLwpoCode } = useMutation({ + mutationFn: (data: LwpoCodeFormData) => + isRegistration ? lwpoCodeApi.createLwpoCode(data) : lwpoCodeApi.updateLwpoCode(upCd as string, lwpoCd as string, data), + onSuccess: () => { + alert(isRegistration ? "등록되었습니다." : "수정되었습니다."); + onOpenChange?.(false); + }, + }); + + const form = useForm({ + resolver: zodResolver(LwpoCodeFormSchema), + values: { + upCd: upCd ?? "", + lwpoCd: detailData?.lwpoCd ?? "", + lwpoCdNm: detailData?.lwpoCdNm ?? "", + lwpoCdExpln: detailData?.lwpoCdExpln ?? "", + useYn: detailData?.useYn ?? "Y", + delYn: "N", + unit: detailData?.unit ?? "", + prntLwpoCd: detailData?.prntLwpoCd ?? "", + sortSeq: detailData?.sortSeq ?? 0, + }, + }); + + const handleSave = async () => { + if (await confirm(isRegistration ? "등록하시겠습니까?" : "수정하시겠습니까?")) { + const data = form.getValues(); + await saveLwpoCode(data); + } + }; + + return ( + + + + {isRegistration ? "하위코드 등록" : "하위코드 관리"} + +
+ { + e.preventDefault(); + handleSave(); + }} className="space-y-4"> + + + + + + + + 하위코드 + + ( + + + + + + )} + /> + + + + 하위코드명 + + ( + + + + + + )} + /> + + + + 하위코드설명 + + ( + + + + + + )} + /> + + + {upCodeData?.cdSe === "CODECLUNIT" && ( + + 단위 + + ( + + + + + + )} + /> + + + )} + {upCodeData?.cdSe === "CODECLTREE" && ( + + 상위하위코드 + + ( + + + + + + )} + /> + + + )} + + 정렬순서 + + ( + + + + + + )} + /> + + + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+ + + + + + +
+ +
+
+ ); +} + +export { ManagementLwpoCodeDialog }; diff --git a/apps/cs/src/pages/code/ui/upcode-dialog.tsx b/apps/cs/src/pages/code/ui/upcode-dialog.tsx new file mode 100644 index 000000000..034ba6e56 --- /dev/null +++ b/apps/cs/src/pages/code/ui/upcode-dialog.tsx @@ -0,0 +1,256 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { ENDPOINT } from "@/shared/config"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Input, + RadioGroup, + RadioGroupItem, + Label, +} from "@common/components"; +import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { upCodeApi } from "@/entities/code/api/upcode-service"; +import { upCodeFormSchema, type UpCodeFormData } from "@/entities/code/model/upcode-interface"; +import { CodeSelect } from "@/entities/code"; +import { SiteSelect } from "@/entities/site"; + +type ManagementUpCodeDialogProps = { + upCd?: string; + open?: boolean; + onOpenChange?: (open: boolean) => void; + isRegistration?: boolean; +} & Omit, 'open' | 'onOpenChange'>; + + +function ManagementUpCodeDialog({ + upCd, + open, + onOpenChange, + isRegistration = false, + ...props +}: ManagementUpCodeDialogProps) { + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + + const { data: detailData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd], + queryFn: () => upCodeApi.upCodeDetail(upCd as string), + enabled: !!upCd && !isRegistration, + }); + + const { mutateAsync: saveUpCode } = useMutation({ + mutationFn: (data: UpCodeFormData) => + isRegistration ? upCodeApi.createUpCode(data) : upCodeApi.updateUpCode(upCd as string, data), + onSuccess: () => { + alert(isRegistration ? "등록되었습니다." : "수정되었습니다."); + onOpenChange?.(false); + }, + }); + + const form = useForm({ + resolver: zodResolver(upCodeFormSchema), + values: { + upCd: detailData?.upCd ?? "", + upCdNm: detailData?.upCdNm ?? "", + cdSe: detailData?.cdSe ?? "", + siteId: detailData?.siteId ?? "", + upCdExpln: detailData?.upCdExpln ?? "", + useYn: detailData?.useYn ?? "Y", + delYn: "N", + }, + }); + + const handleSave = async () => { + if (await confirm(isRegistration ? "등록하시겠습니까?" : "수정하시겠습니까?")) { + const data = form.getValues(); + await saveUpCode(data); + } + }; + + return ( + + + + {isRegistration ? "상위코드 등록" : "상위코드 관리"} + +
+ { + e.preventDefault(); + handleSave(); + }} className="space-y-4"> + + + + + + + + 상위코드 + + ( + + + + + + )} + /> + + + + 상위코드명 + + ( + + + + + + )} + /> + + + + 상위코드설명 + + ( + + + + + + )} + /> + + + + 코드구분 + + ( + + + + + + )} + /> + + + + 사이트 + + ( + + + + + + )} + /> + + + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+ + + + + + +
+ +
+
+ ); +} + +export { ManagementUpCodeDialog }; diff --git a/apps/cs/src/pages/site/ui/site-page.tsx b/apps/cs/src/pages/site/ui/site-page.tsx index 9ffd1294e..58978d068 100644 --- a/apps/cs/src/pages/site/ui/site-page.tsx +++ b/apps/cs/src/pages/site/ui/site-page.tsx @@ -23,7 +23,7 @@ function SitePage() { }); const [pagination, setPagination] = useState({ pageIndex: 0, - pageSize: 5, + pageSize: 10, }); const { data: siteList, refetch } = useQuery({ diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 0ff7efe42..094bb3776 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -36,6 +36,8 @@ export const ENDPOINT = { BANNERS: "/cms-service/api/v1/bnrs", TEMPLATE: "/cms-service/api/v1/tmplt", TERMS: "/cms-service/api/v1/trms", + LWPO_CODE: "/cms-service/api/v1/lwpo-cds", + UP_CODE: "/cms-service/api/v1/up-codes", }, BOARD_SERVICE: { BBS: "/board-service/api/v1/bbs", diff --git a/apps/cs/tsconfig.json b/apps/cs/tsconfig.json index 17db758c4..cf5fa126f 100644 --- a/apps/cs/tsconfig.json +++ b/apps/cs/tsconfig.json @@ -42,6 +42,6 @@ "**/*.jsx", "**/*.json", ".next/types/**/*.ts" - ], +, "../common/features/src/entities/site/ui/site-select.tsx" ], "exclude": ["**/node_modules", "**/.*/*"] } From 480e402d4e1bdf12860f4de4c8d7b6e80f0a3545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Tue, 20 May 2025 10:22:22 +0900 Subject: [PATCH 02/64] =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cs/tsconfig.json b/apps/cs/tsconfig.json index cf5fa126f..17db758c4 100644 --- a/apps/cs/tsconfig.json +++ b/apps/cs/tsconfig.json @@ -42,6 +42,6 @@ "**/*.jsx", "**/*.json", ".next/types/**/*.ts" -, "../common/features/src/entities/site/ui/site-select.tsx" ], + ], "exclude": ["**/node_modules", "**/.*/*"] } From 4a5f9c7488420fc0ef21805a247aca4e2aa03dd7 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 20 May 2025 10:42:58 +0900 Subject: [PATCH 03/64] =?UTF-8?q?refactor:=20setBbsInfo=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/entities/menu/lib/menu-wrapper.tsx | 16 ++-------------- apps/cs/src/widgets/post/ui/post-page.tsx | 17 +++++++++++++++-- apps/cs/src/widgets/post/ui/post.tsx | 15 +++++++++++++-- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/apps/cs/src/entities/menu/lib/menu-wrapper.tsx b/apps/cs/src/entities/menu/lib/menu-wrapper.tsx index 277cae8d3..48fe5e6ed 100644 --- a/apps/cs/src/entities/menu/lib/menu-wrapper.tsx +++ b/apps/cs/src/entities/menu/lib/menu-wrapper.tsx @@ -2,7 +2,6 @@ import { useEffect } from "react"; import { usePathname, useRouter } from "next/navigation"; -import { useLocalStorage } from "@frontend-opensource/use-react-hooks"; import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu"; import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; import { SITE_ID } from "@/shared/config"; @@ -14,14 +13,8 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => { const pathname = usePathname(); const { setMessage: alert } = useAlertStore((state) => state); - const { - pageMenuAuthority, - setBbsInfo, - setCurrentMenuInfo, - setPageMenuAuthority, - } = useMenuStore((state) => state); - - const [storedBbsInfo] = useLocalStorage("bbs-info", ""); + const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } = + useMenuStore((state) => state); const getCurrentMenuInfo = async () => { const menuRoles = await MenuApi.getMenuRoles(SITE_ID); @@ -60,11 +53,6 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => { alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/")); }, [pageMenuAuthority]); - useEffect(() => { - if (!storedBbsInfo) return; - setBbsInfo(JSON.parse(storedBbsInfo)); - }, [storedBbsInfo]); - return <>{children}; }; diff --git a/apps/cs/src/widgets/post/ui/post-page.tsx b/apps/cs/src/widgets/post/ui/post-page.tsx index 6bbc4de03..7a553ef4d 100644 --- a/apps/cs/src/widgets/post/ui/post-page.tsx +++ b/apps/cs/src/widgets/post/ui/post-page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import { @@ -21,6 +21,7 @@ import { SelectValue, Skeleton, } from "@common/components"; +import { BoardApi } from "@/entities/board"; import { PostApi, type Post, type PostParams } from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; import { useMenuStore, usePrompt } from "@/shared/lib"; @@ -32,7 +33,9 @@ function PostPage({ bbsId }: { bbsId: string }) { const router = useRouter(); const pathname = usePathname(); const { prompt } = usePrompt(); - const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state); + const { bbsInfo, pageMenuAuthority, setBbsInfo } = useMenuStore( + (state) => state, + ); const thumbRefs = useRef>(new Map()); const [postParams, setPostParams] = useState({ @@ -45,6 +48,11 @@ function PostPage({ bbsId }: { bbsId: string }) { }); const [rowSelection, setRowSelection] = useState({}); + const { data: boardInfo } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.BBS, bbsId], + queryFn: () => BoardApi.getBoardInfo(bbsId), + }); + const { data: upndFixPostList } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, bbsInfo], queryFn: () => @@ -124,6 +132,11 @@ function PostPage({ bbsId }: { bbsId: string }) { router.push(`${pathname}/${row.pstNo}`); }; + useEffect(() => { + if (!boardInfo) return; + setBbsInfo(boardInfo); + }, [boardInfo]); + return (
diff --git a/apps/cs/src/widgets/post/ui/post.tsx b/apps/cs/src/widgets/post/ui/post.tsx index 370c9cc1d..fcb727569 100644 --- a/apps/cs/src/widgets/post/ui/post.tsx +++ b/apps/cs/src/widgets/post/ui/post.tsx @@ -12,14 +12,20 @@ import { useAlertStore, useMenuStore } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { PostAction } from "./post-action"; import { PostComment } from "./post-comment"; +import { BoardApi } from "@/entities/board"; function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); const { setMessage: alert } = useAlertStore((state) => state); - const { bbsInfo } = useMenuStore((state) => state); + const { bbsInfo, setBbsInfo } = useMenuStore((state) => state); const [filename, setFilename] = useState(""); + const { data: boardInfo } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.BBS, bbsId], + queryFn: () => BoardApi.getBoardInfo(bbsId), + }); + const { data: postData } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), @@ -105,6 +111,11 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { } }, [downloadResponse]); + useEffect(() => { + if (!boardInfo) return; + setBbsInfo(boardInfo); + }, [boardInfo]); + return ( <> @@ -150,7 +161,7 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
)} -
+          
             {postData?.pstCn ?? ""}
           
{isFetching ? ( From 12eb3b64380f2eae361f055aabacdd8551cfd6bc Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 20 May 2025 13:34:15 +0900 Subject: [PATCH 04/64] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/src/entities/posting/ui/posting-dialog.tsx | 2 +- .../features/src/widgets/post/ui/posting-period-selector.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/common/features/src/entities/posting/ui/posting-dialog.tsx b/apps/common/features/src/entities/posting/ui/posting-dialog.tsx index 3cf0eaeeb..e125eb7a8 100644 --- a/apps/common/features/src/entities/posting/ui/posting-dialog.tsx +++ b/apps/common/features/src/entities/posting/ui/posting-dialog.tsx @@ -289,7 +289,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) { {parseInt(rememberPeriodCode[site.siteId] ?? "") === 0 && ( + onValueChange={(date) => handlePeriodDateChange(site.siteId)( date as DateRange | undefined, ) diff --git a/apps/common/features/src/widgets/post/ui/posting-period-selector.tsx b/apps/common/features/src/widgets/post/ui/posting-period-selector.tsx index 1673df8c9..9c23995a6 100644 --- a/apps/common/features/src/widgets/post/ui/posting-period-selector.tsx +++ b/apps/common/features/src/widgets/post/ui/posting-period-selector.tsx @@ -76,9 +76,7 @@ function PostingPeriodSelector({ ) )} - {periodCode === "0" && ( - - )} + {periodCode === "0" && }
); } From 68c5c24b7ca23c3e00d383d4498f6fa9670ef80a Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 20 May 2025 15:18:27 +0900 Subject: [PATCH 05/64] =?UTF-8?q?feat:=20=ED=8C=9D=EC=97=85=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/app/(with-base-layout)/popup/page.tsx | 2 + .../popup/register/page.tsx | 2 + apps/cs/src/pages/popup/api/popup-service.ts | 25 ++ apps/cs/src/pages/popup/index.ts | 2 + .../src/pages/popup/model/popup-interface.ts | 49 +++ .../src/pages/popup/model/table-columns.tsx | 43 +++ apps/cs/src/pages/popup/ui/create-page.tsx | 49 +++ apps/cs/src/pages/popup/ui/popup-form.tsx | 333 ++++++++++++++++++ apps/cs/src/pages/popup/ui/popup-page.tsx | 92 +++++ apps/cs/src/shared/config/api.ts | 3 +- .../widgets/post/ui/general-file-uploader.tsx | 3 +- 11 files changed, 600 insertions(+), 3 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/popup/page.tsx create mode 100644 apps/cs/app/(with-base-layout)/popup/register/page.tsx create mode 100644 apps/cs/src/pages/popup/api/popup-service.ts create mode 100644 apps/cs/src/pages/popup/index.ts create mode 100644 apps/cs/src/pages/popup/model/popup-interface.ts create mode 100644 apps/cs/src/pages/popup/model/table-columns.tsx create mode 100644 apps/cs/src/pages/popup/ui/create-page.tsx create mode 100644 apps/cs/src/pages/popup/ui/popup-form.tsx create mode 100644 apps/cs/src/pages/popup/ui/popup-page.tsx diff --git a/apps/cs/app/(with-base-layout)/popup/page.tsx b/apps/cs/app/(with-base-layout)/popup/page.tsx new file mode 100644 index 000000000..684e5bc58 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/popup/page.tsx @@ -0,0 +1,2 @@ +import { PopupPage } from "@/pages/popup"; +export default PopupPage; diff --git a/apps/cs/app/(with-base-layout)/popup/register/page.tsx b/apps/cs/app/(with-base-layout)/popup/register/page.tsx new file mode 100644 index 000000000..d330844d3 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/popup/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/popup"; +export default CreatePage; diff --git a/apps/cs/src/pages/popup/api/popup-service.ts b/apps/cs/src/pages/popup/api/popup-service.ts new file mode 100644 index 000000000..59b67ea9b --- /dev/null +++ b/apps/cs/src/pages/popup/api/popup-service.ts @@ -0,0 +1,25 @@ +import { ENDPOINT } from "@/shared/config"; +import { + popupFormSchema, + type PopupFormData, + type PopupList, + type PopupParams, + type PopupService, +} from "../model/popup-interface"; +import { client } from "@/shared/api"; + +export class PopupApiService implements PopupService { + async getPopupList(params: PopupParams): Promise { + const response = await client.get(ENDPOINT.CMS_SERVICE.POPUPS, { + params, + }); + return response.data; + } + + async createPopup(data: PopupFormData): Promise { + const validatedData = popupFormSchema.parse(data); + await client.post(ENDPOINT.CMS_SERVICE.POPUPS, validatedData); + } +} + +export const PopupApi = new PopupApiService(); diff --git a/apps/cs/src/pages/popup/index.ts b/apps/cs/src/pages/popup/index.ts new file mode 100644 index 000000000..dfc7388ff --- /dev/null +++ b/apps/cs/src/pages/popup/index.ts @@ -0,0 +1,2 @@ +export { CreatePage } from "./ui/create-page"; +export { PopupPage } from "./ui/popup-page"; diff --git a/apps/cs/src/pages/popup/model/popup-interface.ts b/apps/cs/src/pages/popup/model/popup-interface.ts new file mode 100644 index 000000000..51d78a0fb --- /dev/null +++ b/apps/cs/src/pages/popup/model/popup-interface.ts @@ -0,0 +1,49 @@ +import type { PageableList, PageableParams } from "@/shared/model"; +import { z } from "zod"; + +export interface PopupParams extends PageableParams { + keyword?: string; + popupTypeCd?: string; + contsTypeCd?: string; + useYn?: string; +} + +export interface Popup { + popupSn: number; + popupTypeCd: string; + popupTypeNm: string; + contsTypeCd: string; + contsTypeNm: string; + popupTtl: string; + popupWdth: number; + popupHgt: number; + popupEndUseYn: string; + useYn: string; + mdfcnDt: string; + mdfr: string; + mdfrNm: string; +} + +export const popupFormSchema = z.object({ + popupTypeCd: z.string(), + contsTypeCd: z.string(), + popupTtl: z.string(), + popupCn: z.string().nullish(), + popupWdth: z.number(), + popupHgt: z.number(), + popupVtclPstn: z.number(), + popupHrzntltyPstn: z.number(), + popupEndUseYn: z.string(), + popupLnkgUrl: z.string().nullish(), + atflCd: z.string().nullish(), + useYn: z.string(), + rgtr: z.string(), +}); + +export type PopupList = PageableList; +export type PopupFormData = z.infer; + +export interface PopupService { + getPopupList(params: PopupParams): Promise; + createPopup(data: PopupFormData): Promise; +} diff --git a/apps/cs/src/pages/popup/model/table-columns.tsx b/apps/cs/src/pages/popup/model/table-columns.tsx new file mode 100644 index 000000000..ad3938e2b --- /dev/null +++ b/apps/cs/src/pages/popup/model/table-columns.tsx @@ -0,0 +1,43 @@ +import dayjs from "dayjs"; +import { type ColumnDef } from "@tanstack/react-table"; +import type { Popup } from "./popup-interface"; + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const { pageIndex, pageSize } = table.getState().pagination; + const totalRows = table.getRowCount(); + return totalRows - (pageIndex * pageSize + row.index); + }, + }, + { + accessorKey: "popupTtl", + header: "제목", + }, + { + accessorKey: "popupTypeNm", + header: "팝업유형", + }, + { + accessorKey: "contsTypeNm", + header: "콘텐츠유형", + }, + { + accessorKey: "useYn", + header: "사용여부", + }, + { + accessorKey: "mdfcnDt", + header: "최종수정일", + cell: ({ row }) => { + const date = dayjs(row.original.mdfcnDt); + return date.format("YYYY-MM-DD"); + }, + }, + { + accessorKey: "mdfrNm", + header: "최종수정자", + }, +]; diff --git a/apps/cs/src/pages/popup/ui/create-page.tsx b/apps/cs/src/pages/popup/ui/create-page.tsx new file mode 100644 index 000000000..f157fb3c6 --- /dev/null +++ b/apps/cs/src/pages/popup/ui/create-page.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { useMutation } from "@tanstack/react-query"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useUserStore, useAlertStore } from "@/shared/lib"; +import { popupFormSchema, type PopupFormData } from "../model/popup-interface"; +import { PopupApi } from "../api/popup-service"; +import { PopupForm } from "./popup-form"; +import { useEffect } from "react"; + +function CreatePage() { + const router = useRouter(); + const { userInfo } = useUserStore((state) => state); + const { setMessage: alert } = useAlertStore((state) => state); + + const { mutateAsync: createPopup } = useMutation({ + mutationFn: (data: PopupFormData) => PopupApi.createPopup(data), + onSuccess: () => { + alert("등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(popupFormSchema), + defaultValues: { + contsTypeCd: "39", + popupEndUseYn: "Y", + useYn: "Y", + }, + }); + + useEffect(() => { + if (!userInfo || !form) return; + form.setValue("rgtr", userInfo.userUnqId); + }, [userInfo, form]); + + return ( +
+
+

팝업 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/popup/ui/popup-form.tsx b/apps/cs/src/pages/popup/ui/popup-form.tsx new file mode 100644 index 000000000..3e0774fab --- /dev/null +++ b/apps/cs/src/pages/popup/ui/popup-form.tsx @@ -0,0 +1,333 @@ +import dynamic from "next/dynamic"; +import { type UseFormReturn } from "react-hook-form"; +import { + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import { FormWrapper } from "@/widgets/form-wrapper"; +import { CodeSelect } from "@/entities/code"; +import { FormItem, FormControl, FormField } from "@/shared/ui/form"; +import type { PopupFormData } from "../model/popup-interface"; +import { GeneralFileUploader } from "@/widgets/post/ui/general-file-uploader"; +import { useState } from "react"; + +const Editor = dynamic( + () => import("@/entities/editor").then((module) => module.Editor), + { ssr: false }, +); + +type PopupFormProps = { + form: UseFormReturn; + popupSn?: string; + handleSave: (data: PopupFormData) => Promise; + handleDelete?: () => Promise; +}; + +function PopupForm({ form, popupSn, ...props }: PopupFormProps) { + const [deleteAttachmentIds, setDeleteAttachmentIds] = useState([]); + + return ( + +
+ + + + + + + + 제목 + + ( + + + + + + )} + /> + + + + 팝업 크기 + +
+ ( + + + { + const value = e.target.value; + if (isNaN(parseInt(value))) return; + field.onChange(parseInt(value)); + }} + /> + + + )} + /> + × + ( + + + { + const value = e.target.value; + if (isNaN(parseInt(value))) return; + field.onChange(parseInt(value)); + }} + /> + + + )} + /> +
+
+
+ + 팝업 위치 + +
+ ( + + + { + const value = e.target.value; + if (isNaN(parseInt(value))) return; + field.onChange(parseInt(value)); + }} + /> + + + )} + /> + / + ( + + + { + const value = e.target.value; + if (isNaN(parseInt(value))) return; + field.onChange(parseInt(value)); + }} + /> + + + )} + /> +
+
+
+ + 팝업 유형 + + ( + + + + + + )} + /> + + + + 콘텐츠 유형 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 링크 주소 + + ( + + + + + + )} + /> + + + + 팝업 내용 + + ( + + + + + + )} + /> + + + + 첨부 파일 + + + + + + 팝업 그만 보기 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ ); +} + +export { PopupForm }; diff --git a/apps/cs/src/pages/popup/ui/popup-page.tsx b/apps/cs/src/pages/popup/ui/popup-page.tsx new file mode 100644 index 000000000..ced5bd80c --- /dev/null +++ b/apps/cs/src/pages/popup/ui/popup-page.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, +} from "@tanstack/react-table"; +import { Button, DataTable, Input, Label } from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { PopupApi } from "../api/popup-service"; +import { columns } from "../model/table-columns"; +import type { Popup, PopupParams } from "../model/popup-interface"; + +function PopupPage() { + const router = useRouter(); + + const [popupParams, setPopupParams] = useState({ + keyword: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: popupList, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.POPUPS], + queryFn: () => + PopupApi.getPopupList({ + ...popupParams, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + const handleRowClick = (row: Popup) => { + router.push(`/popup/register/${row.popupSn}`); + }; + + const table = useReactTable({ + data: popupList?.content ?? [], + columns, + rowCount: popupList?.totalElements ?? 0, + state: { + pagination, + }, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }); + + return ( +
+
+
+ + + setPopupParams({ ...popupParams, keyword: e.target.value }) + } + /> + +
+
+
+
+

팝업 목록

+ +
+
+ +
+
+
+ ); +} + +export { PopupPage }; diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 7aef8df12..b8f822ab7 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -37,7 +37,8 @@ export const ENDPOINT = { TEMPLATE: "/cms-service/api/v1/tmplt", TERMS: "/cms-service/api/v1/trms", LWPO_CODE: "/cms-service/api/v1/lwpo-cds", - UP_CODE: "/cms-service/api/v1/up-codes", + UP_CODE: "/cms-service/api/v1/up-codes", + POPUPS: "/cms-service/api/v1/popups", }, BOARD_SERVICE: { BBS: "/board-service/api/v1/bbs", diff --git a/apps/cs/src/widgets/post/ui/general-file-uploader.tsx b/apps/cs/src/widgets/post/ui/general-file-uploader.tsx index 99088aa3e..e6806cae8 100644 --- a/apps/cs/src/widgets/post/ui/general-file-uploader.tsx +++ b/apps/cs/src/widgets/post/ui/general-file-uploader.tsx @@ -4,10 +4,9 @@ import type { HttpResponse, Upload } from "tus-js-client"; import { Button, FileUploader } from "@common/components"; import { handleTusUpload } from "@/entities/tus"; import { type Attachment } from "@/entities/attachment"; -import type { PostFormData, PostUpdateFormData } from "@/entities/post"; import { setCookie, useAlertStore, useMenuStore } from "@/shared/lib"; -function GeneralFileUploader({ +function GeneralFileUploader({ form, attachmentData, deleteAttachmentIds, From 6f2a5c630808199f955ab6d0d79a7518a2404d62 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 20 May 2025 16:39:24 +0900 Subject: [PATCH 06/64] =?UTF-8?q?feat:=20=ED=8C=9D=EC=97=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/post/ui/general-file-uploader.tsx | 7 +- .../popup/register/[popupSn]/page.tsx | 10 +++ apps/cs/src/pages/popup/api/popup-service.ts | 18 ++++ apps/cs/src/pages/popup/index.ts | 1 + .../src/pages/popup/model/popup-interface.ts | 24 +++++- apps/cs/src/pages/popup/ui/create-page.tsx | 2 +- apps/cs/src/pages/popup/ui/modify-page.tsx | 53 ++++++++++++ apps/cs/src/pages/popup/ui/popup-form.tsx | 85 ++++++++++++++----- .../widgets/post/ui/general-file-uploader.tsx | 4 +- 9 files changed, 174 insertions(+), 30 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/popup/register/[popupSn]/page.tsx create mode 100644 apps/cs/src/pages/popup/ui/modify-page.tsx diff --git a/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx b/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx index 99088aa3e..90edbc58b 100644 --- a/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx +++ b/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx @@ -4,10 +4,9 @@ import type { HttpResponse, Upload } from "tus-js-client"; import { Button, FileUploader } from "@common/components"; import { handleTusUpload } from "@/entities/tus"; import { type Attachment } from "@/entities/attachment"; -import type { PostFormData, PostUpdateFormData } from "@/entities/post"; import { setCookie, useAlertStore, useMenuStore } from "@/shared/lib"; -function GeneralFileUploader({ +function GeneralFileUploader({ form, attachmentData, deleteAttachmentIds, @@ -89,10 +88,10 @@ function GeneralFileUploader({ +
{Object.entries(attachments).map(([atflId, attachment]) => { if (deleteAttachmentIds.includes(atflId)) return null; return ( diff --git a/apps/cs/app/(with-base-layout)/popup/register/[popupSn]/page.tsx b/apps/cs/app/(with-base-layout)/popup/register/[popupSn]/page.tsx new file mode 100644 index 000000000..e7388967f --- /dev/null +++ b/apps/cs/app/(with-base-layout)/popup/register/[popupSn]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/popup"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ popupSn: string }>; +}) { + const { popupSn } = await params; + return ; +} diff --git a/apps/cs/src/pages/popup/api/popup-service.ts b/apps/cs/src/pages/popup/api/popup-service.ts index 59b67ea9b..d32c7c0cc 100644 --- a/apps/cs/src/pages/popup/api/popup-service.ts +++ b/apps/cs/src/pages/popup/api/popup-service.ts @@ -1,10 +1,13 @@ import { ENDPOINT } from "@/shared/config"; import { popupFormSchema, + popupUpdateFormSchema, type PopupFormData, + type PopupInfo, type PopupList, type PopupParams, type PopupService, + type PopupUpdateFormData, } from "../model/popup-interface"; import { client } from "@/shared/api"; @@ -16,10 +19,25 @@ export class PopupApiService implements PopupService { return response.data; } + async getPopupInfo(popupSn: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.POPUPS}/${popupSn}`, + ); + return response.data; + } + async createPopup(data: PopupFormData): Promise { const validatedData = popupFormSchema.parse(data); await client.post(ENDPOINT.CMS_SERVICE.POPUPS, validatedData); } + + async updatePopup(popupSn: number, data: PopupUpdateFormData): Promise { + const validatedData = popupUpdateFormSchema.parse(data); + await client.put( + `${ENDPOINT.CMS_SERVICE.POPUPS}/${popupSn}`, + validatedData, + ); + } } export const PopupApi = new PopupApiService(); diff --git a/apps/cs/src/pages/popup/index.ts b/apps/cs/src/pages/popup/index.ts index dfc7388ff..a320768bd 100644 --- a/apps/cs/src/pages/popup/index.ts +++ b/apps/cs/src/pages/popup/index.ts @@ -1,2 +1,3 @@ export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; export { PopupPage } from "./ui/popup-page"; diff --git a/apps/cs/src/pages/popup/model/popup-interface.ts b/apps/cs/src/pages/popup/model/popup-interface.ts index 51d78a0fb..6952d873f 100644 --- a/apps/cs/src/pages/popup/model/popup-interface.ts +++ b/apps/cs/src/pages/popup/model/popup-interface.ts @@ -1,5 +1,5 @@ -import type { PageableList, PageableParams } from "@/shared/model"; import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; export interface PopupParams extends PageableParams { keyword?: string; @@ -24,6 +24,16 @@ export interface Popup { mdfrNm: string; } +export interface PopupInfo extends Popup { + popupCn: string; + popupVtclPstn: number; + popupHrzntltyPstn: number; + popupLnkgUrl: string; + atflCd: string; + regDt: string; + rgTrNm: string; +} + export const popupFormSchema = z.object({ popupTypeCd: z.string(), contsTypeCd: z.string(), @@ -40,10 +50,22 @@ export const popupFormSchema = z.object({ rgtr: z.string(), }); +export const popupUpdateFormSchema = popupFormSchema + .omit({ + rgtr: true, + }) + .extend({ + mdfr: z.string(), + }) + .partial(); + export type PopupList = PageableList; export type PopupFormData = z.infer; +export type PopupUpdateFormData = z.infer; export interface PopupService { getPopupList(params: PopupParams): Promise; + getPopupInfo(popupSn: number): Promise; createPopup(data: PopupFormData): Promise; + updatePopup(popupSn: number, data: PopupUpdateFormData): Promise; } diff --git a/apps/cs/src/pages/popup/ui/create-page.tsx b/apps/cs/src/pages/popup/ui/create-page.tsx index f157fb3c6..723a10090 100644 --- a/apps/cs/src/pages/popup/ui/create-page.tsx +++ b/apps/cs/src/pages/popup/ui/create-page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { useMutation } from "@tanstack/react-query"; @@ -8,7 +9,6 @@ import { useUserStore, useAlertStore } from "@/shared/lib"; import { popupFormSchema, type PopupFormData } from "../model/popup-interface"; import { PopupApi } from "../api/popup-service"; import { PopupForm } from "./popup-form"; -import { useEffect } from "react"; function CreatePage() { const router = useRouter(); diff --git a/apps/cs/src/pages/popup/ui/modify-page.tsx b/apps/cs/src/pages/popup/ui/modify-page.tsx new file mode 100644 index 000000000..db556a53b --- /dev/null +++ b/apps/cs/src/pages/popup/ui/modify-page.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { ENDPOINT } from "@/shared/config"; +import { + popupUpdateFormSchema, + type PopupUpdateFormData, +} from "../model/popup-interface"; +import { PopupApi } from "../api/popup-service"; +import { PopupForm } from "./popup-form"; + +function ModifyPage({ popupSn }: { popupSn: number }) { + const router = useRouter(); + const { userInfo } = useUserStore((state) => state); + const { setMessage: alert } = useAlertStore((state) => state); + + const { data: popupInfo } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, popupSn], + queryFn: () => PopupApi.getPopupInfo(popupSn), + enabled: !!popupSn, + }); + + const { mutateAsync: updatePopup } = useMutation({ + mutationFn: (data: PopupUpdateFormData) => + PopupApi.updatePopup(popupSn, data), + onSuccess: () => { + alert("수정되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(popupUpdateFormSchema), + values: { + ...popupInfo, + mdfr: userInfo?.userUnqId ?? "", + }, + }); + + return ( +
+
+

팝업 수정

+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/popup/ui/popup-form.tsx b/apps/cs/src/pages/popup/ui/popup-form.tsx index 3e0774fab..6ab790ac4 100644 --- a/apps/cs/src/pages/popup/ui/popup-form.tsx +++ b/apps/cs/src/pages/popup/ui/popup-form.tsx @@ -1,5 +1,7 @@ import dynamic from "next/dynamic"; -import { type UseFormReturn } from "react-hook-form"; +import { useEffect, useState } from "react"; +import type { Path, UseFormReturn } from "react-hook-form"; +import { useQuery } from "@tanstack/react-query"; import { Input, Label, @@ -12,29 +14,64 @@ import { TableRow, } from "@common/components"; import { FormWrapper } from "@/widgets/form-wrapper"; -import { CodeSelect } from "@/entities/code"; -import { FormItem, FormControl, FormField } from "@/shared/ui/form"; -import type { PopupFormData } from "../model/popup-interface"; import { GeneralFileUploader } from "@/widgets/post/ui/general-file-uploader"; -import { useState } from "react"; +import { AttachmentApi } from "@/entities/attachment"; +import { CodeSelect } from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { setCookie } from "@/shared/lib"; +import { FormItem, FormControl, FormField } from "@/shared/ui/form"; +import type { + PopupFormData, + PopupUpdateFormData, +} from "../model/popup-interface"; const Editor = dynamic( () => import("@/entities/editor").then((module) => module.Editor), { ssr: false }, ); -type PopupFormProps = { - form: UseFormReturn; +type PopupFormProps = { + form: UseFormReturn; popupSn?: string; - handleSave: (data: PopupFormData) => Promise; + handleSave: (data: T) => Promise; handleDelete?: () => Promise; }; -function PopupForm({ form, popupSn, ...props }: PopupFormProps) { +function PopupForm({ + form, + popupSn, + handleSave, + ...props +}: PopupFormProps) { + const atflCd = form.getValues("atflCd" as Path); const [deleteAttachmentIds, setDeleteAttachmentIds] = useState([]); + const { data: attachmentData } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd], + queryFn: () => AttachmentApi.getAttachmentsInfo(atflCd as string), + enabled: !!atflCd, + }); + + const saveHandlerInterceptor = async (data: T) => { + if (deleteAttachmentIds.length > 0) { + await Promise.all( + deleteAttachmentIds.map(AttachmentApi.deleteAttachmentByAtflId), + ); + } + await handleSave(data); + }; + + useEffect(() => { + setCookie("atfl-cd-key", ""); + }, []); + return ( - +
@@ -47,7 +84,7 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) { } render={({ field }) => ( @@ -69,7 +106,7 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) {
} render={({ field }) => ( @@ -92,7 +129,7 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) { × } render={({ field }) => ( @@ -121,7 +158,7 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) {
} render={({ field }) => ( @@ -144,7 +181,7 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) { / } render={({ field }) => ( @@ -172,7 +209,7 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) { } render={({ field }) => ( @@ -191,12 +228,13 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) { } render={({ field }) => ( @@ -220,7 +258,7 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) { } render={({ field }) => ( @@ -241,12 +279,12 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) { } render={({ field }) => ( @@ -260,6 +298,7 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) { @@ -270,13 +309,14 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) { } render={({ field }) => (
@@ -299,13 +339,14 @@ function PopupForm({ form, popupSn, ...props }: PopupFormProps) { } render={({ field }) => (
diff --git a/apps/cs/src/widgets/post/ui/general-file-uploader.tsx b/apps/cs/src/widgets/post/ui/general-file-uploader.tsx index e6806cae8..90edbc58b 100644 --- a/apps/cs/src/widgets/post/ui/general-file-uploader.tsx +++ b/apps/cs/src/widgets/post/ui/general-file-uploader.tsx @@ -88,10 +88,10 @@ function GeneralFileUploader({ +
{Object.entries(attachments).map(([atflId, attachment]) => { if (deleteAttachmentIds.includes(atflId)) return null; return ( From bc4513d3fcc3c80bb4a8053577b64479aff364e3 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 20 May 2025 17:33:28 +0900 Subject: [PATCH 07/64] =?UTF-8?q?feat:=20=EA=B6=8C=ED=95=9C=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/entities/menu-authority/index.ts | 1 + .../menu-authority/ui/authority-button.tsx | 26 ++++++ .../cs/src/entities/menu/lib/menu-wrapper.tsx | 9 +- .../entities/posting/ui/posting-dialog.tsx | 9 +- apps/cs/src/pages/(post)/faq/ui/faq-page.tsx | 31 +++---- .../src/pages/recent/ui/move-post-dialog.tsx | 8 +- apps/cs/src/pages/recent/ui/recent-page.tsx | 83 ++++++++++--------- apps/cs/src/widgets/post/ui/post-page.tsx | 33 ++++---- 8 files changed, 118 insertions(+), 82 deletions(-) create mode 100644 apps/cs/src/entities/menu-authority/ui/authority-button.tsx diff --git a/apps/cs/src/entities/menu-authority/index.ts b/apps/cs/src/entities/menu-authority/index.ts index 07433a098..2a555b77e 100644 --- a/apps/cs/src/entities/menu-authority/index.ts +++ b/apps/cs/src/entities/menu-authority/index.ts @@ -1 +1,2 @@ +export { AuthorityButton } from "./ui/authority-button"; export { ManagementAuthorityDialog } from "./ui/management-authority-dialog"; diff --git a/apps/cs/src/entities/menu-authority/ui/authority-button.tsx b/apps/cs/src/entities/menu-authority/ui/authority-button.tsx new file mode 100644 index 000000000..257ab6e5d --- /dev/null +++ b/apps/cs/src/entities/menu-authority/ui/authority-button.tsx @@ -0,0 +1,26 @@ +import { useMenuStore } from "@/shared/lib"; +import { Button, cn, type ButtonProps } from "@common/components"; +import type { PageMenuAuthority } from "../model/menu-authority-interface"; + +type AuthorityButtonProps = { + authority: keyof PageMenuAuthority; +} & ButtonProps; + +function AuthorityButton({ + authority, + className, + children, + ...props +}: AuthorityButtonProps) { + const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority); + + const isAuthority = pageMenuAuthority?.[authority] === "Y"; + + return ( + + ); +} + +export { AuthorityButton }; diff --git a/apps/cs/src/entities/menu/lib/menu-wrapper.tsx b/apps/cs/src/entities/menu/lib/menu-wrapper.tsx index 48fe5e6ed..cb5c92192 100644 --- a/apps/cs/src/entities/menu/lib/menu-wrapper.tsx +++ b/apps/cs/src/entities/menu/lib/menu-wrapper.tsx @@ -25,10 +25,10 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => { const getPageMenuAuthority = async () => { const menuCd = getCookie("menu-code-key"); if (!menuCd) return; - const pageMenuAuthority = await MenuAuthorityApi.getPageMenuAuthority( + const response = await MenuAuthorityApi.getPageMenuAuthority( parseInt(menuCd), ); - return pageMenuAuthority; + return response; }; const checkInquiryAuthority = () => { @@ -40,9 +40,8 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => { const currentMenu = await getCurrentMenuInfo(); if (currentMenu) setCurrentMenuInfo(currentMenu); - const pageMenuAuthority = await getPageMenuAuthority(); - if (pageMenuAuthority?.length) - setPageMenuAuthority(pageMenuAuthority[0]!); + const response = await getPageMenuAuthority(); + if (response?.length) setPageMenuAuthority(response[0]!); })(); }, []); diff --git a/apps/cs/src/entities/posting/ui/posting-dialog.tsx b/apps/cs/src/entities/posting/ui/posting-dialog.tsx index ac31ad0dc..6aef8cceb 100644 --- a/apps/cs/src/entities/posting/ui/posting-dialog.tsx +++ b/apps/cs/src/entities/posting/ui/posting-dialog.tsx @@ -26,6 +26,7 @@ import { cn, } from "@common/components"; import { CodeSelect } from "@/entities/code/@x/posting"; +import { AuthorityButton } from "@/entities/menu-authority"; import { SiteApi, type Site } from "@/entities/site/@x/posting"; import type { PageableList } from "@/shared/model"; import { ENDPOINT } from "@/shared/config"; @@ -218,10 +219,14 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) { return ( - + diff --git a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx index 24ffada45..2c1858899 100644 --- a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx +++ b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx @@ -22,6 +22,7 @@ import { } from "@common/components"; import { AttachmentApi } from "@/entities/attachment"; import { PostApi, type PostParams } from "@/entities/post"; +import { AuthorityButton } from "@/entities/menu-authority"; import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; @@ -30,7 +31,7 @@ function FaqPage({ bbsId }: { bbsId: string }) { const pathname = usePathname(); const { prompt } = usePrompt(); const { setMessage: alert } = useAlertStore((state) => state); - const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state); + const { bbsInfo } = useMenuStore((state) => state); const [postParams, setPostParams] = useState({ keywordType: "title", @@ -140,20 +141,20 @@ function FaqPage({ bbsId }: { bbsId: string }) {

콘텐츠 목록

- {pageMenuAuthority?.regAuthrtYn === "Y" && ( - - )} - {pageMenuAuthority?.delAuthrtYn === "Y" && ( - - )} + router.push(`${pathname}/register`)} + > + 등록 + + + 선택삭제 +
diff --git a/apps/cs/src/pages/recent/ui/move-post-dialog.tsx b/apps/cs/src/pages/recent/ui/move-post-dialog.tsx index 64afef317..0afa6b75c 100644 --- a/apps/cs/src/pages/recent/ui/move-post-dialog.tsx +++ b/apps/cs/src/pages/recent/ui/move-post-dialog.tsx @@ -12,6 +12,7 @@ import { } from "@common/components"; import type { Board } from "@/entities/board"; import type { MovePost } from "@/entities/post"; +import { AuthorityButton } from "@/entities/menu-authority"; type MovePostDialogProps = { selectedPosts: MovePost[]; @@ -36,9 +37,12 @@ function MovePostDialog({ return ( - +

diff --git a/apps/cs/src/pages/recent/ui/recent-page.tsx b/apps/cs/src/pages/recent/ui/recent-page.tsx index 407b90d49..217e4318a 100644 --- a/apps/cs/src/pages/recent/ui/recent-page.tsx +++ b/apps/cs/src/pages/recent/ui/recent-page.tsx @@ -30,8 +30,9 @@ import { type RecentPost, type RecentPostParams, } from "@/entities/post"; -import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib"; import { BoardApi } from "@/entities/board"; +import { AuthorityButton } from "@/entities/menu-authority"; +import { useAlertStore, usePrompt } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { columns } from "../model/table-columns"; import { MovePostDialog } from "./move-post-dialog"; @@ -46,7 +47,6 @@ function RecentPage() { const { prompt } = usePrompt(); const { confirm } = useConfirm(); const { setMessage: alert } = useAlertStore((state) => state); - const { pageMenuAuthority } = useMenuStore((state) => state); const [dateRange, setDateRange] = useState(); const [pagination, setPagination] = useState({ pageIndex: 0, @@ -272,45 +272,46 @@ function RecentPage() {

콘텐츠 목록

- {pageMenuAuthority?.mngAuthrtYn === "Y" && - (isDeletePostData ? ( - <> - - - - ) : ( - <> - ({ - fromBbsId: row.original.bbsId, - pstNo: row.original.pstNo, - }))} - boardList={boardList?.content ?? []} - onMovePost={movePost} - /> - - - ))} + {isDeletePostData ? ( + <> + + 게시물 복원 + + + 완전삭제 + + + ) : ( + <> + ({ + fromBbsId: row.original.bbsId, + pstNo: row.original.pstNo, + }))} + boardList={boardList?.content ?? []} + onMovePost={movePost} + /> + + 선택삭제 + + + )} { From 923a120217ee79efab3159a25abec5dd17480c96 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 20 May 2025 17:34:30 +0900 Subject: [PATCH 08/64] =?UTF-8?q?feat:=20=ED=8C=9D=EC=97=85=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=B0=98=EC=98=81=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../popup/model/posting-table-columns.tsx | 42 ++++++++++ .../src/pages/popup/model/table-columns.tsx | 22 +++++ apps/cs/src/pages/popup/ui/modify-page.tsx | 84 +++++++++++++++++-- apps/cs/src/pages/popup/ui/popup-page.tsx | 47 ++++++++++- 4 files changed, 186 insertions(+), 9 deletions(-) create mode 100644 apps/cs/src/pages/popup/model/posting-table-columns.tsx diff --git a/apps/cs/src/pages/popup/model/posting-table-columns.tsx b/apps/cs/src/pages/popup/model/posting-table-columns.tsx new file mode 100644 index 000000000..d9de20543 --- /dev/null +++ b/apps/cs/src/pages/popup/model/posting-table-columns.tsx @@ -0,0 +1,42 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Checkbox } from "@common/components"; +import type { Post } from "@/entities/posting"; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, + { + accessorKey: "siteNm", + header: "사이트", + }, + { + accessorKey: "pstgPrdSeNm", + header: "게시기간", + cell: ({ row }) => { + const pstgPrdSeNm = row.original.pstgPrdSeNm; + const pstgBgngDt = row.original.pstgBgngDt; + const pstgEndDt = row.original.pstgEndDt; + return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`; + }, + }, +]; diff --git a/apps/cs/src/pages/popup/model/table-columns.tsx b/apps/cs/src/pages/popup/model/table-columns.tsx index ad3938e2b..b223f689b 100644 --- a/apps/cs/src/pages/popup/model/table-columns.tsx +++ b/apps/cs/src/pages/popup/model/table-columns.tsx @@ -1,8 +1,30 @@ import dayjs from "dayjs"; import { type ColumnDef } from "@tanstack/react-table"; import type { Popup } from "./popup-interface"; +import { Checkbox } from "@common/components"; export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, { id: "no", header: "NO", diff --git a/apps/cs/src/pages/popup/ui/modify-page.tsx b/apps/cs/src/pages/popup/ui/modify-page.tsx index db556a53b..69d1171b5 100644 --- a/apps/cs/src/pages/popup/ui/modify-page.tsx +++ b/apps/cs/src/pages/popup/ui/modify-page.tsx @@ -1,29 +1,49 @@ "use client"; +import { useState } from "react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; -import { useMutation, useQuery } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type RowSelectionState, +} from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { DataTable } from "@common/components"; +import { AuthorityButton } from "@/entities/menu-authority"; +import { PostingApi, type PostPayload } from "@/entities/posting"; import { useAlertStore, useUserStore } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { popupUpdateFormSchema, type PopupUpdateFormData, } from "../model/popup-interface"; +import { columns } from "../model/posting-table-columns"; import { PopupApi } from "../api/popup-service"; import { PopupForm } from "./popup-form"; function ModifyPage({ popupSn }: { popupSn: number }) { const router = useRouter(); + const queryClient = useQueryClient(); + const { confirm } = useConfirm(); const { userInfo } = useUserStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state); + const [rowSelection, setRowSelection] = useState({}); + const { data: popupInfo } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, popupSn], queryFn: () => PopupApi.getPopupInfo(popupSn), enabled: !!popupSn, }); + const { data: postHistory } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, popupSn], + queryFn: () => PostingApi.getPostingHistory({ pstgTrgtNo: popupSn }), + }); + const { mutateAsync: updatePopup } = useMutation({ mutationFn: (data: PopupUpdateFormData) => PopupApi.updatePopup(popupSn, data), @@ -32,6 +52,16 @@ function ModifyPage({ popupSn }: { popupSn: number }) { }, }); + const { mutateAsync: savePostingHistory } = useMutation({ + mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, popupSn], + }); + alert("삭제되었습니다."); + }, + }); + const form = useForm({ resolver: zodResolver(popupUpdateFormSchema), values: { @@ -40,12 +70,56 @@ function ModifyPage({ popupSn }: { popupSn: number }) { }, }); + const table = useReactTable({ + data: postHistory?.content ?? [], + columns, + state: { + rowSelection, + }, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onRowSelectionChange: setRowSelection, + }); + + const handleDeletePostingHistory = async () => { + const data = table.getFilteredSelectedRowModel().rows.map((row) => ({ + ...row.original, + mode: "D", + })); + + if (await confirm("삭제하시겠습니까?")) { + savePostingHistory(data); + setRowSelection({}); + } + }; + return ( -
-
-

팝업 수정

+
+
+
+

팝업 수정

+
+ +
+
+
+

사이트 게시 이력

+
+
+
+ + 이력삭제 + +
+ +
-
); } diff --git a/apps/cs/src/pages/popup/ui/popup-page.tsx b/apps/cs/src/pages/popup/ui/popup-page.tsx index ced5bd80c..3c9b4fddd 100644 --- a/apps/cs/src/pages/popup/ui/popup-page.tsx +++ b/apps/cs/src/pages/popup/ui/popup-page.tsx @@ -1,19 +1,27 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable, type PaginationState, + type RowSelectionState, } from "@tanstack/react-table"; import { Button, DataTable, Input, Label } from "@common/components"; +import { AuthorityButton } from "@/entities/menu-authority"; +import { POSTING_TYPE, PostingDialog } from "@/entities/posting"; import { ENDPOINT } from "@/shared/config"; import { PopupApi } from "../api/popup-service"; import { columns } from "../model/table-columns"; import type { Popup, PopupParams } from "../model/popup-interface"; +type PostingTarget = { + pstgTrgtNm: string; + pstgTrgtNo: number; +}; + function PopupPage() { const router = useRouter(); @@ -24,6 +32,8 @@ function PopupPage() { pageIndex: 0, pageSize: 10, }); + const [rowSelection, setRowSelection] = useState({}); + const [postingTargets, setPostingTargets] = useState([]); const { data: popupList, refetch } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.POPUPS], @@ -51,12 +61,30 @@ function PopupPage() { rowCount: popupList?.totalElements ?? 0, state: { pagination, + rowSelection, }, - onPaginationChange: setPagination, - getCoreRowModel: getCoreRowModel(), manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, }); + useEffect(() => { + const postingTargets = table + .getFilteredSelectedRowModel() + .rows.map((row) => ({ + pstgTrgtNm: row.original.popupTtl, + pstgTrgtNo: row.original.popupSn, + })); + + if (!postingTargets.length) { + setPostingTargets([]); + return; + } + + setPostingTargets(postingTargets); + }, [rowSelection]); + return (
@@ -79,7 +107,18 @@ function PopupPage() {

팝업 목록

- +
+ + router.push("/popup/register")} + > + 등록 + +
From 2889de235ab468df2e0e2071a41f916f9eb47783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Tue, 20 May 2025 21:38:11 +0900 Subject: [PATCH 09/64] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EB=B9=8C=EB=93=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/entities/site/ui/site-select.tsx | 1 - .../cs/src/pages/code/model/table-columns.tsx | 76 +++++++++---------- apps/cs/src/pages/code/ui/code-page.tsx | 33 ++++---- 3 files changed, 49 insertions(+), 61 deletions(-) diff --git a/apps/cs/src/entities/site/ui/site-select.tsx b/apps/cs/src/entities/site/ui/site-select.tsx index af2773958..61f412b4d 100644 --- a/apps/cs/src/entities/site/ui/site-select.tsx +++ b/apps/cs/src/entities/site/ui/site-select.tsx @@ -15,7 +15,6 @@ type SiteSelectProps = { } & React.ComponentPropsWithoutRef; function SiteSelect({ - siteId, defaultValue, placeholder = "선택", ...props diff --git a/apps/cs/src/pages/code/model/table-columns.tsx b/apps/cs/src/pages/code/model/table-columns.tsx index 1a486bd5a..f42f6463f 100644 --- a/apps/cs/src/pages/code/model/table-columns.tsx +++ b/apps/cs/src/pages/code/model/table-columns.tsx @@ -1,18 +1,40 @@ import dayjs from "dayjs"; -import { type ColumnDef} from "@tanstack/react-table"; +import { type ColumnDef } from "@tanstack/react-table"; import { Button } from "@common/components"; import type { UpCode } from "@/entities/code/model/upcode-interface"; import type { LwpoCode } from "@/entities/code/model/lwpocode-interface"; -type UpCodeTableMeta = { - onManagementClick?: (row: UpCode) => void; +const UpCodeManageButton = () => { + return ( + + ); }; -type LwpoCodeTableMeta = { - onManagementClick?: (row: LwpoCode) => void; +const LwpoCodeManageButton = () => { + return ( + + ); }; - -export const upCodecolumns: ColumnDef[] = [ + +export const upCodecolumns: ColumnDef[] = [ { id: "no", header: "NO", @@ -52,26 +74,13 @@ export const upCodecolumns: ColumnDef[] = [ }, }, { - id: "management", + id: "actions", header: "관리", - cell: ({ row, table }) => { - const onManagementClick = (table.options.meta as UpCodeTableMeta)?.onManagementClick; - return ( - - ); - }, - }, + cell: () => , + } ]; -export const lowCodecolumns: ColumnDef[] = [ +export const lowCodecolumns: ColumnDef[] = [ { id: "no", header: "NO", @@ -103,21 +112,8 @@ export const lowCodecolumns: ColumnDef[] = [ }, }, { - id: "management", + id: "actions", header: "관리", - cell: ({ row, table }) => { - const onManagementClick = (table.options.meta as LwpoCodeTableMeta)?.onManagementClick; - return ( - - ); - }, - }, + cell: () => , + } ]; diff --git a/apps/cs/src/pages/code/ui/code-page.tsx b/apps/cs/src/pages/code/ui/code-page.tsx index 4a4fa74da..237676c8a 100644 --- a/apps/cs/src/pages/code/ui/code-page.tsx +++ b/apps/cs/src/pages/code/ui/code-page.tsx @@ -68,22 +68,20 @@ function CodePage() { enabled: !!lwpoCodeQuery.upCd, }); - const upCodehandleSubmit = (e: React.FormEvent) => { e.preventDefault(); refetchUpCode(); }; const upCodehandleRowClick = (row: UpCode) => { - setSelectedUpCode(row); - setLwpoCodeQuery({ ...lwpoCodeQuery, upCd: row.upCd }); - refetchLwpoCode(); - }; - - const handleManagementClick = (row: UpCode) => { - setSelectedUpCode(row); - setIsRegistrationMode(false); - setIsManagementDialogOpen(true); + if (selectedUpCode?.upCd === row.upCd) { + setIsRegistrationMode(false); + setIsManagementDialogOpen(true); + } else { + setSelectedUpCode(row); + setLwpoCodeQuery({ ...lwpoCodeQuery, upCd: row.upCd }); + refetchLwpoCode(); + } }; const handleRegistrationClick = () => { @@ -92,7 +90,7 @@ function CodePage() { setIsManagementDialogOpen(true); }; - const handleLwpoCodeManagementClick = (row: LwpoCode) => { + const lwpoCodehandleRowClick = (row: LwpoCode) => { setSelectedLwpoCode(row); setIsLwpoCodeRegistrationMode(false); setIsLwpoCodeManagementDialogOpen(true); @@ -108,8 +106,7 @@ function CodePage() { setIsLwpoCodeManagementDialogOpen(true); }; - - const upCodeTable = useReactTable({ + const upCodeTable = useReactTable({ data: upCodeList?.content ?? [], columns: upCodecolumns, rowCount: upCodeList?.totalElements ?? 0, @@ -120,13 +117,10 @@ function CodePage() { onPaginationChange: setUpCodePagination, getCoreRowModel: getCoreRowModel(), manualPagination: true, - meta: { - onManagementClick: handleManagementClick, - }, getRowId: (row) => row.upCd, }); - const lwpoCodeTable = useReactTable({ + const lwpoCodeTable = useReactTable({ data: lwpoCodeList?.content ?? [], columns: lowCodecolumns, rowCount: lwpoCodeList?.totalElements ?? 0, @@ -137,9 +131,6 @@ function CodePage() { onPaginationChange: setLwpoCodePagination, getCoreRowModel: getCoreRowModel(), manualPagination: true, - meta: { - onManagementClick: handleLwpoCodeManagementClick, - }, getRowId: (row) => row.lwpoCd, }); @@ -218,6 +209,7 @@ function CodePage() {
@@ -245,4 +237,5 @@ function CodePage() { } export { CodePage }; + \ No newline at end of file From 0539b7f1f945de5458f93e393ec4e006a5a81a49 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 21 May 2025 15:00:16 +0900 Subject: [PATCH 10/64] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/entities/code/api/lwpocode-service.ts | 22 +- .../src/entities/code/api/upcode-service.ts | 3 +- apps/cs/src/entities/code/index.ts | 23 +- .../entities/code/model/lwpocode-interface.ts | 99 ++-- .../entities/code/model/upcode-interface.ts | 75 ++- .../code/model/lwpocode-table-columns.tsx | 70 +++ .../cs/src/pages/code/model/table-columns.tsx | 119 ----- .../pages/code/model/upcode-table-columns.tsx | 64 +++ apps/cs/src/pages/code/ui/code-page.tsx | 249 +--------- apps/cs/src/pages/code/ui/lowcode-dialog.tsx | 292 ----------- apps/cs/src/pages/code/ui/lwpocode-dialog.tsx | 321 ++++++++++++ .../cs/src/pages/code/ui/lwpocode-section.tsx | 98 ++++ apps/cs/src/pages/code/ui/upcode-dialog.tsx | 465 +++++++++--------- apps/cs/src/pages/code/ui/upcode-section.tsx | 101 ++++ .../module/ui/management-authority-dialog.tsx | 201 ++++---- packages/common/assets/src/styles/base.css | 2 + .../components/src/ui/data-table/index.tsx | 41 +- tailwind.config.preset.ts | 1 + 18 files changed, 1164 insertions(+), 1082 deletions(-) create mode 100644 apps/cs/src/pages/code/model/lwpocode-table-columns.tsx delete mode 100644 apps/cs/src/pages/code/model/table-columns.tsx create mode 100644 apps/cs/src/pages/code/model/upcode-table-columns.tsx delete mode 100644 apps/cs/src/pages/code/ui/lowcode-dialog.tsx create mode 100644 apps/cs/src/pages/code/ui/lwpocode-dialog.tsx create mode 100644 apps/cs/src/pages/code/ui/lwpocode-section.tsx create mode 100644 apps/cs/src/pages/code/ui/upcode-section.tsx diff --git a/apps/cs/src/entities/code/api/lwpocode-service.ts b/apps/cs/src/entities/code/api/lwpocode-service.ts index 045a6ee6d..b8f11fa22 100644 --- a/apps/cs/src/entities/code/api/lwpocode-service.ts +++ b/apps/cs/src/entities/code/api/lwpocode-service.ts @@ -25,9 +25,12 @@ export class LwpoCodeApiService implements LwpoCodeService { return response.data; } - public async LwpoCodeDetail(upCd: string, LwpoCd: string): Promise { + public async lwpoCodeDetail( + upCd: string, + lwpoCd: string, + ): Promise { const response = await client.get( - `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${LwpoCd}/up-cd/${upCd}`, + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`, ); return response.data; } @@ -41,21 +44,24 @@ export class LwpoCodeApiService implements LwpoCodeService { return response.data; } - public async updateLwpoCode(upCd: string, LwpoCd: string, data: LwpoCodeFormData): Promise { + public async updateLwpoCode( + upCd: string, + lwpoCd: string, + data: LwpoCodeFormData, + ): Promise { const validatedData = LwpoCodeFormSchema.parse(data); const response = await client.put( - `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${LwpoCd}/up-cd/${upCd}`, + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`, validatedData, ); return response.data; } - public async deleteLwpoCode(upCd: string, LwpoCd: string): Promise { + public async deleteLwpoCode(upCd: string, lwpoCd: string): Promise { await client.delete( - `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${LwpoCd}/up-cd/${upCd}`, + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`, ); } - } -export const lwpoCodeApi = new LwpoCodeApiService(); +export const LwpoCodeApi = new LwpoCodeApiService(); diff --git a/apps/cs/src/entities/code/api/upcode-service.ts b/apps/cs/src/entities/code/api/upcode-service.ts index 86c936a1f..dcccd9534 100644 --- a/apps/cs/src/entities/code/api/upcode-service.ts +++ b/apps/cs/src/entities/code/api/upcode-service.ts @@ -42,7 +42,6 @@ export class UpCodeApiService implements UpCodeService { ); return response.data; } - } -export const upCodeApi = new UpCodeApiService(); +export const UpCodeApi = new UpCodeApiService(); diff --git a/apps/cs/src/entities/code/index.ts b/apps/cs/src/entities/code/index.ts index 4cb1c1547..e166e2c29 100644 --- a/apps/cs/src/entities/code/index.ts +++ b/apps/cs/src/entities/code/index.ts @@ -2,13 +2,18 @@ export { CodeApi } from "./api/code-service"; export type { ICode } from "./model/code-interface"; export { CodeSelect } from "./ui/code-select"; +export { UpCodeApi } from "./api/upcode-service"; +export { + upCodeFormSchema, + type UpCode, + type UpCodeFormData, + type UpCodeParams, +} from "./model/upcode-interface"; -export { upCodeApi } from "./api/upcode-service"; -export type { UpCode } from "./model/upcode-interface"; -export { upCodeFormSchema, type UpCodeFormData } from "./model/upcode-interface"; - -export { lwpoCodeApi} from "./api/lwpocode-service"; -export type { LwpoCode } from "./model/lwpocode-interface"; -export { LwpoCodeFormSchema, type LwpoCodeFormData } from "./model/lwpocode-interface"; - - +export { LwpoCodeApi } from "./api/lwpocode-service"; +export { + LwpoCodeFormSchema, + type LwpoCode, + type LwpoCodeFormData, + type LwpoCodeParams, +} from "./model/lwpocode-interface"; diff --git a/apps/cs/src/entities/code/model/lwpocode-interface.ts b/apps/cs/src/entities/code/model/lwpocode-interface.ts index eec2b82d2..785d86e1e 100644 --- a/apps/cs/src/entities/code/model/lwpocode-interface.ts +++ b/apps/cs/src/entities/code/model/lwpocode-interface.ts @@ -2,60 +2,67 @@ import { z } from "zod"; import type { PageableParams, PageableList } from "@/shared/model"; export interface LwpoCodeParams extends PageableParams { - upCd?: string; - lwpoCd?: string; - lwpoCdNm?: string; - useYn?: string; + upCd?: string; + lwpoCd?: string; + lwpoCdNm?: string; + useYn?: string; } - + export interface LwpoCode { - lwpoCd: string; - upCd: string; - lwpoCdNm: string; - lwpoCdExpln: string; - unit: string; - prntLwpoCd: string; - useYn: string; - sortSeq: number; - regDt: string; - rgTr: string; - rgTrNm: string; - mdfcnDt: string; - mdFr: string; + lwpoCd: string; + upCd: string; + lwpoCdNm: string; + lwpoCdExpln: string; + unit: string; + prntLwpoCd: string; + useYn: string; + sortSeq: number; + regDt: string; + rgTr: string; + rgTrNm: string; + mdfcnDt: string; + mdFr: string; } - + export interface LwpoCodeDetail extends LwpoCodeFormData { - lwpoCd: string; - upCd: string; - lwpoCdNm: string; - lwpoCdExpln: string; - unit: string; - prntLwpoCd: string; - useYn: string; - sortSeq: 0; + lwpoCd: string; + upCd: string; + lwpoCdNm: string; + lwpoCdExpln: string; + unit: string; + prntLwpoCd: string; + useYn: string; + sortSeq: 0; } - + export const LwpoCodeFormSchema = z.object({ - lwpoCd: z.string().max(10).nonempty(), - upCd: z.string().max(10).nonempty(), - lwpoCdNm: z.string().max(100).nonempty(), - lwpoCdExpln: z.string().max(1000).nullish(), - unit: z.string().max(20).nullish(), - prntLwpoCd: z.string().max(10).nullish(), - useYn: z.string(), - sortSeq: z.number(), - delYn: z.string(), + lwpoCd: z.string().max(10).nonempty(), + upCd: z.string().max(10).nonempty(), + lwpoCdNm: z.string().max(100).nonempty(), + lwpoCdExpln: z.string().max(1000).nullish(), + unit: z.string().max(20).nullish(), + prntLwpoCd: z.string().max(10).nullish(), + useYn: z.string(), + sortSeq: z.number(), + delYn: z.string(), }); - + export type LwpoCodeFormData = z.infer; export type LwpoCodeList = PageableList; - + export interface LwpoCodeService { - getLwpoCodeList(params: LwpoCodeParams): Promise; - getUpCodeLwpoCodeList(upCd: string): Promise; - LwpoCodeDetail(upCd: string, LwpoCode: string): Promise; - updateLwpoCode(upCd: string, LwpoCode: string, data: LwpoCodeFormData): Promise; - createLwpoCode(data: LwpoCodeFormData): Promise; - deleteLwpoCode(upCd: string, LwpoCode: string, data: LwpoCodeFormData): Promise; + getLwpoCodeList(params: LwpoCodeParams): Promise; + getUpCodeLwpoCodeList(upCd: string): Promise; + lwpoCodeDetail(upCd: string, lwpoCd: string): Promise; + updateLwpoCode( + upCd: string, + lwpoCd: string, + data: LwpoCodeFormData, + ): Promise; + createLwpoCode(data: LwpoCodeFormData): Promise; + deleteLwpoCode( + upCd: string, + lwpoCd: string, + data: LwpoCodeFormData, + ): Promise; } - \ No newline at end of file diff --git a/apps/cs/src/entities/code/model/upcode-interface.ts b/apps/cs/src/entities/code/model/upcode-interface.ts index d3e9680c1..73bf4f3ae 100644 --- a/apps/cs/src/entities/code/model/upcode-interface.ts +++ b/apps/cs/src/entities/code/model/upcode-interface.ts @@ -2,52 +2,51 @@ import { z } from "zod"; import type { PageableParams, PageableList } from "@/shared/model"; export interface UpCodeParams extends PageableParams { - keyword?: string; - upCd?: string; - cdSe?: string; - useYn?: string; - siteId?: string; + keyword?: string; + upCd?: string; + cdSe?: string; + useYn?: string; + siteId?: string; } - + export interface UpCode { - upCd: string; - upCdNm: string; - upCdExpln: string; - cdSe: string; - siteId: string; - useYn: string; - regDt: string; + upCd: string; + upCdNm: string; + upCdExpln: string; + cdSe: string; + siteId: string; + useYn: string; + regDt: string; } - + export interface UpCodeDetail extends UpCodeFormData { - upCd: string; - upCdNm: string; - upCdExpln: string; - cdSe: string; - siteId: string; - useYn: string; - delYn: string; - regDt: string; - rgTrNm: string; + upCd: string; + upCdNm: string; + upCdExpln: string; + cdSe: string; + siteId: string; + useYn: string; + delYn: string; + regDt: string; + rgTrNm: string; } - + export const upCodeFormSchema = z.object({ - upCd: z.string().max(10).nonempty(), - upCdNm: z.string().max(100).nonempty(), - upCdExpln: z.string().max(1000).nullish(), - cdSe: z.string().max(10).nonempty(), - siteId: z.string().max(2).nullish(), - useYn: z.string(), - delYn: z.string(), + upCd: z.string().max(10).nonempty(), + upCdNm: z.string().max(100).nonempty(), + upCdExpln: z.string().max(1000).nullish(), + cdSe: z.string().max(10).nonempty(), + siteId: z.string().max(2).nullish(), + useYn: z.string(), + delYn: z.string(), }); - + export type UpCodeFormData = z.infer; export type UpCodeList = PageableList; - + export interface UpCodeService { - getUpCodeList(params: UpCodeParams): Promise; - upCodeDetail(upCodeId: string): Promise; - updateUpCode(upCodeId: string, data: UpCodeFormData): Promise; - createUpCode(data: UpCodeFormData): Promise; + getUpCodeList(params: UpCodeParams): Promise; + upCodeDetail(upCodeId: string): Promise; + updateUpCode(upCodeId: string, data: UpCodeFormData): Promise; + createUpCode(data: UpCodeFormData): Promise; } - \ No newline at end of file diff --git a/apps/cs/src/pages/code/model/lwpocode-table-columns.tsx b/apps/cs/src/pages/code/model/lwpocode-table-columns.tsx new file mode 100644 index 000000000..1bea87275 --- /dev/null +++ b/apps/cs/src/pages/code/model/lwpocode-table-columns.tsx @@ -0,0 +1,70 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button } from "@common/components"; +import type { LwpoCode } from "@/entities/code"; +import { LwpoCodeManagementDialog } from "../ui/lwpocode-dialog"; +import { useState } from "react"; + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const { pageIndex, pageSize } = table.getState().pagination; + return totalRows - (pageIndex * pageSize + row.index); + }, + }, + { + accessorKey: "lwpoCd", + header: "하위코드", + }, + { + accessorKey: "lwpoCdNm", + header: "하위코드명", + }, + { + accessorKey: "useYn", + header: "사용 여부", + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => { + const date = dayjs(row.original.regDt); + return date.format("YYYY-MM-DD"); + }, + }, + { + id: "actions", + header: "관리", + cell: ({ row }) => ( +
e.stopPropagation()}> + +
+ ), + }, +]; + +const LwpoCodeManageButton = ({ + lwpoCd, + upCd, +}: { + lwpoCd: string; + upCd: string; +}) => { + const [dialogOpen, setDialogOpen] = useState(false); + return ( + + + + ); +}; diff --git a/apps/cs/src/pages/code/model/table-columns.tsx b/apps/cs/src/pages/code/model/table-columns.tsx deleted file mode 100644 index f42f6463f..000000000 --- a/apps/cs/src/pages/code/model/table-columns.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import dayjs from "dayjs"; -import { type ColumnDef } from "@tanstack/react-table"; -import { Button } from "@common/components"; -import type { UpCode } from "@/entities/code/model/upcode-interface"; -import type { LwpoCode } from "@/entities/code/model/lwpocode-interface"; - -const UpCodeManageButton = () => { - return ( - - ); -}; - -const LwpoCodeManageButton = () => { - return ( - - ); -}; - -export const upCodecolumns: ColumnDef[] = [ - { - id: "no", - header: "NO", - cell: ({ row, table }) => { - const isSelected = row.getIsSelected(); - if (isSelected) { - return "▶"; - } - const totalRows = table.getRowCount(); - const currentPage = table.getState().pagination.pageIndex; - const pageSize = table.getState().pagination.pageSize; - return totalRows - (currentPage * pageSize + row.index); - }, - }, - { - accessorKey: "upCd", - header: "상위코드", - }, - { - accessorKey: "upCdNm", - header: "상위코드명", - }, - { - accessorKey: "cdSe", - header: "코드구분", - }, - { - accessorKey: "useYn", - header: "사용여부", - }, - { - accessorKey: "regDt", - header: "등록일", - cell: ({ row }) => { - const date = dayjs(row.original.regDt); - return date.format("YYYY-MM-DD"); - }, - }, - { - id: "actions", - header: "관리", - cell: () => , - } -]; - -export const lowCodecolumns: ColumnDef[] = [ - { - id: "no", - header: "NO", - cell: ({ row, table }) => { - const totalRows = table.getRowCount(); - const currentPage = table.getState().pagination.pageIndex; - const pageSize = table.getState().pagination.pageSize; - return totalRows - (currentPage * pageSize + row.index); - }, - }, - { - accessorKey: "lwpoCd", - header: "하위코드", - }, - { - accessorKey: "lwpoCdNm", - header: "하위코드명", - }, - { - accessorKey: "useYn", - header: "사용 여부", - }, - { - accessorKey: "regDt", - header: "등록일", - cell: ({ row }) => { - const date = dayjs(row.original.regDt); - return date.format("YYYY-MM-DD"); - }, - }, - { - id: "actions", - header: "관리", - cell: () => , - } -]; diff --git a/apps/cs/src/pages/code/model/upcode-table-columns.tsx b/apps/cs/src/pages/code/model/upcode-table-columns.tsx new file mode 100644 index 000000000..c35ea4d70 --- /dev/null +++ b/apps/cs/src/pages/code/model/upcode-table-columns.tsx @@ -0,0 +1,64 @@ +import dayjs from "dayjs"; +import { useState } from "react"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button } from "@common/components"; +import type { UpCode } from "@/entities/code"; +import { UpCodeManagementDialog } from "../ui/upcode-dialog"; + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const { pageIndex, pageSize } = table.getState().pagination; + return totalRows - (pageIndex * pageSize + row.index); + }, + }, + { + accessorKey: "upCd", + header: "상위코드", + }, + { + accessorKey: "upCdNm", + header: "상위코드명", + }, + { + accessorKey: "cdSe", + header: "코드구분", + }, + { + accessorKey: "useYn", + header: "사용여부", + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => { + const date = dayjs(row.original.regDt); + return date.format("YYYY-MM-DD"); + }, + }, + { + id: "actions", + header: "관리", + cell: ({ row }) => ( +
e.stopPropagation()}> + +
+ ), + }, +]; + +const UpCodeManageButton = ({ upCd }: { upCd: string }) => { + const [dialogOpen, setDialogOpen] = useState(false); + return ( + + + + ); +}; diff --git a/apps/cs/src/pages/code/ui/code-page.tsx b/apps/cs/src/pages/code/ui/code-page.tsx index 237676c8a..61e7fb8ea 100644 --- a/apps/cs/src/pages/code/ui/code-page.tsx +++ b/apps/cs/src/pages/code/ui/code-page.tsx @@ -1,241 +1,20 @@ "use client"; -import "@common/assets/styles/grid.css"; +import { useState } from "react"; -import { useEffect, useState } from "react"; -import { keepPreviousData, useQuery } from "@tanstack/react-query"; -import { - getCoreRowModel, - useReactTable, - type PaginationState, -} from "@tanstack/react-table"; -import { Button, DataTable, Input, Label } from "@common/components"; -import { upCodeApi } from "@/entities/code/api/upcode-service"; -import { type UpCode, type UpCodeParams } from "@/entities/code/model/upcode-interface"; -import { type LwpoCode, type LwpoCodeParams } from "@/entities/code/model/lwpocode-interface"; -import { ENDPOINT } from "@/shared/config"; -import { upCodecolumns, lowCodecolumns } from "../model/table-columns"; -import { lwpoCodeApi } from "@/entities/code/api/lwpocode-service"; -import { ManagementUpCodeDialog } from "./upcode-dialog"; -import { ManagementLwpoCodeDialog } from "./lowcode-dialog"; +import { UpCodeSection } from "./upcode-section"; +import { LwpoCodeSection } from "./lwpocode-section"; function CodePage() { - const [selectedUpCode, setSelectedUpCode] = useState(null); - const [isManagementDialogOpen, setIsManagementDialogOpen] = useState(false); - - const [isRegistrationMode, setIsRegistrationMode] = useState(false); - const [isLwpoCodeRegistrationMode, setIsLwpoCodeRegistrationMode] = useState(false); - - const [selectedLwpoCode, setSelectedLwpoCode] = useState(null); - const [isLwpoCodeManagementDialogOpen, setIsLwpoCodeManagementDialogOpen] = useState(false); - - const [upCodeQuery, setUpCodeQuery] = useState({ - upCd: "", - }); - const [lwpoCodeQuery, setLwpoCodeQuery] = useState({ - upCd: "", - lwpoCd: "", - }); - const [upCodePagination, setUpCodePagination] = useState({ - pageIndex: 0, - pageSize: 10, - }); - - const [lwpoCodePagination, setLwpoCodePagination] = useState({ - pageIndex: 0, - pageSize: 1000, - }); - - const { data: upCodeList, refetch: refetchUpCode } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE], - queryFn: () => - upCodeApi.getUpCodeList({ - ...upCodeQuery, - page: upCodePagination.pageIndex, - size: upCodePagination.pageSize, - }), - placeholderData: keepPreviousData, - }); - - const { data: lwpoCodeList, refetch: refetchLwpoCode } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, lwpoCodeQuery], - queryFn: () => - lwpoCodeApi.getLwpoCodeList({ - ...lwpoCodeQuery, - page: lwpoCodePagination.pageIndex, - size: lwpoCodePagination.pageSize, - }), - enabled: !!lwpoCodeQuery.upCd, - }); - - const upCodehandleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - refetchUpCode(); - }; - - const upCodehandleRowClick = (row: UpCode) => { - if (selectedUpCode?.upCd === row.upCd) { - setIsRegistrationMode(false); - setIsManagementDialogOpen(true); - } else { - setSelectedUpCode(row); - setLwpoCodeQuery({ ...lwpoCodeQuery, upCd: row.upCd }); - refetchLwpoCode(); - } - }; - - const handleRegistrationClick = () => { - setSelectedUpCode(null); - setIsRegistrationMode(true); - setIsManagementDialogOpen(true); - }; - - const lwpoCodehandleRowClick = (row: LwpoCode) => { - setSelectedLwpoCode(row); - setIsLwpoCodeRegistrationMode(false); - setIsLwpoCodeManagementDialogOpen(true); - }; - - const handleLwpoCodeRegistrationClick = () => { - if (!selectedUpCode?.upCd) { - alert("상위코드를 선택해주세요."); - return; - } - setSelectedLwpoCode(null); - setIsLwpoCodeRegistrationMode(true); - setIsLwpoCodeManagementDialogOpen(true); - }; - - const upCodeTable = useReactTable({ - data: upCodeList?.content ?? [], - columns: upCodecolumns, - rowCount: upCodeList?.totalElements ?? 0, - state: { - pagination: upCodePagination, - rowSelection: selectedUpCode ? { [selectedUpCode.upCd]: true } : {}, - }, - onPaginationChange: setUpCodePagination, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - getRowId: (row) => row.upCd, - }); - - const lwpoCodeTable = useReactTable({ - data: lwpoCodeList?.content ?? [], - columns: lowCodecolumns, - rowCount: lwpoCodeList?.totalElements ?? 0, - state: { - pagination: lwpoCodePagination, - rowSelection: selectedLwpoCode ? { [selectedLwpoCode.lwpoCd]: true } : {}, - }, - onPaginationChange: setLwpoCodePagination, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - getRowId: (row) => row.lwpoCd, - }); - - const lwpoCodehandleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - refetchLwpoCode(); - }; - - useEffect(() => { - refetchUpCode(); - }, [upCodePagination]); - - return ( -
-
-
-
-
- - - setUpCodeQuery({ - ...upCodeQuery, - keyword: e.target.value, - }) - } - /> - - -
-
-
-

상위코드 목록

- -
-
- -
-
-
-
-
-
- - setLwpoCodeQuery({ - ...lwpoCodeQuery, - lwpoCdNm: e.target.value - }) - } - /> - - -
-
-
-

하위코드 목록

- -
-
- -
-
-
-
- {isManagementDialogOpen && ( - - )} - {isLwpoCodeManagementDialogOpen && ( - - )} + const [selectedUpCd, setSelectedUpCd] = useState(""); + return ( +
+
+ +
- ); - } - - export { CodePage }; - - \ No newline at end of file +
+ ); +} + +export { CodePage }; diff --git a/apps/cs/src/pages/code/ui/lowcode-dialog.tsx b/apps/cs/src/pages/code/ui/lowcode-dialog.tsx deleted file mode 100644 index 1c64c8ade..000000000 --- a/apps/cs/src/pages/code/ui/lowcode-dialog.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { - Button, - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Input, - RadioGroup, - RadioGroupItem, - Label, -} from "@common/components"; -import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; -import { lwpoCodeApi } from "@/entities/code/api/lwpocode-service"; -import { LwpoCodeFormSchema, type LwpoCodeFormData } from "@/entities/code/model/lwpocode-interface"; -import { ENDPOINT } from "@/shared/config"; -import { upCodeApi } from "@/entities/code/api/upcode-service"; - -type ManagementLwpoCodeDialogProps = { - upCd?: string; - lwpoCd?: string; - open?: boolean; - onOpenChange?: (open: boolean) => void; - isRegistration?: boolean; -} & Omit, 'open' | 'onOpenChange'>; - -function ManagementLwpoCodeDialog({ - lwpoCd, - upCd, - open, - onOpenChange, - isRegistration = false, - ...props -}: ManagementLwpoCodeDialogProps) { - const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); - - const { data: detailData } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, lwpoCd], - queryFn: () => lwpoCodeApi.LwpoCodeDetail(upCd as string, lwpoCd as string), - enabled: !!upCd && !!lwpoCd && !isRegistration, - }); - - const { data: upCodeData } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd], - queryFn: () => upCodeApi.upCodeDetail(upCd as string), - enabled: !!upCd, - }); - - const { mutateAsync: saveLwpoCode } = useMutation({ - mutationFn: (data: LwpoCodeFormData) => - isRegistration ? lwpoCodeApi.createLwpoCode(data) : lwpoCodeApi.updateLwpoCode(upCd as string, lwpoCd as string, data), - onSuccess: () => { - alert(isRegistration ? "등록되었습니다." : "수정되었습니다."); - onOpenChange?.(false); - }, - }); - - const form = useForm({ - resolver: zodResolver(LwpoCodeFormSchema), - values: { - upCd: upCd ?? "", - lwpoCd: detailData?.lwpoCd ?? "", - lwpoCdNm: detailData?.lwpoCdNm ?? "", - lwpoCdExpln: detailData?.lwpoCdExpln ?? "", - useYn: detailData?.useYn ?? "Y", - delYn: "N", - unit: detailData?.unit ?? "", - prntLwpoCd: detailData?.prntLwpoCd ?? "", - sortSeq: detailData?.sortSeq ?? 0, - }, - }); - - const handleSave = async () => { - if (await confirm(isRegistration ? "등록하시겠습니까?" : "수정하시겠습니까?")) { - const data = form.getValues(); - await saveLwpoCode(data); - } - }; - - return ( - - - - {isRegistration ? "하위코드 등록" : "하위코드 관리"} - -
- { - e.preventDefault(); - handleSave(); - }} className="space-y-4"> -
- - - - - - - 하위코드 - - ( - - - - - - )} - /> - - - - 하위코드명 - - ( - - - - - - )} - /> - - - - 하위코드설명 - - ( - - - - - - )} - /> - - - {upCodeData?.cdSe === "CODECLUNIT" && ( - - 단위 - - ( - - - - - - )} - /> - - - )} - {upCodeData?.cdSe === "CODECLTREE" && ( - - 상위하위코드 - - ( - - - - - - )} - /> - - - )} - - 정렬순서 - - ( - - - - - - )} - /> - - - - 사용여부 - - ( - - - -
- - -
-
- - -
-
-
-
- )} - /> -
-
-
-
- - - - - - - - - - - ); -} - -export { ManagementLwpoCodeDialog }; diff --git a/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx b/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx new file mode 100644 index 000000000..40615c33b --- /dev/null +++ b/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx @@ -0,0 +1,321 @@ +import { memo } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Input, + RadioGroup, + RadioGroupItem, + Label, + DialogTrigger, +} from "@common/components"; +import { + UpCodeApi, + LwpoCodeApi, + LwpoCodeFormSchema, + type LwpoCodeFormData, +} from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form"; + +type ManagementLwpoCodeDialogProps = { + lwpoCd?: string; + upCd?: string; + children?: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}; + +const LwpoCodeManagementDialog = memo( + ({ + lwpoCd, + upCd, + children, + open, + onOpenChange, + ...props + }: ManagementLwpoCodeDialogProps) => { + const isRegistration = !lwpoCd; + + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + + const { data: detailData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, lwpoCd], + queryFn: () => LwpoCodeApi.lwpoCodeDetail(upCd!, lwpoCd!), + enabled: !!upCd && !isRegistration, + }); + + const { data: upCodeData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd], + queryFn: () => UpCodeApi.upCodeDetail(upCd!), + enabled: !!upCd, + }); + + const { mutateAsync: createLwpoCode } = useMutation({ + mutationFn: (data: LwpoCodeFormData) => LwpoCodeApi.createLwpoCode(data), + onSuccess: () => { + alert("등록되었습니다.", () => onOpenChange?.(false)); + }, + }); + + const { mutateAsync: updateLwpoCode } = useMutation({ + mutationFn: (data: LwpoCodeFormData) => + LwpoCodeApi.updateLwpoCode(upCd!, lwpoCd!, data), + onSuccess: () => { + alert("수정되었습니다.", () => onOpenChange?.(false)); + }, + }); + + const form = useForm({ + resolver: zodResolver(LwpoCodeFormSchema), + values: { + lwpoCd: detailData?.lwpoCd ?? "", + upCd: upCd ?? "", + lwpoCdNm: detailData?.lwpoCdNm ?? "", + lwpoCdExpln: detailData?.lwpoCdExpln ?? "", + unit: detailData?.unit ?? "", + prntLwpoCd: detailData?.prntLwpoCd ?? "", + useYn: detailData?.useYn ?? "Y", + sortSeq: detailData?.sortSeq ?? 0, + delYn: "N", + }, + }); + + const onSubmit = + (callback: (data: LwpoCodeFormData) => void) => + async (data: LwpoCodeFormData) => { + const confirmMessage = isRegistration + ? "등록하시겠습니까?" + : "수정하시겠습니까?"; + + if (await confirm(confirmMessage)) callback(data); + }; + + return ( + + {children} + + + + {isRegistration ? "하위코드 등록" : "하위코드 관리"} + + +
+ + + + + + + + + 하위코드 + + ( + + + + + + )} + /> + + + + 하위코드명 + + ( + + + + + + )} + /> + + + + 하위코드설명 + + ( + + + + + + )} + /> + + + {upCodeData?.cdSe === "CODECLUNIT" && ( + + 단위 + + ( + + + + + + )} + /> + + + )} + {upCodeData?.cdSe === "CODECLTREE" && ( + + 상위하위코드 + + ( + + + + + + )} + /> + + + )} + + 정렬순서 + + ( + + + + + + )} + /> + + + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+ + + + + + +
+ +
+
+ ); + }, + (prevProps, nextProps) => { + return ( + prevProps.lwpoCd === nextProps.lwpoCd && + prevProps.upCd === nextProps.upCd && + prevProps.open === nextProps.open + ); + }, +); + +LwpoCodeManagementDialog.displayName = "LwpoCodeManagementDialog"; + +export { LwpoCodeManagementDialog }; diff --git a/apps/cs/src/pages/code/ui/lwpocode-section.tsx b/apps/cs/src/pages/code/ui/lwpocode-section.tsx new file mode 100644 index 000000000..fd1df2d8e --- /dev/null +++ b/apps/cs/src/pages/code/ui/lwpocode-section.tsx @@ -0,0 +1,98 @@ +import { useState } from "react"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, +} from "@tanstack/react-table"; +import { Button, DataTable, Input, Label } from "@common/components"; +import { LwpoCodeApi, type LwpoCodeParams } from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/lwpocode-table-columns"; +import { LwpoCodeManagementDialog } from "./lwpocode-dialog"; + +type LwpoCodeSectionProps = { + upCd?: string; +}; + +function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) { + const [dialogOpen, setDialogOpen] = useState(false); + const [lwpoCodeQuery, setLwpoCodeQuery] = useState({ + lwpoCdNm: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: lwpoCodeList, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, pagination], + queryFn: () => + LwpoCodeApi.getLwpoCodeList({ + ...lwpoCodeQuery, + upCd, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + enabled: !!upCd, + placeholderData: keepPreviousData, + }); + + const table = useReactTable({ + data: lwpoCodeList?.content ?? [], + columns, + rowCount: lwpoCodeList?.totalElements ?? 0, + state: { pagination }, + manualPagination: true, + enableMultiRowSelection: false, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + getRowId: (row) => row.lwpoCd, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + return ( +
+
+
+ + + setLwpoCodeQuery({ + ...lwpoCodeQuery, + lwpoCdNm: e.target.value, + }) + } + /> + +
+
+
+
+

하위코드 목록

+ + + +
+
+ +
+
+
+ ); +} + +export { LwpoCodeSection }; diff --git a/apps/cs/src/pages/code/ui/upcode-dialog.tsx b/apps/cs/src/pages/code/ui/upcode-dialog.tsx index 034ba6e56..03bd2fd0b 100644 --- a/apps/cs/src/pages/code/ui/upcode-dialog.tsx +++ b/apps/cs/src/pages/code/ui/upcode-dialog.tsx @@ -1,8 +1,8 @@ +import { memo } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery } from "@tanstack/react-query"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { ENDPOINT } from "@/shared/config"; import { Button, Dialog, @@ -20,237 +20,262 @@ import { RadioGroup, RadioGroupItem, Label, + DialogTrigger, } from "@common/components"; -import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; -import { upCodeApi } from "@/entities/code/api/upcode-service"; -import { upCodeFormSchema, type UpCodeFormData } from "@/entities/code/model/upcode-interface"; -import { CodeSelect } from "@/entities/code"; +import { + CodeSelect, + UpCodeApi, + upCodeFormSchema, + type UpCodeFormData, +} from "@/entities/code"; import { SiteSelect } from "@/entities/site"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form"; -type ManagementUpCodeDialogProps = { +type UpCodeManagementDialogProps = { upCd?: string; + children?: React.ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void; - isRegistration?: boolean; -} & Omit, 'open' | 'onOpenChange'>; +}; +const UpCodeManagementDialog = memo( + ({ + upCd, + children, + open, + onOpenChange, + ...props + }: UpCodeManagementDialogProps) => { + const isRegistration = !upCd; -function ManagementUpCodeDialog({ - upCd, - open, - onOpenChange, - isRegistration = false, - ...props -}: ManagementUpCodeDialogProps) { - const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); - const { data: detailData } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd], - queryFn: () => upCodeApi.upCodeDetail(upCd as string), - enabled: !!upCd && !isRegistration, - }); + const { data: detailData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd], + queryFn: () => UpCodeApi.upCodeDetail(upCd!), + enabled: !!upCd, + }); - const { mutateAsync: saveUpCode } = useMutation({ - mutationFn: (data: UpCodeFormData) => - isRegistration ? upCodeApi.createUpCode(data) : upCodeApi.updateUpCode(upCd as string, data), - onSuccess: () => { - alert(isRegistration ? "등록되었습니다." : "수정되었습니다."); - onOpenChange?.(false); - }, - }); + const { mutateAsync: createUpCode } = useMutation({ + mutationFn: (data: UpCodeFormData) => UpCodeApi.createUpCode(data), + onSuccess: () => { + alert("등록되었습니다.", () => onOpenChange?.(false)); + }, + }); - const form = useForm({ - resolver: zodResolver(upCodeFormSchema), - values: { - upCd: detailData?.upCd ?? "", - upCdNm: detailData?.upCdNm ?? "", - cdSe: detailData?.cdSe ?? "", - siteId: detailData?.siteId ?? "", - upCdExpln: detailData?.upCdExpln ?? "", - useYn: detailData?.useYn ?? "Y", - delYn: "N", - }, - }); + const { mutateAsync: updateUpCode } = useMutation({ + mutationFn: (data: UpCodeFormData) => UpCodeApi.updateUpCode(upCd!, data), + onSuccess: () => { + alert("수정되었습니다.", () => onOpenChange?.(false)); + }, + }); - const handleSave = async () => { - if (await confirm(isRegistration ? "등록하시겠습니까?" : "수정하시겠습니까?")) { - const data = form.getValues(); - await saveUpCode(data); - } - }; + const form = useForm({ + resolver: zodResolver(upCodeFormSchema), + values: { + upCd: detailData?.upCd ?? "", + upCdNm: detailData?.upCdNm ?? "", + cdSe: detailData?.cdSe ?? "", + siteId: detailData?.siteId ?? "", + upCdExpln: detailData?.upCdExpln ?? "", + useYn: detailData?.useYn ?? "Y", + delYn: "N", + }, + }); - return ( - - - - {isRegistration ? "상위코드 등록" : "상위코드 관리"} - -
- { - e.preventDefault(); - handleSave(); - }} className="space-y-4"> - - - - - - - - 상위코드 - - ( - - - - - - )} - /> - - - - 상위코드명 - - ( - - - - - - )} - /> - - - - 상위코드설명 - - ( - - - - - - )} - /> - - - - 코드구분 - - ( - - - - - - )} - /> - - - - 사이트 - - ( - - - - - - )} - /> - - - - 사용여부 - - ( - - - -
- - -
-
- - -
-
-
-
- )} - /> -
-
-
-
- - - - - - -
- -
-
- ); -} + + + + + + + + + ); + }, + (prevProps, nextProps) => { + return ( + prevProps.upCd === nextProps.upCd && prevProps.open === nextProps.open + ); + }, +); -export { ManagementUpCodeDialog }; +UpCodeManagementDialog.displayName = "UpCodeManagementDialog"; + +export { UpCodeManagementDialog }; diff --git a/apps/cs/src/pages/code/ui/upcode-section.tsx b/apps/cs/src/pages/code/ui/upcode-section.tsx new file mode 100644 index 000000000..7e3baefc9 --- /dev/null +++ b/apps/cs/src/pages/code/ui/upcode-section.tsx @@ -0,0 +1,101 @@ +import { useState } from "react"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, +} from "@tanstack/react-table"; +import { Button, DataTable, Input, Label } from "@common/components"; +import { UpCodeApi, type UpCode, type UpCodeParams } from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/upcode-table-columns"; +import { UpCodeManagementDialog } from "./upcode-dialog"; + +type UpCodeSectionProps = { + handleSelectedUpCd: (upCd: string) => void; +}; + +function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) { + const [dialogOpen, setDialogOpen] = useState(false); + const [upCodeQuery, setUpCodeQuery] = useState({ + keyword: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: upCodeList, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, pagination], + queryFn: () => + UpCodeApi.getUpCodeList({ + ...upCodeQuery, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const table = useReactTable({ + data: upCodeList?.content ?? [], + columns, + rowCount: upCodeList?.totalElements ?? 0, + state: { pagination }, + manualPagination: true, + enableMultiRowSelection: false, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + getRowId: (row) => row.upCd, + }); + + const handleRowClick = (row: UpCode) => { + table.setRowSelection({ [row.upCd]: true }); + handleSelectedUpCd(row.upCd); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + return ( +
+
+
+ + + setUpCodeQuery({ + ...upCodeQuery, + keyword: e.target.value, + }) + } + /> + +
+
+
+
+

상위코드 목록

+ + + +
+
+ +
+
+
+ ); +} + +export { UpCodeSection }; diff --git a/apps/cs/src/pages/module/ui/management-authority-dialog.tsx b/apps/cs/src/pages/module/ui/management-authority-dialog.tsx index dae061340..737b12121 100644 --- a/apps/cs/src/pages/module/ui/management-authority-dialog.tsx +++ b/apps/cs/src/pages/module/ui/management-authority-dialog.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, memo } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery } from "@tanstack/react-query"; @@ -41,110 +41,117 @@ type ManagementAuthorityDialogProps = { rowData?: Authorizations; } & React.ComponentProps; -function ManagementAuthorityDialog({ - authorizationNo, - rowData, - ...props -}: ManagementAuthorityDialogProps) { - const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); +const ManagementAuthorityDialog = memo( + ({ authorizationNo, rowData, ...props }: ManagementAuthorityDialogProps) => { + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); - const [isOpened, setIsOpened] = useState(false); - const [rowSelection, setRowSelection] = useState({}); + const [isOpened, setIsOpened] = useState(false); + const [rowSelection, setRowSelection] = useState({}); - const { data: roleAuthorizationList } = useQuery({ - queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, authorizationNo], - queryFn: () => - RoleAuthorizationsApi.getRoleAuthorizationsList({ authorizationNo }), - enabled: !!authorizationNo && isOpened, - }); + const { data: roleAuthorizationList } = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, authorizationNo], + queryFn: () => + RoleAuthorizationsApi.getRoleAuthorizationsList({ authorizationNo }), + enabled: !!authorizationNo && isOpened, + }); - const { mutateAsync: saveRoleAuthorization } = useMutation({ - mutationFn: (data: RoleAuthorizationsData) => - RoleAuthorizationsApi.createRoleAuthorizations(data), - onSuccess: () => { - alert("저장되었습니다."); - }, - }); + const { mutateAsync: saveRoleAuthorization } = useMutation({ + mutationFn: (data: RoleAuthorizationsData) => + RoleAuthorizationsApi.createRoleAuthorizations(data), + onSuccess: () => { + alert("저장되었습니다."); + }, + }); - const form = useForm({ - resolver: zodResolver(roleAuthorizationsFormSchema), - values: { - roleAuthorizations: - roleAuthorizationList?.content.map( - ({ roleId, authorizationNo, useYn }) => ({ - roleId, - authorizationNo, - authYn: useYn, - }), - ) ?? [], - }, - }); + const form = useForm({ + resolver: zodResolver(roleAuthorizationsFormSchema), + values: { + roleAuthorizations: + roleAuthorizationList?.content.map( + ({ roleId, authorizationNo, useYn }) => ({ + roleId, + authorizationNo, + authYn: useYn, + }), + ) ?? [], + }, + }); - const table = useReactTable({ - data: roleAuthorizationList?.content ?? [], - columns: createColumns(form), - state: { - rowSelection, - }, - getCoreRowModel: getCoreRowModel(), - onRowSelectionChange: setRowSelection, - }); + const table = useReactTable({ + data: roleAuthorizationList?.content ?? [], + columns: createColumns(form), + state: { + rowSelection, + }, + getCoreRowModel: getCoreRowModel(), + onRowSelectionChange: setRowSelection, + }); - const handleSave = async () => { - if (await confirm("저장하시겠습니까?")) { - const data = form.getValues("roleAuthorizations"); - await saveRoleAuthorization(data); - } - }; + const handleSave = async () => { + if (await confirm("저장하시겠습니까?")) { + const data = form.getValues("roleAuthorizations"); + await saveRoleAuthorization(data); + } + }; - return ( - - - - - - - 모듈API 권한관리 - -
- - - - API명 - 요청 메소드 - API URL - - - - - - {rowData?.authorizationName} - - {rowData?.httpMethodCode} - {rowData?.urlPatternValue} - - -
-
- -
-
- - - - - - -
-
- ); -} + + + + + + + ); + }, + (prevProps, nextProps) => { + return prevProps.authorizationNo === nextProps.authorizationNo; + }, +); + +ManagementAuthorityDialog.displayName = "ManagementAuthorityDialog"; export { ManagementAuthorityDialog }; diff --git a/packages/common/assets/src/styles/base.css b/packages/common/assets/src/styles/base.css index 7fc103b43..006382076 100644 --- a/packages/common/assets/src/styles/base.css +++ b/packages/common/assets/src/styles/base.css @@ -12,6 +12,7 @@ --popover-foreground: #0a0a0a; --primary: #2094fa; --primary-foreground: #fafafa; + --primary-transparent: rgba(32, 148, 250, 0.1); --secondary: #f5f5f5; --secondary-foreground: #0f1e33; --muted: #f5f5f5; @@ -54,6 +55,7 @@ --popover-foreground: #fafafa; --primary: #2094fa; --primary-foreground: #171717; + --primary-transparent: rgba(32, 148, 250, 0.1); --secondary: #262626; --secondary-foreground: #fafafa; --muted: #262626; diff --git a/packages/common/components/src/ui/data-table/index.tsx b/packages/common/components/src/ui/data-table/index.tsx index 2b9879879..82492cd66 100644 --- a/packages/common/components/src/ui/data-table/index.tsx +++ b/packages/common/components/src/ui/data-table/index.tsx @@ -42,22 +42,31 @@ function DataTable({ - {table.getRowModel().rows.map((row) => ( - onRowClick?.(row.original)} - className={cn(onRowClick && "cursor-pointer hover:bg-accent")} - > - {row.getVisibleCells().map((cell) => { - const { meta } = cell.column.columnDef; - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ); - })} - - ))} + {table.getRowModel().rows.map((row) => { + const isSelectedRow = row.getIsSelected(); + return ( + onRowClick?.(row.original)} + className={cn( + onRowClick && "cursor-pointer hover:bg-accent", + isSelectedRow && "!bg-primary-transparent", + )} + > + {row.getVisibleCells().map((cell) => { + const { meta } = cell.column.columnDef; + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ); + })} + + ); + })}
{viewPagination && } diff --git a/tailwind.config.preset.ts b/tailwind.config.preset.ts index 5e59ebb5b..4637a88a0 100644 --- a/tailwind.config.preset.ts +++ b/tailwind.config.preset.ts @@ -21,6 +21,7 @@ export default { primary: { DEFAULT: "var(--primary)", foreground: "var(--primary-foreground)", + transparent: "var(--primary-transparent)", }, secondary: { DEFAULT: "var(--secondary)", From 566b75bc9f851b035072b121c12ee82b75e85630 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 21 May 2025 16:51:09 +0900 Subject: [PATCH 11/64] =?UTF-8?q?feat:=20=ED=8C=9D=EC=97=85=20=EB=AF=B8?= =?UTF-8?q?=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/popup/api/popup-service.ts | 11 +-- .../src/pages/popup/model/preview-template.ts | 72 +++++++++++++++++++ .../src/pages/popup/model/table-columns.tsx | 51 ++++++++++++- 3 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 apps/cs/src/pages/popup/model/preview-template.ts diff --git a/apps/cs/src/pages/popup/api/popup-service.ts b/apps/cs/src/pages/popup/api/popup-service.ts index d32c7c0cc..b82d5f473 100644 --- a/apps/cs/src/pages/popup/api/popup-service.ts +++ b/apps/cs/src/pages/popup/api/popup-service.ts @@ -12,26 +12,29 @@ import { import { client } from "@/shared/api"; export class PopupApiService implements PopupService { - async getPopupList(params: PopupParams): Promise { + public async getPopupList(params: PopupParams): Promise { const response = await client.get(ENDPOINT.CMS_SERVICE.POPUPS, { params, }); return response.data; } - async getPopupInfo(popupSn: number): Promise { + public async getPopupInfo(popupSn: number): Promise { const response = await client.get( `${ENDPOINT.CMS_SERVICE.POPUPS}/${popupSn}`, ); return response.data; } - async createPopup(data: PopupFormData): Promise { + public async createPopup(data: PopupFormData): Promise { const validatedData = popupFormSchema.parse(data); await client.post(ENDPOINT.CMS_SERVICE.POPUPS, validatedData); } - async updatePopup(popupSn: number, data: PopupUpdateFormData): Promise { + public async updatePopup( + popupSn: number, + data: PopupUpdateFormData, + ): Promise { const validatedData = popupUpdateFormSchema.parse(data); await client.put( `${ENDPOINT.CMS_SERVICE.POPUPS}/${popupSn}`, diff --git a/apps/cs/src/pages/popup/model/preview-template.ts b/apps/cs/src/pages/popup/model/preview-template.ts new file mode 100644 index 000000000..79bfd8b31 --- /dev/null +++ b/apps/cs/src/pages/popup/model/preview-template.ts @@ -0,0 +1,72 @@ +export const previewTemplate = ( + popupSn: number, + popupTitle: string, + popupCn: string, +) => { + return ` + + + + + 팝업 미리보기 + + + + + + + + +`; +}; diff --git a/apps/cs/src/pages/popup/model/table-columns.tsx b/apps/cs/src/pages/popup/model/table-columns.tsx index b223f689b..09c291c84 100644 --- a/apps/cs/src/pages/popup/model/table-columns.tsx +++ b/apps/cs/src/pages/popup/model/table-columns.tsx @@ -1,7 +1,9 @@ import dayjs from "dayjs"; import { type ColumnDef } from "@tanstack/react-table"; +import { Button, Checkbox } from "@common/components"; +import { PopupApi } from "../api/popup-service"; import type { Popup } from "./popup-interface"; -import { Checkbox } from "@common/components"; +import { previewTemplate } from "./preview-template"; export const columns: ColumnDef[] = [ { @@ -62,4 +64,51 @@ export const columns: ColumnDef[] = [ accessorKey: "mdfrNm", header: "최종수정자", }, + { + id: "preview", + header: "미리보기", + cell: ({ row }) => , + }, ]; + +const PreviewButton = ({ popupSn }: { popupSn: number }) => { + const handlePreview = async (e: React.MouseEvent) => { + e.stopPropagation(); + + try { + const cookieName = `cs-popup-hide-for-day-${popupSn}`; + const cookies = document.cookie.split(";"); + const popupCookie = cookies.find((cookie) => + cookie.trim().startsWith(`${cookieName}=`), + ); + if (popupCookie) return; + + const response = await PopupApi.getPopupInfo(popupSn); + if (!response) return; + + const width = response.popupWdth; + const height = response.popupHgt; + const left = response.popupHrzntltyPstn + window.screenX; + const top = response.popupVtclPstn + window.screenY; + const popupWindow = window.open( + "", + `popup_preview_${popupSn}`, + `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,status=no,location=no,menubar=no`, + ); + if (!popupWindow) return; + + popupWindow.document.write( + previewTemplate(popupSn, response.popupTtl, response.popupCn), + ); + popupWindow.document.close(); + } catch (error) { + console.error(error); + } + }; + + return ( + + ); +}; From 8467b0a4e30574bf5d78694321c9de40bfecdb6c Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 21 May 2025 18:27:54 +0900 Subject: [PATCH 12/64] =?UTF-8?q?WIP:=20common/network=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/provider/popup-provider.tsx | 5 +++ .../common/components/src/ui/layout/base.tsx | 32 +++++++++++-------- .../common/config/src/initialize-config.ts | 1 + packages/common/config/src/initializer.ts | 19 +++++++++++ packages/common/network/client.ts | 1 + 5 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 packages/common/components/src/provider/popup-provider.tsx diff --git a/packages/common/components/src/provider/popup-provider.tsx b/packages/common/components/src/provider/popup-provider.tsx new file mode 100644 index 000000000..f6ef68ca1 --- /dev/null +++ b/packages/common/components/src/provider/popup-provider.tsx @@ -0,0 +1,5 @@ +function PopupProvider({ children }: { children: React.ReactNode }) { + return <>{children}; +} + +export { PopupProvider }; diff --git a/packages/common/components/src/ui/layout/base.tsx b/packages/common/components/src/ui/layout/base.tsx index 21d2ca7c6..f5db1c50a 100644 --- a/packages/common/components/src/ui/layout/base.tsx +++ b/packages/common/components/src/ui/layout/base.tsx @@ -1,10 +1,11 @@ import type { QueryClient } from "@tanstack/react-query"; -import type { MenuItem } from "../../api"; import { QueryProvider } from "../../provider/query-provider"; +import { PopupProvider } from "../../provider/popup-provider"; +import type { MenuItem } from "../../api"; +import { SidebarProvider } from "../sidebar"; import { LNB } from "../lnb"; import { Header } from "../header"; import { Footer } from "./footer"; -import { SidebarProvider } from "../sidebar"; import "./common.css"; export function BaseLayout({ @@ -20,20 +21,25 @@ export function BaseLayout({ }) { return ( - -
-
-
- -
-
- {children} + + +
+
+
+ +
+
+ {children} +
+
-
-
- + + ); } diff --git a/packages/common/config/src/initialize-config.ts b/packages/common/config/src/initialize-config.ts index e25010321..5076e65c9 100644 --- a/packages/common/config/src/initialize-config.ts +++ b/packages/common/config/src/initialize-config.ts @@ -24,6 +24,7 @@ let isInitialized = false; let envConfig: EnvConfig = { ...defaultConfig }; export function initializeConfig(values: Partial): void { + console.log("initializeConfig >>>> values: ", values); if (isInitialized) { console.warn("Configuration already initialized. Skipping."); return; diff --git a/packages/common/config/src/initializer.ts b/packages/common/config/src/initializer.ts index 896225d88..1e814e8bb 100644 --- a/packages/common/config/src/initializer.ts +++ b/packages/common/config/src/initializer.ts @@ -18,3 +18,22 @@ export function initializeNextClientConfig(): void { }; initializeConfig(env); } + +export function initializeViteClientConfig(): void { + if (typeof window === "undefined") { + console.warn("initializeViteClientConfig called on the server. Skipping."); + return; + } + + const env: Partial = { + PORT: import.meta.env.VITE_PORT, + PROXY_HOST: import.meta.env.VITE_PROXY_HOST, + SERVER_API_URL: import.meta.env.VITE_SERVER_API_URL, + SITE_ID: import.meta.env.VITE_SITE_ID, + CLAIM_NAME: import.meta.env.VITE_CLAIM_NAME, + ACCESS_TOKEN: import.meta.env.VITE_ACCESS_TOKEN, + REFRESH_TOKEN: import.meta.env.VITE_REFRESH_TOKEN, + AUTH_USER_ID: import.meta.env.VITE_AUTH_USER_ID, + }; + initializeConfig(env); +} diff --git a/packages/common/network/client.ts b/packages/common/network/client.ts index 1b82d31c6..6f9ee3bd2 100644 --- a/packages/common/network/client.ts +++ b/packages/common/network/client.ts @@ -51,6 +51,7 @@ class Client { public static getInstance(config: ClientConfig): Client { if (!Client.instance) { + console.log("getInstance >>>> config: ", config); Client.instance = new Client(config); } return Client.instance; From a27e9f1cdd3a33f20dd601184841204a6115d293 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 23 May 2025 23:41:30 +0900 Subject: [PATCH 13/64] =?UTF-8?q?refactor:=20getConfig=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/app/providers/app-provider.tsx | 4 -- package.json | 1 + packages/common/config/src/index.ts | 1 - .../common/config/src/initialize-config.ts | 56 ++++++++++++------- packages/common/config/src/initializer.ts | 39 ------------- packages/common/network/client.ts | 1 - 6 files changed, 37 insertions(+), 65 deletions(-) delete mode 100644 packages/common/config/src/initializer.ts diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index 7732d34ba..87c0ec616 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -10,13 +10,9 @@ import { PromptProvider, StoreProvider } from "@/shared/providers"; import { AlertDialog } from "@/shared/ui/alert-dialog"; import { PromptDialog } from "@/shared/ui/prompt-dialog"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; -import { initializeNextClientConfig } from "@common/config"; const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { const queryClient = new QueryClient(); - - initializeNextClientConfig(); - return ( diff --git a/package.json b/package.json index 159f30a50..5f8057c3e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ ], "scripts": { "build": "turbo run build --filter=!wr", + "build:common": "turbo run build --filter=@common/*", "dev": "turbo run dev", "dev:cs": "turbo run dev --filter=cs", "dev:wr": "turbo run dev --filter=wr", diff --git a/packages/common/config/src/index.ts b/packages/common/config/src/index.ts index dd33ecdef..94b333c08 100644 --- a/packages/common/config/src/index.ts +++ b/packages/common/config/src/index.ts @@ -1,2 +1 @@ export * from "./initialize-config"; -export * from "./initializer"; diff --git a/packages/common/config/src/initialize-config.ts b/packages/common/config/src/initialize-config.ts index 5076e65c9..f1d3178f5 100644 --- a/packages/common/config/src/initialize-config.ts +++ b/packages/common/config/src/initialize-config.ts @@ -13,23 +13,14 @@ const defaultConfig: EnvConfig = { PORT: "3000", PROXY_HOST: "", SERVER_API_URL: "", - SITE_ID: "CS", + SITE_ID: "", CLAIM_NAME: "Authorization", ACCESS_TOKEN: "access-token", REFRESH_TOKEN: "refresh-token", AUTH_USER_ID: "token-id", }; -let isInitialized = false; -let envConfig: EnvConfig = { ...defaultConfig }; - -export function initializeConfig(values: Partial): void { - console.log("initializeConfig >>>> values: ", values); - if (isInitialized) { - console.warn("Configuration already initialized. Skipping."); - return; - } - +export function generateEnvConfig(values: Partial): EnvConfig { const filteredValues = Object.entries(values).reduce((acc, [key, value]) => { if (value !== undefined && value !== null) { acc[key as keyof EnvConfig] = value; @@ -37,19 +28,44 @@ export function initializeConfig(values: Partial): void { return acc; }, {} as Partial); - if (filteredValues.PORT && !filteredValues.PROXY_HOST) { - filteredValues.PROXY_HOST = `http://localhost:${filteredValues.PORT}`; + if (!filteredValues.PROXY_HOST) { + filteredValues.PROXY_HOST = `http://localhost:${filteredValues.PORT || defaultConfig.PORT}`; } - envConfig = { ...defaultConfig, ...filteredValues }; - isInitialized = true; + return { ...defaultConfig, ...filteredValues }; +} + +export function getNextConfig(): EnvConfig { + const env: Partial = { + PORT: process.env.NEXT_PUBLIC_PORT, + PROXY_HOST: process.env.NEXT_PUBLIC_PROXY_HOST, + SERVER_API_URL: process.env.NEXT_PUBLIC_SERVER_API_URL, + SITE_ID: process.env.NEXT_PUBLIC_SITE_ID, + CLAIM_NAME: process.env.NEXT_PUBLIC_CLAIM_NAME, + ACCESS_TOKEN: process.env.NEXT_PUBLIC_ACCESS_TOKEN, + REFRESH_TOKEN: process.env.NEXT_PUBLIC_REFRESH_TOKEN, + AUTH_USER_ID: process.env.NEXT_PUBLIC_AUTH_USER_ID, + }; + return generateEnvConfig(env); +} + +export function getViteConfig(): EnvConfig { + const env: Partial = { + PORT: import.meta.env.VITE_PORT, + PROXY_HOST: import.meta.env.VITE_PROXY_HOST, + SERVER_API_URL: import.meta.env.VITE_SERVER_API_URL, + SITE_ID: import.meta.env.VITE_SITE_ID, + CLAIM_NAME: import.meta.env.VITE_CLAIM_NAME, + ACCESS_TOKEN: import.meta.env.VITE_ACCESS_TOKEN, + REFRESH_TOKEN: import.meta.env.VITE_REFRESH_TOKEN, + AUTH_USER_ID: import.meta.env.VITE_AUTH_USER_ID, + }; + return generateEnvConfig(env); } export function getConfig(): EnvConfig { - if (!isInitialized) { - console.warn( - "Attempted to access config before initialization. Returning default values." - ); + if (typeof import.meta?.env !== "undefined") { + return getViteConfig(); } - return envConfig; + return getNextConfig(); } diff --git a/packages/common/config/src/initializer.ts b/packages/common/config/src/initializer.ts deleted file mode 100644 index 1e814e8bb..000000000 --- a/packages/common/config/src/initializer.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { initializeConfig, type EnvConfig } from "./initialize-config"; - -export function initializeNextClientConfig(): void { - if (typeof window === "undefined") { - console.warn("initializeNextClientConfig called on the server. Skipping."); - return; - } - - const env: Partial = { - PORT: process.env.NEXT_PUBLIC_PORT, - PROXY_HOST: process.env.NEXT_PUBLIC_PROXY_HOST, - SERVER_API_URL: process.env.NEXT_PUBLIC_SERVER_API_URL, - SITE_ID: process.env.NEXT_PUBLIC_SITE_ID, - CLAIM_NAME: process.env.NEXT_PUBLIC_CLAIM_NAME, - ACCESS_TOKEN: process.env.NEXT_PUBLIC_ACCESS_TOKEN, - REFRESH_TOKEN: process.env.NEXT_PUBLIC_REFRESH_TOKEN, - AUTH_USER_ID: process.env.NEXT_PUBLIC_AUTH_USER_ID, - }; - initializeConfig(env); -} - -export function initializeViteClientConfig(): void { - if (typeof window === "undefined") { - console.warn("initializeViteClientConfig called on the server. Skipping."); - return; - } - - const env: Partial = { - PORT: import.meta.env.VITE_PORT, - PROXY_HOST: import.meta.env.VITE_PROXY_HOST, - SERVER_API_URL: import.meta.env.VITE_SERVER_API_URL, - SITE_ID: import.meta.env.VITE_SITE_ID, - CLAIM_NAME: import.meta.env.VITE_CLAIM_NAME, - ACCESS_TOKEN: import.meta.env.VITE_ACCESS_TOKEN, - REFRESH_TOKEN: import.meta.env.VITE_REFRESH_TOKEN, - AUTH_USER_ID: import.meta.env.VITE_AUTH_USER_ID, - }; - initializeConfig(env); -} diff --git a/packages/common/network/client.ts b/packages/common/network/client.ts index 6f9ee3bd2..1b82d31c6 100644 --- a/packages/common/network/client.ts +++ b/packages/common/network/client.ts @@ -51,7 +51,6 @@ class Client { public static getInstance(config: ClientConfig): Client { if (!Client.instance) { - console.log("getInstance >>>> config: ", config); Client.instance = new Client(config); } return Client.instance; From 6a7218b8ce5a39f5db3ad1973ccb2983c2fb8865 Mon Sep 17 00:00:00 2001 From: bicochan Date: Sat, 24 May 2025 00:47:27 +0900 Subject: [PATCH 14/64] =?UTF-8?q?feat:=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=ED=8C=9D=EC=97=85=20header=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/features/package.json | 2 +- .../src/app/providers/app-provider.tsx | 4 - apps/cs/package.json | 2 +- .../src/pages/popup/model/table-columns.tsx | 7 - packages/common/assets/package.json | 2 +- packages/common/components/package.json | 2 +- packages/common/components/src/api/index.ts | 1 + packages/common/components/src/api/popup.ts | 69 ++++++++++ packages/common/components/src/config/api.ts | 1 + .../common/components/src/ui/header/index.tsx | 128 +++++++++++++++++- packages/common/config/package.json | 2 +- packages/common/network/package.json | 2 +- 12 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 packages/common/components/src/api/popup.ts diff --git a/apps/common/features/package.json b/apps/common/features/package.json index 8ec6639bf..0bd642b8e 100644 --- a/apps/common/features/package.json +++ b/apps/common/features/package.json @@ -1,6 +1,6 @@ { "name": "features", - "version": "0.0.0", + "version": "0.1.0", "private": true, "scripts": { "dev": "next dev -p 13000", diff --git a/apps/common/features/src/app/providers/app-provider.tsx b/apps/common/features/src/app/providers/app-provider.tsx index b82fc1d71..721f04bb6 100644 --- a/apps/common/features/src/app/providers/app-provider.tsx +++ b/apps/common/features/src/app/providers/app-provider.tsx @@ -9,13 +9,9 @@ import { StoreProvider, PromptProvider } from "@/shared/providers"; import { AlertDialog } from "@/shared/ui/alert-dialog"; import { PromptDialog } from "@/shared/ui/prompt-dialog"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; -import { initializeNextClientConfig } from "@common/config"; const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { const queryClient = new QueryClient(); - - initializeNextClientConfig(); - return ( diff --git a/apps/cs/package.json b/apps/cs/package.json index f699334d3..dce55ccd3 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -1,6 +1,6 @@ { "name": "cs", - "version": "0.0.0", + "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", diff --git a/apps/cs/src/pages/popup/model/table-columns.tsx b/apps/cs/src/pages/popup/model/table-columns.tsx index 09c291c84..04920f201 100644 --- a/apps/cs/src/pages/popup/model/table-columns.tsx +++ b/apps/cs/src/pages/popup/model/table-columns.tsx @@ -76,13 +76,6 @@ const PreviewButton = ({ popupSn }: { popupSn: number }) => { e.stopPropagation(); try { - const cookieName = `cs-popup-hide-for-day-${popupSn}`; - const cookies = document.cookie.split(";"); - const popupCookie = cookies.find((cookie) => - cookie.trim().startsWith(`${cookieName}=`), - ); - if (popupCookie) return; - const response = await PopupApi.getPopupInfo(popupSn); if (!response) return; diff --git a/packages/common/assets/package.json b/packages/common/assets/package.json index 15ae2fb43..71d71cf53 100644 --- a/packages/common/assets/package.json +++ b/packages/common/assets/package.json @@ -1,6 +1,6 @@ { "name": "@common/assets", - "version": "0.0.0", + "version": "1.0.0", "private": true, "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/packages/common/components/package.json b/packages/common/components/package.json index ca166abc7..5df2501d4 100644 --- a/packages/common/components/package.json +++ b/packages/common/components/package.json @@ -1,6 +1,6 @@ { "name": "@common/components", - "version": "0.0.0", + "version": "1.0.0", "private": true, "sideEffects": false, "type": "module", diff --git a/packages/common/components/src/api/index.ts b/packages/common/components/src/api/index.ts index fd0c1fc18..10d68732c 100644 --- a/packages/common/components/src/api/index.ts +++ b/packages/common/components/src/api/index.ts @@ -1,4 +1,5 @@ export * from "./client"; export * from "./menu"; +export * from "./popup"; export * from "./site"; export * from "./user"; diff --git a/packages/common/components/src/api/popup.ts b/packages/common/components/src/api/popup.ts new file mode 100644 index 000000000..85c5ac312 --- /dev/null +++ b/packages/common/components/src/api/popup.ts @@ -0,0 +1,69 @@ +import { getApiClient } from "./client"; +import { ENDPOINT } from "../config"; + +export interface PopupInfo { + popupSn: number; + popupTypeCd: string; + popupTypeNm: string; + contsTypeCd: string; + contsTypeNm: string; + popupTtl: string; + popupCn: string; + popupWdth: number; + popupHgt: number; + popupVtclPstn: number; + popupHrzntltyPstn: number; + popupEndUseYn: string; + popupLnkgUrl: string; + atflCd: string; + useYn: string; + regDt: string; + mdfcnDt: string; + rgTrNm: string; + mdfrNm: string; +} + +interface SortConfig { + empty: boolean; + sorted: boolean; + unsorted: boolean; +} + +interface PageableConfig { + sort: SortConfig; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; +} + +interface PageableList { + content: T[]; + pageable: PageableConfig; + sort: SortConfig; + totalPages: number; + totalElements: number; + number: number; + numberOfElements: number; + size: number; + first: boolean; + last: boolean; + empty: boolean; +} + +const client = getApiClient(); + +export const getPopupList = async () => { + const response = await client.get>( + `${ENDPOINT.CMS_SERVICE.POPUPS}`, + ); + return response.data; +}; + +export const getPopupInfo = async (popupSn: number) => { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.POPUPS}/${popupSn}`, + ); + return response.data; +}; diff --git a/packages/common/components/src/config/api.ts b/packages/common/components/src/config/api.ts index 07428479b..2b2612ffb 100644 --- a/packages/common/components/src/config/api.ts +++ b/packages/common/components/src/config/api.ts @@ -2,6 +2,7 @@ export const ENDPOINT = { CMS_SERVICE: { MENU_ROLES: "/cms-service/api/v1/menu-roles", SITES: "/cms-service/api/v1/sites", + POPUPS: "/cms-service/api/v1/popups", }, USER_SERVICE: { USERS: "/user-service/api/v1/users", diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index 285fad049..db1115b18 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -1,9 +1,15 @@ +import { useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import { images } from "@common/assets"; import { getConfig } from "@common/config"; import { ENDPOINT } from "../../config"; import { setCookie } from "../../lib"; -import { getSiteInfo, getUserInfo } from "../../api"; +import { + getPopupInfo, + getPopupList, + getSiteInfo, + getUserInfo, +} from "../../api"; import { DropdownMenu, DropdownMenuTrigger, @@ -35,6 +41,53 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { enabled: !viewOnly, }); + const { data: popupList } = useQuery({ + queryKey: ["ENDPOINT.CMS_SERVICE.POPUPS"], + queryFn: () => getPopupList(), + enabled: false, + }); + + useEffect(() => { + if (!popupList?.content.length) return; + + const openPopups = async () => { + for (const popup of popupList.content) { + try { + const cookieName = `cs-popup-hide-for-day-${popup.popupSn}`; + const cookies = document.cookie.split(";"); + const popupCookie = cookies.find((cookie) => + cookie.trim().startsWith(`${cookieName}=`), + ); + if (popupCookie) continue; + + const response = await getPopupInfo(popup.popupSn); + if (!response) continue; + + const width = response.popupWdth; + const height = response.popupHgt; + const left = response.popupHrzntltyPstn + window.screenX; + const top = response.popupVtclPstn + window.screenY; + const popupWindow = window.open( + "", + `popup_preview_${popup.popupSn}`, + `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,status=no,location=no,menubar=no`, + ); + if (!popupWindow) continue; + + popupWindow.document.write( + previewTemplate(popup.popupSn, response.popupTtl, response.popupCn), + ); + popupWindow.document.close(); + } catch (error) { + console.error("Failed to open popup:", error); + continue; + } + } + }; + + openPopups(); + }, [popupList]); + return (
@@ -72,3 +125,76 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
); } + +const previewTemplate = ( + popupSn: number, + popupTitle: string, + popupCn: string, +) => { + return ` + + + + + 팝업 미리보기 + + + + + + + + +`; +}; diff --git a/packages/common/config/package.json b/packages/common/config/package.json index 5f71fca42..fac19c4e0 100644 --- a/packages/common/config/package.json +++ b/packages/common/config/package.json @@ -1,6 +1,6 @@ { "name": "@common/config", - "version": "0.1.0", + "version": "1.0.0", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/common/network/package.json b/packages/common/network/package.json index 23659bac7..b59676eab 100644 --- a/packages/common/network/package.json +++ b/packages/common/network/package.json @@ -1,6 +1,6 @@ { "name": "@common/network", - "version": "0.0.0", + "version": "1.0.0", "private": true, "main": "./index.ts", "types": "./index.ts", From d351b8e24ebfd210fadc486bc2a6cfd11ab79c3d Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 26 May 2025 14:59:48 +0900 Subject: [PATCH 15/64] =?UTF-8?q?chore:=20=ED=8C=9D=EC=97=85=20=EB=AF=B8?= =?UTF-8?q?=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=ED=8E=98=EC=B9=98=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/components/src/ui/header/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index db1115b18..56e20191e 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -42,9 +42,8 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { }); const { data: popupList } = useQuery({ - queryKey: ["ENDPOINT.CMS_SERVICE.POPUPS"], + queryKey: [ENDPOINT.CMS_SERVICE.POPUPS], queryFn: () => getPopupList(), - enabled: false, }); useEffect(() => { From a9887eaac6f2ec6b882f62607cde8ddc9eac05e9 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 27 May 2025 13:23:56 +0900 Subject: [PATCH 16/64] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(with-base-layout)/schedule/page.tsx | 2 + .../schedule/register/page.tsx | 2 + apps/cs/package.json | 5 + .../pages/schedule/api/schedule-service.ts | 24 ++ apps/cs/src/pages/schedule/index.ts | 2 + .../schedule/model/schedule-interface.ts | 55 +++ apps/cs/src/pages/schedule/ui/create-page.tsx | 66 ++++ .../pages/schedule/ui/date-time-picker.tsx | 123 +++++++ .../src/pages/schedule/ui/schedule-form.tsx | 331 ++++++++++++++++++ .../src/pages/schedule/ui/schedule-page.tsx | 63 ++++ apps/cs/src/shared/config/api.ts | 1 + packages/common/components/package.json | 1 + .../components/src/ui/date-picker/index.tsx | 15 +- packages/common/components/src/ui/index.ts | 1 + .../components/src/ui/scroll-area/index.tsx | 48 +++ pnpm-lock.yaml | 56 +++ 16 files changed, 792 insertions(+), 3 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/schedule/page.tsx create mode 100644 apps/cs/app/(with-base-layout)/schedule/register/page.tsx create mode 100644 apps/cs/src/pages/schedule/api/schedule-service.ts create mode 100644 apps/cs/src/pages/schedule/index.ts create mode 100644 apps/cs/src/pages/schedule/model/schedule-interface.ts create mode 100644 apps/cs/src/pages/schedule/ui/create-page.tsx create mode 100644 apps/cs/src/pages/schedule/ui/date-time-picker.tsx create mode 100644 apps/cs/src/pages/schedule/ui/schedule-form.tsx create mode 100644 apps/cs/src/pages/schedule/ui/schedule-page.tsx create mode 100644 packages/common/components/src/ui/scroll-area/index.tsx diff --git a/apps/cs/app/(with-base-layout)/schedule/page.tsx b/apps/cs/app/(with-base-layout)/schedule/page.tsx new file mode 100644 index 000000000..ec64bf14c --- /dev/null +++ b/apps/cs/app/(with-base-layout)/schedule/page.tsx @@ -0,0 +1,2 @@ +import { SchedulePage } from "@/pages/schedule"; +export default SchedulePage; diff --git a/apps/cs/app/(with-base-layout)/schedule/register/page.tsx b/apps/cs/app/(with-base-layout)/schedule/register/page.tsx new file mode 100644 index 000000000..7013caac7 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/schedule/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/schedule"; +export default CreatePage; diff --git a/apps/cs/package.json b/apps/cs/package.json index dce55ccd3..ee45de4a7 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -14,6 +14,10 @@ "@common/config": "workspace:*", "@fontsource-variable/noto-sans-kr": "^5.2.5", "@frontend-opensource/use-react-hooks": "^1.1.4", + "@fullcalendar/core": "^6.1.17", + "@fullcalendar/daygrid": "^6.1.17", + "@fullcalendar/interaction": "^6.1.17", + "@fullcalendar/react": "^6.1.17", "@hookform/resolvers": "^4.1.3", "@minoru/react-dnd-treeview": "3.5.1-alpha.1", "@radix-ui/react-icons": "^1.3.2", @@ -26,6 +30,7 @@ "chart.js": "^4.4.8", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", + "lucide-react": "^0.474.0", "next": "15.3.0", "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", diff --git a/apps/cs/src/pages/schedule/api/schedule-service.ts b/apps/cs/src/pages/schedule/api/schedule-service.ts new file mode 100644 index 000000000..e69878daf --- /dev/null +++ b/apps/cs/src/pages/schedule/api/schedule-service.ts @@ -0,0 +1,24 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + Schedule, + ScheduleFormData, + ScheduleParams, + ScheduleService, +} from "../model/schedule-interface"; + +class ScheduleApiService implements ScheduleService { + public async getSchedules(params?: ScheduleParams): Promise { + const response = await client.get( + ENDPOINT.CMS_SERVICE.SCHEDULES, + { params }, + ); + return response.data; + } + + public async createSchedule(data: ScheduleFormData): Promise { + await client.post(ENDPOINT.CMS_SERVICE.SCHEDULES, data); + } +} + +export const ScheduleApi = new ScheduleApiService(); diff --git a/apps/cs/src/pages/schedule/index.ts b/apps/cs/src/pages/schedule/index.ts new file mode 100644 index 000000000..7495f12e3 --- /dev/null +++ b/apps/cs/src/pages/schedule/index.ts @@ -0,0 +1,2 @@ +export { SchedulePage } from "./ui/schedule-page"; +export { CreatePage } from "./ui/create-page"; diff --git a/apps/cs/src/pages/schedule/model/schedule-interface.ts b/apps/cs/src/pages/schedule/model/schedule-interface.ts new file mode 100644 index 000000000..8064d9f87 --- /dev/null +++ b/apps/cs/src/pages/schedule/model/schedule-interface.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; + +export interface ScheduleParams { + siteId?: string; + schdlTtl?: string; + schdlSeCd?: string; + schdlBgngYmd?: string; + schdlEndYmd?: string; + reptYn?: string; + commSchdlYn?: string; + useYn?: string; +} + +export interface Schedule { + schdlSn: number; + siteId: string; + schdlTtl: string; + schdlSeCd: string; + schdlSeNm: string; + schdlBgngDt: string; + schdlEndDt: string; + reptYn: string; + commSchdlYn: string; +} + +const scheduleRepeatSchema = z.object({ + schdlSn: z.number().nullish(), + reptCycleCd: z.string().nullish(), + reptDayVl: z.number().nullish(), + reptDowVl: z.number().nullish(), + reptMmVl: z.number().nullish(), + reptQuVl: z.number().nullish(), + reptEndDt: z.string().nullish(), +}); + +export const scheduleFormSchema = z.object({ + siteId: z.string(), + schdlTtl: z.string(), + schdlSeCd: z.string(), + schdlBgngDt: z.string(), + schdlEndDt: z.string(), + schdlCn: z.string().nullish(), + reptYn: z.string().nullish(), + commSchdlYn: z.string().nullish(), + useYn: z.string().nullish(), + atflCd: z.string().nullish(), + reptDto: scheduleRepeatSchema.nullish(), +}); + +export type ScheduleFormData = z.infer; + +export interface ScheduleService { + getSchedules(params?: ScheduleParams): Promise; + createSchedule(data: ScheduleFormData): Promise; +} diff --git a/apps/cs/src/pages/schedule/ui/create-page.tsx b/apps/cs/src/pages/schedule/ui/create-page.tsx new file mode 100644 index 000000000..579689354 --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/create-page.tsx @@ -0,0 +1,66 @@ +"use client"; + +import dayjs from "dayjs"; +import { useForm } from "react-hook-form"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useMutation } from "@tanstack/react-query"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useAlertStore } from "@/shared/lib"; +import { SITE_ID } from "@/shared/config"; +import { + scheduleFormSchema, + type ScheduleFormData, +} from "../model/schedule-interface"; +import { ScheduleApi } from "../api/schedule-service"; +import { ScheduleForm } from "./schedule-form"; + +function CreatePage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const dateStr = searchParams?.get("dateStr"); + const { setMessage: alert } = useAlertStore((state) => state); + + const { mutateAsync: createSchedule } = useMutation({ + mutationFn: ScheduleApi.createSchedule, + onSuccess: () => { + alert("일정이 등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(scheduleFormSchema), + defaultValues: { + siteId: SITE_ID, + schdlBgngDt: dateStr + ? dayjs(dateStr).format("YYYY-MM-DD 00:00:00") + : undefined, + schdlEndDt: dateStr + ? dayjs(dateStr).format("YYYY-MM-DD 23:59:59") + : undefined, + reptYn: "N", + commSchdlYn: "N", + useYn: "Y", + atflCd: null, + reptDto: null, + }, + }); + + const handleSave = async (data: ScheduleFormData) => { + // const reptDtoValid = Object.values(data.reptDto ?? {}).every( + // (value) => value === null, + // ); + // if (reptDtoValid) data.reptDto = null; + await createSchedule(data); + }; + + return ( +
+
+

스케줄 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/schedule/ui/date-time-picker.tsx b/apps/cs/src/pages/schedule/ui/date-time-picker.tsx new file mode 100644 index 000000000..0302253b9 --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/date-time-picker.tsx @@ -0,0 +1,123 @@ +"use client"; + +import dayjs from "dayjs"; +import { useState } from "react"; +import { CalendarIcon } from "lucide-react"; +import { + Button, + Calendar, + Popover, + PopoverContent, + PopoverTrigger, + ScrollArea, + ScrollBar, + cn, +} from "@common/components"; + +type DateTimePickerProps = { + date?: Date; + classes?: { trigger?: string }; + onDateChange?: (date: Date) => void; +}; + +function DateTimePicker({ date, classes, onDateChange }: DateTimePickerProps) { + const [isOpen, setIsOpen] = useState(false); + + const hours = Array.from({ length: 24 }, (_, i) => i); + const minutes = Array.from({ length: 60 }, (_, i) => i); + + const handleDateSelect = (selectedDate: Date | undefined) => { + if (!selectedDate) return; + onDateChange?.(selectedDate); + }; + + const handleTimeChange = (type: "hour" | "minute", value: string) => { + if (date) { + const newDate = new Date(date); + if (type === "hour") { + newDate.setHours(parseInt(value)); + } else if (type === "minute") { + newDate.setMinutes(parseInt(value)); + } + onDateChange?.(newDate); + } + }; + + return ( + + + + + +
+ +
+ +
+ {hours.reverse().map((hour) => ( + + ))} +
+ +
+ +
+ {minutes.map((minute) => ( + + ))} +
+ +
+
+
+
+
+ ); +} + +export { DateTimePicker }; diff --git a/apps/cs/src/pages/schedule/ui/schedule-form.tsx b/apps/cs/src/pages/schedule/ui/schedule-form.tsx new file mode 100644 index 000000000..bf34b02d1 --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/schedule-form.tsx @@ -0,0 +1,331 @@ +import dayjs from "dayjs"; +import { useState } from "react"; +import { type UseFormReturn } from "react-hook-form"; +import { + Checkbox, + DatePicker, + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Textarea, +} from "@common/components"; +import { GeneralFileUploader } from "@/widgets/post/ui/general-file-uploader"; +import { FormWrapper } from "@/widgets/form-wrapper"; +import { CodeSelect } from "@/entities/code"; +import { FormItem, FormControl, FormField } from "@/shared/ui/form"; +import type { ScheduleFormData } from "../model/schedule-interface"; +import { DateTimePicker } from "./date-time-picker"; + +type ScheduleFormProps = { + form: UseFormReturn; + scheduleId?: string; + handleSave: (data: ScheduleFormData) => Promise; + handleDelete?: () => Promise; +}; + +function ScheduleForm({ form, scheduleId, ...props }: ScheduleFormProps) { + const isModifyMode = !!scheduleId; + + const [beginDate, setBeginDate] = useState( + dayjs(form?.getValues("schdlBgngDt"))?.toDate(), + ); + const [endDate, setEndDate] = useState( + dayjs(form?.getValues("schdlEndDt"))?.toDate(), + ); + const [isAllDay, setIsAllDay] = useState(false); + const [deleteAttachmentIds, setDeleteAttachmentIds] = useState([]); + + const handleDateTimeChange = + (name: "schdlBgngDt" | "schdlEndDt") => (date?: Date) => { + if (!date) return; + form.setValue(name, dayjs(date).format("YYYY-MM-DD HH:mm:ss")); + setIsAllDay(false); + }; + + return ( + +
+ + + + + + + + 제목 + + ( + + + + + + )} + /> + + + + 일정 기간 + +
+
+ ( + { + setBeginDate(date); + handleDateTimeChange("schdlBgngDt")(date); + }} + {...field} + /> + )} + /> + ~ + ( + { + setEndDate(date); + handleDateTimeChange("schdlEndDt")(date); + }} + {...field} + /> + )} + /> +
+
+ { + setIsAllDay(checked === true); + if (checked === true) { + const baseDate = form.getValues("schdlBgngDt"); + if (baseDate) { + const newStartDate = dayjs(baseDate).format( + "YYYY-MM-DD 00:00:00", + ); + const newEndDate = dayjs(baseDate).format( + "YYYY-MM-DD 23:59:59", + ); + setBeginDate(dayjs(newStartDate).toDate()); + setEndDate(dayjs(newEndDate).toDate()); + form.setValue("schdlBgngDt", newStartDate); + form.setValue("schdlEndDt", newEndDate); + } + } + }} + /> + +
+
+
+
+ + 일정 반복 + +
+
+ ( + + +
+ { + field.onChange(checked ? "Y" : "N"); + }} + /> + +
+
+
+ )} + /> + ( + + + + value && field.onChange(value) + } + placeholder="반복 주기 선택" + disabled={form.watch("reptYn") === "N"} + /> + + + )} + /> + ( + + + + value && field.onChange(value) + } + className="w-auto px-4" + placeholder="반복 종료일 선택" + disabled={form.watch("reptYn") === "N"} + /> + + + )} + /> +
+
+
+
+ + 일정 유형 + + ( + + + + + + )} + /> + + + + 일정 내용 + + ( + + +