'use client'; import { useEffect, useMemo, useState } from 'react'; import { Search, Star } from 'lucide-react'; import { CTRL_NODE_TYPES, useControlMode } from './hooks/useControlMode'; import { getMetaTableList } from '@/lib/api/meta'; interface ControlPaletteProps { onDropTable: (tableName: string, x: number, y: number) => void; onDropControl: (type: string, x: number, y: number) => void; } /** * 제어 모드 팔레트 — 사이드바 교체 * - 검색박스 * - ⭐ 시연용 추천 (화이트리스트) * - DB 테이블 max-height + 내부 스크롤 * - 영어/한국어 동시 표시 * - 제어 노드 16종 카테고리별 그룹 */ // 시연용 추천 화이트리스트 (있을 만한 ERP 표준 테이블 + 메뉴 캡쳐에서 확인된 것) const RECOMMENDED_TABLES = [ 'user_info', 'department', 'role_info', 'menu_master', 'authority_master', 'approval_definitions', 'approval_requests', 'approval_lines', 'audit_log', 'attach_file_info', ]; // 도메인 아이콘 매핑 (prefix 기준) function pickIcon(name: string): string { const n = name.toLowerCase(); if (n.startsWith('user') || n === 'user_info') return '👤'; if (n.startsWith('department') || n.startsWith('dept')) return '🏢'; if (n.startsWith('role') || n.startsWith('authority')) return '🛡'; if (n.startsWith('menu')) return '📂'; if (n.startsWith('approval')) return '✋'; if (n.startsWith('audit') || n.startsWith('log')) return '📜'; if (n.startsWith('attach') || n.startsWith('file')) return '📎'; if (n.startsWith('mail')) return '📨'; if (n.startsWith('ai_')) return '🤖'; if (n.startsWith('order')) return '📦'; if (n.startsWith('project')) return '📋'; if (n.startsWith('barcode') || n.startsWith('label')) return '🏷'; if (n.startsWith('batch')) return '⚙'; if (n.startsWith('config') || n.startsWith('setting')) return '⚙'; return '🗂'; } export function ControlPalette(_props: ControlPaletteProps) { const [tables, setTables] = useState[]>([]); const [search, setSearch] = useState(''); const mode = useControlMode((s) => s.mode); const isEditMode = mode === 'edit'; useEffect(() => { getMetaTableList().then(setTables).catch(() => {}); }, []); // 검색 + 추천/일반 분리 const { recommended, others } = useMemo(() => { const q = search.trim().toLowerCase(); const filtered = q ? tables.filter((t) => { const name = String(t.table_name ?? t.TABLE_NAME ?? '').toLowerCase(); const label = String(t.table_label ?? t.TABLE_LABEL ?? '').toLowerCase(); return name.includes(q) || label.includes(q); }) : tables; const rec: Record[] = []; const oth: Record[] = []; filtered.forEach((t) => { const name = String(t.table_name ?? t.TABLE_NAME ?? '').toLowerCase(); if (RECOMMENDED_TABLES.includes(name)) rec.push(t); else oth.push(t); }); // 추천은 화이트리스트 순서 유지 rec.sort((a, b) => { const an = String(a.table_name ?? a.TABLE_NAME ?? '').toLowerCase(); const bn = String(b.table_name ?? b.TABLE_NAME ?? '').toLowerCase(); return RECOMMENDED_TABLES.indexOf(an) - RECOMMENDED_TABLES.indexOf(bn); }); return { recommended: rec, others: oth }; }, [tables, search]); const handleDragStart = (e: React.DragEvent, data: Record) => { e.dataTransfer.setData('text/plain', JSON.stringify(data)); e.dataTransfer.effectAllowed = 'copy'; }; const catLabels: Record = { 트리거: '트리거', 조건: '조건 / 분기', 액션: '액션', 흐름: '흐름 제어', 연동: '외부 연동', 기록: '기록', }; const cats = ['트리거', '조건', '액션', '흐름', '연동', '기록']; const renderTableItem = (t: Record, isRecommended: boolean) => { const name = t.table_name ?? t.TABLE_NAME; const rawLabel = t.table_label ?? t.TABLE_LABEL; const label = rawLabel && rawLabel !== name ? rawLabel : null; const icon = pickIcon(String(name)); return (
handleDragStart(e, { kind: 'table', name })} > {icon} {label ?? name} {label && {name}} {isRecommended && }
); }; return (
{/* 헤더 */}
제어 팔레트 {!isEditMode && ( 편집 모드에서 활성 )}
{/* 검색박스 */}
setSearch(e.target.value)} disabled={!isEditMode} />
{/* 주요 테이블 (자주 쓰는 ERP 표준) */} {recommended.length > 0 && ( <>
주요 테이블 {recommended.length}
{recommended.map((t) => renderTableItem(t, true))}
)} {/* 전체 DB 테이블 (max-height + 내부 스크롤) */}
DB 테이블 {others.length > 0 && {others.length}}
{others.map((t) => renderTableItem(t, false))} {others.length === 0 && search && (
검색 결과 없음
)} {others.length === 0 && !search && tables.length === 0 && (
로딩 중…
)}
{/* 제어 노드 카테고리별 */} {cats.map((cat) => { const items = Object.entries(CTRL_NODE_TYPES).filter(([, d]) => { if (d.cat !== cat) return false; if (!search.trim()) return true; const q = search.trim().toLowerCase(); return d.label.toLowerCase().includes(q); }); if (!items.length) return null; return (
{catLabels[cat] ?? cat}
{items.map(([type, def]) => (
handleDragStart(e, { kind: 'control', type })} > {def.icon} {def.label}
))}
); })}
); }