"use client"; import { useState, useCallback, useEffect, useRef, useMemo } from "react"; import { Search, X } from "lucide-react"; import { NODE_PALETTE, NODE_CATEGORIES } from "./sidebar/nodePaletteConfig"; import type { NodePaletteItem } from "@/types/node-editor"; const TOSS_CATEGORY_LABELS: Record = { source: "데이터를 가져와요", transform: "데이터를 가공해요", action: "데이터를 저장해요", external: "외부로 연결해요", utility: "도구", }; const TOSS_NODE_DESCRIPTIONS: Record = { tableSource: "내부 데이터베이스에서 데이터를 읽어와요", externalDBSource: "외부 데이터베이스에 연결해서 데이터를 가져와요", restAPISource: "REST API를 호출해서 데이터를 받아와요", condition: "조건에 따라 데이터 흐름을 나눠요", dataTransform: "데이터를 원하는 형태로 바꿔요", aggregate: "합계, 평균 등 집계 연산을 수행해요", formulaTransform: "수식을 이용해서 새로운 값을 계산해요", insertAction: "데이터를 테이블에 새로 추가해요", updateAction: "기존 데이터를 수정해요", deleteAction: "데이터를 삭제해요", upsertAction: "있으면 수정하고, 없으면 새로 추가해요", emailAction: "이메일을 자동으로 보내요", scriptAction: "외부 스크립트를 실행해요", httpRequestAction: "HTTP 요청을 보내요", procedureCallAction: "DB 프로시저를 호출해요", comment: "메모를 남겨요", }; interface CommandPaletteProps { isOpen: boolean; onClose: () => void; onSelectNode: (nodeType: string) => void; } export function CommandPalette({ isOpen, onClose, onSelectNode }: CommandPaletteProps) { const [query, setQuery] = useState(""); const [focusIndex, setFocusIndex] = useState(0); const inputRef = useRef(null); const listRef = useRef(null); const filteredItems = useMemo(() => { if (!query.trim()) return NODE_PALETTE; const q = query.toLowerCase(); return NODE_PALETTE.filter( (item) => item.label.toLowerCase().includes(q) || item.description.toLowerCase().includes(q) || item.type.toLowerCase().includes(q) || (TOSS_NODE_DESCRIPTIONS[item.type] || "").toLowerCase().includes(q), ); }, [query]); const groupedItems = useMemo(() => { const groups: { category: string; label: string; items: NodePaletteItem[] }[] = []; for (const cat of NODE_CATEGORIES) { const items = filteredItems.filter((i) => i.category === cat.id); if (items.length > 0) { groups.push({ category: cat.id, label: TOSS_CATEGORY_LABELS[cat.id] || cat.label, items, }); } } return groups; }, [filteredItems]); const flatItems = useMemo(() => groupedItems.flatMap((g) => g.items), [groupedItems]); useEffect(() => { if (isOpen) { setQuery(""); setFocusIndex(0); setTimeout(() => inputRef.current?.focus(), 50); } }, [isOpen]); useEffect(() => { setFocusIndex(0); }, [query]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Escape") { onClose(); } else if (e.key === "ArrowDown") { e.preventDefault(); setFocusIndex((i) => Math.min(i + 1, flatItems.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setFocusIndex((i) => Math.max(i - 1, 0)); } else if (e.key === "Enter" && flatItems[focusIndex]) { onSelectNode(flatItems[focusIndex].type); onClose(); } }, [flatItems, focusIndex, onClose, onSelectNode], ); useEffect(() => { const focused = listRef.current?.querySelector('[data-focused="true"]'); focused?.scrollIntoView({ block: "nearest" }); }, [focusIndex]); if (!isOpen) return null; return (
{/* backdrop */}
{/* palette */}
{/* 검색 */}
setQuery(e.target.value)} onKeyDown={handleKeyDown} placeholder="어떤 노드를 추가할까요?" className="flex-1 bg-transparent text-sm text-zinc-200 outline-none placeholder:text-zinc-600" />
{/* 목록 */}
{groupedItems.length === 0 ? (
“{query}”에 해당하는 노드를 찾지 못했어요
) : ( groupedItems.map((group) => { let groupStartIdx = 0; for (const g of groupedItems) { if (g.category === group.category) break; groupStartIdx += g.items.length; } return (
{group.label}
{group.items.map((item, idx) => { const globalIdx = groupStartIdx + idx; const isFocused = globalIdx === focusIndex; return ( ); })}
); }) )}
{/* 하단 힌트 */}
Enter {" "} 선택 Esc {" "} 닫기 ↑↓ {" "} 이동
); }