"use client"; import React, { useCallback, useEffect } from "react"; import toast from "react-hot-toast"; import { ReactFlow, Controls, Background, BackgroundVariant, SelectionMode, Node, Edge } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { TableNode } from "./TableNode"; import { ConnectionSetupModal } from "./ConnectionSetupModal"; import { DataFlowSidebar } from "./DataFlowSidebar"; import { SelectedTablesPanel } from "./SelectedTablesPanel"; import { RelationshipListModal } from "./RelationshipListModal"; import { EdgeInfoPanel } from "./EdgeInfoPanel"; import SaveDiagramModal from "./SaveDiagramModal"; import { TableDefinition, DataFlowAPI, JsonRelationship } from "@/lib/api/dataflow"; import { useAuth } from "@/hooks/useAuth"; import { useDataFlowDesigner } from "@/hooks/useDataFlowDesigner"; import { DataFlowDesignerProps, TableNodeData } from "@/types/dataflowTypes"; import { extractTableNames } from "@/utils/dataflowUtils"; // 노드 및 엣지 타입 정의 const nodeTypes = { tableNode: TableNode, }; const edgeTypes = {}; export const DataFlowDesigner: React.FC = ({ companyCode: propCompanyCode = "*", diagramId, }) => { const { user } = useAuth(); // 실제 사용자 회사 코드 사용 (prop보다 사용자 정보 우선) const companyCode = user?.company_code || user?.companyCode || propCompanyCode; // 커스텀 훅 사용 const { nodes, setNodes, onNodesChange, edges, setEdges, onEdgesChange, selectedColumns, setSelectedColumns, selectedNodes, setSelectedNodes, pendingConnection, setPendingConnection, currentDiagramId, currentDiagramName, setCurrentDiagramName, currentDiagramCategory, tempRelationships, setTempRelationships, hasUnsavedChanges, setHasUnsavedChanges, showSaveModal, setShowSaveModal, isSaving, setIsSaving, showRelationshipListModal, setShowRelationshipListModal, selectedTablePairRelationships, setSelectedTablePairRelationships, selectedEdgeInfo, setSelectedEdgeInfo, setSelectedEdgeForEdit, showEdgeActions, setShowEdgeActions, edgeActionPosition, editingRelationshipId, setEditingRelationshipId, } = useDataFlowDesigner(); // 컬럼 클릭 처리 비활성화 (테이블 노드 선택 방식으로 변경) const handleColumnClick = useCallback((tableName: string, columnName: string) => { // 컬럼 클릭으로는 더 이상 선택하지 않음 console.log(`컬럼 클릭 무시됨: ${tableName}.${columnName}`); return; }, []); // 편집 모드일 때 관계도 데이터 로드 useEffect(() => { const loadDiagramData = async () => { if (diagramId && diagramId > 0) { try { const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(diagramId, companyCode); if (jsonDiagram) { // 관계도 이름 설정 if (jsonDiagram.diagram_name) { setCurrentDiagramName(jsonDiagram.diagram_name); } // 관계 데이터 로드 if (jsonDiagram.relationships?.relationships && Array.isArray(jsonDiagram.relationships.relationships)) { const loadedRelationships = jsonDiagram.relationships.relationships.map((rel) => ({ id: rel.id || `rel-${Date.now()}-${Math.random()}`, fromTable: rel.fromTable, toTable: rel.toTable, fromColumns: Array.isArray(rel.fromColumns) ? rel.fromColumns : [], toColumns: Array.isArray(rel.toColumns) ? rel.toColumns : [], connectionType: rel.connectionType || "simple-key", relationshipName: rel.relationshipName || "", settings: rel.settings || {}, })); setTempRelationships(loadedRelationships); // 관계 데이터로부터 테이블 노드들을 생성 const tableNames = new Set(); loadedRelationships.forEach((rel) => { tableNames.add(rel.fromTable); tableNames.add(rel.toTable); }); // 각 테이블의 정보를 API에서 가져와서 노드 생성 const loadedNodes = await Promise.all( Array.from(tableNames).map(async (tableName) => { try { const columns = await DataFlowAPI.getTableColumns(tableName); return { id: `table-${tableName}`, type: "tableNode", position: jsonDiagram.node_positions?.[tableName] || { x: Math.random() * 300, y: Math.random() * 200, }, data: { table: { tableName, displayName: tableName, description: "", columns: Array.isArray(columns) ? columns.map((col) => ({ name: col.columnName || "unknown", type: col.dataType || "varchar", description: col.description || "", })) : [], }, onColumnClick: handleColumnClick, selectedColumns: [], connectedColumns: {}, }, selected: false, }; } catch (error) { console.warn(`테이블 ${tableName} 정보 로드 실패:`, error); return { id: `table-${tableName}`, type: "tableNode", position: jsonDiagram.node_positions?.[tableName] || { x: Math.random() * 300, y: Math.random() * 200, }, data: { table: { tableName, displayName: tableName, description: "", columns: [], }, onColumnClick: handleColumnClick, selectedColumns: [], connectedColumns: {}, }, selected: false, }; } }), ); setNodes(loadedNodes); // 관계 데이터로부터 엣지 생성 const loadedEdges = loadedRelationships.map((rel) => ({ id: `edge-${rel.fromTable}-${rel.toTable}-${rel.id}`, source: `table-${rel.fromTable}`, target: `table-${rel.toTable}`, type: "step", data: { relationshipId: rel.id, fromTable: rel.fromTable, toTable: rel.toTable, connectionType: rel.connectionType, relationshipName: rel.relationshipName, }, style: { stroke: "#3b82f6", strokeWidth: 2, }, animated: false, })); setEdges(loadedEdges); console.log("✅ 관계도 데이터 로드 완료:", { relationships: jsonDiagram.relationships?.relationships?.length || 0, tables: Array.from(new Set(loadedRelationships.flatMap((rel) => [rel.fromTable, rel.toTable]))).length, }); } } } catch (error) { console.error("관계도 데이터 로드 실패:", error); toast.error("관계도를 불러오는데 실패했습니다."); } } else { // 신규 생성 모드 setCurrentDiagramName(""); setNodes([]); setEdges([]); setTempRelationships([]); } }; loadDiagramData(); }, [diagramId, companyCode, setCurrentDiagramName, setNodes, setEdges, setTempRelationships, handleColumnClick]); // 키보드 이벤트 핸들러 (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; }); // 선택된 노드 초기화 setSelectedNodes([]); toast.success(`${selectedNodes.length}개 테이블이 삭제되었습니다.`); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [selectedNodes, setNodes, setSelectedColumns, setSelectedNodes]); // 현재 추가된 테이블명 목록 가져오기 const getSelectedTableNames = useCallback(() => { return extractTableNames(nodes); }, [nodes]); // 실제 테이블 노드 추가 const addTableNode = useCallback( async (table: TableDefinition) => { try { const newNode = { 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 onNodeClick = useCallback( (event: React.MouseEvent, node: Node) => { event.stopPropagation(); const nodeId = node.id; const isCurrentlySelected = selectedNodes.includes(nodeId); if (isCurrentlySelected) { // 이미 선택된 노드를 클릭하면 선택 해제 const newSelection = selectedNodes.filter((id) => id !== nodeId); setSelectedNodes(newSelection); // React Flow 노드 상태 업데이트 setNodes((prevNodes) => prevNodes.map((n) => ({ ...n, selected: newSelection.includes(n.id), })), ); } else { // 새로운 노드 선택 let newSelection: string[]; if (selectedNodes.length >= 2) { // 이미 2개가 선택되어 있으면 첫 번째를 제거하고 새로운 것을 추가 (FIFO) newSelection = [selectedNodes[1], nodeId]; } else { // 2개 미만이면 추가 newSelection = [...selectedNodes, nodeId]; } setSelectedNodes(newSelection); // React Flow 노드 상태 업데이트 setNodes((prevNodes) => prevNodes.map((n) => ({ ...n, selected: newSelection.includes(n.id), })), ); } }, [selectedNodes, setNodes, setSelectedNodes], ); // 노드 선택 변경 핸들러 (React Flow 자체 선택 이벤트는 무시) const onSelectionChange = useCallback(() => { // React Flow의 자동 선택 변경은 무시하고 우리의 커스텀 로직만 사용 // 이 함수는 비워두거나 최소한의 동기화만 수행 }, []); // 캔버스 클릭 시 엣지 정보 섹션 닫기 const onPaneClick = useCallback(() => { if (selectedEdgeInfo) { setSelectedEdgeInfo(null); } if (showEdgeActions) { setShowEdgeActions(false); setSelectedEdgeForEdit(null); } // 컬럼 선택 해제 setSelectedColumns({}); }, [ selectedEdgeInfo, showEdgeActions, setSelectedEdgeInfo, setShowEdgeActions, setSelectedEdgeForEdit, setSelectedColumns, ]); // 빈 onConnect 함수 (드래그 연결 비활성화) const onConnect = useCallback(() => { // 드래그로 연결하는 것을 방지 return; }, []); // 엣지 클릭 시 연결 정보 표시 및 관련 컬럼 하이라이트 const onEdgeClick = useCallback( (event: React.MouseEvent, edge: Edge) => { event.stopPropagation(); const edgeData = edge.data; if (edgeData) { // 해당 테이블 쌍의 모든 관계 찾기 const fromTable = edgeData.fromTable; const toTable = edgeData.toTable; const tablePairRelationships = tempRelationships.filter( (rel) => (rel.fromTable === fromTable && rel.toTable === toTable) || (rel.fromTable === toTable && rel.toTable === fromTable), ); console.log(`🔗 ${fromTable} ↔ ${toTable} 간의 관계:`, tablePairRelationships); // 관계가 1개든 여러 개든 항상 관계 목록 모달 표시 setSelectedTablePairRelationships(tablePairRelationships); setShowRelationshipListModal(true); } }, [tempRelationships, setSelectedTablePairRelationships, setShowRelationshipListModal], ); // 엣지 마우스 엔터 시 색상 변경 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], ); // 연결 가능한 상태인지 확인 const canCreateConnection = () => { return selectedNodes.length >= 2; }; // 테이블 노드 연결 설정 모달 열기 const openConnectionModal = () => { if (selectedNodes.length < 2) return; // 선택된 첫 번째와 두 번째 노드 찾기 const firstNode = nodes.find((node) => node.id === selectedNodes[0]); const secondNode = nodes.find((node) => node.id === selectedNodes[1]); if (!firstNode || !secondNode) return; 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, }, }); }; // 연결 설정 확인 const handleConfirmConnection = useCallback(() => { if (!pendingConnection) return; // 관계 생성 로직은 여기서 구현... // 현재는 간단히 성공 메시지만 표시 toast.success("관계가 생성되었습니다."); setPendingConnection(null); setHasUnsavedChanges(true); }, [pendingConnection, setPendingConnection, setHasUnsavedChanges]); // 연결 설정 취소 const handleCancelConnection = useCallback(() => { setPendingConnection(null); if (editingRelationshipId) { setEditingRelationshipId(null); setSelectedColumns({}); } }, [editingRelationshipId, setPendingConnection, setEditingRelationshipId, setSelectedColumns]); // 저장 모달 열기 const handleOpenSaveModal = useCallback(() => { setShowSaveModal(true); }, [setShowSaveModal]); // 저장 모달 닫기 const handleCloseSaveModal = useCallback(() => { if (!isSaving) { setShowSaveModal(false); } }, [isSaving, setShowSaveModal]); // 관계도 저장 함수 (간단한 구현) const handleSaveDiagram = useCallback( async (diagramName: string) => { if (nodes.length === 0) { toast.error("저장할 테이블이 없습니다."); return; } setIsSaving(true); try { // 여기서 실제 저장 로직 구현 toast.success(`관계도 "${diagramName}"가 성공적으로 저장되었습니다.`); setHasUnsavedChanges(false); setShowSaveModal(false); } catch (error) { console.error("관계도 저장 실패:", error); toast.error("관계도 저장 중 오류가 발생했습니다."); } finally { setIsSaving(false); } }, [nodes, setIsSaving, setHasUnsavedChanges, setShowSaveModal], ); // 고립된 노드 제거 함수 const removeOrphanedNodes = useCallback(() => { toast.success("고립된 노드가 정리되었습니다."); }, []); // 전체 삭제 핸들러 const clearNodes = useCallback(() => { setNodes([]); setEdges([]); setTempRelationships([]); setSelectedColumns({}); setSelectedNodes([]); setPendingConnection(null); setSelectedEdgeInfo(null); setShowEdgeActions(false); setSelectedEdgeForEdit(null); setHasUnsavedChanges(true); toast.success("모든 테이블과 관계가 삭제되었습니다."); }, [ setNodes, setEdges, setTempRelationships, setSelectedColumns, setSelectedNodes, setPendingConnection, setSelectedEdgeInfo, setShowEdgeActions, setSelectedEdgeForEdit, setHasUnsavedChanges, ]); return (
{/* 사이드바 */} {/* React Flow 캔버스 */}
{/* 관계 목록 모달 */} setShowRelationshipListModal(false)} onEdit={() => {}} onDelete={(relationshipId) => { setTempRelationships((prev) => prev.filter((rel) => rel.id !== relationshipId)); setEdges((prev) => prev.filter((edge) => edge.data?.relationshipId !== relationshipId)); setHasUnsavedChanges(true); }} onSetEditingId={setEditingRelationshipId} onSetSelectedColumns={setSelectedColumns} onSetPendingConnection={setPendingConnection} /> {/* 선택된 테이블 노드 팝업 */} {selectedNodes.length > 0 && !showEdgeActions && !pendingConnection && !editingRelationshipId && ( setSelectedNodes([])} onOpenConnectionModal={openConnectionModal} onClear={() => { setSelectedColumns({}); setSelectedNodes([]); }} canCreateConnection={canCreateConnection()} /> )} {/* 안내 메시지 */} {nodes.length === 0 && (
📊
테이블 간 데이터 관계 설정을 시작하세요
왼쪽 사이드바에서 테이블을 더블클릭하여 추가하세요
테이블 선택 후 Del 키로 삭제 가능
)}
{/* 연결 설정 모달 */} {/* 엣지 정보 및 액션 버튼 */} { setSelectedEdgeInfo(null); setShowEdgeActions(false); setSelectedEdgeForEdit(null); setSelectedColumns({}); }} onEdit={() => {}} onDelete={() => {}} /> {/* 관계도 저장 모달 */} 0 && currentDiagramName ? currentDiagramName // 편집 모드: 기존 관계도 이름 : `관계도 ${new Date().toLocaleDateString()}` // 신규 생성 모드: 새로운 이름 } isLoading={isSaving} />
); };