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/menu-tree.tsx | 282 +++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 lib/menu-v2/components/menu-tree.tsx (limited to 'lib/menu-v2/components/menu-tree.tsx') diff --git a/lib/menu-v2/components/menu-tree.tsx b/lib/menu-v2/components/menu-tree.tsx new file mode 100644 index 00000000..7d3ab077 --- /dev/null +++ b/lib/menu-v2/components/menu-tree.tsx @@ -0,0 +1,282 @@ +"use client"; + +import { useCallback } from "react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + ChevronRight, + ChevronDown, + ChevronUp, + Folder, + FolderOpen, + File, + Pencil, + Plus, + ArrowUpDown, + EyeOff, +} from "lucide-react"; +import type { MenuTreeNode } from "../types"; + +interface MenuTreeProps { + nodes: MenuTreeNode[]; + onEdit: (node: MenuTreeNode) => void; + onMoveUp: (nodeId: number) => void; + onMoveDown: (nodeId: number) => void; + onMoveTo: (node: MenuTreeNode) => void; + onAddGroup: (parentId: number) => void; + expandedIds: Set; + onExpandedIdsChange: (ids: Set) => void; + isPending?: boolean; +} + +interface TreeItemProps { + node: MenuTreeNode; + depth: number; + isFirst: boolean; + isLast: boolean; + onEdit: (node: MenuTreeNode) => void; + onMoveUp: (nodeId: number) => void; + onMoveDown: (nodeId: number) => void; + onMoveTo: (node: MenuTreeNode) => void; + onAddGroup: (parentId: number) => void; + isExpanded: boolean; + onToggleExpand: () => void; + isPending?: boolean; +} + +function TreeItem({ + node, + depth, + isFirst, + isLast, + onEdit, + onMoveUp, + onMoveDown, + onMoveTo, + onAddGroup, + isExpanded, + onToggleExpand, + isPending, +}: TreeItemProps) { + const isMenuGroup = node.nodeType === "menu_group"; + const isGroup = node.nodeType === "group"; + const isMenu = node.nodeType === "menu"; + const isTopLevel = node.parentId === null; + const hasChildren = node.children && node.children.length > 0; + const isExpandable = isMenuGroup || isGroup; + + // Move To is disabled for: + // - menu_group (always at top level, cannot be moved) + // - top-level menu (parentId === null, can only reorder with up/down) + const canMoveTo = !isMenuGroup && !isTopLevel; + + const getIcon = () => { + if (isMenuGroup || isGroup) { + return isExpanded ? ( + + ) : ( + + ); + } + return ; + }; + + const getTypeLabel = () => { + switch (node.nodeType) { + case "menu_group": return "Menu Group"; + case "group": return "Group"; + case "menu": return "Menu"; + default: return ""; + } + }; + + return ( +
+ {/* Expand/Collapse */} + {isExpandable ? ( + + ) : ( +
+ )} + + {/* Icon */} + {getIcon()} + + {/* Title */} + + {node.titleKo} + {node.titleEn && ( + [{node.titleEn}] + )} + + + {/* Hidden indicator */} + {!node.isActive && ( + + )} + + {/* Path (for menus) */} + {isMenu && node.menuPath && ( + + {node.menuPath} + + )} + + {/* Type Badge */} + + {getTypeLabel()} + + + {/* Active indicator */} +
+ + {/* Actions */} +
+ {/* Move Up */} + + + {/* Move Down */} + + + {/* Move To (different parent) - disabled for top level nodes */} + + + {/* Edit */} + + + {/* Add Sub-Group (for menu groups only) */} + {isMenuGroup && ( + + )} +
+
+ ); +} + +export function MenuTree({ + nodes, + onEdit, + onMoveUp, + onMoveDown, + onMoveTo, + onAddGroup, + expandedIds, + onExpandedIdsChange, + isPending, +}: MenuTreeProps) { + const toggleExpand = useCallback((nodeId: number) => { + const next = new Set(expandedIds); + if (next.has(nodeId)) { + next.delete(nodeId); + } else { + next.add(nodeId); + } + onExpandedIdsChange(next); + }, [expandedIds, onExpandedIdsChange]); + + const renderTree = (nodeList: MenuTreeNode[], depth: number) => { + return nodeList.map((node, index) => { + const isExpanded = expandedIds.has(node.id); + const isExpandable = node.nodeType === "menu_group" || node.nodeType === "group"; + const hasChildren = node.children && node.children.length > 0; + + return ( +
+ toggleExpand(node.id)} + isPending={isPending} + /> + {isExpandable && isExpanded && hasChildren && ( +
+ {renderTree(node.children!, depth + 1)} +
+ )} +
+ ); + }); + }; + + return
{renderTree(nodes, 0)}
; +} + + -- cgit v1.2.3