"use client"; import { useState, useEffect, useCallback, useMemo } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Loader2, Search, ChevronRight, ChevronDown, FolderOpen, FileText } from "lucide-react"; import { menuApi } from "@/lib/api/menu"; import { MenuItem } from "@/types/menu"; import { cn } from "@/lib/utils"; interface MenuSelectModalProps { isOpen: boolean; onClose: () => void; onConfirm: (menuObjids: number[]) => void; selectedMenuObjids?: number[]; } // 트리 구조의 메뉴 노드 interface MenuTreeNode { objid: string; menuNameKor: string; menuUrl: string; level: number; children: MenuTreeNode[]; parentObjId: string; } export function MenuSelectModal({ isOpen, onClose, onConfirm, selectedMenuObjids = [], }: MenuSelectModalProps) { const [menus, setMenus] = useState([]); const [isLoading, setIsLoading] = useState(false); const [searchText, setSearchText] = useState(""); const [selectedIds, setSelectedIds] = useState>(new Set(selectedMenuObjids)); const [expandedIds, setExpandedIds] = useState>(new Set()); // 초기 선택 상태 동기화 useEffect(() => { if (isOpen) { setSelectedIds(new Set(selectedMenuObjids)); } }, [isOpen, selectedMenuObjids]); // 메뉴 목록 로드 useEffect(() => { if (isOpen) { fetchMenus(); } }, [isOpen]); const fetchMenus = async () => { setIsLoading(true); try { const response = await menuApi.getUserMenus(); if (response.success && response.data) { setMenus(response.data); // 처음 2레벨까지 자동 확장 const initialExpanded = new Set(); response.data.forEach((menu) => { const level = menu.lev || menu.LEV || 1; if (level <= 2) { initialExpanded.add(menu.objid || menu.OBJID || ""); } }); setExpandedIds(initialExpanded); } } catch (error) { console.error("메뉴 로드 오류:", error); } finally { setIsLoading(false); } }; // 메뉴 트리 구조 생성 const menuTree = useMemo(() => { const menuMap = new Map(); const rootMenus: MenuTreeNode[] = []; // 모든 메뉴를 노드로 변환 menus.forEach((menu) => { const objid = menu.objid || menu.OBJID || ""; const parentObjId = menu.parentObjId || menu.PARENT_OBJ_ID || ""; const menuNameKor = menu.menuNameKor || menu.MENU_NAME_KOR || menu.translated_name || menu.TRANSLATED_NAME || ""; const menuUrl = menu.menuUrl || menu.MENU_URL || ""; const level = menu.lev || menu.LEV || 1; menuMap.set(objid, { objid, menuNameKor, menuUrl, level, children: [], parentObjId, }); }); // 부모-자식 관계 설정 menus.forEach((menu) => { const objid = menu.objid || menu.OBJID || ""; const parentObjId = menu.parentObjId || menu.PARENT_OBJ_ID || ""; const node = menuMap.get(objid); if (!node) return; // 최상위 메뉴인지 확인 (parent가 없거나, 특정 루트 ID) const parent = menuMap.get(parentObjId); if (parent) { parent.children.push(node); } else { rootMenus.push(node); } }); // 자식 메뉴 정렬 const sortChildren = (nodes: MenuTreeNode[]) => { nodes.sort((a, b) => a.menuNameKor.localeCompare(b.menuNameKor, "ko")); nodes.forEach((node) => sortChildren(node.children)); }; sortChildren(rootMenus); return rootMenus; }, [menus]); // 검색 필터링 const filteredTree = useMemo(() => { if (!searchText.trim()) return menuTree; const searchLower = searchText.toLowerCase(); // 검색어에 맞는 노드와 그 조상 노드를 포함 const filterNodes = (nodes: MenuTreeNode[]): MenuTreeNode[] => { return nodes .map((node) => { const filteredChildren = filterNodes(node.children); const matches = node.menuNameKor.toLowerCase().includes(searchLower); if (matches || filteredChildren.length > 0) { return { ...node, children: filteredChildren, }; } return null; }) .filter((node): node is MenuTreeNode => node !== null); }; return filterNodes(menuTree); }, [menuTree, searchText]); // 체크박스 토글 const toggleSelect = useCallback((objid: string) => { const numericId = Number(objid); setSelectedIds((prev) => { const next = new Set(prev); if (next.has(numericId)) { next.delete(numericId); } else { next.add(numericId); } return next; }); }, []); // 확장/축소 토글 const toggleExpand = useCallback((objid: string) => { setExpandedIds((prev) => { const next = new Set(prev); if (next.has(objid)) { next.delete(objid); } else { next.add(objid); } return next; }); }, []); // 확인 버튼 클릭 const handleConfirm = () => { onConfirm(Array.from(selectedIds)); onClose(); }; // 메뉴 노드 렌더링 const renderMenuNode = (node: MenuTreeNode, depth: number = 0) => { const hasChildren = node.children.length > 0; const isExpanded = expandedIds.has(node.objid); const isSelected = selectedIds.has(Number(node.objid)); return (
toggleSelect(node.objid)} > {/* 확장/축소 버튼 */} {hasChildren ? ( ) : (
)} {/* 체크박스 - 모든 메뉴에서 선택 가능 */} toggleSelect(node.objid)} onClick={(e) => e.stopPropagation()} /> {/* 아이콘 */} {hasChildren ? ( ) : ( )} {/* 메뉴명 */} {node.menuNameKor}
{/* 자식 메뉴 */} {hasChildren && isExpanded && (
{node.children.map((child) => renderMenuNode(child, depth + 1))}
)}
); }; return ( 사용 메뉴 선택 이 리포트를 사용할 메뉴를 선택하세요. 선택한 메뉴에서 이 리포트를 사용할 수 있습니다. {/* 검색 */}
setSearchText(e.target.value)} className="pl-10" />
{/* 선택된 메뉴 수 */}
{selectedIds.size}개 메뉴 선택됨
{/* 메뉴 트리 */} {isLoading ? (
메뉴 로드 중...
) : filteredTree.length === 0 ? (
{searchText ? "검색 결과가 없습니다." : "표시할 메뉴가 없습니다."}
) : (
{filteredTree.map((node) => renderMenuNode(node))}
)}
); }