feat: 메뉴 등록 및 수정 작업

This commit is contained in:
2025-03-26 17:00:35 +09:00
parent f03b07738e
commit b26e7e3d10
7 changed files with 78 additions and 59 deletions
+11 -3
View File
@@ -5,9 +5,17 @@ import type {
MenuFormData, MenuFormData,
MenuService, MenuService,
TreeMenu, TreeMenu,
TreeNode,
} from "../model/menu-interface"; } from "../model/menu-interface";
export class MenuApiService implements MenuService { export class MenuApiService implements MenuService {
public async getMenuInfo(menuCd: number): Promise<Menu> {
const response = await client.get<Menu>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
);
return response.data;
}
public async createMenu(data: MenuFormData): Promise<void> { public async createMenu(data: MenuFormData): Promise<void> {
const response = await client.post<void>( const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}`, `${ENDPOINT.CMS_SERVICE.MENUS}`,
@@ -27,14 +35,14 @@ export class MenuApiService implements MenuService {
return response.data; return response.data;
} }
public async getTreeMenuList(siteId: string): Promise<Menu[]> { public async getTreeMenuList(siteId: string): Promise<TreeMenu[]> {
const response = await client.get<Menu[]>( const response = await client.get<TreeMenu[]>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`, `${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
); );
return response.data; return response.data;
} }
public async modifyTreeMenu(siteId: string, data: TreeMenu[]): Promise<void> { public async modifyTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
const response = await client.put<void>( const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`, `${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
data, data,
+18 -17
View File
@@ -1,9 +1,9 @@
import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview"; import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview";
import type { Menu, TreeMenu } from "../model/menu-interface"; import type { TreeMenu, TreeNode } from "../model/menu-interface";
export const generateTreeData = (data: Menu[]): NodeModel<Menu>[] => { export const generateTreeData = (data: TreeMenu[]): NodeModel<TreeMenu>[] => {
return data.reduce<NodeModel<Menu>[]>((acc, menu) => { return data.reduce<NodeModel<TreeMenu>[]>((acc, menu) => {
const node: NodeModel<Menu> = { const node: NodeModel<TreeMenu> = {
id: menu.menuCd ?? 0, id: menu.menuCd ?? 0,
parent: menu.parentId ?? 0, parent: menu.parentId ?? 0,
text: menu.menuKornNm, text: menu.menuKornNm,
@@ -22,9 +22,9 @@ export const generateTreeData = (data: Menu[]): NodeModel<Menu>[] => {
}; };
export const generateNewTreeData = ( export const generateNewTreeData = (
treeData: NodeModel<Menu>[], treeData: NodeModel<TreeMenu>[],
options: DropOptions<Menu>, options: DropOptions<TreeMenu>,
): TreeMenu | null => { ): TreeNode | null => {
const { dragSourceId, dragSource, dropTargetId, dropTarget } = options; const { dragSourceId, dragSource, dropTargetId, dropTarget } = options;
if (!dragSource?.data) return null; if (!dragSource?.data) return null;
@@ -38,7 +38,7 @@ export const generateNewTreeData = (
(node) => node.parent === dragSourceId && node.data, (node) => node.parent === dragSourceId && node.data,
); );
const baseNode = (node: NodeModel<Menu>): TreeMenu => { const baseNode = (node: NodeModel<TreeMenu>): TreeNode => {
if (!node.data) return {}; if (!node.data) return {};
return { return {
menuCd: node.data?.menuCd, menuCd: node.data?.menuCd,
@@ -47,20 +47,20 @@ export const generateNewTreeData = (
}; };
}; };
const treeMenu: TreeMenu = { const treeNode: TreeNode = {
...baseNode(dragSource), ...baseNode(dragSource),
parentId: dropTargetId === 0 ? null : (dropTargetId as number), parentId: dropTargetId === 0 ? null : (dropTargetId as number),
sortSeq: newSortSeq, sortSeq: newSortSeq,
levelNo: (dropTarget?.data?.levelNo ?? 0) + 1, levelNo: (dropTarget?.data?.levelNo ?? 0) + 1,
}; };
const result: TreeMenu = { const result: TreeNode = {
...treeMenu, ...treeNode,
children: childNodes.map((child) => ({ children: childNodes.map((child) => ({
...baseNode(child), ...baseNode(child),
parentId: child.data?.parentId === 0 ? null : (dropTargetId as number), parentId: child.data?.parentId === 0 ? null : (dropTargetId as number),
sortSeq: child.data?.sortSeq, sortSeq: child.data?.sortSeq,
levelNo: (treeMenu?.levelNo ?? 0) + 1, levelNo: (treeNode?.levelNo ?? 0) + 1,
})), })),
}; };
@@ -68,21 +68,22 @@ export const generateNewTreeData = (
}; };
export const handleSelectNode = export const handleSelectNode =
(callback: (node: NodeModel<Menu>) => void) => (node: NodeModel<Menu>) => { (callback: (node: NodeModel<TreeMenu>) => void) =>
(node: NodeModel<TreeMenu>) => {
callback(node); callback(node);
}; };
export const handleDropNode = export const handleDropNode =
(callback: (newTree: TreeMenu) => void) => (callback: (newTree: TreeNode) => void) =>
(newTree: NodeModel<Menu>[], options: DropOptions<Menu>) => { (newTree: NodeModel<TreeMenu>[], options: DropOptions<TreeMenu>) => {
const newTreeData = generateNewTreeData(newTree, options); const newTreeData = generateNewTreeData(newTree, options);
if (!newTreeData) return; if (!newTreeData) return;
callback(newTreeData); callback(newTreeData);
}; };
export const handleNodeTextChange = export const handleNodeTextChange =
(treeData: NodeModel<Menu>[]) => (treeData: NodeModel<TreeMenu>[]) =>
(callback: (newTree: NodeModel<Menu>[]) => void) => (callback: (newTree: NodeModel<TreeMenu>[]) => void) =>
(id: NodeModel["id"], value: string) => { (id: NodeModel["id"], value: string) => {
const newTree = treeData.map((node) => { const newTree = treeData.map((node) => {
if (node.id === id) { if (node.id === id) {
+17 -15
View File
@@ -1,31 +1,31 @@
import { z } from "zod"; import { z } from "zod";
export const MENU_TYPE = { export const MENU_TYPE = {
EMPTY: "EMPTY", EMPTY: "empty",
CONTENTS: "CONTENTS", CONTENTS: "contents",
BOARD: "BOARD", BOARD: "board",
INSIDE: "INSIDE", INSIDE: "inside",
OUTSIDE: "OUTSIDE", OUTSIDE: "outside",
BOTTOM: "BOTTOM", BOTTOM: "bottom",
} as const; } as const;
export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE]; export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE];
export interface TreeMenu { export interface TreeNode {
menuCd?: number; menuCd?: number;
name?: string; name?: string;
sortSeq?: number; sortSeq?: number;
parentId?: number | null; parentId?: number | null;
levelNo?: number; levelNo?: number;
icon?: string; icon?: string;
children?: TreeMenu[]; children?: TreeNode[];
} }
export interface Menu extends Omit<TreeMenu, "name" | "icon" | "children"> { export interface TreeMenu extends Omit<TreeNode, "name" | "icon" | "children"> {
menuKornNm: string; menuKornNm: string;
menuEngNm: string; menuEngNm: string;
iconNm: string; iconNm: string;
children: Menu[]; children: TreeMenu[];
urlPath: string; urlPath: string;
menuLnkgTypeCd: string; menuLnkgTypeCd: string;
menuLnkgSn: number; menuLnkgSn: number;
@@ -53,12 +53,12 @@ export const menuFormSchema = z.object({
MENU_TYPE.BOTTOM, MENU_TYPE.BOTTOM,
]), ]),
menuLnkgSn: z.number().nullish(), menuLnkgSn: z.number().nullish(),
otptYn: z.string().default("Y").nullish(), otptYn: z.string().default("Y"),
prntMenuCd: z.number().nullish(), prntMenuCd: z.number().nullish(),
sortSeq: z.number(), sortSeq: z.number(),
evlUseYn: z.string().default("Y").nullish(), evlUseYn: z.string().default("Y"),
qrcd: z.string().nullish(), qrcd: z.string().nullish(),
useYn: z.string().default("Y").nullish(), useYn: z.string().default("Y"),
menuLnkgSnNm: z.string().nullish(), menuLnkgSnNm: z.string().nullish(),
menuPicUserUnqId: z.string().nullish(), menuPicUserUnqId: z.string().nullish(),
menuPicUserUnqIdNm: z.string().nullish(), menuPicUserUnqIdNm: z.string().nullish(),
@@ -66,14 +66,16 @@ export const menuFormSchema = z.object({
}); });
export type MenuFormData = z.infer<typeof menuFormSchema>; export type MenuFormData = z.infer<typeof menuFormSchema>;
export type Menu = MenuFormData & { menuCd: number };
export interface MenuService { export interface MenuService {
getMenuInfo: (menuCd: number) => Promise<Menu>;
createMenu: (data: MenuFormData) => Promise<void>; createMenu: (data: MenuFormData) => Promise<void>;
modifyMenu: ( modifyMenu: (
menuCd: number, menuCd: number,
data: Omit<MenuFormData, "siteId">, data: Omit<MenuFormData, "siteId">,
) => Promise<void>; ) => Promise<void>;
getTreeMenuList: (siteId: string) => Promise<Menu[]>; getTreeMenuList: (siteId: string) => Promise<TreeMenu[]>;
modifyTreeMenu: (siteId: string, data: TreeMenu[]) => Promise<void>; modifyTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
modifyMenuName: (menuCd: number, name: string) => Promise<void>; modifyMenuName: (menuCd: number, name: string) => Promise<void>;
} }
+6 -6
View File
@@ -48,22 +48,22 @@ function MenuForm({
const onSubmit = async (data: MenuFormData) => { const onSubmit = async (data: MenuFormData) => {
if (isModifyMode) { if (isModifyMode) {
onModify(data); await onModify(data);
} else { } else {
onCreate(data); await onCreate(data);
} }
}; };
const onCreate = (data: MenuFormData) => { const onCreate = async (data: MenuFormData) => {
handleConfirm(async () => { await handleConfirm(async () => {
if (await confirm("저장하시겠습니까?")) { if (await confirm("저장하시겠습니까?")) {
handleCreate(data); handleCreate(data);
} }
}); });
}; };
const onModify = (data: MenuFormData) => { const onModify = async (data: MenuFormData) => {
handleConfirm(async () => { await handleConfirm(async () => {
if (await confirm("수정하시겠습니까?")) { if (await confirm("수정하시겠습니까?")) {
handleModify({ menuCd: menuCd!, data }); handleModify({ menuCd: menuCd!, data });
} }
+3 -3
View File
@@ -5,15 +5,15 @@ import { Button, Input } from "@common/components/ui";
import { cn } from "@common/components/lib"; import { cn } from "@common/components/lib";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import { MenuApi } from "../api/menu-service"; import { MenuApi } from "../api/menu-service";
import type { Menu } from "../model/menu-interface"; import type { TreeMenu } from "../model/menu-interface";
type MenuNodeProps = { type MenuNodeProps = {
node: NodeModel<Menu>; node: NodeModel<TreeMenu>;
depth: number; depth: number;
isOpen: boolean; isOpen: boolean;
isSelected: boolean; isSelected: boolean;
onToggle: (id: NodeModel["id"]) => void; onToggle: (id: NodeModel["id"]) => void;
onSelect: (node: NodeModel<Menu>) => void; onSelect: (node: NodeModel<TreeMenu>) => void;
}; };
function MenuNode({ function MenuNode({
+17 -9
View File
@@ -17,7 +17,7 @@ import { SiteApi } from "@/entities/site";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import { PageLayout } from "@/shared/ui/page-layout"; import { PageLayout } from "@/shared/ui/page-layout";
import { useAlertStore } from "@/shared/lib/use-alert-store"; import { useAlertStore } from "@/shared/lib/use-alert-store";
import type { Menu, MenuType } from "../model/menu-interface"; import type { TreeMenu } from "../model/menu-interface";
import { MenuApi } from "../api/menu-service"; import { MenuApi } from "../api/menu-service";
import { import {
MENU_TYPE, MENU_TYPE,
@@ -33,7 +33,13 @@ function MenuPage() {
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const [siteId, setSiteId] = useState(""); const [siteId, setSiteId] = useState("");
const [selectedMenu, setSelectedMenu] = useState<Menu | null>(null); const [selectedMenu, setSelectedMenu] = useState<TreeMenu | null>(null);
const { data: menuInfo } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, selectedMenu?.menuCd],
queryFn: () => MenuApi.getMenuInfo(selectedMenu?.menuCd ?? 0),
enabled: !!selectedMenu?.menuCd,
});
const { data: treeData } = useQuery({ const { data: treeData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId], queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
@@ -91,13 +97,15 @@ function MenuPage() {
menuLnkgTypeCd: MENU_TYPE.EMPTY, menuLnkgTypeCd: MENU_TYPE.EMPTY,
}, },
values: { values: {
...selectedMenu, ...menuInfo,
siteId, siteId,
sortSeq: selectedMenu?.sortSeq ?? 1, sortSeq: menuInfo?.sortSeq ?? 1,
levelNo: selectedMenu?.levelNo ?? 1, levelNo: menuInfo?.levelNo ?? 1,
menuKornNm: selectedMenu?.menuKornNm ?? "", menuKornNm: menuInfo?.menuKornNm ?? "",
menuLnkgTypeCd: menuLnkgTypeCd: menuInfo?.menuLnkgTypeCd ?? MENU_TYPE.EMPTY,
(selectedMenu?.menuLnkgTypeCd as MenuType) ?? MENU_TYPE.EMPTY, otptYn: menuInfo?.otptYn ?? "Y",
evlUseYn: menuInfo?.evlUseYn ?? "Y",
useYn: menuInfo?.useYn ?? "Y",
}, },
}); });
@@ -106,7 +114,7 @@ function MenuPage() {
form.setValue("siteId", value); form.setValue("siteId", value);
}; };
const handleNodeSelect = (node: NodeModel<Menu>) => { const handleNodeSelect = (node: NodeModel<TreeMenu>) => {
if (!node.data) return; if (!node.data) return;
setSelectedMenu(node.data); setSelectedMenu(node.data);
}; };
+6 -6
View File
@@ -10,15 +10,15 @@ import {
} from "@minoru/react-dnd-treeview"; } from "@minoru/react-dnd-treeview";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import { MenuApi } from "../api/menu-service"; import { MenuApi } from "../api/menu-service";
import type { Menu, TreeMenu } from "../model/menu-interface"; import type { TreeMenu, TreeNode } from "../model/menu-interface";
import { handleDropNode, handleSelectNode } from "../lib/utils"; import { handleDropNode, handleSelectNode } from "../lib/utils";
import { MenuNode } from "./menu-node"; import { MenuNode } from "./menu-node";
type MenuTreeProps = { type MenuTreeProps = {
treeData: NodeModel<Menu>[]; treeData: NodeModel<TreeMenu>[];
siteId: string; siteId: string;
selectedMenu: Menu | null; selectedMenu: TreeMenu | null;
onSelectNode: (node: NodeModel<Menu>) => void; onSelectNode: (node: NodeModel<TreeMenu>) => void;
}; };
function MenuTree({ function MenuTree({
@@ -30,7 +30,7 @@ function MenuTree({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutate: modifyTreeMenu } = useMutation({ const { mutate: modifyTreeMenu } = useMutation({
mutationFn: (newTree: TreeMenu) => mutationFn: (newTree: TreeNode) =>
MenuApi.modifyTreeMenu(siteId, [newTree]), MenuApi.modifyTreeMenu(siteId, [newTree]),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
@@ -44,7 +44,7 @@ function MenuTree({
<Tree <Tree
tree={treeData} tree={treeData}
rootId={0} rootId={0}
render={(node: NodeModel<Menu>, { depth, isOpen, onToggle }) => ( render={(node: NodeModel<TreeMenu>, { depth, isOpen, onToggle }) => (
<MenuNode <MenuNode
node={node} node={node}
depth={depth} depth={depth}