'use client'; import { useEffect, useRef } from 'react'; import { MousePointerClick } from 'lucide-react'; import { useControlMode } from './hooks/useControlMode'; import { FlowViewer } from './FlowViewer'; import { getMetaFields } from '@/lib/api/meta'; import type { FieldConfig } from '@/types/invyone-component'; import { ContextBar } from './ide/ContextBar'; import { LeftRail } from './ide/LeftRail'; import { RightRail } from './ide/RightRail'; import { Canvas } from './ide/Canvas'; import { StatusBar } from './ide/StatusBar'; import { CtrlFab } from './ide/CtrlFab'; import '@/styles/control-mode.css'; interface ControlModeProps { dashboardId: string; cards: Record[]; canvasRef: React.RefObject; } /** * 제어 모드 — Control IDE (v3 V3Takeover 베이스) * * 흐름: * 1) ⚡ 토글 ON → 대시보드 카드들이 흐려지고 FlowViewer 가 호버 토폴로지 표시 + ctrl-mode-hint + FAB * 2) 카드 클릭 → IDE 5-분할 takeover (ContextBar / LeftRail / Canvas / RightRail / StatusBar) * 3) ContextBar 의 4-segmented tabs 로 READ / EDIT / RUN / HISTORY 전환 * 4) ContextBar 의 ✕ 닫기 → 카드 선택 해제 (제어 유지) * 5) ContextBar 의 제어 종료 → 제어 모드 OFF */ export function ControlMode({ dashboardId, cards, canvasRef }: ControlModeProps) { const active = useControlMode((s) => s.active); const mode = useControlMode((s) => s.mode); const selectedCardId = useControlMode((s) => s.selectedCardId); const tablePositions = useControlMode((s) => s.tablePositions); const flowEdges = useControlMode((s) => s.flowEdges); const setSelectedCardId = useControlMode((s) => s.setSelectedCardId); const toggleControlMode = useControlMode((s) => s.toggleControlMode); const setRuleNodes = useControlMode((s) => s.setRuleNodes); const setRuleConnections = useControlMode((s) => s.setRuleConnections); const editInitDone = useRef(null); const selectedCard = selectedCardId ? cards.find((c) => (c.card_id ?? c.CARD_ID ?? c.id) === selectedCardId) ?? null : null; // edit 진입 시 자동 노드 등록: // - view 에서 펼쳐진 테이블이 있으면 그것들 + 관계선 // - 없으면 primary_table 1개만 좌측에 카드로 등장 (사용자가 거기서 컬럼별 마우스 연결로 룰 작성) useEffect(() => { if (!active || mode !== 'edit' || !selectedCardId) return; const key = `${selectedCardId}:${mode}:${Object.keys(tablePositions).join(',')}`; if (editInitDone.current === key) return; const { ruleNodes } = useControlMode.getState(); if (ruleNodes.length > 0) { editInitDone.current = key; return; } editInitDone.current = key; const hasView = Object.keys(tablePositions).length > 0; if (!hasView) return; // primary_table 자동 등장 X — 사용자가 LeftRail 에서 드래그할 때만 추가 // view 에서 펼쳐진 테이블 우선 if (hasView) { const tableIdMap: Record = {}; Object.keys(tablePositions).forEach((name) => { tableIdMap[name] = `tbl-${name}`; }); const xs = Object.values(tablePositions).map((p) => p.x); const ys = Object.values(tablePositions).map((p) => p.y); const minX = xs.length ? Math.min(...xs) : 0; const minY = ys.length ? Math.min(...ys) : 0; (async () => { const newNodes: Record[] = await Promise.all( Object.entries(tablePositions).map(async ([name, pos]) => { let columns: FieldConfig[] = []; let label = name; try { const meta = await getMetaFields(name); columns = (meta.fields ?? []).filter((f: FieldConfig) => !f.system); // 모든 컬럼 로드 (Phase 2 dropdown 용) label = meta.table_label ?? name; } catch { /* 빈 컬럼 */ } return { id: tableIdMap[name], type: 'table', table_name: name, label, x: pos.x - minX + 50, y: pos.y - minY + 50, columns, }; }), ); const newConns: Record[] = []; flowEdges.forEach((edge: Record, i: number) => { if (typeof edge.from === 'string' && edge.from.startsWith('CARD:')) return; const fromId = tableIdMap[edge.from]; const toId = tableIdMap[edge.to]; if (!fromId || !toId) return; newConns.push({ id: `conn-edit-${i}`, from_node_id: fromId, from_port: 'out', to_node_id: toId, to_port: 'in', }); }); setRuleNodes(newNodes); setRuleConnections(newConns); })(); } }, [active, mode, selectedCardId, tablePositions, flowEdges, setRuleNodes, setRuleConnections]); // mode 가 view 로 돌아가거나 카드 변경 시 init guard 리셋 useEffect(() => { if (mode === 'view' || !selectedCardId) { editInitDone.current = null; } }, [mode, selectedCardId]); // 캔버스에 mode 클래스 + 선택 카드 강조 클래스 useEffect(() => { const cv = canvasRef.current; if (!cv) return; cv.classList.toggle('control-mode-edit', active && mode === 'edit'); return () => { cv.classList.remove('control-mode-edit'); }; }, [active, mode, canvasRef]); // 선택된 카드 element 에 data-flow-active 토글 useEffect(() => { const cv = canvasRef.current; if (!cv) return; cv.querySelectorAll('[data-card-id]').forEach((el) => { const id = el.dataset.cardId; if (active && id === selectedCardId) { el.setAttribute('data-flow-active', '1'); } else { el.removeAttribute('data-flow-active'); } }); return () => { cv.querySelectorAll('[data-card-id]').forEach((el) => { el.removeAttribute('data-flow-active'); }); }; }, [active, selectedCardId, canvasRef]); if (!active) return null; return ( <> {/* 카드 미선택 — FlowViewer (호버 토폴로지) + 안내 칩 + FAB */} {!selectedCard && ( <>
카드를 클릭하면 Control IDE 가 펼쳐집니다
)} {/* 카드 선택 — IDE 5-분할 takeover */} {selectedCard && (
setSelectedCardId(null)} onCtrlExit={toggleControlMode} />
)} ); } /** 호환성 stub — 외부에서 이름으로만 import 하는 경우 */ export function ControlPaletteWrapper() { return null; }