diff --git a/apps/cs/src/pages/menu/model/menu-interface.ts b/apps/cs/src/pages/menu/model/menu-interface.ts index 31f319c82..91043a76c 100644 --- a/apps/cs/src/pages/menu/model/menu-interface.ts +++ b/apps/cs/src/pages/menu/model/menu-interface.ts @@ -23,16 +23,16 @@ export interface TreeNode { export interface TreeMenu extends Omit { menuKornNm: string; - menuEngNm: string; - iconNm: string; - children: TreeMenu[]; - urlPath: string; + menuEngNm?: string; + iconNm?: string; + children?: TreeMenu[]; + urlPath?: string; menuLnkgTypeCd: string; - menuLnkgSn: number; - qrcd: string; + menuLnkgSn?: number; + qrcd?: string; useYn: string; otptYn: string; - requiredUrlPath: boolean; + requiredUrlPath?: boolean; } export const menuFormSchema = z.object({ diff --git a/apps/cs/src/pages/menu/ui/editable-node.tsx b/apps/cs/src/pages/menu/ui/editable-node.tsx index 25e3af49a..9ada835e2 100644 --- a/apps/cs/src/pages/menu/ui/editable-node.tsx +++ b/apps/cs/src/pages/menu/ui/editable-node.tsx @@ -1,14 +1,23 @@ -import { Button, Input } from "@common/components/ui"; import type { ComponentProps } from "react"; +import { Button, Input } from "@common/components/ui"; type EditableNodeProps = { + depth?: number; onSave: () => void; onCancel: () => void; } & ComponentProps; -function EditableNode({ onSave, onCancel, ...props }: EditableNodeProps) { +function EditableNode({ + depth, + onSave, + onCancel, + ...props +}: EditableNodeProps) { return ( - <> +
- +
); } diff --git a/apps/cs/src/pages/menu/ui/menu-node.tsx b/apps/cs/src/pages/menu/ui/menu-node.tsx index add24f3af..bce0f8ac2 100644 --- a/apps/cs/src/pages/menu/ui/menu-node.tsx +++ b/apps/cs/src/pages/menu/ui/menu-node.tsx @@ -1,6 +1,6 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import type { NodeModel } from "@minoru/react-dnd-treeview"; +import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview"; import { Button } from "@common/components/ui"; import { cn } from "@common/components/lib"; import { ENDPOINT } from "@/shared/config"; @@ -13,6 +13,8 @@ type MenuNodeProps = { depth: number; isOpen: boolean; isSelected: boolean; + isAdditionalParent: boolean; + treeMethods: TreeMethods | null; onToggle: (id: NodeModel["id"]) => void; onSelect: (node: NodeModel) => void; }; @@ -22,6 +24,8 @@ function MenuNode({ depth, isOpen, isSelected, + isAdditionalParent, + treeMethods, onToggle, onSelect, }: MenuNodeProps) { @@ -58,6 +62,10 @@ function MenuNode({ setNodeName(text); }; + useEffect(() => { + if (isAdditionalParent) treeMethods?.open(id); + }, [isAdditionalParent]); + return (
(null); + const [additionalNode, setAdditionalNode] = + useState | null>(null); const { data: menuInfo } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.MENUS, selectedMenu?.menuCd], @@ -65,7 +67,7 @@ function MenuPage() { })), }); - const { mutate: createMenu } = useMutation({ + const { mutateAsync: createMenu } = useMutation({ mutationFn: (data: MenuFormData) => MenuApi.createMenu(data), onSuccess: () => { alert("저장되었습니다."); @@ -99,8 +101,8 @@ function MenuPage() { values: { ...menuInfo, siteId, - sortSeq: menuInfo?.sortSeq ?? 1, levelNo: menuInfo?.levelNo ?? 1, + sortSeq: menuInfo?.sortSeq ?? 1, menuKornNm: menuInfo?.menuKornNm ?? "", menuLnkgTypeCd: menuInfo?.menuLnkgTypeCd ?? MENU_TYPE.EMPTY, otptYn: menuInfo?.otptYn ?? "Y", @@ -109,21 +111,64 @@ function MenuPage() { }, }); + const additionalNodeForm = useForm({ + resolver: zodResolver(menuFormSchema), + defaultValues: { + menuLnkgTypeCd: MENU_TYPE.EMPTY, + levelNo: 1, + sortSeq: 1, + otptYn: "Y", + evlUseYn: "Y", + useYn: "Y", + }, + }); + + const handleAdditionalNodeSave = async () => { + await createMenu({ + ...additionalNodeForm.getValues(), + prntMenuCd: additionalNode?.parent as number, + }); + setAdditionalNode(null); + }; + + const handleAdditionalNodeCancel = () => { + additionalNodeForm.setValue("menuKornNm", ""); + setAdditionalNode(null); + }; + + const handleDelete = async () => {}; + const handleSiteIdChange = (value: string) => { - setSiteId(value); form.setValue("siteId", value); + additionalNodeForm.setValue("siteId", value); + + setSiteId(value); + setSelectedMenu(null); }; const handleNodeSelect = (node: NodeModel) => { if (!node.data) return; if (node.data.menuCd === selectedMenu?.menuCd) { setSelectedMenu(null); + additionalNodeForm.setValue("levelNo", 1); return; } setSelectedMenu(node.data); + additionalNodeForm.setValue("levelNo", (node.data.levelNo ?? 0) + 1); }; - const handleDelete = async () => {}; + const handleAddMenu = () => { + if (additionalNode) return; + setAdditionalNode({ + id: -1, + parent: selectedMenu?.menuCd ?? 0, + text: "", + }); + }; + + const tree = additionalNode + ? [...(treeData ?? []), ...(additionalNode ? [additionalNode] : [])] + : (treeData ?? []); return ( @@ -144,19 +189,32 @@ function MenuPage() { {siteId && ( <>
- -
diff --git a/apps/cs/src/pages/menu/ui/menu-tree.tsx b/apps/cs/src/pages/menu/ui/menu-tree.tsx index a7d6b0fd3..59f55b4c2 100644 --- a/apps/cs/src/pages/menu/ui/menu-tree.tsx +++ b/apps/cs/src/pages/menu/ui/menu-tree.tsx @@ -1,33 +1,44 @@ "use client"; +import { useRef } from "react"; import { DndProvider } from "react-dnd"; +import type { UseFormReturn } from "react-hook-form"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Tree, MultiBackend, getBackendOptions, type NodeModel, + type TreeMethods, } from "@minoru/react-dnd-treeview"; import { ENDPOINT } from "@/shared/config"; import { MenuApi } from "../api/menu-service"; -import type { TreeMenu, TreeNode } from "../model/menu-interface"; +import type { MenuFormData, TreeMenu, TreeNode } from "../model/menu-interface"; import { handleDropNode, handleSelectNode } from "../lib/utils"; import { MenuNode } from "./menu-node"; +import { EditableNode } from "./editable-node"; type MenuTreeProps = { - treeData: NodeModel[]; + tree: NodeModel[]; siteId: string; selectedMenu: TreeMenu | null; + additionalNodeForm: UseFormReturn; onSelectNode: (node: NodeModel) => void; + onAdditionalNodeSave: () => void; + onAdditionalNodeCancel: () => void; }; function MenuTree({ - treeData, + tree, siteId, selectedMenu, + additionalNodeForm, onSelectNode, + onAdditionalNodeSave, + onAdditionalNodeCancel, }: MenuTreeProps) { const queryClient = useQueryClient(); + const treeMethods = useRef(null); const { mutate: modifyTreeMenu } = useMutation({ mutationFn: (newTree: TreeNode) => @@ -39,21 +50,46 @@ function MenuTree({ }, }); + const additionalNode = tree.find((n) => n.id === -1); + + const isAdditionalParent = (node: NodeModel): boolean => { + if (!additionalNode) return false; + return additionalNode?.parent === node.id; + }; + return ( , { depth, isOpen, onToggle }) => ( - - )} + ref={treeMethods} + render={(node: NodeModel, { depth, isOpen, onToggle }) => { + if (node.id === -1) { + return ( + + additionalNodeForm.setValue("menuKornNm", e.target.value) + } + depth={depth} + onSave={onAdditionalNodeSave} + onCancel={onAdditionalNodeCancel} + /> + ); + } + return ( + + ); + }} onDrop={handleDropNode(modifyTreeMenu)} classes={{ root: "p-4",