'use client'; import { useEffect, useState, useMemo, useCallback, useImperativeHandle, forwardRef } from 'react'; import { useRouter } from 'next/navigation'; import { useMenuItems, type MenuItem } from '@/stores/menuStore'; import { CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, } from '@/components/ui/command'; import { Folder, ChevronRight } from 'lucide-react'; // 평탄화된 메뉴 아이템 타입 interface FlatMenuItem { id: string; label: string; path: string; icon: React.ComponentType<{ className?: string }>; breadcrumb: string[]; // 부모 메뉴 경로 (예: ['판매관리', '거래처관리']) depth: number; } // 외부에서 제어 가능한 ref 타입 export interface CommandMenuSearchRef { open: () => void; close: () => void; toggle: () => void; } // 메뉴 트리를 평탄화하는 함수 function flattenMenuItems( items: MenuItem[], parentBreadcrumb: string[] = [] ): FlatMenuItem[] { const result: FlatMenuItem[] = []; for (const item of items) { const currentBreadcrumb = [...parentBreadcrumb, item.label]; // 실제 경로가 있는 메뉴만 추가 (# 제외) if (item.path && item.path !== '#') { result.push({ id: item.id, label: item.label, path: item.path, icon: item.icon || Folder, breadcrumb: currentBreadcrumb, depth: currentBreadcrumb.length, }); } // 자식 메뉴 재귀 처리 if (item.children && item.children.length > 0) { result.push(...flattenMenuItems(item.children, currentBreadcrumb)); } } return result; } const CommandMenuSearch = forwardRef((_, ref) => { const [open, setOpen] = useState(false); const [search, setSearch] = useState(''); const router = useRouter(); const menuItems = useMenuItems(); // 외부에서 제어할 수 있도록 ref 노출 useImperativeHandle(ref, () => ({ open: () => setOpen(true), close: () => setOpen(false), toggle: () => setOpen((prev) => !prev), })); // 평탄화된 메뉴 목록 (메모이제이션) const flatMenuItems = useMemo(() => { return flattenMenuItems(menuItems); }, [menuItems]); // 검색 필터링 (한글 초성 검색 지원) const filteredItems = useMemo(() => { if (!search.trim()) { return flatMenuItems; } const searchLower = search.toLowerCase(); return flatMenuItems.filter((item) => { // 메뉴 이름으로 검색 if (item.label.toLowerCase().includes(searchLower)) { return true; } // breadcrumb 전체 경로로 검색 const fullPath = item.breadcrumb.join(' ').toLowerCase(); if (fullPath.includes(searchLower)) { return true; } // 경로로 검색 if (item.path.toLowerCase().includes(searchLower)) { return true; } return false; }); }, [flatMenuItems, search]); // 메뉴 선택 핸들러 const handleSelect = useCallback( (item: FlatMenuItem) => { setOpen(false); setSearch(''); router.push(item.path); }, [router] ); // 키보드 단축키 (Ctrl+K / Cmd+K) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setOpen((prev) => !prev); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, []); // 다이얼로그 닫힐 때 검색어 초기화 const handleOpenChange = (isOpen: boolean) => { setOpen(isOpen); if (!isOpen) { setSearch(''); } }; return ( 검색 결과가 없습니다. {filteredItems.map((item) => { const IconComponent = item.icon; return ( handleSelect(item)} className="flex items-center gap-2 cursor-pointer" >
{item.breadcrumb.map((crumb, index) => ( {index > 0 && ( )} {crumb} ))}
{item.path}
); })}
); }); CommandMenuSearch.displayName = 'CommandMenuSearch'; export default CommandMenuSearch;