Files
invyone/frontend/components/control/ControlMode.tsx
T
DDD1542 2f398ae0b3 chore: 제어모드 IDE 작업 + v2/legacy 레지스트리 컴포넌트 폐기
- 제어모드 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 후속 노트
2026-05-19 21:31:03 +09:00

186 lines
7.2 KiB
TypeScript

'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<string, any>[];
canvasRef: React.RefObject<HTMLDivElement | null>;
}
/**
* 제어 모드 — 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<string | null>(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<string, string> = {};
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<string, any>[] = 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<string, any>[] = [];
flowEdges.forEach((edge: Record<string, any>, 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<HTMLElement>('[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<HTMLElement>('[data-card-id]').forEach((el) => {
el.removeAttribute('data-flow-active');
});
};
}, [active, selectedCardId, canvasRef]);
if (!active) return null;
return (
<>
{/* 카드 미선택 — FlowViewer (호버 토폴로지) + 안내 칩 + FAB */}
{!selectedCard && (
<>
<FlowViewer cards={cards} canvasRef={canvasRef} dashboardId={dashboardId} />
<div className="ctrl-mode-hint">
<MousePointerClick size={13} style={{ color: 'rgb(0, 154, 150)' }} />
<span> <b>Control IDE</b> </span>
</div>
<CtrlFab onExit={toggleControlMode} />
</>
)}
{/* 카드 선택 — IDE 5-분할 takeover */}
{selectedCard && (
<div className="ctrl-ide-shell">
<ContextBar
selectedCard={selectedCard}
onExit={() => setSelectedCardId(null)}
onCtrlExit={toggleControlMode}
/>
<LeftRail cards={cards} selectedCardId={selectedCardId!} />
<div className="ctrl-ide-canvas">
<Canvas card={selectedCard} canvasRef={canvasRef} dashboardId={dashboardId} />
</div>
<RightRail selectedCard={selectedCard} />
<StatusBar selectedCard={selectedCard} />
</div>
)}
</>
);
}
/** 호환성 stub — 외부에서 이름으로만 import 하는 경우 */
export function ControlPaletteWrapper() {
return null;
}