"use client"; import React, { useState, useMemo, useRef, useEffect, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { Search, X, ChevronDown } from "lucide-react"; import * as LucideIcons from "lucide-react"; type IconComponent = React.FC<{ className?: string }>; // lucide-react에서 아이콘 컴포넌트만 필터링 (유틸 함수, 타입 등 제외) const EXCLUDED_EXPORTS = new Set([ "createLucideIcon", "defaultAttributes", "Icon", "icons", "default", ]); // PascalCase인지 확인 (아이콘 컴포넌트는 모두 PascalCase) const isPascalCase = (str: string): boolean => /^[A-Z][a-zA-Z0-9]*$/.test(str); // 한글 키워드 매핑 (자주 쓰는 아이콘에 한글 검색어 추가) const KOREAN_KEYWORDS: Record = { Home: ["홈", "메인", "대시보드"], FileText: ["문서", "파일", "텍스트"], Users: ["사용자", "회원", "인사", "팀"], User: ["사용자", "회원", "개인"], Settings: ["설정", "관리", "시스템"], Shield: ["보안", "권한", "관리자"], Package: ["제품", "품목", "패키지", "상품"], BarChart3: ["통계", "차트", "분석", "리포트"], BarChart2: ["통계", "차트", "분석"], BarChart: ["통계", "차트"], Building2: ["회사", "조직", "건물", "부서"], Building: ["회사", "건물"], ShoppingCart: ["영업", "판매", "주문", "장바구니"], ShoppingBag: ["쇼핑", "가방", "구매"], Truck: ["물류", "배송", "운송", "출하"], Warehouse: ["창고", "재고", "입고"], Factory: ["생산", "공장", "제조"], Wrench: ["설비", "유지보수", "수리", "도구"], ClipboardCheck: ["품질", "검사", "체크리스트"], ClipboardList: ["작업지시", "지시서", "할일"], Clipboard: ["클립보드", "복사"], DollarSign: ["회계", "금액", "비용", "가격"], Receipt: ["영수증", "청구", "전표"], Calendar: ["일정", "캘린더", "날짜"], CalendarDays: ["일정", "캘린더", "날짜", "일"], Clock: ["시간", "이력", "히스토리"], FolderOpen: ["폴더", "분류", "카테고리"], Folder: ["폴더", "분류", "그룹"], FolderPlus: ["폴더추가", "분류추가"], Database: ["데이터", "DB", "저장소"], Globe: ["글로벌", "다국어", "웹", "세계"], Mail: ["메일", "이메일"], Bell: ["알림", "벨", "통지"], BellRing: ["알림", "벨", "울림"], Search: ["검색", "조회", "찾기"], ListOrdered: ["목록", "리스트", "순서"], List: ["목록", "리스트"], LayoutGrid: ["그리드", "레이아웃", "화면"], LayoutDashboard: ["대시보드", "레이아웃"], Tag: ["태그", "라벨", "분류"], Tags: ["태그", "라벨", "분류", "복수"], BookOpen: ["문서", "매뉴얼", "가이드"], Book: ["책", "문서"], Boxes: ["BOM", "자재", "부품", "구성"], Box: ["박스", "상자", "제품"], GitBranch: ["흐름", "분기", "프로세스"], Workflow: ["워크플로우", "플로우", "프로세스"], ArrowRightLeft: ["이동", "전환", "교환"], ArrowRight: ["오른쪽", "다음", "진행"], ArrowLeft: ["왼쪽", "이전", "뒤로"], ArrowUp: ["위", "상승", "업"], ArrowDown: ["아래", "하강", "다운"], Layers: ["레이어", "계층", "구조"], PieChart: ["파이차트", "통계", "비율"], TrendingUp: ["추세", "성장", "상승"], TrendingDown: ["추세", "하락", "하강"], AlertTriangle: ["경고", "주의"], AlertCircle: ["경고", "주의", "원"], CheckCircle: ["완료", "승인", "확인"], CheckCircle2: ["완료", "승인", "확인"], Check: ["확인", "체크"], Cog: ["톱니바퀴", "설정", "옵션"], Map: ["지도", "위치", "경로"], MapPin: ["지도핀", "위치", "장소"], Printer: ["프린터", "인쇄", "출력"], UserCog: ["사용자설정", "계정", "프로필"], UserPlus: ["사용자추가", "회원가입"], UserCheck: ["사용자확인", "인증"], Key: ["키", "권한", "인증", "보안"], Lock: ["잠금", "보안", "비밀번호"], LockOpen: ["잠금해제", "열기"], Unlock: ["잠금해제"], Hammer: ["작업", "공구", "수리"], Ruler: ["측정", "규격", "사양"], Scan: ["스캔", "바코드", "QR"], QrCode: ["QR코드", "큐알"], ScrollText: ["계약", "문서", "스크롤"], HandCoins: ["구매", "발주", "거래"], CircleDollarSign: ["매출", "수익", "원가"], FileSpreadsheet: ["엑셀", "스프레드시트", "표"], FilePlus2: ["신규", "추가", "등록"], FilePlus: ["파일추가", "신규"], FileCheck2: ["승인", "결재", "확인"], FileCheck: ["파일확인"], Zap: ["전기", "에너지", "빠른"], Gauge: ["게이지", "성능", "속도"], HardDrive: ["저장", "서버", "디스크"], Monitor: ["모니터", "화면", "디스플레이"], Smartphone: ["모바일", "스마트폰", "앱"], Lightbulb: ["아이디어", "제안", "개선"], Star: ["별", "즐겨찾기", "중요"], Heart: ["좋아요", "관심", "찜"], Bookmark: ["북마크", "저장", "즐겨찾기"], Flag: ["플래그", "깃발", "표시"], Award: ["수상", "인증", "포상"], Trophy: ["트로피", "우승", "성과"], Target: ["목표", "타겟", "대상"], Crosshair: ["크로스헤어", "조준", "정확"], Eye: ["보기", "조회", "미리보기"], EyeOff: ["숨기기", "비공개"], Image: ["이미지", "사진", "그림"], Camera: ["카메라", "사진", "촬영"], Video: ["비디오", "영상", "동영상"], Music: ["음악", "오디오", "사운드"], Mic: ["마이크", "음성", "녹음"], Phone: ["전화", "연락", "콜"], PhoneCall: ["통화", "전화"], MessageSquare: ["메시지", "채팅", "대화"], MessageCircle: ["메시지", "채팅"], Send: ["보내기", "전송", "발송"], Share2: ["공유", "전달"], Link: ["링크", "연결", "URL"], ExternalLink: ["외부링크", "새창"], Download: ["다운로드", "내려받기"], Upload: ["업로드", "올리기"], CloudUpload: ["클라우드업로드", "올리기"], CloudDownload: ["클라우드다운로드", "내려받기"], Cloud: ["클라우드", "구름"], Server: ["서버", "시스템"], Cpu: ["CPU", "프로세서", "처리"], Wifi: ["와이파이", "네트워크", "무선"], Activity: ["활동", "모니터링", "심박"], Thermometer: ["온도", "온도계", "측정"], Droplets: ["물", "수질", "액체"], Wind: ["바람", "공기", "환기"], Sun: ["태양", "밝기", "낮"], Moon: ["달", "야간", "다크모드"], Umbrella: ["우산", "보호", "보험"], Compass: ["나침반", "방향", "가이드"], Navigation: ["네비게이션", "안내"], RotateCcw: ["되돌리기", "새로고침", "초기화"], RefreshCw: ["새로고침", "갱신", "동기화"], Repeat: ["반복", "되풀이"], Shuffle: ["셔플", "무작위", "랜덤"], Filter: ["필터", "거르기", "조건"], SlidersHorizontal: ["슬라이더", "조정", "필터"], Maximize2: ["최대화", "전체화면"], Minimize2: ["최소화", "축소"], Move: ["이동", "옮기기"], Copy: ["복사", "복제"], Scissors: ["가위", "잘라내기"], Trash2: ["삭제", "쓰레기통", "휴지통"], Trash: ["삭제", "쓰레기"], Archive: ["보관", "아카이브", "저장"], ArchiveRestore: ["복원", "복구"], Plus: ["추가", "더하기", "플러스"], Minus: ["빼기", "마이너스", "제거"], PlusCircle: ["추가", "원형추가"], MinusCircle: ["제거", "원형제거"], XCircle: ["닫기", "취소", "제거"], Info: ["정보", "안내", "도움말"], HelpCircle: ["도움말", "질문", "안내"], CircleAlert: ["경고", "주의", "원형경고"], Ban: ["금지", "차단", "비허용"], ShieldCheck: ["보안확인", "인증완료"], ShieldAlert: ["보안경고", "위험"], LogIn: ["로그인", "접속"], LogOut: ["로그아웃", "종료"], Power: ["전원", "켜기/끄기"], ToggleLeft: ["토글", "스위치", "끄기"], ToggleRight: ["토글", "스위치", "켜기"], Percent: ["퍼센트", "비율", "할인"], Hash: ["해시", "번호", "코드"], AtSign: ["앳", "이메일", "골뱅이"], Code: ["코드", "개발", "프로그래밍"], Terminal: ["터미널", "명령어", "콘솔"], Table: ["테이블", "표", "데이터"], Table2: ["테이블", "표"], Columns: ["컬럼", "열", "항목"], Rows: ["행", "줄"], Grid3x3: ["그리드", "격자", "표"], PanelLeft: ["패널", "사이드바", "왼쪽"], PanelRight: ["패널", "사이드바", "오른쪽"], Split: ["분할", "나누기"], Combine: ["결합", "합치기"], Network: ["네트워크", "연결망"], Radio: ["라디오", "옵션"], CircleDot: ["원형점", "선택"], SquareCheck: ["체크박스", "선택"], Square: ["사각형", "상자"], Circle: ["원", "동그라미"], Triangle: ["삼각형", "세모"], Hexagon: ["육각형", "벌집"], Diamond: ["다이아몬드", "마름모"], Pen: ["펜", "작성", "편집"], Pencil: ["연필", "수정", "편집"], PenLine: ["펜라인", "서명"], Eraser: ["지우개", "삭제", "초기화"], Palette: ["팔레트", "색상", "디자인"], Paintbrush: ["브러시", "페인트", "디자인"], Figma: ["피그마", "디자인"], Type: ["타입", "글꼴", "폰트"], Bold: ["굵게", "볼드"], Italic: ["기울임", "이탤릭"], AlignLeft: ["왼쪽정렬"], AlignCenter: ["가운데정렬"], AlignRight: ["오른쪽정렬"], Footprints: ["발자국", "추적", "이력"], Fingerprint: ["지문", "인증", "보안"], ScanLine: ["스캔라인", "인식"], Barcode: ["바코드"], CreditCard: ["신용카드", "결제", "카드"], Wallet: ["지갑", "결제", "자금"], Banknote: ["지폐", "현금", "돈"], Coins: ["동전", "코인"], PiggyBank: ["저금통", "저축", "예산"], Landmark: ["랜드마크", "은행", "기관"], Store: ["매장", "상점", "가게"], GraduationCap: ["졸업", "교육", "학습"], School: ["학교", "교육", "훈련"], Library: ["도서관", "라이브러리"], BookMarked: ["북마크", "표시된책"], Notebook: ["노트북", "공책", "메모"], NotebookPen: ["노트작성", "메모"], FileArchive: ["압축파일", "아카이브"], FileAudio: ["오디오파일", "음악파일"], FileVideo: ["비디오파일", "영상파일"], FileImage: ["이미지파일", "사진파일"], FileCode: ["코드파일", "소스파일"], FileJson: ["JSON파일", "데이터파일"], FileCog: ["파일설정", "환경설정"], FileSearch: ["파일검색", "문서검색"], FileWarning: ["파일경고", "주의파일"], FileX: ["파일삭제", "파일제거"], Files: ["파일들", "다중파일"], FolderSearch: ["폴더검색"], FolderCog: ["폴더설정"], FolderInput: ["입력폴더", "수신"], FolderOutput: ["출력폴더", "발신"], FolderSync: ["폴더동기화"], FolderTree: ["폴더트리", "계층구조"], Inbox: ["받은편지함", "수신"], MailOpen: ["메일열기", "읽음"], MailPlus: ["메일추가", "새메일"], CalendarCheck: ["일정확인", "예약확인"], CalendarPlus: ["일정추가", "새일정"], CalendarX: ["일정취소", "일정삭제"], Timer: ["타이머", "시간측정"], Hourglass: ["모래시계", "대기", "로딩"], AlarmClock: ["알람", "시계"], Watch: ["시계", "손목시계"], Rocket: ["로켓", "출시", "배포"], Plane: ["비행기", "항공", "운송"], Ship: ["배", "선박", "해운"], Car: ["자동차", "차량"], Bus: ["버스", "대중교통"], Train: ["기차", "열차", "철도"], Bike: ["자전거", "이동"], Fuel: ["연료", "주유"], Construction: ["공사", "건설", "설치"], HardHat: ["안전모", "건설", "안전"], Shovel: ["삽", "건설", "시공"], Drill: ["드릴", "공구"], Nut: ["너트", "부품", "볼트"], Plug: ["플러그", "전원", "연결"], Cable: ["케이블", "선", "연결"], Battery: ["배터리", "충전"], BatteryCharging: ["충전중", "배터리"], Signal: ["신호", "강도"], Antenna: ["안테나", "수신"], Bluetooth: ["블루투스", "무선"], Usb: ["USB", "연결"], SquareStack: ["스택", "쌓기", "레이어"], Component: ["컴포넌트", "부품", "구성요소"], Puzzle: ["퍼즐", "조각", "모듈"], Blocks: ["블록", "구성요소"], GitCommit: ["커밋", "변경"], GitMerge: ["병합", "머지"], GitPullRequest: ["풀리퀘스트", "요청"], GitCompare: ["비교", "차이"], CirclePlay: ["재생", "플레이"], CirclePause: ["일시정지", "멈춤"], CircleStop: ["정지", "중지"], SkipForward: ["다음", "건너뛰기"], SkipBack: ["이전", "뒤로"], Volume2: ["볼륨", "소리"], VolumeX: ["음소거"], Headphones: ["헤드폰", "오디오"], Speaker: ["스피커", "소리"], Projector: ["프로젝터", "발표"], Presentation: ["프레젠테이션", "발표"], GanttChart: ["간트차트", "일정관리", "프로젝트"], KanbanSquare: ["칸반", "보드", "프로젝트"], ListTodo: ["할일목록", "체크리스트"], ListChecks: ["체크목록", "확인목록"], ListFilter: ["필터목록", "조건목록"], ListTree: ["트리목록", "계층목록"], StretchHorizontal: ["가로확장"], StretchVertical: ["세로확장"], Maximize: ["최대화"], Minimize: ["최소화"], Expand: ["확장", "펼치기"], Shrink: ["축소", "줄이기"], ZoomIn: ["확대"], ZoomOut: ["축소"], Focus: ["포커스", "집중"], Crosshairs: ["조준", "대상"], Locate: ["위치찾기", "현재위치"], LocateFixed: ["위치고정"], LocateOff: ["위치끄기"], Spline: ["스플라인", "곡선"], BrainCircuit: ["AI", "인공지능", "두뇌"], Brain: ["두뇌", "지능", "생각"], Bot: ["봇", "로봇", "자동화"], Sparkles: ["반짝", "AI", "마법"], Wand2: ["마법봉", "자동", "AI"], FlaskConical: ["실험", "연구", "시험"], TestTube: ["시험관", "검사", "테스트"], Microscope: ["현미경", "분석", "연구"], Stethoscope: ["청진기", "의료", "진단"], Syringe: ["주사기", "의료"], Pill: ["약", "의약품"], HeartPulse: ["심박", "건강", "의료"], Dna: ["DNA", "유전", "생명과학"], Atom: ["원자", "과학", "화학"], Beaker: ["비커", "실험", "화학"], Scale: ["저울", "무게", "측정"], Weight: ["무게", "중량"], Ratio: ["비율", "비교"], Calculator: ["계산기", "계산"], Binary: ["이진수", "코드"], Regex: ["정규식", "패턴"], Variable: ["변수", "값"], FunctionSquare: ["함수", "기능"], Braces: ["중괄호", "코드"], Brackets: ["대괄호", "배열"], Parentheses: ["소괄호", "그룹"], Tally5: ["집계", "카운트", "합계"], Sigma: ["시그마", "합계", "총합"], Infinity: ["무한", "반복"], Pi: ["파이", "수학"], Omega: ["오메가", "마지막"], }; interface IconEntry { name: string; component: IconComponent; keywords: string[]; } // 모든 Lucide 아이콘을 동적으로 가져오기 const ALL_ICONS: IconEntry[] = (() => { const entries: IconEntry[] = []; for (const [name, maybeComponent] of Object.entries(LucideIcons)) { if (EXCLUDED_EXPORTS.has(name)) continue; if (!isPascalCase(name)) continue; // lucide-react 아이콘은 forwardRef + memo로 감싸진 React 컴포넌트 (object) const comp = maybeComponent as any; const isReactComponent = typeof comp === "function" || (typeof comp === "object" && comp !== null && comp.$$typeof); if (!isReactComponent) continue; const koreanKw = KOREAN_KEYWORDS[name] || []; entries.push({ name, component: comp as IconComponent, keywords: [...koreanKw, name.toLowerCase()], }); } return entries.sort((a, b) => a.name.localeCompare(b.name)); })(); export function getIconComponent(iconName: string | null | undefined): IconComponent | null { if (!iconName) return null; const entry = ALL_ICONS.find((e) => e.name === iconName); return entry?.component || null; } interface MenuIconPickerProps { value: string; onChange: (iconName: string) => void; label?: string; } export const MenuIconPicker: React.FC = ({ value, onChange, label = "메뉴 아이콘", }) => { const [isOpen, setIsOpen] = useState(false); const [searchText, setSearchText] = useState(""); const [visibleCount, setVisibleCount] = useState(120); const containerRef = useRef(null); const scrollRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false); setSearchText(""); } }; if (isOpen) { document.addEventListener("mousedown", handleClickOutside); } return () => document.removeEventListener("mousedown", handleClickOutside); }, [isOpen]); // 드롭다운 열릴 때 표시 개수 초기화 useEffect(() => { if (isOpen) setVisibleCount(120); }, [isOpen]); // 검색어 변경 시 표시 개수 초기화 useEffect(() => { setVisibleCount(120); }, [searchText]); const filteredIcons = useMemo(() => { if (!searchText) return ALL_ICONS; const lower = searchText.toLowerCase(); return ALL_ICONS.filter( (entry) => entry.name.toLowerCase().includes(lower) || entry.keywords.some((kw) => kw.includes(lower)) ); }, [searchText]); // 스크롤 끝에 도달하면 더 로드 const handleScroll = useCallback(() => { const el = scrollRef.current; if (!el) return; if (el.scrollTop + el.clientHeight >= el.scrollHeight - 40) { setVisibleCount((prev) => Math.min(prev + 120, filteredIcons.length)); } }, [filteredIcons.length]); const selectedIcon = ALL_ICONS.find((e) => e.name === value); const SelectedIconComponent = selectedIcon?.component; return (
{isOpen && (
setSearchText(e.target.value)} className="h-8 pl-8 text-sm" onClick={(e) => e.stopPropagation()} autoFocus />
{!searchText && (

총 {ALL_ICONS.length}개 아이콘

)}
{filteredIcons.slice(0, visibleCount).map((entry) => { const IconComp = entry.component; return ( ); })}
{filteredIcons.length === 0 && (

검색 결과가 없습니다.

)}
)}
); };