From 04ed774ff60a83c00711d4e8615cb4122954dba5 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Thu, 4 Dec 2025 19:46:55 +0900 Subject: (김준회) 메뉴 관리기능 초안 개발 (시딩 필요) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/menu-v2/components/add-node-dialog.tsx | 186 +++++++++++ lib/menu-v2/components/domain-tabs.tsx | 25 ++ lib/menu-v2/components/edit-node-dialog.tsx | 215 +++++++++++++ lib/menu-v2/components/menu-tree-manager.tsx | 364 ++++++++++++++++++++++ lib/menu-v2/components/menu-tree.tsx | 282 +++++++++++++++++ lib/menu-v2/components/move-to-dialog.tsx | 87 ++++++ lib/menu-v2/components/unassigned-menus-panel.tsx | 178 +++++++++++ 7 files changed, 1337 insertions(+) create mode 100644 lib/menu-v2/components/add-node-dialog.tsx create mode 100644 lib/menu-v2/components/domain-tabs.tsx create mode 100644 lib/menu-v2/components/edit-node-dialog.tsx create mode 100644 lib/menu-v2/components/menu-tree-manager.tsx create mode 100644 lib/menu-v2/components/menu-tree.tsx create mode 100644 lib/menu-v2/components/move-to-dialog.tsx create mode 100644 lib/menu-v2/components/unassigned-menus-panel.tsx (limited to 'lib/menu-v2/components') diff --git a/lib/menu-v2/components/add-node-dialog.tsx b/lib/menu-v2/components/add-node-dialog.tsx new file mode 100644 index 00000000..b6762820 --- /dev/null +++ b/lib/menu-v2/components/add-node-dialog.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import type { + MenuDomain, + CreateMenuGroupInput, + CreateGroupInput, + CreateTopLevelMenuInput +} from "../types"; + +type DialogType = "menu_group" | "group" | "top_level_menu"; + +interface AddNodeDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + type: DialogType; + domain: MenuDomain; + parentId?: number; // group 생성 시 필요 + onSave: (data: CreateMenuGroupInput | CreateGroupInput | CreateTopLevelMenuInput) => Promise; +} + +interface FormData { + titleKo: string; + titleEn: string; + menuPath: string; +} + +export function AddNodeDialog({ + open, + onOpenChange, + type, + domain, + parentId, + onSave, +}: AddNodeDialogProps) { + const { + register, + handleSubmit, + reset, + formState: { isSubmitting, errors }, + } = useForm({ + defaultValues: { + titleKo: "", + titleEn: "", + menuPath: "", + }, + }); + + const getTitle = () => { + switch (type) { + case "menu_group": + return "Add Menu Group"; + case "group": + return "Add Group"; + case "top_level_menu": + return "Add Top-Level Menu"; + default: + return "Add"; + } + }; + + const getDescription = () => { + switch (type) { + case "menu_group": + return "A dropdown trigger displayed in the header navigation."; + case "group": + return "Groups menus within a menu group."; + case "top_level_menu": + return "A single link displayed in the header navigation."; + default: + return ""; + } + }; + + const onSubmit = async (data: FormData) => { + let saveData: CreateMenuGroupInput | CreateGroupInput | CreateTopLevelMenuInput; + + if (type === "menu_group") { + saveData = { + titleKo: data.titleKo, + titleEn: data.titleEn || undefined, + }; + } else if (type === "group" && parentId) { + saveData = { + parentId, + titleKo: data.titleKo, + titleEn: data.titleEn || undefined, + }; + } else if (type === "top_level_menu") { + saveData = { + titleKo: data.titleKo, + titleEn: data.titleEn || undefined, + menuPath: data.menuPath, + }; + } else { + return; + } + + await onSave(saveData); + reset(); + onOpenChange(false); + }; + + const handleClose = () => { + reset(); + onOpenChange(false); + }; + + return ( + + + + {getTitle()} + {getDescription()} + + +
+
+ {/* Korean Name */} +
+ + + {errors.titleKo && ( +

{errors.titleKo.message}

+ )} +
+ + {/* English Name */} +
+ + +
+ + {/* Menu Path for Top-Level Menu */} + {type === "top_level_menu" && ( +
+ + + {errors.menuPath && ( +

{errors.menuPath.message}

+ )} +

+ e.g., /{domain}/report, /{domain}/faq +

+
+ )} +
+ + + + + +
+
+
+ ); +} diff --git a/lib/menu-v2/components/domain-tabs.tsx b/lib/menu-v2/components/domain-tabs.tsx new file mode 100644 index 00000000..e52fa80b --- /dev/null +++ b/lib/menu-v2/components/domain-tabs.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import type { MenuDomain } from "../types"; + +interface DomainTabsProps { + value: MenuDomain; + onChange: (domain: MenuDomain) => void; +} + +export function DomainTabs({ value, onChange }: DomainTabsProps) { + return ( + onChange(v as MenuDomain)}> + + + EVCP (Internal) + + + Partners (Vendors) + + + + ); +} + diff --git a/lib/menu-v2/components/edit-node-dialog.tsx b/lib/menu-v2/components/edit-node-dialog.tsx new file mode 100644 index 00000000..9631a611 --- /dev/null +++ b/lib/menu-v2/components/edit-node-dialog.tsx @@ -0,0 +1,215 @@ +"use client"; + +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import type { MenuTreeNode, UpdateNodeInput } from "../types"; + +interface EditNodeDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + node: MenuTreeNode | null; + onSave: (nodeId: number, data: UpdateNodeInput) => Promise; +} + +interface FormData { + titleKo: string; + titleEn: string; + descriptionKo: string; + descriptionEn: string; + scrId: string; + isActive: boolean; +} + +export function EditNodeDialog({ + open, + onOpenChange, + node, + onSave, +}: EditNodeDialogProps) { + const { + register, + handleSubmit, + reset, + setValue, + watch, + formState: { isSubmitting }, + } = useForm({ + defaultValues: { + titleKo: "", + titleEn: "", + descriptionKo: "", + descriptionEn: "", + scrId: "", + isActive: true, + }, + }); + + const isActive = watch("isActive"); + + useEffect(() => { + if (node) { + reset({ + titleKo: node.titleKo, + titleEn: node.titleEn || "", + descriptionKo: node.descriptionKo || "", + descriptionEn: node.descriptionEn || "", + scrId: node.scrId || "", + isActive: node.isActive, + }); + } + }, [node, reset]); + + const onSubmit = async (data: FormData) => { + if (!node) return; + + await onSave(node.id, { + titleKo: data.titleKo, + titleEn: data.titleEn || undefined, + descriptionKo: data.descriptionKo || undefined, + descriptionEn: data.descriptionEn || undefined, + scrId: data.scrId || undefined, + isActive: data.isActive, + }); + + onOpenChange(false); + }; + + const getTypeLabel = () => { + switch (node?.nodeType) { + case "menu_group": + return "Menu Group"; + case "group": + return "Group"; + case "menu": + return "Menu"; + case "additional": + return "Additional Menu"; + default: + return "Node"; + } + }; + + const showMenuFields = node?.nodeType === "menu" || node?.nodeType === "additional"; + + return ( + + + + Edit {getTypeLabel()} + + {node?.menuPath && ( + {node.menuPath} + )} + + + +
+
+ {/* Korean Name */} +
+ + +
+ + {/* English Name */} +
+ + +
+ + {/* Korean Description */} + {showMenuFields && ( +
+ +