Files
invyone/frontend/components/control/hooks/usePortDrag.ts
T
2026-04-10 13:33:37 +09:00

118 lines
3.7 KiB
TypeScript

import { useRef, useCallback, useEffect } from 'react';
import { useControlMode, genConnId } from './useControlMode';
import { bezierPath } from '../ConnectionLine';
/**
* 포트 연결 드래그 로직 (mockup startPortDrag/finishPortDrag 포팅)
* output 포트 mousedown → 임시 선 → input 포트 mouseup → 연결 생성
*/
export function usePortDrag(canvasRef: React.RefObject<HTMLDivElement | null>) {
const { addRuleConnection, ruleConnections } = useControlMode();
const dragRef = useRef<{
fromNodeId: string;
fromPort: string;
line: SVGPathElement;
x1: number;
y1: number;
} | null>(null);
const startDrag = useCallback((nodeId: string, port: string, e: React.MouseEvent) => {
const cv = canvasRef.current;
if (!cv) return;
// SVG 확보
let svg = cv.querySelector('#rule-svg') as SVGSVGElement | null;
if (!svg) {
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.id = 'rule-svg';
svg.classList.add('ctrl-svg');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.style.overflow = 'visible';
cv.appendChild(svg);
}
const cr = cv.getBoundingClientRect();
const portEl = (e.target as HTMLElement).closest('.ctrl-io-port') as HTMLElement;
if (!portEl) return;
const pr = portEl.getBoundingClientRect();
const x1 = pr.left + pr.width / 2 - cr.left + cv.scrollLeft;
const y1 = pr.top + pr.height / 2 - cr.top + cv.scrollTop;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
line.classList.add('rule-temp-line');
line.setAttribute('d', `M${x1},${y1} L${x1},${y1}`);
svg.appendChild(line);
dragRef.current = { fromNodeId: nodeId, fromPort: port, line, x1, y1 };
cv.classList.add('port-dragging');
portEl.classList.add('port-active');
}, [canvasRef]);
const finishDrag = useCallback((toNodeId: string, toPort: string) => {
const d = dragRef.current;
if (!d) return;
// 같은 노드 연결 방지
if (d.fromNodeId === toNodeId) {
cleanup();
return;
}
// 중복 방지
if (ruleConnections.find((c) =>
c.from_node_id === d.fromNodeId && c.from_port === d.fromPort && c.to_node_id === toNodeId
)) {
cleanup();
return;
}
addRuleConnection({
id: genConnId(),
from_node_id: d.fromNodeId,
from_port: d.fromPort,
to_node_id: toNodeId,
to_port: toPort,
});
cleanup();
}, [addRuleConnection, ruleConnections]);
const cleanup = useCallback(() => {
const d = dragRef.current;
if (!d) return;
d.line.remove();
canvasRef.current?.classList.remove('port-dragging');
document.querySelectorAll('.port-active').forEach((el) => el.classList.remove('port-active'));
document.querySelectorAll('.port-hover').forEach((el) => el.classList.remove('port-hover'));
dragRef.current = null;
}, [canvasRef]);
// 마우스 이동/종료 전역 핸들러
useEffect(() => {
const onMove = (e: MouseEvent) => {
const d = dragRef.current;
if (!d) return;
const cv = canvasRef.current;
if (!cv) return;
const cr = cv.getBoundingClientRect();
const x2 = e.clientX - cr.left + cv.scrollLeft;
const y2 = e.clientY - cr.top + cv.scrollTop;
d.line.setAttribute('d', bezierPath(d.x1, d.y1, x2, y2));
};
const onUp = () => {
if (dragRef.current) cleanup();
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
return () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
}, [canvasRef, cleanup]);
return { startDrag, finishDrag };
}