From 46c3f504329b3aaf7e9d112675bc4995bff5ef52 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 7 Jul 2025 13:58:29 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[apps/cs]=20refactor:=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EB=B0=8F=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/entities/menu/api/menu-service.ts | 37 +++++++- apps/cs/src/entities/menu/index.ts | 9 +- .../src/entities/menu/model/menu-interface.ts | 58 +++++++++++++ apps/cs/src/pages/menu/api/menu-service.ts | 65 -------------- .../src/pages/menu/api/tree-menu-service.ts | 33 +++++++ apps/cs/src/pages/menu/lib/utils.ts | 2 +- .../cs/src/pages/menu/model/menu-interface.ts | 86 ------------------- .../pages/menu/model/tree-menu-interface.ts | 29 +++++++ apps/cs/src/pages/menu/ui/base-menu-form.tsx | 2 +- apps/cs/src/pages/menu/ui/menu-form.tsx | 14 ++- apps/cs/src/pages/menu/ui/menu-node.tsx | 6 +- apps/cs/src/pages/menu/ui/menu-page.tsx | 17 ++-- apps/cs/src/pages/menu/ui/menu-tree.tsx | 7 +- apps/cs/src/pages/menu/ui/type-menu-form.tsx | 2 +- 14 files changed, 194 insertions(+), 173 deletions(-) delete mode 100644 apps/cs/src/pages/menu/api/menu-service.ts create mode 100644 apps/cs/src/pages/menu/api/tree-menu-service.ts delete mode 100644 apps/cs/src/pages/menu/model/menu-interface.ts create mode 100644 apps/cs/src/pages/menu/model/tree-menu-interface.ts diff --git a/apps/cs/src/entities/menu/api/menu-service.ts b/apps/cs/src/entities/menu/api/menu-service.ts index 4424f8d37..7dd0aecd9 100644 --- a/apps/cs/src/entities/menu/api/menu-service.ts +++ b/apps/cs/src/entities/menu/api/menu-service.ts @@ -1,6 +1,11 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; -import type { MenuItem, MenuService } from "../model/menu-interface"; +import type { + MenuService, + Menu, + MenuItem, + MenuFormData, +} from "../model/menu-interface"; export class MenuApiService implements MenuService { async getMenuRoles(siteId: string): Promise { @@ -9,6 +14,36 @@ export class MenuApiService implements MenuService { ); return response.data; } + + public async getMenuInfo(menuCd: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`, + ); + return response.data; + } + + public async createMenu(data: MenuFormData): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENUS}`, + data, + ); + return response.data; + } + + public async updateMenu( + menuCd: number, + data: Omit, + ): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`, + data, + ); + return response.data; + } + + public async deleteMenu(menuCd: number): Promise { + await client.delete(`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`); + } } export const MenuApi = new MenuApiService(); diff --git a/apps/cs/src/entities/menu/index.ts b/apps/cs/src/entities/menu/index.ts index 04f63e38b..e4a9cec40 100644 --- a/apps/cs/src/entities/menu/index.ts +++ b/apps/cs/src/entities/menu/index.ts @@ -1,2 +1,9 @@ +export { MenuApi } from "./api/menu-service"; +export { + MENU_TYPE, + menuFormSchema, + type MenuType, + type MenuItem, + type MenuFormData, +} from "./model/menu-interface"; export { MenuWrapper } from "./lib/menu-wrapper"; -export type { MenuItem } from "./model/menu-interface"; diff --git a/apps/cs/src/entities/menu/model/menu-interface.ts b/apps/cs/src/entities/menu/model/menu-interface.ts index 12c7d007a..285c28b04 100644 --- a/apps/cs/src/entities/menu/model/menu-interface.ts +++ b/apps/cs/src/entities/menu/model/menu-interface.ts @@ -1,3 +1,16 @@ +import { z } from "zod"; + +export const MENU_TYPE = { + EMPTY: "EMPTY", + CONTENTS: "CONTENTS", + BOARD: "BOARD", + INSIDE: "INSIDE", + OUTSIDE: "OUTSIDE", + BOTTOM: "BOTTOM", +} as const; + +export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE]; + export interface MenuItem { menuCd: number; menuKornNm: string; @@ -18,6 +31,51 @@ export interface MenuItem { requiredUrlPath: boolean; } +export const menuFormSchema = z.object({ + siteId: z.string(), + menuKornNm: z.string(), + menuSubKornNm: z.string().nullish(), + menuEngNm: z.string().nullish(), + menuSubEngNm: z.string().nullish(), + levelNo: z.number(), + urlPath: z.string().nullish(), + iconNm: z.string().nullish(), + menuLnkgTypeCd: z.enum([ + MENU_TYPE.EMPTY, + MENU_TYPE.CONTENTS, + MENU_TYPE.BOARD, + MENU_TYPE.INSIDE, + MENU_TYPE.OUTSIDE, + MENU_TYPE.BOTTOM, + ]), + menuLnkgSn: z.number().nullish(), + otptYn: z.string().default("Y"), + prntMenuCd: z.number().nullish(), + sortSeq: z.number(), + evlUseYn: z.string().default("Y"), + qrcd: z.string().nullish(), + useYn: z.string().default("Y"), + newWndwYn: z.string().nullish(), + menuLnkgSnNm: z.string().nullish(), + menuPicUserUnqId: z.string().nullish(), + menuPicUserUnqIdNm: z.string().nullish(), + menuPicDsgnYmd: z.string().nullish(), +}); + +export type MenuFormData = z.infer; + +export type Menu = Omit & { + menuCd: number; + iconName?: string; +}; + export interface MenuService { getMenuRoles(siteId: string): Promise; + getMenuInfo: (menuCd: number) => Promise; + createMenu: (data: MenuFormData) => Promise; + updateMenu: ( + menuCd: number, + data: Omit, + ) => Promise; + deleteMenu: (menuCd: number) => Promise; } diff --git a/apps/cs/src/pages/menu/api/menu-service.ts b/apps/cs/src/pages/menu/api/menu-service.ts deleted file mode 100644 index dbb67cb3e..000000000 --- a/apps/cs/src/pages/menu/api/menu-service.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import type { - Menu, - MenuFormData, - MenuService, - TreeMenu, - TreeNode, -} from "../model/menu-interface"; - -export class MenuApiService implements MenuService { - public async getMenuInfo(menuCd: number): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`, - ); - return response.data; - } - - public async createMenu(data: MenuFormData): Promise { - const response = await client.post( - `${ENDPOINT.CMS_SERVICE.MENUS}`, - data, - ); - return response.data; - } - - public async updateMenu( - menuCd: number, - data: Omit, - ): Promise { - const response = await client.put( - `${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`, - data, - ); - return response.data; - } - - public async deleteMenu(menuCd: number): Promise { - await client.delete(`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`); - } - - public async getTreeMenuList(siteId: string): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`, - ); - return response.data; - } - - public async updateTreeMenu(siteId: string, data: TreeNode[]): Promise { - const response = await client.put( - `${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`, - data, - ); - return response.data; - } - - public async updateMenuName(menuCd: number, name: string): Promise { - const response = await client.put( - `${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`, - ); - return response.data; - } -} - -export const MenuApi = new MenuApiService(); diff --git a/apps/cs/src/pages/menu/api/tree-menu-service.ts b/apps/cs/src/pages/menu/api/tree-menu-service.ts new file mode 100644 index 000000000..9f36f5383 --- /dev/null +++ b/apps/cs/src/pages/menu/api/tree-menu-service.ts @@ -0,0 +1,33 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + TreeMenuService, + TreeMenu, + TreeNode, +} from "../model/tree-menu-interface"; + +export class TreeMenuApiService implements TreeMenuService { + public async getTreeMenuList(siteId: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`, + ); + return response.data; + } + + public async updateTreeMenu(siteId: string, data: TreeNode[]): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`, + data, + ); + return response.data; + } + + public async updateMenuName(menuCd: number, name: string): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`, + ); + return response.data; + } +} + +export const TreeMenuApi = new TreeMenuApiService(); diff --git a/apps/cs/src/pages/menu/lib/utils.ts b/apps/cs/src/pages/menu/lib/utils.ts index f6d13be33..0e219047d 100644 --- a/apps/cs/src/pages/menu/lib/utils.ts +++ b/apps/cs/src/pages/menu/lib/utils.ts @@ -3,7 +3,7 @@ import type { TreeUserOrganization, TreeUser, } from "@/entities/user-organization"; -import type { TreeMenu, TreeNode } from "../model/menu-interface"; +import type { TreeMenu, TreeNode } from "../model/tree-menu-interface"; export const generateMenuTreeData = ( data: TreeMenu[], diff --git a/apps/cs/src/pages/menu/model/menu-interface.ts b/apps/cs/src/pages/menu/model/menu-interface.ts deleted file mode 100644 index f9650e0e2..000000000 --- a/apps/cs/src/pages/menu/model/menu-interface.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { z } from "zod"; - -export const MENU_TYPE = { - EMPTY: "EMPTY", - CONTENTS: "CONTENTS", - BOARD: "BOARD", - INSIDE: "INSIDE", - OUTSIDE: "OUTSIDE", - BOTTOM: "BOTTOM", -} as const; - -export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE]; - -export interface TreeNode { - menuCd?: number; - name?: string; - sortSeq?: number; - parentId?: number | null; - levelNo?: number; - icon?: string; - children?: TreeNode[]; -} - -export interface TreeMenu extends Omit { - menuKornNm: string; - menuEngNm?: string; - iconNm?: string; - children?: TreeMenu[]; - urlPath?: string; - menuLnkgTypeCd: string; - menuLnkgSn?: number; - qrcd?: string; - useYn: string; - otptYn: string; - requiredUrlPath?: boolean; -} - -export const menuFormSchema = z.object({ - siteId: z.string(), - menuKornNm: z.string(), - menuSubKornNm: z.string().nullish(), - menuEngNm: z.string().nullish(), - menuSubEngNm: z.string().nullish(), - levelNo: z.number(), - urlPath: z.string().nullish(), - iconNm: z.string().nullish(), - menuLnkgTypeCd: z.enum([ - MENU_TYPE.EMPTY, - MENU_TYPE.CONTENTS, - MENU_TYPE.BOARD, - MENU_TYPE.INSIDE, - MENU_TYPE.OUTSIDE, - MENU_TYPE.BOTTOM, - ]), - menuLnkgSn: z.number().nullish(), - otptYn: z.string().default("Y"), - prntMenuCd: z.number().nullish(), - sortSeq: z.number(), - evlUseYn: z.string().default("Y"), - qrcd: z.string().nullish(), - useYn: z.string().default("Y"), - newWndwYn: z.string().nullish(), - menuLnkgSnNm: z.string().nullish(), - menuPicUserUnqId: z.string().nullish(), - menuPicUserUnqIdNm: z.string().nullish(), - menuPicDsgnYmd: z.string().nullish(), -}); - -export type MenuFormData = z.infer; -export type Menu = Omit & { - menuCd: number; - iconName?: string; -}; - -export interface MenuService { - getMenuInfo: (menuCd: number) => Promise; - createMenu: (data: MenuFormData) => Promise; - updateMenu: ( - menuCd: number, - data: Omit, - ) => Promise; - deleteMenu: (menuCd: number) => Promise; - getTreeMenuList: (siteId: string) => Promise; - updateTreeMenu: (siteId: string, data: TreeNode[]) => Promise; - updateMenuName: (menuCd: number, name: string) => Promise; -} diff --git a/apps/cs/src/pages/menu/model/tree-menu-interface.ts b/apps/cs/src/pages/menu/model/tree-menu-interface.ts new file mode 100644 index 000000000..39a6f92c9 --- /dev/null +++ b/apps/cs/src/pages/menu/model/tree-menu-interface.ts @@ -0,0 +1,29 @@ +export interface TreeNode { + menuCd?: number; + name?: string; + sortSeq?: number; + parentId?: number | null; + levelNo?: number; + icon?: string; + children?: TreeNode[]; +} + +export interface TreeMenu extends Omit { + menuKornNm: string; + menuEngNm?: string; + iconNm?: string; + children?: TreeMenu[]; + urlPath?: string; + menuLnkgTypeCd: string; + menuLnkgSn?: number; + qrcd?: string; + useYn: string; + otptYn: string; + requiredUrlPath?: boolean; +} + +export interface TreeMenuService { + getTreeMenuList: (siteId: string) => Promise; + updateTreeMenu: (siteId: string, data: TreeNode[]) => Promise; + updateMenuName: (menuCd: number, name: string) => Promise; +} diff --git a/apps/cs/src/pages/menu/ui/base-menu-form.tsx b/apps/cs/src/pages/menu/ui/base-menu-form.tsx index fceb66549..eeae63255 100644 --- a/apps/cs/src/pages/menu/ui/base-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/base-menu-form.tsx @@ -10,8 +10,8 @@ import { TableHead, TableRow, } from "@common/components"; +import { MENU_TYPE, type MenuFormData } from "@/entities/menu"; import { FormItem, FormControl, FormField } from "@/shared/ui/form"; -import { MENU_TYPE, type MenuFormData } from "../model/menu-interface"; import { InfoTooltip } from "@/shared/ui/info-tooltip"; interface BaseMenuFormProps { diff --git a/apps/cs/src/pages/menu/ui/menu-form.tsx b/apps/cs/src/pages/menu/ui/menu-form.tsx index 0a45214eb..1cba9ddf3 100644 --- a/apps/cs/src/pages/menu/ui/menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/menu-form.tsx @@ -1,9 +1,9 @@ import type { UseFormReturn } from "react-hook-form"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button } from "@common/components"; -import { Form } from "@/shared/ui/form"; +import type { MenuFormData } from "@/entities/menu"; import { ManagementAuthorityDialog } from "@/entities/menu-authority"; -import type { MenuFormData } from "../model/menu-interface"; +import { Form } from "@/shared/ui/form"; import { BaseMenuForm } from "./base-menu-form"; import { TypeMenuForm } from "./type-menu-form"; @@ -69,7 +69,15 @@ function MenuForm({

{isModifyMode ? "메뉴 수정" : "메뉴 등록"}

- +
+ + + +
diff --git a/apps/cs/src/pages/menu/ui/menu-node.tsx b/apps/cs/src/pages/menu/ui/menu-node.tsx index 5cda4f522..4a2b3624f 100644 --- a/apps/cs/src/pages/menu/ui/menu-node.tsx +++ b/apps/cs/src/pages/menu/ui/menu-node.tsx @@ -4,8 +4,8 @@ import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview"; import { Button, cn } from "@common/components"; import { useMutationWrapper } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; -import { MenuApi } from "../api/menu-service"; -import type { TreeMenu } from "../model/menu-interface"; +import { TreeMenuApi } from "../api/tree-menu-service"; +import type { TreeMenu } from "../model/tree-menu-interface"; import { EditableNode } from "./editable-node"; type MenuNodeProps = { @@ -37,7 +37,7 @@ function MenuNode({ const { mutateAsync: updateMenuName } = useMutationWrapper({ mutationFn: (nodeName: string) => { const encodedName = encodeURIComponent(nodeName); - return MenuApi.updateMenuName(id as number, encodedName); + return TreeMenuApi.updateMenuName(id as number, encodedName); }, onSuccess: () => { queryClient.invalidateQueries({ diff --git a/apps/cs/src/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx index d212dc970..2d3dedc16 100644 --- a/apps/cs/src/pages/menu/ui/menu-page.tsx +++ b/apps/cs/src/pages/menu/ui/menu-page.tsx @@ -14,19 +14,20 @@ import { SelectTrigger, SelectValue, } from "@common/components"; -import { SiteApi } from "@/entities/site"; -import { ENDPOINT } from "@/shared/config"; -import { useAlertStore, useMutationWrapper } from "@/shared/lib"; -import type { TreeMenu } from "../model/menu-interface"; -import { MenuApi } from "../api/menu-service"; import { + MenuApi, MENU_TYPE, menuFormSchema, type MenuFormData, -} from "../model/menu-interface"; +} from "@/entities/menu"; +import { SiteApi } from "@/entities/site"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { generateMenuTreeData } from "../lib/utils"; +import { TreeMenuApi } from "../api/tree-menu-service"; +import type { TreeMenu } from "../model/tree-menu-interface"; import { MenuTree } from "./menu-tree"; import { MenuForm } from "./menu-form"; -import { generateMenuTreeData } from "../lib/utils"; function MenuPage() { const queryClient = useQueryClient(); @@ -46,7 +47,7 @@ function MenuPage() { const { data: menuTreeData } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId], - queryFn: () => MenuApi.getTreeMenuList(siteId), + queryFn: () => TreeMenuApi.getTreeMenuList(siteId), select: (data) => { const treeData = generateMenuTreeData(data); const defaultSortSeq = diff --git a/apps/cs/src/pages/menu/ui/menu-tree.tsx b/apps/cs/src/pages/menu/ui/menu-tree.tsx index a1fa4dacb..5ef15062c 100644 --- a/apps/cs/src/pages/menu/ui/menu-tree.tsx +++ b/apps/cs/src/pages/menu/ui/menu-tree.tsx @@ -11,8 +11,9 @@ import { import { Button } from "@common/components"; import { ENDPOINT } from "@/shared/config"; import { useMutationWrapper } from "@/shared/lib"; -import { MenuApi } from "../api/menu-service"; -import type { MenuFormData, TreeMenu, TreeNode } from "../model/menu-interface"; +import type { MenuFormData } from "@/entities/menu"; +import { TreeMenuApi } from "../api/tree-menu-service"; +import type { TreeMenu, TreeNode } from "../model/tree-menu-interface"; import { handleDropNode, handleSelectNode } from "../lib/utils"; import { MenuNode } from "./menu-node"; import { EditableNode } from "./editable-node"; @@ -41,7 +42,7 @@ function MenuTree({ const { mutate: updateTreeMenu } = useMutationWrapper({ mutationFn: (newTree: TreeNode) => - MenuApi.updateTreeMenu(siteId, [newTree]), + TreeMenuApi.updateTreeMenu(siteId, [newTree]), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.MENUS], diff --git a/apps/cs/src/pages/menu/ui/type-menu-form.tsx b/apps/cs/src/pages/menu/ui/type-menu-form.tsx index ac62e2520..eeb50e805 100644 --- a/apps/cs/src/pages/menu/ui/type-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/type-menu-form.tsx @@ -16,8 +16,8 @@ import { import type { BoardPopup } from "@/entities/board"; import type { Contents } from "@/entities/content"; import type { TreeUser } from "@/entities/user-organization"; +import type { MenuFormData, MenuType } from "@/entities/menu"; import { FormItem, FormControl, FormField } from "@/shared/ui/form"; -import type { MenuFormData, MenuType } from "../model/menu-interface"; import { OrganizationSearchDialog } from "./organization-search-dialog"; import { ContentSearchDialog } from "./content-search-dialog"; import { BoardSearchDialog } from "./board-search-dialog"; From 1e524ba9c565fa9659f4521aa5de390b0dddb18e Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 8 Jul 2025 09:51:59 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[apps/cs]=20feat:=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EB=B6=88?= =?UTF-8?q?=EB=9F=AC=EC=98=A4=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/entities/menu/api/menu-service.ts | 21 ++++++++ .../src/entities/menu/model/menu-interface.ts | 14 ++++++ apps/cs/src/pages/menu/ui/menu-form.tsx | 49 +++++++++++++++++-- apps/cs/src/shared/config/api.ts | 2 +- 4 files changed, 82 insertions(+), 4 deletions(-) diff --git a/apps/cs/src/entities/menu/api/menu-service.ts b/apps/cs/src/entities/menu/api/menu-service.ts index 7dd0aecd9..33ad06837 100644 --- a/apps/cs/src/entities/menu/api/menu-service.ts +++ b/apps/cs/src/entities/menu/api/menu-service.ts @@ -5,6 +5,7 @@ import type { Menu, MenuItem, MenuFormData, + MenuTemporarilyStoredData, } from "../model/menu-interface"; export class MenuApiService implements MenuService { @@ -44,6 +45,26 @@ export class MenuApiService implements MenuService { public async deleteMenu(menuCd: number): Promise { await client.delete(`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`); } + + public async menuTemporaryStorage( + menuCd: number, + data: MenuFormData, + ): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENU_TEMPORARY_STORAGE}/${menuCd}`, + data, + ); + return response.data; + } + + public async getMenuTemporarilyStoredData( + menuCd: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_TEMPORARY_STORAGE}/menu/${menuCd}/user`, + ); + return response.data; + } } export const MenuApi = new MenuApiService(); diff --git a/apps/cs/src/entities/menu/model/menu-interface.ts b/apps/cs/src/entities/menu/model/menu-interface.ts index 285c28b04..b1693f616 100644 --- a/apps/cs/src/entities/menu/model/menu-interface.ts +++ b/apps/cs/src/entities/menu/model/menu-interface.ts @@ -31,6 +31,16 @@ export interface MenuItem { requiredUrlPath: boolean; } +export interface MenuTemporarilyStoredData { + tmprStrgSn: number; + menuCd: number; + menuTmprStrg: string; + regDt: string; + rgtr: string; + mdfcnDt: string; + mdfr: string; +} + export const menuFormSchema = z.object({ siteId: z.string(), menuKornNm: z.string(), @@ -78,4 +88,8 @@ export interface MenuService { data: Omit, ) => Promise; deleteMenu: (menuCd: number) => Promise; + menuTemporaryStorage: (menuCd: number, data: MenuFormData) => Promise; + getMenuTemporarilyStoredData: ( + menuCd: 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 1cba9ddf3..f1400a369 100644 --- a/apps/cs/src/pages/menu/ui/menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/menu-form.tsx @@ -1,11 +1,14 @@ import type { UseFormReturn } from "react-hook-form"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button } from "@common/components"; -import type { MenuFormData } from "@/entities/menu"; +import { MenuApi, menuFormSchema, type MenuFormData } from "@/entities/menu"; import { ManagementAuthorityDialog } from "@/entities/menu-authority"; import { Form } from "@/shared/ui/form"; 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"; interface MenuFormProps { form: UseFormReturn; @@ -31,6 +34,20 @@ function MenuForm({ const isModifyMode = !!menuCd; const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: menuTemporarilyStoredData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_TEMPORARY_STORAGE, menuCd], + queryFn: () => MenuApi.getMenuTemporarilyStoredData(menuCd!), + enabled: !!menuCd, + }); + + const { mutate: menuTemporaryStorage } = useMutationWrapper({ + mutationFn: () => MenuApi.menuTemporaryStorage(menuCd!, form.getValues()), + onSuccess: () => { + alert("메뉴 임시 저장이 완료되었습니다."); + }, + }); const onSubmit = async (data: MenuFormData) => { if (isModifyMode) { @@ -58,6 +75,22 @@ function MenuForm({ } }; + const handleMenuTemporaryStorage = () => { + if (!menuCd || !menuFormSchema.parse(form.getValues())) return; + menuTemporaryStorage(); + }; + + const handleLoadTemporaryData = () => { + if (!menuTemporarilyStoredData?.[0]?.menuTmprStrg) { + alert("불러올 임시 저장 데이터가 없습니다."); + return; + } + + const storedData = menuTemporarilyStoredData[0].menuTmprStrg; + const parsedData = JSON.parse(storedData); + form.reset(parsedData); + }; + const menuType = form.watch("menuLnkgTypeCd"); const isSubmitDisabled = !form.watch("siteId"); @@ -70,10 +103,20 @@ function MenuForm({ {isModifyMode ? "메뉴 수정" : "메뉴 등록"}
- - diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 3159eb23c..28dac3d20 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -22,7 +22,6 @@ export const ENDPOINT = { LOGINLOG: "/user-service/api/v1/login-logs", APILOG: "/user-service/api/v1/api-logs", PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs", - }, CMS_SERVICE: { SITES: "/cms-service/api/v1/sites", @@ -33,6 +32,7 @@ export const ENDPOINT = { MENUS: "/cms-service/api/v1/menus", 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", CONTENTS: "/cms-service/api/v1/contents", CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", ORGANIZATIONS: "/cms-service/api/v1/ognzs", From 13ac6d638b5b07927698b7533880d0c90dd6e2e0 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 8 Jul 2025 19:18:41 +0900 Subject: [PATCH 3/4] =?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", From 49a699f5332804a25edf761e710c5da282dca5da Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 8 Jul 2025 19:22:05 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[apps/cs]=20chore:=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EC=B2=AD=20=EC=BD=94=EB=93=9C=20=ED=95=84=ED=84=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/users/ui/users-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cs/src/pages/users/ui/users-form.tsx b/apps/cs/src/pages/users/ui/users-form.tsx index d69e918b0..0ddfde713 100644 --- a/apps/cs/src/pages/users/ui/users-form.tsx +++ b/apps/cs/src/pages/users/ui/users-form.tsx @@ -140,7 +140,7 @@ function UsersForm({ }, [laeSeCd, watchDtlInstCd]); const enoCdSelectData = useMemo( - () => laeSeCd?.filter((code) => code.unit?.startsWith("G2")) ?? [], + () => laeSeCd?.filter((code) => code.lwpoCd?.startsWith("G2")) ?? [], [laeSeCd], );