104 lines
2.8 KiB
TypeScript
104 lines
2.8 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, useCallback } from 'react';
|
|
import { CTRL_NODE_TYPES, useControlMode } from './hooks/useControlMode';
|
|
import { PortHandle } from './PortHandle';
|
|
|
|
interface ControlNodeProps {
|
|
node: Record<string, any>;
|
|
onDragStart?: (nodeId: string, port: string, e: React.MouseEvent) => void;
|
|
onDragEnd?: (nodeId: string, port: string) => void;
|
|
}
|
|
|
|
/**
|
|
* 제어 노드 (16종) — mockup buildCtrlNode 포팅
|
|
*/
|
|
export function ControlNode({ node, onDragStart, onDragEnd }: ControlNodeProps) {
|
|
const { removeRuleNode, moveRuleNode, setConfigNodeId } = useControlMode();
|
|
const nodeRef = useRef<HTMLDivElement>(null);
|
|
|
|
const def = CTRL_NODE_TYPES[node.type];
|
|
if (!def) return null;
|
|
|
|
const outPorts = def.out || [{ port: 'out', label: '→', cls: '' }];
|
|
|
|
const handleHeadMouseDown = useCallback((e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const sx = e.clientX, sy = e.clientY;
|
|
const sl = node.x, st = node.y;
|
|
const el = nodeRef.current;
|
|
if (el) el.style.zIndex = '30';
|
|
|
|
const mv = (ev: MouseEvent) => {
|
|
moveRuleNode(node.id, sl + ev.clientX - sx, st + ev.clientY - sy);
|
|
};
|
|
const up = () => {
|
|
if (el) el.style.zIndex = '20';
|
|
document.removeEventListener('mousemove', mv);
|
|
document.removeEventListener('mouseup', up);
|
|
};
|
|
document.addEventListener('mousemove', mv);
|
|
document.addEventListener('mouseup', up);
|
|
}, [node.id, node.x, node.y, moveRuleNode]);
|
|
|
|
return (
|
|
<div
|
|
ref={nodeRef}
|
|
className="ctrl-action-node"
|
|
data-node-id={node.id}
|
|
data-node-type={node.type}
|
|
style={{
|
|
left: node.x,
|
|
top: node.y,
|
|
['--na-rgb' as string]: def.rgb,
|
|
}}
|
|
>
|
|
{/* Input 포트 */}
|
|
<PortHandle
|
|
nodeId={node.id}
|
|
port="in"
|
|
type="in"
|
|
onDragEnd={onDragEnd}
|
|
/>
|
|
|
|
{/* 헤더 */}
|
|
<div className="ctrl-an-head" onMouseDown={handleHeadMouseDown}>
|
|
<div className="ctrl-an-icon">{def.icon}</div>
|
|
<span className="ctrl-an-name">{def.label}</span>
|
|
<button
|
|
className="ctrl-an-del"
|
|
onClick={(e) => { e.stopPropagation(); removeRuleNode(node.id); }}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
{/* 본문 */}
|
|
<div
|
|
className="ctrl-an-body"
|
|
onClick={() => setConfigNodeId(node.id)}
|
|
>
|
|
<div className="ctrl-an-summary">
|
|
{node.config?.summary || '클릭하여 설정'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Output 포트 */}
|
|
<div className="ctrl-an-ports-out">
|
|
{outPorts.map((p) => (
|
|
<PortHandle
|
|
key={p.port}
|
|
nodeId={node.id}
|
|
port={p.port}
|
|
type="out"
|
|
cls={p.cls}
|
|
label={p.label}
|
|
onDragStart={onDragStart}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|