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

207 lines
5.5 KiB
TypeScript

import { useCallback } from 'react';
/**
* 트리 확산 애니메이션용 위치 계산 + 타이밍 (mockup calcFlowPositions 포팅)
*
* 카드 우측에서 depth별로 트리 형태 배치
* depth별 시간 지연으로 선 → 노드 순차 등장
*/
interface FlowEdge {
from: string;
to: string;
type: string;
label: string;
}
/**
* BFS로 카드에서 도달 가능한 전체 체인 계산
* ★ 양방향 탐색: outgoing (from===cur) + incoming (to===cur)
* → inbound 관계도 놓치지 않음
*/
export function buildFlowChain(
rootKey: string,
allEdges: FlowEdge[]
): { edges: FlowEdge[]; depths: Record<string, number> } {
const reachable = new Set([rootKey]);
const queue = [rootKey];
while (queue.length) {
const cur = queue.shift()!;
// ★ outgoing: from === cur
allEdges
.filter((e) => e.from === cur)
.forEach((e) => {
if (!reachable.has(e.to)) {
reachable.add(e.to);
queue.push(e.to);
}
});
// ★ incoming: to === cur (양방향 탐색)
allEdges
.filter((e) => e.to === cur)
.forEach((e) => {
if (!reachable.has(e.from)) {
reachable.add(e.from);
queue.push(e.from);
}
});
}
const edges = allEdges.filter((e) => reachable.has(e.from) && reachable.has(e.to));
// depth 계산 (양방향)
const depths: Record<string, number> = { [rootKey]: 0 };
const q2 = [rootKey];
while (q2.length) {
const cur = q2.shift()!;
// outgoing
edges
.filter((e) => e.from === cur)
.forEach((e) => {
if (depths[e.to] === undefined) {
depths[e.to] = depths[cur] + 1;
q2.push(e.to);
}
});
// incoming
edges
.filter((e) => e.to === cur)
.forEach((e) => {
if (depths[e.from] === undefined) {
depths[e.from] = depths[cur] + 1;
q2.push(e.from);
}
});
}
return { edges, depths };
}
/**
* 카드 우측에 테이블 노드를 트리 형태로 배치
*/
export function calcFlowPositions(
cardRight: number,
cardCenterY: number,
canvasHeight: number,
depths: Record<string, number>
): Record<string, { x: number; y: number }> {
const startX = cardRight + 80;
// depth별 노드 그룹핑 (CARD: 제외)
const depthNodes: Record<number, string[]> = {};
Object.entries(depths).forEach(([name, d]) => {
if (name.startsWith('CARD:')) return;
if (!depthNodes[d]) depthNodes[d] = [];
depthNodes[d].push(name);
});
const maxD = Math.max(1, ...Object.keys(depthNodes).map(Number));
const colGap = Math.max(270, Math.min(350, (1200 - startX - 230) / maxD));
const rowGap = 240;
const pos: Record<string, { x: number; y: number }> = {};
Object.entries(depthNodes).forEach(([dStr, nodes]) => {
const di = parseInt(dStr);
const totalH = nodes.length * rowGap;
const startY = Math.max(20, (canvasHeight - totalH) / 2);
nodes.forEach((name, i) => {
pos[name] = {
x: startX + (di - 1) * colGap,
y: startY + i * rowGap,
};
});
});
return pos;
}
/**
* depth별 애니메이션 타이밍 계산
* 선 → 노드 순서로 연쇄 등장
*/
export function calcAnimationTimings(
edges: FlowEdge[],
depths: Record<string, number>
): { edge: FlowEdge; lineDelay: number; nodeDelay: number }[] {
const STEP = 500;
const NODE_D = 350;
// 엣지를 depth별 그룹핑
const depthEdges: Record<number, FlowEdge[]> = {};
edges.forEach((edge) => {
const fd = depths[edge.from] ?? 0;
const d = fd + 1;
if (!depthEdges[d]) depthEdges[d] = [];
depthEdges[d].push(edge);
});
const result: { edge: FlowEdge; lineDelay: number; nodeDelay: number }[] = [];
const maxDepth = Math.max(0, ...Object.keys(depthEdges).map(Number));
for (let d = 1; d <= maxDepth; d++) {
const edgesAtDepth = depthEdges[d] || [];
const base = 300 + (d - 1) * STEP;
edgesAtDepth.forEach((edge, i) => {
result.push({
edge,
lineDelay: base + i * 120,
nodeDelay: base + i * 120 + NODE_D,
});
});
}
return result;
}
/**
* useFlowAnimation — 흐름 표시 관리 훅
*/
export function useFlowAnimation() {
const showFlow = useCallback(
(
cardId: string,
sourceTable: string,
relations: Record<string, any>[],
cardRect: { right: number; centerY: number },
canvasHeight: number
) => {
// 1. 엣지 구성: 카드 → 소스 테이블 + relations
const rootKey = `CARD:${cardId}`;
const allEdges: FlowEdge[] = [
{ from: rootKey, to: sourceTable, type: 'source', label: '데이터 소스' },
];
relations.forEach((rel) => {
const type = rel.relation_type ?? rel.RELATION_TYPE ?? 'auto';
const label = rel.label ?? rel.LABEL ?? `${rel.source_table ?? rel.SOURCE_TABLE}${rel.target_table ?? rel.TARGET_TABLE}`;
const from = rel.source_table ?? rel.SOURCE_TABLE;
const to = rel.target_table ?? rel.TARGET_TABLE;
if (from && to) {
allEdges.push({ from, to, type, label });
}
});
// 2. BFS 체인 + depth 계산
const { edges, depths } = buildFlowChain(rootKey, allEdges);
// 3. 위치 계산
const positions = calcFlowPositions(
cardRect.right,
cardRect.centerY,
canvasHeight,
depths
);
// 4. 애니메이션 타이밍
const timings = calcAnimationTimings(edges, depths);
return { edges, depths, positions, timings };
},
[]
);
return { showFlow };
}