2f398ae0b3
- 제어모드 IDE: ControlCardPanel, control/ide/* (Canvas/LeftRail/RightRail/PanZoomStage/V3RuleNode 등), schemas, lib/api/control - 레지스트리 정리: aggregation-widget, status-count, section-card/paper, table-list(legacy/v2), tabs-widget 폐기 → table/_shared/ 로 통합 - InvLegacyButtonConfigPanel cp 마이그레이션 - canonical data view cleanup 후속 노트
222 lines
7.9 KiB
TypeScript
222 lines
7.9 KiB
TypeScript
'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<Record<string, any>[]>([]);
|
|
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<string, any>[] = [];
|
|
const oth: Record<string, any>[] = [];
|
|
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<string, any>) => {
|
|
e.dataTransfer.setData('text/plain', JSON.stringify(data));
|
|
e.dataTransfer.effectAllowed = 'copy';
|
|
};
|
|
|
|
const catLabels: Record<string, string> = {
|
|
트리거: '트리거',
|
|
조건: '조건 / 분기',
|
|
액션: '액션',
|
|
흐름: '흐름 제어',
|
|
연동: '외부 연동',
|
|
기록: '기록',
|
|
};
|
|
const cats = ['트리거', '조건', '액션', '흐름', '연동', '기록'];
|
|
|
|
const renderTableItem = (t: Record<string, any>, 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 (
|
|
<div
|
|
key={name}
|
|
className={`ctrl-palette-item${isRecommended ? ' ctrl-palette-item-rec' : ''}`}
|
|
draggable
|
|
title={`${label ?? name}${label ? ` (${name})` : ''} — 캔버스로 드래그`}
|
|
onDragStart={(e) => handleDragStart(e, { kind: 'table', name })}
|
|
>
|
|
<span className="cp-icon">{icon}</span>
|
|
<span className="cp-label">
|
|
<span className="cp-label-main">{label ?? name}</span>
|
|
{label && <span className="cp-label-sub">{name}</span>}
|
|
</span>
|
|
{isRecommended && <Star size={9} className="cp-rec-star" />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="ctrl-palette">
|
|
{/* 헤더 */}
|
|
<div className="ctrl-palette-header">
|
|
<span className="ctrl-palette-header-title">제어 팔레트</span>
|
|
{!isEditMode && (
|
|
<span className="ctrl-palette-header-hint">편집 모드에서 활성</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* 검색박스 */}
|
|
<div className="ctrl-palette-search-wrap">
|
|
<Search size={11} className="ctrl-palette-search-icon" />
|
|
<input
|
|
type="text"
|
|
className="ctrl-palette-search"
|
|
placeholder="테이블 / 노드 검색…"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
disabled={!isEditMode}
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
className={`ctrl-palette-scroll${!isEditMode ? ' disabled' : ''}`}
|
|
style={{ pointerEvents: isEditMode ? 'auto' : 'none' }}
|
|
>
|
|
{/* 주요 테이블 (자주 쓰는 ERP 표준) */}
|
|
{recommended.length > 0 && (
|
|
<>
|
|
<div className="ctrl-palette-section ctrl-palette-section-rec">
|
|
<Star size={9} style={{ marginRight: 3, fill: 'currentColor' }} />
|
|
주요 테이블
|
|
<span className="ctrl-palette-section-count">{recommended.length}</span>
|
|
</div>
|
|
<div className="ctrl-palette-tables">
|
|
{recommended.map((t) => renderTableItem(t, true))}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 전체 DB 테이블 (max-height + 내부 스크롤) */}
|
|
<div className="ctrl-palette-section">
|
|
DB 테이블
|
|
{others.length > 0 && <span className="ctrl-palette-section-count">{others.length}</span>}
|
|
</div>
|
|
<div className="ctrl-palette-tables ctrl-palette-tables-others">
|
|
{others.map((t) => renderTableItem(t, false))}
|
|
{others.length === 0 && search && (
|
|
<div className="ctrl-palette-empty">검색 결과 없음</div>
|
|
)}
|
|
{others.length === 0 && !search && tables.length === 0 && (
|
|
<div className="ctrl-palette-empty">로딩 중…</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 제어 노드 카테고리별 */}
|
|
{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 (
|
|
<div key={cat}>
|
|
<div className="ctrl-palette-section">{catLabels[cat] ?? cat}</div>
|
|
{items.map(([type, def]) => (
|
|
<div
|
|
key={type}
|
|
className="ctrl-palette-item"
|
|
draggable
|
|
title={`${def.label} — 캔버스로 드래그`}
|
|
onDragStart={(e) => handleDragStart(e, { kind: 'control', type })}
|
|
>
|
|
<span className="cp-icon">{def.icon}</span>
|
|
<span className="cp-label">
|
|
<span className="cp-label-main">{def.label}</span>
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|