227 lines
7.1 KiB
TypeScript
227 lines
7.1 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useRef } from 'react';
|
|
import { useControlMode, genNodeId, CTRL_NODE_TYPES } from './hooks/useControlMode';
|
|
import { usePortDrag } from './hooks/usePortDrag';
|
|
import { ControlNode } from './ControlNode';
|
|
import { TableNode } from './TableNode';
|
|
import { PortHandle } from './PortHandle';
|
|
import { NodeConfigPopover } from './NodeConfigPopover';
|
|
import { bezierPath } from './ConnectionLine';
|
|
import { getMetaFields } from '@/lib/api/meta';
|
|
import type { FieldConfig } from '@/types/invyone-component';
|
|
|
|
interface RuleBuilderProps {
|
|
canvasRef: React.RefObject<HTMLDivElement | null>;
|
|
}
|
|
|
|
/** 테이블 필드 캐시 */
|
|
const fieldCache: Record<string, FieldConfig[]> = {};
|
|
|
|
/**
|
|
* 규칙 빌더 — 편집 모드
|
|
* mockup initRuleBuilder/dropTable/dropControl 포팅
|
|
*/
|
|
export function RuleBuilder({ canvasRef }: RuleBuilderProps) {
|
|
const {
|
|
ruleNodes,
|
|
ruleConnections,
|
|
addRuleNode,
|
|
moveRuleNode,
|
|
} = useControlMode();
|
|
|
|
const { startDrag, finishDrag } = usePortDrag(canvasRef);
|
|
|
|
// 캔버스 드롭 처리
|
|
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
let d: Record<string, any>;
|
|
try { d = JSON.parse(e.dataTransfer.getData('text/plain')); } catch { return; }
|
|
if (!d?.kind) return;
|
|
|
|
const cv = canvasRef.current;
|
|
if (!cv) return;
|
|
const r = cv.getBoundingClientRect();
|
|
const x = e.clientX - r.left + cv.scrollLeft;
|
|
const y = e.clientY - r.top + cv.scrollTop;
|
|
|
|
if (d.kind === 'table') {
|
|
// 중복 방지
|
|
if (ruleNodes.find((n) => n.type === 'table' && n.table_name === d.name)) return;
|
|
|
|
// 필드 로드
|
|
let cols: FieldConfig[] = [];
|
|
if (fieldCache[d.name]) {
|
|
cols = fieldCache[d.name];
|
|
} else {
|
|
try {
|
|
const meta = await getMetaFields(d.name);
|
|
cols = (meta.fields ?? []).filter((f: FieldConfig) => !f.system).slice(0, 8);
|
|
fieldCache[d.name] = cols;
|
|
} catch { /* 빈 필드 */ }
|
|
}
|
|
|
|
addRuleNode({
|
|
id: genNodeId('tbl'),
|
|
type: 'table',
|
|
table_name: d.name,
|
|
label: d.name,
|
|
x: x - 100,
|
|
y: y - 40,
|
|
columns: cols,
|
|
});
|
|
} else if (d.kind === 'control' && CTRL_NODE_TYPES[d.type]) {
|
|
addRuleNode({
|
|
id: genNodeId('ctrl'),
|
|
type: d.type,
|
|
label: CTRL_NODE_TYPES[d.type].label,
|
|
x: x - 80,
|
|
y: y - 30,
|
|
config: {},
|
|
});
|
|
}
|
|
}, [canvasRef, ruleNodes, addRuleNode]);
|
|
|
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'copy';
|
|
}, []);
|
|
|
|
// 노드 좌표에서 포트 위치 계산
|
|
const portPos = useCallback((nodeId: string, port: string) => {
|
|
const node = ruleNodes.find((n) => n.id === nodeId);
|
|
if (!node) return null;
|
|
|
|
if (node.type === 'table') {
|
|
if (port === 'in') return { x: node.x, y: node.y + 18 };
|
|
return { x: node.x + 200, y: node.y + 18 };
|
|
}
|
|
|
|
// 제어 노드
|
|
if (port === 'in') return { x: node.x, y: node.y + 40 };
|
|
|
|
// output 포트 — 타입별 위치
|
|
const def = CTRL_NODE_TYPES[node.type];
|
|
const outPorts = def?.out || [{ port: 'out', label: '→', cls: '' }];
|
|
const idx = outPorts.findIndex((p) => p.port === port);
|
|
const total = outPorts.length;
|
|
const nodeH = 80;
|
|
const centerY = node.y + nodeH / 2;
|
|
const gap = 8;
|
|
const startY = centerY - ((total - 1) * gap) / 2;
|
|
|
|
return { x: node.x + 160, y: startY + idx * gap };
|
|
}, [ruleNodes]);
|
|
|
|
return (
|
|
<>
|
|
{/* 드롭존 (캔버스 전체에 이벤트 걸기 위한 투명 레이어) */}
|
|
<div
|
|
style={{ position: 'absolute', inset: 0, zIndex: 5 }}
|
|
onDrop={handleDrop}
|
|
onDragOver={handleDragOver}
|
|
/>
|
|
|
|
{/* 연결선 SVG */}
|
|
<svg className="ctrl-svg" id="rule-svg" width="100%" height="100%" style={{ overflow: 'visible' }}>
|
|
<defs>
|
|
<marker id="arr-rule" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
|
|
<polygon points="0 0,8 3,0 6" fill="var(--ctrl-cyan)" opacity=".8" />
|
|
</marker>
|
|
<marker id="arr-yes" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
|
|
<polygon points="0 0,8 3,0 6" fill="var(--ctrl-green)" opacity=".8" />
|
|
</marker>
|
|
<marker id="arr-no" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
|
|
<polygon points="0 0,8 3,0 6" fill="var(--v5-text-muted, #888)" opacity=".5" />
|
|
</marker>
|
|
</defs>
|
|
|
|
{ruleConnections.map((c) => {
|
|
const f = portPos(c.from_node_id, c.from_port);
|
|
const t = portPos(c.to_node_id, c.to_port);
|
|
if (!f || !t) return null;
|
|
|
|
const cls = c.from_port === 'yes' ? 'rule-conn-path conn-yes'
|
|
: c.from_port === 'no' ? 'rule-conn-path conn-no'
|
|
: 'rule-conn-path';
|
|
const marker = c.from_port === 'yes' ? 'url(#arr-yes)'
|
|
: c.from_port === 'no' ? 'url(#arr-no)'
|
|
: 'url(#arr-rule)';
|
|
|
|
return (
|
|
<path
|
|
key={c.id}
|
|
d={bezierPath(f.x, f.y, t.x, t.y)}
|
|
className={cls}
|
|
markerEnd={marker}
|
|
/>
|
|
);
|
|
})}
|
|
</svg>
|
|
|
|
{/* 연결 삭제 뱃지 */}
|
|
{ruleConnections.map((c) => {
|
|
const f = portPos(c.from_node_id, c.from_port);
|
|
const t = portPos(c.to_node_id, c.to_port);
|
|
if (!f || !t) return null;
|
|
const mx = (f.x + t.x) / 2, my = (f.y + t.y) / 2;
|
|
|
|
return (
|
|
<div
|
|
key={`badge-${c.id}`}
|
|
className="rule-conn-badge"
|
|
style={{ left: mx, top: my }}
|
|
>
|
|
<span
|
|
className="conn-x"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
useControlMode.getState().removeRuleConnection(c.id);
|
|
}}
|
|
>
|
|
✕
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{/* 노드 렌더 */}
|
|
{ruleNodes.map((node) => {
|
|
if (node.type === 'table') {
|
|
return (
|
|
<div key={node.id} style={{ position: 'absolute', left: 0, top: 0 }}>
|
|
<TableNode
|
|
tableName={node.table_name}
|
|
label={node.label}
|
|
icon="🏢"
|
|
columns={node.columns ?? []}
|
|
x={node.x}
|
|
y={node.y}
|
|
onMove={(_, x, y) => moveRuleNode(node.id, x, y)}
|
|
style={{ overflow: 'visible' }}
|
|
/>
|
|
{/* I/O 포트 */}
|
|
<PortHandle nodeId={node.id} port="in" type="in" isTable onDragEnd={finishDrag} />
|
|
<div style={{ position: 'absolute', left: node.x + 194, top: node.y + 12 }}>
|
|
<PortHandle nodeId={node.id} port="out" type="out" isTable label="→" onDragStart={startDrag} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ControlNode
|
|
key={node.id}
|
|
node={node}
|
|
onDragStart={startDrag}
|
|
onDragEnd={finishDrag}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{/* 설정 팝오버 */}
|
|
<NodeConfigPopover />
|
|
</>
|
|
);
|
|
}
|