"use client"; import React, { useState, useEffect, useCallback } from "react"; import { ReactFlow, Controls, Background, BackgroundVariant, Node, Edge, useNodesState, useEdgesState, MarkerType, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { ScreenDefinition } from "@/types/screen"; import { ScreenNode, TableNode, ScreenNodeData, TableNodeData } from "./ScreenNode"; import { getFieldJoins, getDataFlows, getTableRelations, getMultipleScreenLayoutSummary, ScreenLayoutSummary, } from "@/lib/api/screenGroup"; import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement"; // 노드 타입 등록 const nodeTypes = { screenNode: ScreenNode, tableNode: TableNode, }; // 레이아웃 상수 const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단) const TABLE_Y = 400; // 테이블 노드 Y 위치 (하단) const NODE_WIDTH = 260; // 노드 너비 (조금 넓게) const NODE_GAP = 40; // 노드 간격 interface ScreenRelationFlowProps { screen: ScreenDefinition | null; } // 노드 타입 (Record 확장) type ScreenNodeType = Node>; type TableNodeType = Node>; type AllNodeType = ScreenNodeType | TableNodeType; export function ScreenRelationFlow({ screen }: ScreenRelationFlowProps) { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [loading, setLoading] = useState(false); const [tableColumns, setTableColumns] = useState>({}); // 테이블 컬럼 정보 로드 const loadTableColumns = useCallback( async (tableName: string): Promise => { if (!tableName) return []; if (tableColumns[tableName]) return tableColumns[tableName]; try { const response = await getTableColumns(tableName); if (response.success && response.data && response.data.columns) { const columns = response.data.columns; setTableColumns((prev) => ({ ...prev, [tableName]: columns })); return columns; } } catch (error) { console.error(`테이블 ${tableName} 컬럼 로드 실패:`, error); } return []; }, [tableColumns] ); // 데이터 로드 및 노드/엣지 생성 useEffect(() => { if (!screen) { setNodes([]); setEdges([]); return; } const loadRelations = async () => { setLoading(true); try { // 관계 데이터 로드 const [joinsRes, flowsRes, relationsRes] = await Promise.all([ getFieldJoins(screen.screenId).catch(() => ({ success: false, data: [] })), getDataFlows().catch(() => ({ success: false, data: [] })), getTableRelations({ screen_id: screen.screenId }).catch(() => ({ success: false, data: [] })), ]); const joins = joinsRes.success ? joinsRes.data || [] : []; const flows = flowsRes.success ? flowsRes.data || [] : []; const relations = relationsRes.success ? relationsRes.data || [] : []; // ========== 화면 목록 수집 ========== const screenList: ScreenDefinition[] = [screen]; // 데이터 흐름에서 연결된 화면들 추가 flows.forEach((flow: any) => { if (flow.source_screen_id === screen.screenId && flow.target_screen_id) { const exists = screenList.some((s) => s.screenId === flow.target_screen_id); if (!exists) { screenList.push({ screenId: flow.target_screen_id, screenName: flow.target_screen_name || `화면 ${flow.target_screen_id}`, screenCode: "", tableName: "", companyCode: screen.companyCode, isActive: "Y", createdDate: new Date(), updatedDate: new Date(), } as ScreenDefinition); } } }); // 화면 레이아웃 요약 정보 로드 const screenIds = screenList.map((s) => s.screenId); let layoutSummaries: Record = {}; try { const layoutRes = await getMultipleScreenLayoutSummary(screenIds); if (layoutRes.success && layoutRes.data) { // API 응답이 Record 형태 (screenId -> summary) layoutSummaries = layoutRes.data as Record; } } catch (e) { console.error("레이아웃 요약 로드 실패:", e); } // ========== 상단: 화면 노드들 ========== const screenNodes: ScreenNodeType[] = []; const screenStartX = 50; screenList.forEach((scr, idx) => { const isMain = scr.screenId === screen.screenId; const summary = layoutSummaries[scr.screenId]; screenNodes.push({ id: `screen-${scr.screenId}`, type: "screenNode", position: { x: screenStartX + idx * (NODE_WIDTH + NODE_GAP), y: SCREEN_Y }, data: { label: scr.screenName, subLabel: isMain ? "메인 화면" : "연결 화면", type: "screen", isMain, tableName: scr.tableName, layoutSummary: summary, }, }); }); // ========== 하단: 테이블 노드들 ========== const tableNodes: TableNodeType[] = []; const tableSet = new Set(); // 메인 화면의 테이블 추가 if (screen.tableName) { tableSet.add(screen.tableName); } // 조인된 테이블들 추가 joins.forEach((join: any) => { if (join.save_table) tableSet.add(join.save_table); if (join.join_table) tableSet.add(join.join_table); }); // 테이블 관계에서 추가 relations.forEach((rel: any) => { if (rel.table_name) tableSet.add(rel.table_name); }); // 테이블 노드 배치 (하단, 가로 배치) const tableList = Array.from(tableSet); const tableStartX = 50; for (let idx = 0; idx < tableList.length; idx++) { const tableName = tableList[idx]; const isMainTable = tableName === screen.tableName; // 컬럼 정보 로드 let columns: ColumnTypeInfo[] = []; try { columns = await loadTableColumns(tableName); } catch (e) { // ignore } // 컬럼 정보를 PK/FK 표시와 함께 변환 const formattedColumns = columns.slice(0, 8).map((col) => ({ name: col.displayName || col.columnName || "", type: col.dataType || "", isPrimaryKey: col.isPrimaryKey || col.columnName === "id", isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"), })); tableNodes.push({ id: `table-${tableName}`, type: "tableNode", position: { x: tableStartX + idx * (NODE_WIDTH + NODE_GAP), y: TABLE_Y }, data: { label: tableName, subLabel: isMainTable ? "메인 테이블" : "조인 테이블", isMain: isMainTable, columns: formattedColumns, }, }); } // ========== 엣지: 연결선 생성 ========== const newEdges: Edge[] = []; // 메인 화면 → 메인 테이블 연결 (양방향 CRUD) if (screen.tableName) { newEdges.push({ id: `edge-main`, source: `screen-${screen.screenId}`, target: `table-${screen.tableName}`, sourceHandle: "bottom", targetHandle: "top", type: "smoothstep", animated: true, style: { stroke: "#3b82f6", strokeWidth: 2 }, }); } // 조인 관계 엣지 (테이블 간 - 1:N 관계 표시) joins.forEach((join: any, idx: number) => { if (join.save_table && join.join_table && join.save_table !== join.join_table) { newEdges.push({ id: `edge-join-${idx}`, source: `table-${join.save_table}`, target: `table-${join.join_table}`, sourceHandle: "right", targetHandle: "left", type: "smoothstep", label: "1:N 관계", labelStyle: { fontSize: 10, fill: "#6366f1", fontWeight: 500 }, labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 }, labelBgPadding: [4, 2] as [number, number], markerEnd: { type: MarkerType.ArrowClosed, color: "#6366f1" }, style: { stroke: "#6366f1", strokeWidth: 1.5, strokeDasharray: "5,5" }, }); } }); // 테이블 관계 엣지 (추가 관계) relations.forEach((rel: any, idx: number) => { if (rel.table_name && rel.table_name !== screen.tableName) { // 화면 → 연결 테이블 const edgeExists = newEdges.some( (e) => e.source === `screen-${screen.screenId}` && e.target === `table-${rel.table_name}` ); if (!edgeExists) { newEdges.push({ id: `edge-rel-${idx}`, source: `screen-${screen.screenId}`, target: `table-${rel.table_name}`, sourceHandle: "bottom", targetHandle: "top", type: "smoothstep", label: rel.relation_type === "join" ? "조인" : rel.crud_operations || "", labelStyle: { fontSize: 9, fill: "#10b981" }, labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 }, labelBgPadding: [3, 2] as [number, number], style: { stroke: "#10b981", strokeWidth: 1.5 }, }); } } }); // 데이터 흐름 엣지 (화면 간) flows .filter((flow: any) => flow.source_screen_id === screen.screenId) .forEach((flow: any, idx: number) => { if (flow.target_screen_id) { newEdges.push({ id: `edge-flow-${idx}`, source: `screen-${screen.screenId}`, target: `screen-${flow.target_screen_id}`, sourceHandle: "right", targetHandle: "left", type: "smoothstep", animated: true, label: flow.flow_label || flow.flow_type || "이동", labelStyle: { fontSize: 10, fill: "#8b5cf6", fontWeight: 500 }, labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 }, labelBgPadding: [4, 2] as [number, number], markerEnd: { type: MarkerType.ArrowClosed, color: "#8b5cf6" }, style: { stroke: "#8b5cf6", strokeWidth: 2 }, }); } }); // 최종 노드 배열 합치기 const allNodes: AllNodeType[] = [...screenNodes, ...tableNodes]; // 테이블이 없으면 안내 노드 추가 if (tableNodes.length === 0) { allNodes.push({ id: "hint-table", type: "tableNode", position: { x: 50, y: TABLE_Y }, data: { label: "연결된 테이블 없음", subLabel: "화면에 테이블을 설정하세요", isMain: false, columns: [], }, }); } setNodes(allNodes); setEdges(newEdges); } catch (error) { console.error("관계 데이터 로드 실패:", error); } finally { setLoading(false); } }; loadRelations(); }, [screen, setNodes, setEdges, loadTableColumns]); if (!screen) { return (

화면을 선택하면

데이터 관계가 시각화됩니다

); } if (loading) { return (
로딩 중...
); } return (
); }