"use client"; import React, { useState, useCallback, useEffect, useRef } from "react"; import toast from "react-hot-toast"; import { ReactFlow, Node, Edge, Controls, Background, useNodesState, useEdgesState, BackgroundVariant, SelectionMode, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { TableNode } from "./TableNode"; import { TableSelector } from "./TableSelector"; import { ConnectionSetupModal } from "./ConnectionSetupModal"; import { TableDefinition, TableRelationship, DataFlowAPI, DataFlowDiagram, JsonRelationship, CreateDiagramRequest, } from "@/lib/api/dataflow"; import SaveDiagramModal from "./SaveDiagramModal"; import { useAuth } from "@/hooks/useAuth"; // 고유 ID 생성 함수 const generateUniqueId = (prefix: string, diagramId?: number): string => { const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 9); return `${prefix}-${diagramId || timestamp}-${random}`; }; // 테이블 노드 데이터 타입 정의 interface TableNodeData extends Record { table: { tableName: string; displayName: string; description: string; columns: Array<{ name: string; type: string; description: string; }>; }; onColumnClick: (tableName: string, columnName: string) => void; selectedColumns: string[]; } // 노드 및 엣지 타입 정의 const nodeTypes = { tableNode: TableNode, }; const edgeTypes = {}; interface DataFlowDesignerProps { companyCode?: string; onSave?: (relationships: TableRelationship[]) => void; selectedDiagram?: DataFlowDiagram | string | null; diagramId?: number; relationshipId?: string; // 하위 호환성 유지 onBackToList?: () => void; } // TableRelationship 타입은 dataflow.ts에서 import export const DataFlowDesigner: React.FC = ({ companyCode: propCompanyCode = "*", diagramId, relationshipId, // 하위 호환성 유지 onSave, // eslint-disable-line @typescript-eslint/no-unused-vars selectedDiagram, onBackToList, // eslint-disable-line @typescript-eslint/no-unused-vars }) => { const { user } = useAuth(); // 실제 사용자 회사 코드 사용 (prop보다 사용자 정보 우선) const companyCode = user?.company_code || user?.companyCode || propCompanyCode; const [nodes, setNodes, onNodesChange] = useNodesState>([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [selectedColumns, setSelectedColumns] = useState<{ [tableName: string]: string[]; }>({}); const [selectionOrder, setSelectionOrder] = useState([]); const [selectedNodes, setSelectedNodes] = useState([]); const [pendingConnection, setPendingConnection] = useState<{ fromNode: { id: string; tableName: string; displayName: string }; toNode: { id: string; tableName: string; displayName: string }; fromColumn?: string; toColumn?: string; selectedColumnsData?: { [tableName: string]: { displayName: string; columns: string[]; }; }; } | null>(null); const [relationships, setRelationships] = useState([]); // eslint-disable-line @typescript-eslint/no-unused-vars const [currentDiagramId, setCurrentDiagramId] = useState(null); // 현재 화면의 diagram_id const [selectedEdgeInfo, setSelectedEdgeInfo] = useState<{ relationshipId: string; relationshipName: string; fromTable: string; toTable: string; fromColumns: string[]; toColumns: string[]; relationshipType: string; connectionType: string; connectionInfo: string; } | null>(null); // 선택된 엣지 정보 // 새로운 메모리 기반 상태들 const [tempRelationships, setTempRelationships] = useState([]); // 메모리에 저장된 관계들 const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // 저장되지 않은 변경사항 const [showSaveModal, setShowSaveModal] = useState(false); // 저장 모달 표시 상태 const [isSaving, setIsSaving] = useState(false); // 저장 중 상태 const [currentDiagramName, setCurrentDiagramName] = useState(""); // 현재 편집 중인 관계도 이름 const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars // 편집 모드일 때 관계도 이름 로드 useEffect(() => { const loadDiagramName = async () => { if (diagramId && diagramId > 0) { try { const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(diagramId, companyCode); if (jsonDiagram && jsonDiagram.diagram_name) { setCurrentDiagramName(jsonDiagram.diagram_name); } } catch (error) { console.error("관계도 이름 로드 실패:", error); } } else { setCurrentDiagramName(""); // 신규 생성 모드 } }; loadDiagramName(); }, [diagramId, companyCode]); // 키보드 이벤트 핸들러 (Del 키로 선택된 노드 삭제) useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Delete" && selectedNodes.length > 0) { // 선택된 노드들 삭제 setNodes((prevNodes) => prevNodes.filter((node) => !selectedNodes.includes(node.id))); // 삭제된 노드들과 관련된 선택된 컬럼들도 정리 const deletedTableNames = selectedNodes .filter((nodeId) => nodeId.startsWith("table-")) .map((nodeId) => nodeId.replace("table-", "")); setSelectedColumns((prev) => { const newColumns = { ...prev }; deletedTableNames.forEach((tableName) => { delete newColumns[tableName]; }); return newColumns; }); // 선택 순서도 정리 setSelectionOrder((prev) => prev.filter((tableName) => !deletedTableNames.includes(tableName))); // 선택된 노드 초기화 setSelectedNodes([]); toast.success(`${selectedNodes.length}개 테이블이 삭제되었습니다.`); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [selectedNodes, setNodes]); // 컬럼 클릭 처리 (토글 방식, 최대 2개 테이블만 허용) const handleColumnClick = useCallback((tableName: string, columnName: string) => { setSelectedColumns((prev) => { const currentColumns = prev[tableName] || []; const isSelected = currentColumns.includes(columnName); const selectedTables = Object.keys(prev).filter((name) => prev[name] && prev[name].length > 0); if (isSelected) { // 선택 해제 const newColumns = currentColumns.filter((column) => column !== columnName); if (newColumns.length === 0) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { [tableName]: removed, ...rest } = prev; // 선택 순서에서도 제거 (다음 렌더링에서) setTimeout(() => { setSelectionOrder((order) => order.filter((name) => name !== tableName)); }, 0); return rest; } return { ...prev, [tableName]: newColumns }; } else { // 새 선택 if (selectedTables.length >= 2 && !selectedTables.includes(tableName)) { toast.error("최대 2개 테이블까지만 선택할 수 있습니다."); return prev; } const newColumns = [...currentColumns, columnName]; const newSelection = { ...prev, [tableName]: newColumns }; // 선택 순서 업데이트 (다음 렌더링에서) setTimeout(() => { setSelectionOrder((order) => { if (!order.includes(tableName)) { return [...order, tableName]; } return order; }); }, 0); return newSelection; } }); }, []); // 선택된 관계도의 관계 로드 const loadSelectedDiagramRelationships = useCallback(async () => { const currentDiagramId = diagramId || (relationshipId ? parseInt(relationshipId) : null); if (!currentDiagramId || isNaN(currentDiagramId)) return; try { console.log("🔍 JSON 관계도 로드 시작 (diagramId):", currentDiagramId); toast.loading("관계도를 불러오는 중...", { id: "load-diagram" }); // 새로운 JSON API로 관계도 조회 const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(currentDiagramId); console.log("📋 JSON 관계도 데이터:", jsonDiagram); if (!jsonDiagram || !jsonDiagram.relationships) { throw new Error("관계도 데이터를 찾을 수 없습니다."); } const relationships = jsonDiagram.relationships.relationships || []; const tableNames = jsonDiagram.relationships.tables || []; console.log("📋 관계 목록:", relationships); console.log("📊 테이블 목록:", tableNames); // 메모리에 관계 저장 (기존 관계도 편집 시) setTempRelationships(relationships); setCurrentDiagramId(currentDiagramId); // 테이블 노드 생성을 위한 테이블 정보 로드 // 테이블 정보 로드 const allTables = await DataFlowAPI.getTables(); console.log("🏢 전체 테이블 수:", allTables.length); const tableDefinitions: TableDefinition[] = []; for (const tableName of tableNames) { const foundTable = allTables.find((t) => t.tableName === tableName); console.log(`🔍 테이블 ${tableName} 검색 결과:`, foundTable); if (foundTable) { // 각 테이블의 컬럼 정보를 별도로 가져옴 const columns = await DataFlowAPI.getTableColumns(tableName); const safeColumns = Array.isArray(columns) ? columns : []; console.log(`📋 테이블 ${tableName}의 컬럼 수:`, safeColumns.length); tableDefinitions.push({ tableName: foundTable.tableName, displayName: foundTable.displayName, description: foundTable.description, columns: safeColumns, }); } else { console.warn(`⚠️ 테이블 ${tableName}을 찾을 수 없습니다`); } } // 연결된 컬럼 정보 계산 const connectedColumnsInfo: { [tableName: string]: { [columnName: string]: { direction: "source" | "target" | "both" } }; } = {}; relationships.forEach((rel: JsonRelationship) => { const fromTable = rel.fromTable; const toTable = rel.toTable; const fromColumns = rel.fromColumns || []; const toColumns = rel.toColumns || []; // 소스 테이블의 컬럼들을 source로 표시 if (!connectedColumnsInfo[fromTable]) connectedColumnsInfo[fromTable] = {}; fromColumns.forEach((col: string) => { if (connectedColumnsInfo[fromTable][col]) { connectedColumnsInfo[fromTable][col].direction = "both"; } else { connectedColumnsInfo[fromTable][col] = { direction: "source" }; } }); // 타겟 테이블의 컬럼들을 target으로 표시 if (!connectedColumnsInfo[toTable]) connectedColumnsInfo[toTable] = {}; toColumns.forEach((col: string) => { if (connectedColumnsInfo[toTable][col]) { connectedColumnsInfo[toTable][col].direction = "both"; } else { connectedColumnsInfo[toTable][col] = { direction: "target" }; } }); }); console.log("🔌 연결된 컬럼 정보:", connectedColumnsInfo); // 테이블을 노드로 변환 (자동 레이아웃) const tableNodes = tableDefinitions.map((table, index) => { const x = (index % 3) * 400 + 100; // 3열 배치 const y = Math.floor(index / 3) * 300 + 100; return { id: `table-${table.tableName}`, type: "tableNode", position: { x, y }, data: { table: { tableName: table.tableName, displayName: table.displayName, description: "", // 기존 로드된 노드도 description 없이 통일 columns: Array.isArray(table.columns) ? table.columns.map((col) => ({ name: col.columnName, type: col.dataType || "varchar", description: col.description || "", })) : [], }, onColumnClick: handleColumnClick, selectedColumns: [], // 관계도 로드 시에는 빈 상태로 시작 connectedColumns: connectedColumnsInfo[table.tableName] || {}, } as TableNodeData, }; }); console.log("🎨 생성된 테이블 노드 수:", tableNodes.length); console.log("📍 테이블 노드 상세:", tableNodes); setNodes(tableNodes); // JSON 관계를 엣지로 변환하여 표시 (테이블 간 번들 연결) const relationshipEdges: Edge[] = []; relationships.forEach((rel: JsonRelationship) => { const fromTable = rel.fromTable; const toTable = rel.toTable; const fromColumns = rel.fromColumns || []; const toColumns = rel.toColumns || []; if (fromColumns.length === 0 || toColumns.length === 0) { console.warn("⚠️ 컬럼 정보가 없습니다:", { fromColumns, toColumns }); return; } // 테이블 간 하나의 번들 엣지 생성 (컬럼별 개별 엣지 대신) relationshipEdges.push({ id: generateUniqueId("edge", currentDiagramId), source: `table-${fromTable}`, target: `table-${toTable}`, type: "smoothstep", animated: false, style: { stroke: "#3b82f6", strokeWidth: 2, strokeDasharray: "none", }, data: { relationshipId: rel.id, relationshipName: "기존 관계", relationshipType: rel.relationshipType, connectionType: rel.connectionType, fromTable: fromTable, toTable: toTable, fromColumns: fromColumns, toColumns: toColumns, // 클릭 시 표시할 상세 정보 details: { connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`, relationshipType: rel.relationshipType, connectionType: rel.connectionType, }, }, }); }); console.log("🔗 생성된 관계 에지 수:", relationshipEdges.length); console.log("📍 관계 에지 상세:", relationshipEdges); setEdges(relationshipEdges); toast.success("관계도를 불러왔습니다.", { id: "load-diagram" }); } catch (error) { console.error("선택된 관계도 로드 실패:", error); toast.error("관계도를 불러오는데 실패했습니다.", { id: "load-diagram" }); } }, [diagramId, relationshipId, setNodes, setEdges, handleColumnClick]); // 기존 관계 로드 (새 관계도 생성 시) const loadExistingRelationships = useCallback(async () => { if (selectedDiagram) return; // 선택된 관계도가 있으면 실행하지 않음 try { // 새로운 JSON 기반 시스템에서는 기존 관계를 미리 로드하지 않음 console.log("새 관계도 생성 모드: 빈 캔버스로 시작"); setRelationships([]); // 빈 캔버스로 시작 setEdges([]); } catch (error) { console.error("기존 관계 로드 실패:", error); toast.error("기존 관계를 불러오는데 실패했습니다."); } }, [setEdges, selectedDiagram]); // 컴포넌트 마운트 시 관계 로드 useEffect(() => { if (companyCode) { if (diagramId || relationshipId) { loadSelectedDiagramRelationships(); } else { loadExistingRelationships(); } } }, [companyCode, diagramId, relationshipId, loadExistingRelationships, loadSelectedDiagramRelationships]); // 노드 선택 변경 핸들러 const onSelectionChange = useCallback(({ nodes }: { nodes: Node[] }) => { const selectedNodeIds = nodes.map((node) => node.id); setSelectedNodes(selectedNodeIds); }, []); // 캔버스 클릭 시 엣지 정보 섹션 닫기 const onPaneClick = useCallback(() => { if (selectedEdgeInfo) { setSelectedEdgeInfo(null); } }, [selectedEdgeInfo]); // 빈 onConnect 함수 (드래그 연결 비활성화) const onConnect = useCallback(() => { // 드래그로 연결하는 것을 방지 return; }, []); // 엣지 클릭 시 연결 정보 표시 const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { event.stopPropagation(); const edgeData = edge.data as { relationshipId: string; relationshipName: string; fromTable: string; toTable: string; fromColumns: string[]; toColumns: string[]; relationshipType: string; connectionType: string; details?: { connectionInfo: string; relationshipType: string; connectionType: string; }; }; if (edgeData) { setSelectedEdgeInfo({ relationshipId: edgeData.relationshipId, relationshipName: edgeData.relationshipName || "관계", fromTable: edgeData.fromTable, toTable: edgeData.toTable, fromColumns: edgeData.fromColumns || [], toColumns: edgeData.toColumns || [], relationshipType: edgeData.relationshipType, connectionType: edgeData.connectionType, connectionInfo: edgeData.details?.connectionInfo || `${edgeData.fromTable} → ${edgeData.toTable}`, }); } }, []); // 엣지 마우스 엔터 시 색상 변경 const onEdgeMouseEnter = useCallback( (event: React.MouseEvent, edge: Edge) => { setEdges((eds) => eds.map((e) => e.id === edge.id ? { ...e, style: { ...e.style, stroke: "#1d4ed8", // hover 색상 strokeWidth: 3, }, } : e, ), ); }, [setEdges], ); // 엣지 마우스 리브 시 원래 색상으로 복원 const onEdgeMouseLeave = useCallback( (event: React.MouseEvent, edge: Edge) => { setEdges((eds) => eds.map((e) => e.id === edge.id ? { ...e, style: { ...e.style, stroke: "#3b82f6", // 기본 색상 strokeWidth: 2, }, } : e, ), ); }, [setEdges], ); // 선택된 컬럼이 변경될 때마다 기존 노드들 업데이트 및 selectionOrder 정리 useEffect(() => { setNodes((prevNodes) => prevNodes.map((node) => ({ ...node, data: { ...node.data, selectedColumns: selectedColumns[node.data.table.tableName] || [], }, })), ); // selectionOrder에서 선택되지 않은 테이블들 제거 const activeTables = Object.keys(selectedColumns).filter( (tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0, ); setSelectionOrder((prev) => prev.filter((tableName) => activeTables.includes(tableName))); }, [selectedColumns, setNodes]); // 연결 가능한 상태인지 확인 const canCreateConnection = () => { const selectedTables = Object.keys(selectedColumns).filter( (tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0, ); // 최소 2개의 서로 다른 테이블에서 컬럼이 선택되어야 함 return selectedTables.length >= 2; }; // 컬럼 연결 설정 모달 열기 const openConnectionModal = () => { const selectedTables = Object.keys(selectedColumns).filter( (tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0, ); if (selectedTables.length < 2) return; // 선택 순서에 따라 첫 번째와 두 번째 테이블 설정 const orderedTables = selectionOrder.filter((name) => selectedTables.includes(name)); const firstTableName = orderedTables[0]; const secondTableName = orderedTables[1]; const firstNode = nodes.find((node) => node.data.table.tableName === firstTableName); const secondNode = nodes.find((node) => node.data.table.tableName === secondTableName); if (!firstNode || !secondNode) return; // 첫 번째로 선택된 컬럼들 가져오기 const firstTableColumns = selectedColumns[firstTableName] || []; const secondTableColumns = selectedColumns[secondTableName] || []; setPendingConnection({ fromNode: { id: firstNode.id, tableName: firstNode.data.table.tableName, displayName: firstNode.data.table.displayName, }, toNode: { id: secondNode.id, tableName: secondNode.data.table.tableName, displayName: secondNode.data.table.displayName, }, // 선택된 첫 번째 컬럼을 연결 컬럼으로 설정 fromColumn: firstTableColumns[0] || "", toColumn: secondTableColumns[0] || "", // 선택된 모든 컬럼 정보를 선택 순서대로 전달 selectedColumnsData: (() => { const orderedData: { [key: string]: { displayName: string; columns: string[] } } = {}; // selectionOrder 순서대로 데이터 구성 (첫 번째 선택이 먼저) orderedTables.forEach((tableName) => { const node = nodes.find((n) => n.data.table.tableName === tableName); if (node && selectedColumns[tableName]) { orderedData[tableName] = { displayName: node.data.table.displayName, columns: selectedColumns[tableName], }; } }); return orderedData; })(), }); }; // 실제 테이블 노드 추가 const addTableNode = useCallback( async (table: TableDefinition) => { try { const newNode: Node = { id: `table-${table.tableName}`, type: "tableNode", position: { x: Math.random() * 300, y: Math.random() * 200 }, data: { table: { tableName: table.tableName, displayName: table.displayName || table.tableName, description: "", // 새로 추가된 노드는 description 없이 통일 columns: Array.isArray(table.columns) ? table.columns.map((col) => ({ name: col.columnName || "unknown", type: col.dataType || "varchar", // 기존과 동일한 기본값 사용 description: col.description || "", })) : [], }, onColumnClick: handleColumnClick, selectedColumns: selectedColumns[table.tableName] || [], connectedColumns: {}, // 새로 추가된 노드는 연결 정보 없음 }, }; setNodes((nds) => nds.concat(newNode)); } catch (error) { console.error("테이블 노드 추가 실패:", error); toast.error("테이블 정보를 불러오는데 실패했습니다."); } }, [handleColumnClick, selectedColumns, setNodes], ); // 노드 전체 삭제 const clearNodes = useCallback(() => { setNodes([]); setEdges([]); setSelectedColumns({}); setSelectionOrder([]); setSelectedNodes([]); setCurrentDiagramId(null); // 현재 diagram_id도 초기화 }, [setNodes, setEdges]); // 현재 추가된 테이블명 목록 가져오기 const getSelectedTableNames = useCallback(() => { return nodes.filter((node) => node.id.startsWith("table-")).map((node) => node.data.table.tableName); }, [nodes]); // 연결 설정 확인 const handleConfirmConnection = useCallback( (relationship: TableRelationship) => { if (!pendingConnection) return; // 메모리 기반 관계 생성 (DB 저장 없이) const fromTable = relationship.from_table_name; const toTable = relationship.to_table_name; const fromColumns = relationship.from_column_name .split(",") .map((col) => col.trim()) .filter((col) => col); const toColumns = relationship.to_column_name .split(",") .map((col) => col.trim()) .filter((col) => col); // JSON 형태의 관계 객체 생성 const newRelationship: JsonRelationship = { id: generateUniqueId("rel", Date.now()), fromTable, toTable, fromColumns, toColumns, relationshipType: relationship.relationship_type, connectionType: relationship.connection_type, settings: relationship.settings || {}, }; // 메모리에 관계 추가 setTempRelationships((prev) => [...prev, newRelationship]); setHasUnsavedChanges(true); // 캔버스에 엣지 즉시 표시 const newEdge: Edge = { id: generateUniqueId("edge", Date.now()), source: pendingConnection.fromNode.id, target: pendingConnection.toNode.id, type: "smoothstep", animated: false, style: { stroke: "#3b82f6", strokeWidth: 2, strokeDasharray: "none", }, data: { relationshipId: newRelationship.id, relationshipName: "임시 관계", relationshipType: relationship.relationship_type, connectionType: relationship.connection_type, fromTable, toTable, fromColumns, toColumns, details: { connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`, relationshipType: relationship.relationship_type, connectionType: relationship.connection_type, }, }, }; setEdges((eds) => [...eds, newEdge]); setPendingConnection(null); // 관계 생성 후 선택된 컬럼들 초기화 setSelectedColumns({}); setSelectionOrder([]); console.log("메모리에 관계 생성 완료:", newRelationship); toast.success("관계가 생성되었습니다. 저장 버튼을 눌러 관계도를 저장하세요."); }, [pendingConnection, setEdges], ); // 연결 설정 취소 const handleCancelConnection = useCallback(() => { setPendingConnection(null); }, []); // 관계도 저장 함수 const handleSaveDiagram = useCallback( async (diagramName: string) => { if (tempRelationships.length === 0) { toast.error("저장할 관계가 없습니다."); return; } setIsSaving(true); try { // 연결된 테이블 목록 추출 const connectedTables = Array.from( new Set([...tempRelationships.map((rel) => rel.fromTable), ...tempRelationships.map((rel) => rel.toTable)]), ).sort(); // 저장 요청 데이터 생성 const createRequest: CreateDiagramRequest = { diagram_name: diagramName, relationships: { relationships: tempRelationships, tables: connectedTables, }, }; let savedDiagram; // 편집 모드 vs 신규 생성 모드 구분 if (diagramId && diagramId > 0) { // 편집 모드: 기존 관계도 업데이트 savedDiagram = await DataFlowAPI.updateJsonDataFlowDiagram( diagramId, createRequest, companyCode, user?.userId || "SYSTEM", ); toast.success(`관계도 "${diagramName}"가 성공적으로 수정되었습니다.`); } else { // 신규 생성 모드: 새로운 관계도 생성 savedDiagram = await DataFlowAPI.createJsonDataFlowDiagram( createRequest, companyCode, user?.userId || "SYSTEM", ); toast.success(`관계도 "${diagramName}"가 성공적으로 생성되었습니다.`); } // 성공 처리 setHasUnsavedChanges(false); setShowSaveModal(false); setCurrentDiagramId(savedDiagram.diagram_id); console.log("관계도 저장 완료:", savedDiagram); } catch (error) { console.error("관계도 저장 실패:", error); toast.error("관계도 저장 중 오류가 발생했습니다."); } finally { setIsSaving(false); } }, [tempRelationships, diagramId, companyCode, user?.userId], ); // 저장 모달 열기 const handleOpenSaveModal = useCallback(() => { if (tempRelationships.length === 0) { toast.error("저장할 관계가 없습니다. 먼저 테이블을 연결해주세요."); return; } setShowSaveModal(true); }, [tempRelationships.length]); // 저장 모달 닫기 const handleCloseSaveModal = useCallback(() => { if (!isSaving) { setShowSaveModal(false); } }, [isSaving]); return (
{/* 사이드바 */}

테이블 간 데이터 관계 설정

{/* 테이블 선택기 */} {/* 컨트롤 버튼들 */}
{/* 통계 정보 */}
통계
테이블 노드: {nodes.length}개
연결: {edges.length}개
메모리 관계: {tempRelationships.length}개
관계도 ID: {currentDiagramId || "미설정"}
{hasUnsavedChanges && (
⚠️ 저장되지 않은 변경사항이 있습니다
)}
{/* 선택된 컬럼 정보 */} {Object.keys(selectedColumns).length > 0 && (
선택된 컬럼
{[...new Set(selectionOrder)] .filter((tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0) .map((tableName, index, filteredOrder) => { const columns = selectedColumns[tableName]; const node = nodes.find((n) => n.data.table.tableName === tableName); const displayName = node?.data.table.displayName || tableName; return (
{displayName}
{columns.map((column, columnIndex) => (
{column}
))}
{/* 첫 번째 테이블 다음에 화살표 표시 */} {index === 0 && filteredOrder.length > 1 && (
)}
); })}
)} {/* 선택된 엣지 정보 */} {selectedEdgeInfo && (
🔗 연결 정보
연결
{selectedEdgeInfo.fromTable}({selectedEdgeInfo.fromColumns.join(", ")}) →{" "} {selectedEdgeInfo.toTable}({selectedEdgeInfo.toColumns.join(", ")})
관계 유형
{selectedEdgeInfo.relationshipType}
연결 유형
{selectedEdgeInfo.connectionType}
)}
{/* React Flow 캔버스 */}
{/* 안내 메시지 */} {nodes.length === 0 && (
📊
테이블 간 데이터 관계 설정을 시작하세요
왼쪽 사이드바에서 테이블을 더블클릭하여 추가하세요
테이블 선택 후 Del 키로 삭제 가능
)}
{/* 연결 설정 모달 */} {/* 관계도 저장 모달 */} 0 && currentDiagramName ? currentDiagramName // 편집 모드: 기존 관계도 이름 : `관계도 ${new Date().toLocaleDateString()}` // 신규 생성 모드: 새로운 이름 } isLoading={isSaving} />
); };