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 후속 노트
186 lines
7.2 KiB
TypeScript
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;
|
|
}
|