207 lines
5.5 KiB
TypeScript
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 };
|
|
}
|