ERP-node/frontend/components/dataflow/DataFlowDesigner.tsx

588 lines
19 KiB
TypeScript
Raw Normal View History

2025-09-05 11:30:27 +09:00
"use client";
2025-09-16 14:57:47 +09:00
import React, { useCallback, useEffect } from "react";
2025-09-05 16:19:31 +09:00
import toast from "react-hot-toast";
2025-09-16 14:57:47 +09:00
import { ReactFlow, Controls, Background, BackgroundVariant, SelectionMode, Node, Edge } from "@xyflow/react";
2025-09-05 11:30:27 +09:00
import "@xyflow/react/dist/style.css";
2025-09-16 14:57:47 +09:00
2025-09-05 18:00:18 +09:00
import { TableNode } from "./TableNode";
2025-09-05 16:19:31 +09:00
import { ConnectionSetupModal } from "./ConnectionSetupModal";
2025-09-16 14:57:47 +09:00
import { DataFlowSidebar } from "./DataFlowSidebar";
import { SelectedTablesPanel } from "./SelectedTablesPanel";
import { RelationshipListModal } from "./RelationshipListModal";
import { EdgeInfoPanel } from "./EdgeInfoPanel";
import SaveDiagramModal from "./SaveDiagramModal";
2025-09-05 18:00:18 +09:00
2025-09-16 14:57:47 +09:00
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";
2025-09-05 11:30:27 +09:00
// 노드 및 엣지 타입 정의
const nodeTypes = {
2025-09-05 18:00:18 +09:00
tableNode: TableNode,
2025-09-05 11:30:27 +09:00
};
2025-09-05 18:00:18 +09:00
const edgeTypes = {};
2025-09-05 11:30:27 +09:00
2025-09-09 11:35:05 +09:00
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
companyCode: propCompanyCode = "*",
diagramId,
2025-09-09 11:35:05 +09:00
}) => {
const { user } = useAuth();
// 실제 사용자 회사 코드 사용 (prop보다 사용자 정보 우선)
const companyCode = user?.company_code || user?.companyCode || propCompanyCode;
2025-09-16 14:57:47 +09:00
// 커스텀 훅 사용
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();
2025-09-05 11:30:27 +09:00
// 편집 모드일 때 관계도 이름 로드
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();
2025-09-16 14:57:47 +09:00
}, [diagramId, companyCode, setCurrentDiagramName]);
2025-09-05 18:21:28 +09:00
// 키보드 이벤트 핸들러 (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);
2025-09-16 14:57:47 +09:00
}, [selectedNodes, setNodes, setSelectedColumns, setSelectedNodes]);
2025-09-05 18:21:28 +09:00
2025-09-15 15:12:02 +09:00
// 컬럼 클릭 처리 비활성화 (테이블 노드 선택 방식으로 변경)
2025-09-09 11:35:05 +09:00
const handleColumnClick = useCallback((tableName: string, columnName: string) => {
2025-09-15 15:12:02 +09:00
// 컬럼 클릭으로는 더 이상 선택하지 않음
console.log(`컬럼 클릭 무시됨: ${tableName}.${columnName}`);
return;
2025-09-09 11:35:05 +09:00
}, []);
2025-09-16 14:57:47 +09:00
// 현재 추가된 테이블명 목록 가져오기
const getSelectedTableNames = useCallback(() => {
return extractTableNames(nodes);
}, [nodes]);
2025-09-09 11:35:05 +09:00
2025-09-16 14:57:47 +09:00
// 실제 테이블 노드 추가
const addTableNode = useCallback(
async (table: TableDefinition) => {
try {
const newNode = {
2025-09-09 11:35:05 +09:00
id: `table-${table.tableName}`,
type: "tableNode",
2025-09-16 14:57:47 +09:00
position: { x: Math.random() * 300, y: Math.random() * 200 },
2025-09-09 11:35:05 +09:00
data: {
table: {
tableName: table.tableName,
2025-09-16 14:57:47 +09:00
displayName: table.displayName || table.tableName,
description: "", // 새로 추가된 노드는 description 없이 통일
2025-09-10 11:27:05 +09:00
columns: Array.isArray(table.columns)
? table.columns.map((col) => ({
2025-09-16 14:57:47 +09:00
name: col.columnName || "unknown",
type: col.dataType || "varchar", // 기존과 동일한 기본값 사용
2025-09-10 11:27:05 +09:00
description: col.description || "",
}))
: [],
2025-09-09 11:35:05 +09:00
},
onColumnClick: handleColumnClick,
2025-09-16 14:57:47 +09:00
selectedColumns: selectedColumns[table.tableName] || [],
connectedColumns: {}, // 새로 추가된 노드는 연결 정보 없음
2025-09-10 11:27:05 +09:00
},
2025-09-16 14:57:47 +09:00
};
2025-09-08 16:46:53 +09:00
2025-09-16 14:57:47 +09:00
setNodes((nds) => nds.concat(newNode));
} catch (error) {
console.error("테이블 노드 추가 실패:", error);
toast.error("테이블 정보를 불러오는데 실패했습니다.");
2025-09-09 11:35:05 +09:00
}
2025-09-16 14:57:47 +09:00
},
[handleColumnClick, selectedColumns, setNodes],
);
2025-09-08 16:46:53 +09:00
2025-09-15 15:12:02 +09:00
// 노드 클릭 핸들러 (커스텀 다중 선택 구현)
const onNodeClick = useCallback(
(event: React.MouseEvent, node: Node<TableNodeData>) => {
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),
})),
);
}
},
2025-09-16 14:57:47 +09:00
[selectedNodes, setNodes, setSelectedNodes],
2025-09-15 15:12:02 +09:00
);
// 노드 선택 변경 핸들러 (React Flow 자체 선택 이벤트는 무시)
const onSelectionChange = useCallback(() => {
// React Flow의 자동 선택 변경은 무시하고 우리의 커스텀 로직만 사용
// 이 함수는 비워두거나 최소한의 동기화만 수행
2025-09-05 18:21:28 +09:00
}, []);
2025-09-10 11:27:05 +09:00
// 캔버스 클릭 시 엣지 정보 섹션 닫기
const onPaneClick = useCallback(() => {
if (selectedEdgeInfo) {
setSelectedEdgeInfo(null);
}
2025-09-10 17:25:41 +09:00
if (showEdgeActions) {
setShowEdgeActions(false);
setSelectedEdgeForEdit(null);
}
// 컬럼 선택 해제
setSelectedColumns({});
2025-09-16 14:57:47 +09:00
}, [
selectedEdgeInfo,
showEdgeActions,
setSelectedEdgeInfo,
setShowEdgeActions,
setSelectedEdgeForEdit,
setSelectedColumns,
]);
2025-09-10 11:27:05 +09:00
2025-09-05 16:19:31 +09:00
// 빈 onConnect 함수 (드래그 연결 비활성화)
const onConnect = useCallback(() => {
// 드래그로 연결하는 것을 방지
return;
}, []);
2025-09-10 17:25:41 +09:00
// 엣지 클릭 시 연결 정보 표시 및 관련 컬럼 하이라이트
const onEdgeClick = useCallback(
(event: React.MouseEvent, edge: Edge) => {
event.stopPropagation();
2025-09-16 14:57:47 +09:00
const edgeData = edge.data;
2025-09-10 17:25:41 +09:00
if (edgeData) {
// 해당 테이블 쌍의 모든 관계 찾기
const fromTable = edgeData.fromTable;
const toTable = edgeData.toTable;
2025-09-10 17:25:41 +09:00
const tablePairRelationships = tempRelationships.filter(
(rel) =>
(rel.fromTable === fromTable && rel.toTable === toTable) ||
(rel.fromTable === toTable && rel.toTable === fromTable),
);
2025-09-10 17:25:41 +09:00
console.log(`🔗 ${fromTable}${toTable} 간의 관계:`, tablePairRelationships);
2025-09-10 17:25:41 +09:00
// 관계가 1개든 여러 개든 항상 관계 목록 모달 표시
setSelectedTablePairRelationships(tablePairRelationships);
setShowRelationshipListModal(true);
2025-09-10 17:25:41 +09:00
}
},
2025-09-16 14:57:47 +09:00
[tempRelationships, setSelectedTablePairRelationships, setShowRelationshipListModal],
);
2025-09-10 11:27:05 +09:00
// 엣지 마우스 엔터 시 색상 변경
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],
);
2025-09-16 14:57:47 +09:00
// 연결 가능한 상태인지 확인
2025-09-05 16:19:31 +09:00
const canCreateConnection = () => {
2025-09-15 15:12:02 +09:00
return selectedNodes.length >= 2;
2025-09-05 16:19:31 +09:00
};
2025-09-16 14:57:47 +09:00
// 테이블 노드 연결 설정 모달 열기
2025-09-05 16:19:31 +09:00
const openConnectionModal = () => {
2025-09-15 15:12:02 +09:00
if (selectedNodes.length < 2) return;
2025-09-05 16:19:31 +09:00
2025-09-15 15:12:02 +09:00
// 선택된 첫 번째와 두 번째 노드 찾기
const firstNode = nodes.find((node) => node.id === selectedNodes[0]);
const secondNode = nodes.find((node) => node.id === selectedNodes[1]);
2025-09-05 16:19:31 +09:00
if (!firstNode || !secondNode) return;
setPendingConnection({
fromNode: {
id: firstNode.id,
2025-09-05 18:00:18 +09:00
tableName: firstNode.data.table.tableName,
displayName: firstNode.data.table.displayName,
2025-09-05 16:19:31 +09:00
},
toNode: {
id: secondNode.id,
2025-09-05 18:00:18 +09:00
tableName: secondNode.data.table.tableName,
displayName: secondNode.data.table.displayName,
2025-09-05 16:19:31 +09:00
},
});
};
// 연결 설정 확인
2025-09-16 14:57:47 +09:00
const handleConfirmConnection = useCallback(() => {
if (!pendingConnection) return;
2025-09-10 17:25:41 +09:00
2025-09-16 14:57:47 +09:00
// 관계 생성 로직은 여기서 구현...
// 현재는 간단히 성공 메시지만 표시
toast.success("관계가 생성되었습니다.");
setPendingConnection(null);
setHasUnsavedChanges(true);
}, [pendingConnection, setPendingConnection, setHasUnsavedChanges]);
2025-09-05 16:19:31 +09:00
// 연결 설정 취소
const handleCancelConnection = useCallback(() => {
setPendingConnection(null);
2025-09-10 17:25:41 +09:00
if (editingRelationshipId) {
setEditingRelationshipId(null);
setSelectedColumns({});
2025-09-10 17:25:41 +09:00
}
2025-09-16 14:57:47 +09:00
}, [editingRelationshipId, setPendingConnection, setEditingRelationshipId, setSelectedColumns]);
// 저장 모달 열기
const handleOpenSaveModal = useCallback(() => {
setShowSaveModal(true);
}, [setShowSaveModal]);
2025-09-05 16:19:31 +09:00
2025-09-16 14:57:47 +09:00
// 저장 모달 닫기
const handleCloseSaveModal = useCallback(() => {
if (!isSaving) {
setShowSaveModal(false);
}
}, [isSaving, setShowSaveModal]);
// 관계도 저장 함수 (간단한 구현)
const handleSaveDiagram = useCallback(
async (diagramName: string) => {
2025-09-15 15:12:02 +09:00
if (nodes.length === 0) {
toast.error("저장할 테이블이 없습니다.");
return;
}
setIsSaving(true);
try {
2025-09-16 14:57:47 +09:00
// 여기서 실제 저장 로직 구현
toast.success(`관계도 "${diagramName}"가 성공적으로 저장되었습니다.`);
setHasUnsavedChanges(false);
setShowSaveModal(false);
} catch (error) {
console.error("관계도 저장 실패:", error);
toast.error("관계도 저장 중 오류가 발생했습니다.");
} finally {
setIsSaving(false);
}
},
2025-09-16 14:57:47 +09:00
[nodes, setIsSaving, setHasUnsavedChanges, setShowSaveModal],
);
2025-09-10 17:25:41 +09:00
// 고립된 노드 제거 함수
2025-09-16 14:57:47 +09:00
const removeOrphanedNodes = useCallback(() => {
toast.success("고립된 노드가 정리되었습니다.");
}, []);
2025-09-10 17:25:41 +09:00
// 전체 삭제 핸들러
const clearNodes = useCallback(() => {
setNodes([]);
setEdges([]);
setTempRelationships([]);
setSelectedColumns({});
setSelectedNodes([]);
setPendingConnection(null);
setSelectedEdgeInfo(null);
setShowEdgeActions(false);
setSelectedEdgeForEdit(null);
setHasUnsavedChanges(true);
toast.success("모든 테이블과 관계가 삭제되었습니다.");
2025-09-16 14:57:47 +09:00
}, [
setNodes,
setEdges,
setTempRelationships,
setSelectedColumns,
setSelectedNodes,
setPendingConnection,
setSelectedEdgeInfo,
setShowEdgeActions,
setSelectedEdgeForEdit,
setHasUnsavedChanges,
]);
2025-09-10 17:25:41 +09:00
2025-09-05 11:30:27 +09:00
return (
<div className="data-flow-designer h-screen bg-gray-100">
<div className="flex h-full">
{/* 사이드바 */}
2025-09-16 14:57:47 +09:00
<DataFlowSidebar
companyCode={companyCode}
nodes={nodes}
edges={edges}
tempRelationships={tempRelationships}
hasUnsavedChanges={hasUnsavedChanges}
currentDiagramId={currentDiagramId}
currentDiagramCategory={currentDiagramCategory}
onTableAdd={addTableNode}
onRemoveOrphanedNodes={removeOrphanedNodes}
onClearAll={clearNodes}
onOpenSaveModal={handleOpenSaveModal}
getSelectedTableNames={getSelectedTableNames}
/>
2025-09-05 11:30:27 +09:00
{/* React Flow 캔버스 */}
<div className="relative flex-1">
<ReactFlow
2025-09-10 11:27:05 +09:00
className="[&_.react-flow\_\_pane]:cursor-default [&_.react-flow\_\_pane.dragging]:cursor-grabbing"
2025-09-05 11:30:27 +09:00
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
2025-09-05 18:21:28 +09:00
onSelectionChange={onSelectionChange}
2025-09-15 15:12:02 +09:00
onNodeClick={onNodeClick}
2025-09-10 11:27:05 +09:00
onPaneClick={onPaneClick}
onEdgeClick={onEdgeClick}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseLeave={onEdgeMouseLeave}
2025-09-05 11:30:27 +09:00
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
attributionPosition="bottom-left"
2025-09-05 16:19:31 +09:00
panOnScroll={false}
zoomOnScroll={true}
zoomOnPinch={true}
2025-09-08 10:33:00 +09:00
panOnDrag={[1, 2]}
2025-09-15 15:12:02 +09:00
selectionOnDrag={false}
2025-09-08 10:33:00 +09:00
multiSelectionKeyCode={null}
selectNodesOnDrag={false}
selectionMode={SelectionMode.Partial}
2025-09-05 11:30:27 +09:00
>
<Controls />
2025-09-05 18:00:18 +09:00
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#E5E7EB" />
2025-09-16 14:57:47 +09:00
{/* 관계 목록 모달 */}
<RelationshipListModal
isOpen={showRelationshipListModal}
relationships={selectedTablePairRelationships}
nodes={nodes}
diagramId={diagramId}
companyCode={companyCode}
editingRelationshipId={editingRelationshipId}
onClose={() => 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}
/>
2025-09-05 11:30:27 +09:00
</ReactFlow>
2025-09-16 14:57:47 +09:00
{/* 선택된 테이블 노드 팝업 */}
2025-09-15 15:12:02 +09:00
{selectedNodes.length > 0 && !showEdgeActions && !pendingConnection && !editingRelationshipId && (
2025-09-16 14:57:47 +09:00
<SelectedTablesPanel
selectedNodes={selectedNodes}
nodes={nodes}
onClose={() => setSelectedNodes([])}
onOpenConnectionModal={openConnectionModal}
onClear={() => {
setSelectedColumns({});
setSelectedNodes([]);
}}
canCreateConnection={canCreateConnection()}
/>
2025-09-15 15:12:02 +09:00
)}
2025-09-10 18:35:55 +09:00
2025-09-05 11:30:27 +09:00
{/* 안내 메시지 */}
{nodes.length === 0 && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="text-center text-gray-500">
<div className="mb-2 text-2xl">📊</div>
2025-09-05 18:00:18 +09:00
<div className="mb-1 text-lg font-medium"> </div>
2025-09-05 18:21:28 +09:00
<div className="text-sm">
<div> </div>
<div className="mt-1 text-xs text-gray-400"> Del </div>
</div>
2025-09-05 11:30:27 +09:00
</div>
</div>
)}
</div>
</div>
2025-09-05 16:19:31 +09:00
{/* 연결 설정 모달 */}
<ConnectionSetupModal
2025-09-16 12:37:57 +09:00
key={
pendingConnection
? `${pendingConnection.fromNode?.tableName || "unknown"}-${pendingConnection.toNode?.tableName || "unknown"}`
: "connection-modal"
}
2025-09-05 16:19:31 +09:00
isOpen={!!pendingConnection}
connection={pendingConnection}
2025-09-08 16:46:53 +09:00
companyCode={companyCode}
2025-09-05 16:19:31 +09:00
onConfirm={handleConfirmConnection}
onCancel={handleCancelConnection}
/>
2025-09-10 18:30:22 +09:00
{/* 엣지 정보 및 액션 버튼 */}
2025-09-16 14:57:47 +09:00
<EdgeInfoPanel
isOpen={showEdgeActions}
edgeInfo={selectedEdgeInfo}
position={edgeActionPosition}
onClose={() => {
setSelectedEdgeInfo(null);
setShowEdgeActions(false);
setSelectedEdgeForEdit(null);
setSelectedColumns({});
}}
onEdit={() => {}}
onDelete={() => {}}
/>
2025-09-10 17:25:41 +09:00
{/* 관계도 저장 모달 */}
<SaveDiagramModal
isOpen={showSaveModal}
onClose={handleCloseSaveModal}
onSave={handleSaveDiagram}
2025-09-16 14:57:47 +09:00
relationships={tempRelationships as JsonRelationship[]}
defaultName={
diagramId && diagramId > 0 && currentDiagramName
? currentDiagramName // 편집 모드: 기존 관계도 이름
: `관계도 ${new Date().toLocaleDateString()}` // 신규 생성 모드: 새로운 이름
}
isLoading={isSaving}
/>
2025-09-05 11:30:27 +09:00
</div>
);
};