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

975 lines
34 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 16:49:59 +09:00
import {
TableDefinition,
DataFlowAPI,
JsonRelationship,
TableRelationship,
CreateDiagramRequest,
} from "@/lib/api/dataflow";
2025-09-16 14:57:47 +09:00
import { useAuth } from "@/hooks/useAuth";
import { useDataFlowDesigner } from "@/hooks/useDataFlowDesigner";
import { DataFlowDesignerProps, TableNodeData } from "@/types/dataflowTypes";
2025-09-16 16:49:59 +09:00
import { extractTableNames, extractNodePositions } 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
}) => {
2025-09-16 16:49:59 +09:00
const { user: authUser } = useAuth();
// 실제 사용자 회사 코드 사용 (prop보다 사용자 정보 우선)
2025-09-16 16:49:59 +09:00
const companyCode = authUser?.company_code || authUser?.companyCode || propCompanyCode;
2025-09-16 14:57:47 +09:00
// 커스텀 훅 사용
const {
nodes,
setNodes,
onNodesChange,
edges,
setEdges,
onEdgesChange,
selectedColumns,
setSelectedColumns,
selectedNodes,
setSelectedNodes,
pendingConnection,
setPendingConnection,
currentDiagramId,
setCurrentDiagramId,
2025-09-16 14:57:47 +09:00
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
2025-09-16 15:43:18 +09:00
// 컬럼 클릭 처리 비활성화 (테이블 노드 선택 방식으로 변경)
const handleColumnClick = useCallback((tableName: string, columnName: string) => {
// 컬럼 클릭으로는 더 이상 선택하지 않음
console.log(`컬럼 클릭 무시됨: ${tableName}.${columnName}`);
return;
}, []);
// 편집 모드일 때 관계도 데이터 로드
useEffect(() => {
2025-09-16 15:43:18 +09:00
const loadDiagramData = async () => {
if (diagramId && diagramId > 0) {
try {
// 편집 모드일 때 currentDiagramId 설정
setCurrentDiagramId(diagramId);
const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(diagramId, companyCode);
2025-09-16 15:43:18 +09:00
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 || "",
}));
setTempRelationships(loadedRelationships);
// 관계 데이터로부터 테이블 노드들을 생성
const tableNames = new Set<string>();
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);
}
}
} catch (error) {
2025-09-16 15:43:18 +09:00
console.error("관계도 데이터 로드 실패:", error);
toast.error("관계도를 불러오는데 실패했습니다.");
}
} else {
2025-09-16 15:43:18 +09:00
// 신규 생성 모드
setCurrentDiagramName("");
setNodes([]);
setEdges([]);
setTempRelationships([]);
}
};
2025-09-16 15:43:18 +09:00
loadDiagramData();
}, [
diagramId,
companyCode,
setCurrentDiagramId,
setCurrentDiagramName,
setNodes,
setEdges,
setTempRelationships,
handleColumnClick,
]);
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-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
// 관계가 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 16:49:59 +09:00
const handleConfirmConnection = useCallback(
(relationshipData: TableRelationship) => {
if (!pendingConnection || !relationshipData) return;
if (editingRelationshipId) {
// 편집 모드: 기존 관계 업데이트
const updatedRelationship = {
id: editingRelationshipId,
fromTable: relationshipData.from_table_name,
toTable: relationshipData.to_table_name,
fromColumns: relationshipData.from_column_name ? relationshipData.from_column_name.split(",") : [],
toColumns: relationshipData.to_column_name ? relationshipData.to_column_name.split(",") : [],
connectionType: relationshipData.connection_type as "simple-key" | "data-save" | "external-call",
relationshipName: relationshipData.relationship_name,
settings: relationshipData.settings || {},
};
2025-09-10 17:25:41 +09:00
2025-09-16 16:49:59 +09:00
// tempRelationships에서 기존 관계 업데이트
setTempRelationships((prev) =>
prev.map((rel) => (rel.id === editingRelationshipId ? updatedRelationship : rel)),
);
// 기존 엣지 업데이트
setEdges((prevEdges) =>
prevEdges.map((edge) =>
edge.data?.relationshipId === editingRelationshipId
? {
...edge,
data: {
...edge.data,
relationshipId: editingRelationshipId,
fromTable: relationshipData.from_table_name,
toTable: relationshipData.to_table_name,
connectionType: relationshipData.connection_type,
relationshipName: relationshipData.relationship_name,
},
}
: edge,
),
);
// 편집 모드 종료
setEditingRelationshipId(null);
} else {
// 새로 생성 모드: 새로운 관계 추가
const newRelationship = {
id: `rel-${Date.now()}`,
fromTable: relationshipData.from_table_name,
toTable: relationshipData.to_table_name,
fromColumns: relationshipData.from_column_name ? relationshipData.from_column_name.split(",") : [],
toColumns: relationshipData.to_column_name ? relationshipData.to_column_name.split(",") : [],
connectionType: relationshipData.connection_type as "simple-key" | "data-save" | "external-call",
relationshipName: relationshipData.relationship_name,
settings: relationshipData.settings || {},
};
// tempRelationships 상태 업데이트
setTempRelationships((prev) => [...prev, newRelationship]);
// 새로운 엣지 생성
const newEdge = {
id: `edge-${relationshipData.from_table_name}-${relationshipData.to_table_name}-${Date.now()}`,
source: `table-${relationshipData.from_table_name}`,
target: `table-${relationshipData.to_table_name}`,
type: "step",
data: {
relationshipId: newRelationship.id,
fromTable: relationshipData.from_table_name,
toTable: relationshipData.to_table_name,
connectionType: relationshipData.connection_type,
relationshipName: relationshipData.relationship_name,
},
style: {
stroke: "#3b82f6",
strokeWidth: 2,
},
animated: false,
};
// 엣지 추가
setEdges((prevEdges) => [...prevEdges, newEdge]);
}
setPendingConnection(null);
setHasUnsavedChanges(true);
},
[
pendingConnection,
setPendingConnection,
setHasUnsavedChanges,
setTempRelationships,
setEdges,
editingRelationshipId,
setEditingRelationshipId,
],
);
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]);
2025-09-16 16:49:59 +09:00
// 관계도 저장 함수
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 16:49:59 +09:00
// 노드 위치 정보 추출
const nodePositions = extractNodePositions(nodes);
// 연결된 테이블 목록 추출
const tableNames = extractTableNames(nodes);
// 관계 데이터를 JsonRelationship 형태로 변환 (settings 제거 - relationships는 순수 연결 정보만)
2025-09-16 16:49:59 +09:00
const jsonRelationships: JsonRelationship[] = tempRelationships.map((rel) => ({
id: rel.id,
relationshipName: rel.relationshipName, // 🔥 핵심: 관계 이름 포함
fromTable: rel.fromTable,
toTable: rel.toTable,
fromColumns: rel.fromColumns,
toColumns: rel.toColumns,
connectionType: rel.connectionType,
}));
// 저장 요청 데이터 구성
const saveRequest: CreateDiagramRequest = {
diagram_name: diagramName,
relationships: {
relationships: jsonRelationships,
tables: tableNames,
},
node_positions: nodePositions,
// 카테고리 정보 추가
category: tempRelationships.map((rel) => ({
id: rel.id,
category: rel.connectionType,
})),
// 조건부 연결 설정이 있는 경우 추가
control: tempRelationships
.filter((rel) => rel.settings?.control)
.map((rel) => ({
id: rel.id,
triggerType: rel.settings?.control?.triggerType || "insert",
conditions: (rel.settings?.control?.conditionTree || []).map((condition: Record<string, unknown>) => ({
...condition,
logicalOperator:
condition.logicalOperator === "AND" || condition.logicalOperator === "OR"
? condition.logicalOperator
: undefined,
})),
})),
// 데이터 저장 액션이 있는 경우 추가 (transformFunction 제거)
2025-09-16 16:49:59 +09:00
plan: tempRelationships
.filter((rel) => rel.settings?.actions && Array.isArray(rel.settings.actions))
.map((rel) => ({
id: rel.id,
sourceTable: rel.fromTable,
actions: (rel.settings?.actions || []).map((action: Record<string, unknown>) => ({
id: action.id as string,
name: action.name as string,
actionType: action.actionType as "insert" | "update" | "delete" | "upsert",
fieldMappings: ((action.fieldMappings as Record<string, unknown>[]) || []).map(
(mapping: Record<string, unknown>) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { transformFunction, ...cleanMapping } = mapping;
return cleanMapping as any; // transformFunction 제거 후 타입 캐스팅
},
),
splitConfig: action.splitConfig,
conditions: action.conditions,
})),
})) as any, // plan 전체를 any로 캐스팅
2025-09-16 16:49:59 +09:00
};
if ((diagramId && diagramId > 0) || (currentDiagramId && currentDiagramId > 0)) {
// 기존 관계도 수정 (prop diagramId 또는 내부 currentDiagramId 사용)
const targetDiagramId = diagramId || currentDiagramId;
2025-09-16 16:49:59 +09:00
await DataFlowAPI.updateJsonDataFlowDiagram(
targetDiagramId!,
2025-09-16 16:49:59 +09:00
saveRequest,
companyCode,
authUser?.userId || "SYSTEM",
);
} else {
// 새로운 관계도 생성
const newDiagram = await DataFlowAPI.createJsonDataFlowDiagram(
saveRequest,
companyCode,
authUser?.userId || "SYSTEM",
);
// 새로 생성된 다이어그램 ID를 내부 상태에 저장 (다음 저장부터는 업데이트 모드)
setCurrentDiagramId(newDiagram.diagram_id);
setCurrentDiagramName(newDiagram.diagram_name);
2025-09-16 16:49:59 +09:00
}
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 16:49:59 +09:00
[
nodes,
tempRelationships,
diagramId,
currentDiagramId,
2025-09-16 16:49:59 +09:00
companyCode,
authUser?.userId,
setIsSaving,
setHasUnsavedChanges,
setShowSaveModal,
setCurrentDiagramId,
setCurrentDiagramName,
2025-09-16 16:49:59 +09:00
],
);
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}
2025-09-16 16:49:59 +09:00
relationships={selectedTablePairRelationships.map((rel) => {
// 최신 tempRelationships에서 해당 관계 찾기
const updatedRel = tempRelationships.find((tempRel) => tempRel.id === rel.id);
return updatedRel || rel; // 업데이트된 관계가 있으면 사용, 없으면 원본 사용
})}
2025-09-16 14:57:47 +09:00
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({});
}}
2025-09-16 16:49:59 +09:00
onEdit={() => {
if (selectedEdgeInfo) {
// 기존 관계 찾기
const existingRelationship = tempRelationships.find((rel) => rel.id === selectedEdgeInfo.relationshipId);
if (existingRelationship) {
// 편집 모드로 설정
setEditingRelationshipId(selectedEdgeInfo.relationshipId);
// 연결 설정 모달 열기
const fromTable = nodes.find((node) => node.data?.table?.tableName === selectedEdgeInfo.fromTable);
const toTable = nodes.find((node) => node.data?.table?.tableName === selectedEdgeInfo.toTable);
if (fromTable && toTable) {
setPendingConnection({
fromNode: {
id: fromTable.id,
tableName: selectedEdgeInfo.fromTable,
displayName: fromTable.data?.table?.displayName || selectedEdgeInfo.fromTable,
},
toNode: {
id: toTable.id,
tableName: selectedEdgeInfo.toTable,
displayName: toTable.data?.table?.displayName || selectedEdgeInfo.toTable,
},
selectedColumnsData: {
[selectedEdgeInfo.fromTable]: {
displayName: fromTable.data?.table?.displayName || selectedEdgeInfo.fromTable,
columns: selectedEdgeInfo.fromColumns || [],
},
[selectedEdgeInfo.toTable]: {
displayName: toTable.data?.table?.displayName || selectedEdgeInfo.toTable,
columns: selectedEdgeInfo.toColumns || [],
},
},
existingRelationship: {
relationshipName: existingRelationship.relationshipName,
connectionType: existingRelationship.connectionType,
settings: existingRelationship.settings,
},
});
// 패널 닫기
setSelectedEdgeInfo(null);
setShowEdgeActions(false);
setSelectedEdgeForEdit(null);
}
}
}
}}
onDelete={() => {
if (selectedEdgeInfo) {
// 관계 삭제
setTempRelationships((prev) => prev.filter((rel) => rel.id !== selectedEdgeInfo.relationshipId));
// 엣지 삭제
setEdges((prev) => prev.filter((edge) => edge.data?.relationshipId !== selectedEdgeInfo.relationshipId));
// 변경사항 표시
setHasUnsavedChanges(true);
// 패널 닫기
setSelectedEdgeInfo(null);
setShowEdgeActions(false);
setSelectedEdgeForEdit(null);
setSelectedColumns({});
toast.success("관계가 삭제되었습니다.");
}
}}
2025-09-16 14:57:47 +09:00
/>
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>
);
};