'use client'; /** * SVG 연결선 + 화살표 마커 4종 + 뱃지 * mockup drawTreeLine/addEdgeBadge 포팅 */ interface ConnectionSvgProps { children?: React.ReactNode; } /** SVG 컨테이너 (캔버스 위 오버레이, defs 포함) */ export function ConnectionSvg({ children }: ConnectionSvgProps) { return ( {children} ); } /** * 연결선 path — mockup v3 EditCanvas 의 orthogonal-with-rounded-corners 스타일 * from(x1,y1) → 가로 → 둥근 코너 → 세로 → 둥근 코너 → 가로 → to(x2,y2) * 같은 y 면 직선, 역방향(x1>x2)이면 부드러운 베지어로 fallback (어색한 backward 회피) */ export function bezierPath(x1: number, y1: number, x2: number, y2: number): string { // 역방향 (오른쪽→왼쪽): 직각 라우팅이 카드 위로 휘감으면 어색 → 베지어 사용 if (x2 < x1 - 20) { const dx = x2 - x1; return `M ${x1} ${y1} C ${x1 + Math.abs(dx) * 0.4} ${y1}, ${x2 - Math.abs(dx) * 0.4} ${y2}, ${x2} ${y2}`; } const sign = Math.sign(y2 - y1); if (sign === 0) return `M ${x1} ${y1} L ${x2} ${y2}`; const mx = (x1 + x2) / 2; const r = Math.min(10, Math.abs(y2 - y1) / 2, Math.abs(x2 - x1) / 4); return `M ${x1} ${y1} L ${mx - r} ${y1} Q ${mx} ${y1}, ${mx} ${y1 + sign * r} L ${mx} ${y2 - sign * r} Q ${mx} ${y2}, ${mx + r} ${y2} L ${x2} ${y2}`; } /** 타입별 CSS 클래스 + 마커 */ export function lineStyle(type: string): { cls: string; marker: string } { switch (type) { case 'source': return { cls: 'ctrl-line-tpl', marker: 'url(#arr-src)' }; case 'auto': return { cls: 'ctrl-line-auto', marker: 'url(#arr-auto)' }; case 'cond': return { cls: 'ctrl-line-cond', marker: 'url(#arr-cond)' }; default: return { cls: 'ctrl-line', marker: 'url(#arr-fk)' }; } } interface FlowLineProps { x1: number; y1: number; x2: number; y2: number; type: string; animate?: boolean; delay?: number; } /** 단일 연결선 (SVG path) */ export function FlowLine({ x1, y1, x2, y2, type, animate, delay }: FlowLineProps) { const { cls, marker } = lineStyle(type); const d = bezierPath(x1, y1, x2, y2); if (animate) { return ( { if (!el) return; const len = el.getTotalLength(); el.style.strokeDasharray = String(len); el.style.strokeDashoffset = String(len); requestAnimationFrame(() => { el.style.strokeDashoffset = '0'; }); setTimeout(() => { el.style.transition = 'none'; el.style.strokeDasharray = ''; el.style.strokeDashoffset = ''; el.style.animation = ''; }, 500 + (delay ?? 0)); }} /> ); } return ; } interface FlowBadgeProps { x: number; y: number; label: string; type: string; animate?: boolean; delay?: number; } /** 연결선 위 뱃지 (HTML) */ export function FlowBadge({ x, y, label, type, animate, delay }: FlowBadgeProps) { const cls = type === 'source' ? 'tpl-link' : type === 'auto' ? 'auto' : type === 'cond' ? 'cond' : ''; if (type === 'cond') { const parts = label.split('→'); const condText = (parts[0] || '조건').trim(); const actionText = (parts[1] || '실행').trim(); return ( { if (el && animate) requestAnimationFrame(() => { el.style.opacity = '1'; }); }} > ◇조건 분기 {condText} Yes → {actionText} No → 스킵 ); } return ( { if (el && animate) requestAnimationFrame(() => { el.style.opacity = '1'; }); }} > {label} ); }