From 13ac6d638b5b07927698b7533880d0c90dd6e2e0 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 8 Jul 2025 19:18:41 +0900 Subject: [PATCH] =?UTF-8?q?[apps/cs]=20feat:=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=9D=B4=EB=A0=A5=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/management-authority-dialog.tsx | 1 - .../pages/menu/api/menu-history-service.ts | 28 +++ .../pages/menu/model/menu-history-columns.tsx | 71 ++++++++ .../menu/model/menu-history-interface.ts | 30 ++++ apps/cs/src/pages/menu/ui/menu-form.tsx | 44 +++-- .../src/pages/menu/ui/menu-history-dialog.tsx | 162 ++++++++++++++++++ apps/cs/src/shared/config/api.ts | 1 + 7 files changed, 317 insertions(+), 20 deletions(-) create mode 100644 apps/cs/src/pages/menu/api/menu-history-service.ts create mode 100644 apps/cs/src/pages/menu/model/menu-history-columns.tsx create mode 100644 apps/cs/src/pages/menu/model/menu-history-interface.ts create mode 100644 apps/cs/src/pages/menu/ui/menu-history-dialog.tsx diff --git a/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx index a9ba18333..1a6bc08ce 100644 --- a/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx +++ b/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -74,7 +74,6 @@ function ManagementAuthorityDialog({ menuCd }: ManagementAuthorityDialogProps) { diff --git a/apps/cs/src/pages/menu/api/menu-history-service.ts b/apps/cs/src/pages/menu/api/menu-history-service.ts new file mode 100644 index 000000000..ecb31878b --- /dev/null +++ b/apps/cs/src/pages/menu/api/menu-history-service.ts @@ -0,0 +1,28 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageableList } from "@/shared/model"; +import type { + MenuHistory, + MenuHistorySearchParams, + MenuHistoryService, +} from "../model/menu-history-interface"; + +export class MenuHistoryApiService implements MenuHistoryService { + public async getMenuHistoryList( + params: MenuHistorySearchParams, + ): Promise> { + const response = await client.get>( + `${ENDPOINT.CMS_SERVICE.MENU_HISTORY}`, + { params }, + ); + return response.data; + } + + public async rollbackMenu(hstrySn: number): Promise { + await client.post(`${ENDPOINT.CMS_SERVICE.MENU_HISTORY}/rollback`, { + hstrySn, + }); + } +} + +export const MenuHistoryApi = new MenuHistoryApiService(); diff --git a/apps/cs/src/pages/menu/model/menu-history-columns.tsx b/apps/cs/src/pages/menu/model/menu-history-columns.tsx new file mode 100644 index 000000000..b542e0ff7 --- /dev/null +++ b/apps/cs/src/pages/menu/model/menu-history-columns.tsx @@ -0,0 +1,71 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { Checkbox } from "@common/components"; +import type { MenuHistory } from "./menu-history-interface"; +import dayjs from "dayjs"; + +export const columns: ColumnDef[] = [ + { + id: "select", + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="행 선택" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "chgTypeNm", + header: "이력구분", + meta: { + width: 80, + }, + }, + { + accessorKey: "menuKornNm", + header: "메뉴명", + meta: { + width: 100, + }, + }, + { + accessorKey: "bchgData", + header: "이전 데이터", + cell: ({ row }) => { + const parsed = JSON.parse(row.original.bchgData); + const stringified = JSON.stringify(parsed, null, 2); + return
{stringified}
; + }, + meta: { + width: 250, + }, + }, + { + accessorKey: "achgData", + header: "변경 데이터", + cell: ({ row }) => { + const parsed = JSON.parse(row.original.achgData); + const stringified = JSON.stringify(parsed, null, 2); + return
{stringified}
; + }, + meta: { + width: 250, + }, + }, + { + accessorKey: "regDt", + header: "변경일시", + cell: ({ row }) => { + return dayjs(row.original.regDt).format("YYYY-MM-DD HH:mm:ss"); + }, + }, + { + accessorKey: "rgtrNm", + header: "변경자", + meta: { + width: 80, + }, + }, +]; diff --git a/apps/cs/src/pages/menu/model/menu-history-interface.ts b/apps/cs/src/pages/menu/model/menu-history-interface.ts new file mode 100644 index 000000000..e3e35446d --- /dev/null +++ b/apps/cs/src/pages/menu/model/menu-history-interface.ts @@ -0,0 +1,30 @@ +import type { PageableParams, PageableList } from "@/shared/model"; + +export interface MenuHistorySearchParams extends PageableParams { + keyword?: string; + searchType?: string; + menuCd?: number; + chgTypeCd?: string; + startDt?: string; + endDt?: string; +} + +export interface MenuHistory { + hstrySn: number; + menuCd: number; + chgTypeCd: string; + chgTypeNm: string; + bchgData: string; + achgData: string; + rgtr: string; + regDt: string; + rgtrNm: string; + menuKornNm: string; +} + +export interface MenuHistoryService { + getMenuHistoryList: ( + params: MenuHistorySearchParams, + ) => Promise>; + rollbackMenu: (hstrySn: number) => Promise; +} diff --git a/apps/cs/src/pages/menu/ui/menu-form.tsx b/apps/cs/src/pages/menu/ui/menu-form.tsx index f1400a369..43331067e 100644 --- a/apps/cs/src/pages/menu/ui/menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/menu-form.tsx @@ -1,14 +1,15 @@ +import { useQuery } from "@tanstack/react-query"; import type { UseFormReturn } from "react-hook-form"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button } from "@common/components"; import { MenuApi, menuFormSchema, type MenuFormData } from "@/entities/menu"; import { ManagementAuthorityDialog } from "@/entities/menu-authority"; import { Form } from "@/shared/ui/form"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { BaseMenuForm } from "./base-menu-form"; import { TypeMenuForm } from "./type-menu-form"; -import { useAlertStore, useMutationWrapper } from "@/shared/lib"; -import { ENDPOINT } from "@/shared/config"; -import { useQuery } from "@tanstack/react-query"; +import { MenuHistoryDialog } from "./menu-history-dialog"; interface MenuFormProps { form: UseFormReturn; @@ -103,22 +104,7 @@ function MenuForm({ {isModifyMode ? "메뉴 수정" : "메뉴 등록"}
- - +
@@ -128,6 +114,26 @@ function MenuForm({
+ {isModifyMode && ( + + )} + {isModifyMode && ( + + )} diff --git a/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx b/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx new file mode 100644 index 000000000..7a81ad002 --- /dev/null +++ b/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx @@ -0,0 +1,162 @@ +import { useEffect, useState } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + getCoreRowModel, + type PaginationState, + type RowSelectionState, +} from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + Input, + Label, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib"; +import { MenuHistoryApi } from "../api/menu-history-service"; +import type { MenuHistorySearchParams } from "../model/menu-history-interface"; +import { columns } from "../model/menu-history-columns"; + +type MenuHistoryDialogProps = { + menuCd?: number; +} & React.ComponentProps; + +function MenuHistoryDialog({ menuCd }: MenuHistoryDialogProps) { + const { confirm } = useConfirm(); + const queryClient = useQueryClient(); + const alert = useAlertStore((state) => state.setMessage); + + const [open, setOpen] = useState(false); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [searchParams, setSearchParams] = useState({ + keyword: "", + searchType: "menuKornNm", + }); + const [rowSelection, setRowSelection] = useState({}); + const selectedHistoryId = parseInt(Object.keys(rowSelection)[0] ?? "0"); + + const query = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_HISTORY, pagination], + queryFn: () => + MenuHistoryApi.getMenuHistoryList({ + ...searchParams, + menuCd, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + enabled: !!menuCd && open, + }); + + const { mutateAsync: rollbackMenu } = useMutationWrapper({ + mutationFn: (hstrySn: number) => MenuHistoryApi.rollbackMenu(hstrySn), + onSuccess: () => { + alert("메뉴가 성공적으로 복원되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.MENUS], + }); + setOpen(false); + }, + }); + + const handleRollback = async (hstrySn: number) => { + if (!(await confirm("메뉴를 '이전 데이터'로 되돌리시겠습니까?"))) return; + await rollbackMenu(hstrySn); + }; + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { pagination, rowSelection }, + manualPagination: true, + enableRowSelection: true, + enableMultiRowSelection: false, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + getRowId: (row) => row.hstrySn.toString(), + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + e.stopPropagation(); + query.refetch(); + }; + + useEffect(() => { + if (open) { + setSearchParams({ + ...searchParams, + keyword: "", + }); + setPagination({ + ...pagination, + pageIndex: 0, + }); + setRowSelection({}); + } + }, [open]); + + return ( + + + + + + + 메뉴 이력 관리 + + +
+
+ + + setSearchParams({ ...searchParams, keyword: e.target.value }) + } + className="w-[36rem]" + /> + +
+
+ +
{tableComponent}
+ + + + + + + +
+
+ ); +} + +export { MenuHistoryDialog }; diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 28dac3d20..7001b97aa 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -33,6 +33,7 @@ export const ENDPOINT = { MENU_ROLES: "/cms-service/api/v1/menu-roles", MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", MENU_TEMPORARY_STORAGE: "/cms-service/api/v1/menu-tmpr-strg", + MENU_HISTORY: "/cms-service/api/v1/menu-hstry", CONTENTS: "/cms-service/api/v1/contents", CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", ORGANIZATIONS: "/cms-service/api/v1/ognzs",