2025-09-05 11:30:27 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
2025-09-05 16:19:31 +09:00
|
|
|
|
import React, { useState, useCallback, useEffect, useRef } from "react";
|
|
|
|
|
|
import toast from "react-hot-toast";
|
2025-09-05 11:30:27 +09:00
|
|
|
|
import {
|
|
|
|
|
|
ReactFlow,
|
|
|
|
|
|
Node,
|
|
|
|
|
|
Edge,
|
|
|
|
|
|
Controls,
|
|
|
|
|
|
Background,
|
|
|
|
|
|
useNodesState,
|
|
|
|
|
|
useEdgesState,
|
2025-09-05 18:00:18 +09:00
|
|
|
|
BackgroundVariant,
|
2025-09-08 10:33:00 +09:00
|
|
|
|
SelectionMode,
|
2025-09-05 11:30:27 +09:00
|
|
|
|
} from "@xyflow/react";
|
|
|
|
|
|
import "@xyflow/react/dist/style.css";
|
2025-09-05 18:00:18 +09:00
|
|
|
|
import { TableNode } from "./TableNode";
|
|
|
|
|
|
import { TableSelector } from "./TableSelector";
|
2025-09-05 16:19:31 +09:00
|
|
|
|
import { ConnectionSetupModal } from "./ConnectionSetupModal";
|
2025-09-10 15:30:14 +09:00
|
|
|
|
import {
|
|
|
|
|
|
TableDefinition,
|
|
|
|
|
|
TableRelationship,
|
|
|
|
|
|
DataFlowAPI,
|
|
|
|
|
|
DataFlowDiagram,
|
|
|
|
|
|
JsonRelationship,
|
|
|
|
|
|
CreateDiagramRequest,
|
2025-09-10 17:48:55 +09:00
|
|
|
|
NodePositions,
|
2025-09-10 15:30:14 +09:00
|
|
|
|
} from "@/lib/api/dataflow";
|
|
|
|
|
|
import SaveDiagramModal from "./SaveDiagramModal";
|
|
|
|
|
|
import { useAuth } from "@/hooks/useAuth";
|
2025-09-08 16:46:53 +09:00
|
|
|
|
|
|
|
|
|
|
// 고유 ID 생성 함수
|
2025-09-09 18:42:01 +09:00
|
|
|
|
const generateUniqueId = (prefix: string, diagramId?: number): string => {
|
2025-09-08 16:46:53 +09:00
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
|
const random = Math.random().toString(36).substr(2, 9);
|
2025-09-09 18:42:01 +09:00
|
|
|
|
return `${prefix}-${diagramId || timestamp}-${random}`;
|
2025-09-08 16:46:53 +09:00
|
|
|
|
};
|
2025-09-05 18:00:18 +09:00
|
|
|
|
|
|
|
|
|
|
// 테이블 노드 데이터 타입 정의
|
|
|
|
|
|
interface TableNodeData extends Record<string, unknown> {
|
|
|
|
|
|
table: {
|
|
|
|
|
|
tableName: string;
|
|
|
|
|
|
displayName: string;
|
|
|
|
|
|
description: string;
|
|
|
|
|
|
columns: Array<{
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
type: string;
|
|
|
|
|
|
description: string;
|
|
|
|
|
|
}>;
|
|
|
|
|
|
};
|
|
|
|
|
|
onColumnClick: (tableName: string, columnName: string) => void;
|
|
|
|
|
|
selectedColumns: string[];
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
interface DataFlowDesignerProps {
|
2025-09-09 13:48:57 +09:00
|
|
|
|
companyCode?: string;
|
2025-09-05 18:00:18 +09:00
|
|
|
|
onSave?: (relationships: TableRelationship[]) => void;
|
2025-09-09 13:48:57 +09:00
|
|
|
|
selectedDiagram?: DataFlowDiagram | string | null;
|
2025-09-09 18:42:01 +09:00
|
|
|
|
diagramId?: number;
|
|
|
|
|
|
relationshipId?: string; // 하위 호환성 유지
|
2025-09-09 11:35:05 +09:00
|
|
|
|
onBackToList?: () => void;
|
2025-09-05 18:00:18 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
|
// TableRelationship 타입은 dataflow.ts에서 import
|
2025-09-05 11:30:27 +09:00
|
|
|
|
|
2025-09-09 11:35:05 +09:00
|
|
|
|
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
2025-09-10 15:30:14 +09:00
|
|
|
|
companyCode: propCompanyCode = "*",
|
2025-09-09 18:42:01 +09:00
|
|
|
|
diagramId,
|
|
|
|
|
|
relationshipId, // 하위 호환성 유지
|
2025-09-10 15:30:14 +09:00
|
|
|
|
onSave, // eslint-disable-line @typescript-eslint/no-unused-vars
|
2025-09-09 11:35:05 +09:00
|
|
|
|
selectedDiagram,
|
2025-09-09 12:00:58 +09:00
|
|
|
|
onBackToList, // eslint-disable-line @typescript-eslint/no-unused-vars
|
2025-09-09 11:35:05 +09:00
|
|
|
|
}) => {
|
2025-09-10 15:30:14 +09:00
|
|
|
|
const { user } = useAuth();
|
|
|
|
|
|
|
|
|
|
|
|
// 실제 사용자 회사 코드 사용 (prop보다 사용자 정보 우선)
|
|
|
|
|
|
const companyCode = user?.company_code || user?.companyCode || propCompanyCode;
|
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node<TableNodeData>>([]);
|
|
|
|
|
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
|
|
|
|
|
const [selectedColumns, setSelectedColumns] = useState<{
|
|
|
|
|
|
[tableName: string]: string[];
|
2025-09-05 16:19:31 +09:00
|
|
|
|
}>({});
|
|
|
|
|
|
const [selectionOrder, setSelectionOrder] = useState<string[]>([]);
|
2025-09-05 18:21:28 +09:00
|
|
|
|
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
|
2025-09-05 16:19:31 +09:00
|
|
|
|
const [pendingConnection, setPendingConnection] = useState<{
|
2025-09-05 18:00:18 +09:00
|
|
|
|
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[];
|
2025-09-05 16:19:31 +09:00
|
|
|
|
};
|
|
|
|
|
|
};
|
2025-09-11 10:45:16 +09:00
|
|
|
|
existingRelationship?: {
|
|
|
|
|
|
relationshipName: string;
|
|
|
|
|
|
relationshipType: string;
|
|
|
|
|
|
connectionType: string;
|
|
|
|
|
|
settings?: any;
|
|
|
|
|
|
};
|
2025-09-05 11:30:27 +09:00
|
|
|
|
} | null>(null);
|
2025-09-08 18:18:47 +09:00
|
|
|
|
const [relationships, setRelationships] = useState<TableRelationship[]>([]); // eslint-disable-line @typescript-eslint/no-unused-vars
|
2025-09-09 18:42:01 +09:00
|
|
|
|
const [currentDiagramId, setCurrentDiagramId] = useState<number | null>(null); // 현재 화면의 diagram_id
|
2025-09-10 15:30:14 +09:00
|
|
|
|
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<JsonRelationship[]>([]); // 메모리에 저장된 관계들
|
|
|
|
|
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // 저장되지 않은 변경사항
|
|
|
|
|
|
const [showSaveModal, setShowSaveModal] = useState(false); // 저장 모달 표시 상태
|
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false); // 저장 중 상태
|
|
|
|
|
|
const [currentDiagramName, setCurrentDiagramName] = useState<string>(""); // 현재 편집 중인 관계도 이름
|
2025-09-10 17:25:41 +09:00
|
|
|
|
const [selectedEdgeForEdit, setSelectedEdgeForEdit] = useState<Edge | null>(null); // 수정/삭제할 엣지
|
|
|
|
|
|
const [showEdgeActions, setShowEdgeActions] = useState(false); // 엣지 액션 버튼 표시 상태
|
|
|
|
|
|
const [edgeActionPosition, setEdgeActionPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); // 액션 버튼 위치
|
|
|
|
|
|
const [editingRelationshipId, setEditingRelationshipId] = useState<string | null>(null); // 현재 수정 중인 관계 ID
|
2025-09-09 12:00:58 +09:00
|
|
|
|
const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars
|
2025-09-05 11:30:27 +09:00
|
|
|
|
|
2025-09-10 15:30:14 +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();
|
|
|
|
|
|
}, [diagramId, companyCode]);
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 선택 순서도 정리
|
|
|
|
|
|
setSelectionOrder((prev) => prev.filter((tableName) => !deletedTableNames.includes(tableName)));
|
|
|
|
|
|
|
|
|
|
|
|
// 선택된 노드 초기화
|
|
|
|
|
|
setSelectedNodes([]);
|
|
|
|
|
|
|
|
|
|
|
|
toast.success(`${selectedNodes.length}개 테이블이 삭제되었습니다.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
|
|
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
|
|
|
|
}, [selectedNodes, setNodes]);
|
|
|
|
|
|
|
2025-09-09 11:35:05 +09:00
|
|
|
|
// 컬럼 클릭 처리 (토글 방식, 최대 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 () => {
|
2025-09-09 18:42:01 +09:00
|
|
|
|
const currentDiagramId = diagramId || (relationshipId ? parseInt(relationshipId) : null);
|
|
|
|
|
|
if (!currentDiagramId || isNaN(currentDiagramId)) return;
|
2025-09-09 11:35:05 +09:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-09-10 15:30:14 +09:00
|
|
|
|
console.log("🔍 JSON 관계도 로드 시작 (diagramId):", currentDiagramId);
|
2025-09-09 11:35:05 +09:00
|
|
|
|
toast.loading("관계도를 불러오는 중...", { id: "load-diagram" });
|
2025-09-09 13:48:57 +09:00
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// 새로운 JSON API로 관계도 조회
|
|
|
|
|
|
const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(currentDiagramId);
|
|
|
|
|
|
console.log("📋 JSON 관계도 데이터:", jsonDiagram);
|
2025-09-09 18:42:01 +09:00
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
if (!jsonDiagram || !jsonDiagram.relationships) {
|
|
|
|
|
|
throw new Error("관계도 데이터를 찾을 수 없습니다.");
|
2025-09-09 18:42:01 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
const relationships = jsonDiagram.relationships.relationships || [];
|
|
|
|
|
|
const tableNames = jsonDiagram.relationships.tables || [];
|
2025-09-09 11:35:05 +09:00
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
console.log("📋 관계 목록:", relationships);
|
|
|
|
|
|
console.log("📊 테이블 목록:", tableNames);
|
2025-09-09 18:42:01 +09:00
|
|
|
|
|
2025-09-10 18:30:22 +09:00
|
|
|
|
// 기존 데이터에서 relationshipName이 없는 경우 기본값 설정
|
|
|
|
|
|
const normalizedRelationships = relationships.map((rel: JsonRelationship) => ({
|
|
|
|
|
|
...rel,
|
|
|
|
|
|
relationshipName: rel.relationshipName || `${rel.fromTable} → ${rel.toTable}`, // 기본값 설정
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// 메모리에 관계 저장 (기존 관계도 편집 시)
|
2025-09-10 18:30:22 +09:00
|
|
|
|
setTempRelationships(normalizedRelationships);
|
2025-09-10 15:30:14 +09:00
|
|
|
|
setCurrentDiagramId(currentDiagramId);
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블 노드 생성을 위한 테이블 정보 로드
|
2025-09-09 11:35:05 +09:00
|
|
|
|
|
|
|
|
|
|
// 테이블 정보 로드
|
|
|
|
|
|
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);
|
2025-09-10 11:27:05 +09:00
|
|
|
|
const safeColumns = Array.isArray(columns) ? columns : [];
|
|
|
|
|
|
console.log(`📋 테이블 ${tableName}의 컬럼 수:`, safeColumns.length);
|
2025-09-09 11:35:05 +09:00
|
|
|
|
tableDefinitions.push({
|
|
|
|
|
|
tableName: foundTable.tableName,
|
|
|
|
|
|
displayName: foundTable.displayName,
|
|
|
|
|
|
description: foundTable.description,
|
2025-09-10 11:27:05 +09:00
|
|
|
|
columns: safeColumns,
|
2025-09-09 11:35:05 +09:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn(`⚠️ 테이블 ${tableName}을 찾을 수 없습니다`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-09 12:00:58 +09:00
|
|
|
|
// 연결된 컬럼 정보 계산
|
|
|
|
|
|
const connectedColumnsInfo: {
|
|
|
|
|
|
[tableName: string]: { [columnName: string]: { direction: "source" | "target" | "both" } };
|
|
|
|
|
|
} = {};
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
relationships.forEach((rel: JsonRelationship) => {
|
|
|
|
|
|
const fromTable = rel.fromTable;
|
|
|
|
|
|
const toTable = rel.toTable;
|
|
|
|
|
|
const fromColumns = rel.fromColumns || [];
|
|
|
|
|
|
const toColumns = rel.toColumns || [];
|
2025-09-09 12:00:58 +09:00
|
|
|
|
|
|
|
|
|
|
// 소스 테이블의 컬럼들을 source로 표시
|
|
|
|
|
|
if (!connectedColumnsInfo[fromTable]) connectedColumnsInfo[fromTable] = {};
|
2025-09-10 15:30:14 +09:00
|
|
|
|
fromColumns.forEach((col: string) => {
|
2025-09-09 12:00:58 +09:00
|
|
|
|
if (connectedColumnsInfo[fromTable][col]) {
|
|
|
|
|
|
connectedColumnsInfo[fromTable][col].direction = "both";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
connectedColumnsInfo[fromTable][col] = { direction: "source" };
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 타겟 테이블의 컬럼들을 target으로 표시
|
|
|
|
|
|
if (!connectedColumnsInfo[toTable]) connectedColumnsInfo[toTable] = {};
|
2025-09-10 15:30:14 +09:00
|
|
|
|
toColumns.forEach((col: string) => {
|
2025-09-09 12:00:58 +09:00
|
|
|
|
if (connectedColumnsInfo[toTable][col]) {
|
|
|
|
|
|
connectedColumnsInfo[toTable][col].direction = "both";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
connectedColumnsInfo[toTable][col] = { direction: "target" };
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-10 11:27:05 +09:00
|
|
|
|
console.log("🔌 연결된 컬럼 정보:", connectedColumnsInfo);
|
|
|
|
|
|
|
2025-09-10 17:48:55 +09:00
|
|
|
|
// 저장된 노드 위치 정보 가져오기
|
|
|
|
|
|
const savedNodePositions = jsonDiagram.node_positions || {};
|
|
|
|
|
|
console.log("📍 저장된 노드 위치:", savedNodePositions);
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블을 노드로 변환 (저장된 위치 우선 사용, 없으면 자동 레이아웃)
|
2025-09-09 11:35:05 +09:00
|
|
|
|
const tableNodes = tableDefinitions.map((table, index) => {
|
2025-09-10 17:48:55 +09:00
|
|
|
|
// 저장된 위치가 있으면 사용, 없으면 자동 배치
|
|
|
|
|
|
const savedPosition = savedNodePositions[table.tableName];
|
|
|
|
|
|
const x = savedPosition ? savedPosition.x : (index % 3) * 400 + 100; // 3열 배치
|
|
|
|
|
|
const y = savedPosition ? savedPosition.y : Math.floor(index / 3) * 300 + 100;
|
2025-09-09 11:35:05 +09:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: `table-${table.tableName}`,
|
|
|
|
|
|
type: "tableNode",
|
|
|
|
|
|
position: { x, y },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
table: {
|
|
|
|
|
|
tableName: table.tableName,
|
|
|
|
|
|
displayName: table.displayName,
|
2025-09-10 11:27:05 +09:00
|
|
|
|
description: "", // 기존 로드된 노드도 description 없이 통일
|
|
|
|
|
|
columns: Array.isArray(table.columns)
|
|
|
|
|
|
? table.columns.map((col) => ({
|
|
|
|
|
|
name: col.columnName,
|
|
|
|
|
|
type: col.dataType || "varchar",
|
|
|
|
|
|
description: col.description || "",
|
|
|
|
|
|
}))
|
|
|
|
|
|
: [],
|
2025-09-09 11:35:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
onColumnClick: handleColumnClick,
|
2025-09-10 11:27:05 +09:00
|
|
|
|
selectedColumns: [], // 관계도 로드 시에는 빈 상태로 시작
|
2025-09-09 12:00:58 +09:00
|
|
|
|
connectedColumns: connectedColumnsInfo[table.tableName] || {},
|
2025-09-09 11:35:05 +09:00
|
|
|
|
} as TableNodeData,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🎨 생성된 테이블 노드 수:", tableNodes.length);
|
|
|
|
|
|
console.log("📍 테이블 노드 상세:", tableNodes);
|
|
|
|
|
|
setNodes(tableNodes);
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// JSON 관계를 엣지로 변환하여 표시 (테이블 간 번들 연결)
|
2025-09-09 12:00:58 +09:00
|
|
|
|
const relationshipEdges: Edge[] = [];
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
relationships.forEach((rel: JsonRelationship) => {
|
|
|
|
|
|
const fromTable = rel.fromTable;
|
|
|
|
|
|
const toTable = rel.toTable;
|
|
|
|
|
|
const fromColumns = rel.fromColumns || [];
|
|
|
|
|
|
const toColumns = rel.toColumns || [];
|
2025-09-09 12:00:58 +09:00
|
|
|
|
|
2025-09-09 18:42:01 +09:00
|
|
|
|
if (fromColumns.length === 0 || toColumns.length === 0) {
|
|
|
|
|
|
console.warn("⚠️ 컬럼 정보가 없습니다:", { fromColumns, toColumns });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 11:27:05 +09:00
|
|
|
|
// 테이블 간 하나의 번들 엣지 생성 (컬럼별 개별 엣지 대신)
|
|
|
|
|
|
relationshipEdges.push({
|
2025-09-10 15:30:14 +09:00
|
|
|
|
id: generateUniqueId("edge", currentDiagramId),
|
2025-09-10 11:27:05 +09:00
|
|
|
|
source: `table-${fromTable}`,
|
|
|
|
|
|
target: `table-${toTable}`,
|
|
|
|
|
|
type: "smoothstep",
|
|
|
|
|
|
animated: false,
|
|
|
|
|
|
style: {
|
|
|
|
|
|
stroke: "#3b82f6",
|
|
|
|
|
|
strokeWidth: 2,
|
|
|
|
|
|
strokeDasharray: "none",
|
|
|
|
|
|
},
|
|
|
|
|
|
data: {
|
2025-09-10 15:30:14 +09:00
|
|
|
|
relationshipId: rel.id,
|
2025-09-10 18:30:22 +09:00
|
|
|
|
relationshipName: rel.relationshipName,
|
2025-09-10 15:30:14 +09:00
|
|
|
|
relationshipType: rel.relationshipType,
|
|
|
|
|
|
connectionType: rel.connectionType,
|
2025-09-10 11:27:05 +09:00
|
|
|
|
fromTable: fromTable,
|
|
|
|
|
|
toTable: toTable,
|
|
|
|
|
|
fromColumns: fromColumns,
|
|
|
|
|
|
toColumns: toColumns,
|
|
|
|
|
|
// 클릭 시 표시할 상세 정보
|
|
|
|
|
|
details: {
|
|
|
|
|
|
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
|
2025-09-10 15:30:14 +09:00
|
|
|
|
relationshipType: rel.relationshipType,
|
|
|
|
|
|
connectionType: rel.connectionType,
|
2025-09-09 12:00:58 +09:00
|
|
|
|
},
|
2025-09-10 11:27:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-09-09 12:00:58 +09:00
|
|
|
|
});
|
2025-09-09 11:35:05 +09:00
|
|
|
|
|
|
|
|
|
|
console.log("🔗 생성된 관계 에지 수:", relationshipEdges.length);
|
|
|
|
|
|
console.log("📍 관계 에지 상세:", relationshipEdges);
|
|
|
|
|
|
setEdges(relationshipEdges);
|
2025-09-09 13:48:57 +09:00
|
|
|
|
toast.success("관계도를 불러왔습니다.", { id: "load-diagram" });
|
2025-09-09 11:35:05 +09:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("선택된 관계도 로드 실패:", error);
|
|
|
|
|
|
toast.error("관계도를 불러오는데 실패했습니다.", { id: "load-diagram" });
|
|
|
|
|
|
}
|
2025-09-10 11:27:05 +09:00
|
|
|
|
}, [diagramId, relationshipId, setNodes, setEdges, handleColumnClick]);
|
2025-09-09 11:35:05 +09:00
|
|
|
|
|
|
|
|
|
|
// 기존 관계 로드 (새 관계도 생성 시)
|
2025-09-08 16:46:53 +09:00
|
|
|
|
const loadExistingRelationships = useCallback(async () => {
|
2025-09-09 11:35:05 +09:00
|
|
|
|
if (selectedDiagram) return; // 선택된 관계도가 있으면 실행하지 않음
|
|
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
|
try {
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// 새로운 JSON 기반 시스템에서는 기존 관계를 미리 로드하지 않음
|
|
|
|
|
|
console.log("새 관계도 생성 모드: 빈 캔버스로 시작");
|
|
|
|
|
|
setRelationships([]);
|
2025-09-08 16:46:53 +09:00
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// 빈 캔버스로 시작
|
|
|
|
|
|
setEdges([]);
|
2025-09-08 16:46:53 +09:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("기존 관계 로드 실패:", error);
|
|
|
|
|
|
toast.error("기존 관계를 불러오는데 실패했습니다.");
|
|
|
|
|
|
}
|
2025-09-10 15:30:14 +09:00
|
|
|
|
}, [setEdges, selectedDiagram]);
|
2025-09-08 16:46:53 +09:00
|
|
|
|
|
2025-09-09 11:35:05 +09:00
|
|
|
|
// 컴포넌트 마운트 시 관계 로드
|
2025-09-08 16:46:53 +09:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (companyCode) {
|
2025-09-09 18:42:01 +09:00
|
|
|
|
if (diagramId || relationshipId) {
|
2025-09-09 11:35:05 +09:00
|
|
|
|
loadSelectedDiagramRelationships();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
loadExistingRelationships();
|
|
|
|
|
|
}
|
2025-09-08 16:46:53 +09:00
|
|
|
|
}
|
2025-09-09 18:42:01 +09:00
|
|
|
|
}, [companyCode, diagramId, relationshipId, loadExistingRelationships, loadSelectedDiagramRelationships]);
|
2025-09-08 16:46:53 +09:00
|
|
|
|
|
2025-09-05 18:21:28 +09:00
|
|
|
|
// 노드 선택 변경 핸들러
|
|
|
|
|
|
const onSelectionChange = useCallback(({ nodes }: { nodes: Node<TableNodeData>[] }) => {
|
|
|
|
|
|
const selectedNodeIds = nodes.map((node) => node.id);
|
|
|
|
|
|
setSelectedNodes(selectedNodeIds);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
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({});
|
|
|
|
|
|
}, [selectedEdgeInfo, showEdgeActions]);
|
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
|
|
|
|
// 엣지 클릭 시 연결 정보 표시 및 관련 컬럼 하이라이트
|
2025-09-10 11:27:05 +09:00
|
|
|
|
const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {
|
|
|
|
|
|
event.stopPropagation();
|
2025-09-10 15:30:14 +09:00
|
|
|
|
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;
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
2025-09-10 17:25:41 +09:00
|
|
|
|
|
2025-09-10 11:27:05 +09:00
|
|
|
|
if (edgeData) {
|
2025-09-10 17:25:41 +09:00
|
|
|
|
// 엣지 정보 설정
|
2025-09-10 11:27:05 +09:00
|
|
|
|
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}`,
|
|
|
|
|
|
});
|
2025-09-10 17:25:41 +09:00
|
|
|
|
|
|
|
|
|
|
// 관련 컬럼 하이라이트
|
|
|
|
|
|
const newSelectedColumns: { [tableName: string]: string[] } = {};
|
|
|
|
|
|
|
|
|
|
|
|
// fromTable의 컬럼들 선택
|
|
|
|
|
|
if (edgeData.fromTable && edgeData.fromColumns) {
|
|
|
|
|
|
newSelectedColumns[edgeData.fromTable] = [...edgeData.fromColumns];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// toTable의 컬럼들 선택
|
|
|
|
|
|
if (edgeData.toTable && edgeData.toColumns) {
|
|
|
|
|
|
newSelectedColumns[edgeData.toTable] = [...edgeData.toColumns];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSelectedColumns(newSelectedColumns);
|
|
|
|
|
|
|
|
|
|
|
|
// 액션 버튼 표시
|
|
|
|
|
|
setSelectedEdgeForEdit(edge);
|
|
|
|
|
|
setEdgeActionPosition({ x: event.clientX, y: event.clientY });
|
|
|
|
|
|
setShowEdgeActions(true);
|
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-05 18:00:18 +09:00
|
|
|
|
// 선택된 컬럼이 변경될 때마다 기존 노드들 업데이트 및 selectionOrder 정리
|
2025-09-05 16:19:31 +09:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setNodes((prevNodes) =>
|
|
|
|
|
|
prevNodes.map((node) => ({
|
|
|
|
|
|
...node,
|
2025-09-05 11:30:27 +09:00
|
|
|
|
data: {
|
2025-09-05 16:19:31 +09:00
|
|
|
|
...node.data,
|
2025-09-05 18:00:18 +09:00
|
|
|
|
selectedColumns: selectedColumns[node.data.table.tableName] || [],
|
2025-09-05 11:30:27 +09:00
|
|
|
|
},
|
2025-09-05 16:19:31 +09:00
|
|
|
|
})),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
// selectionOrder에서 선택되지 않은 테이블들 제거
|
|
|
|
|
|
const activeTables = Object.keys(selectedColumns).filter(
|
|
|
|
|
|
(tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0,
|
2025-09-05 16:19:31 +09:00
|
|
|
|
);
|
2025-09-05 18:00:18 +09:00
|
|
|
|
setSelectionOrder((prev) => prev.filter((tableName) => activeTables.includes(tableName)));
|
|
|
|
|
|
}, [selectedColumns, setNodes]);
|
2025-09-05 16:19:31 +09:00
|
|
|
|
|
|
|
|
|
|
// 연결 가능한 상태인지 확인
|
|
|
|
|
|
const canCreateConnection = () => {
|
2025-09-05 18:00:18 +09:00
|
|
|
|
const selectedTables = Object.keys(selectedColumns).filter(
|
|
|
|
|
|
(tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0,
|
2025-09-05 16:19:31 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
// 최소 2개의 서로 다른 테이블에서 컬럼이 선택되어야 함
|
|
|
|
|
|
return selectedTables.length >= 2;
|
2025-09-05 16:19:31 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
// 컬럼 연결 설정 모달 열기
|
2025-09-05 16:19:31 +09:00
|
|
|
|
const openConnectionModal = () => {
|
2025-09-05 18:00:18 +09:00
|
|
|
|
const selectedTables = Object.keys(selectedColumns).filter(
|
|
|
|
|
|
(tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0,
|
2025-09-05 16:19:31 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
if (selectedTables.length < 2) return;
|
2025-09-05 16:19:31 +09:00
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
// 선택 순서에 따라 첫 번째와 두 번째 테이블 설정
|
|
|
|
|
|
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);
|
2025-09-05 16:19:31 +09:00
|
|
|
|
|
|
|
|
|
|
if (!firstNode || !secondNode) return;
|
|
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
|
// 첫 번째로 선택된 컬럼들 가져오기
|
|
|
|
|
|
const firstTableColumns = selectedColumns[firstTableName] || [];
|
|
|
|
|
|
const secondTableColumns = selectedColumns[secondTableName] || [];
|
|
|
|
|
|
|
2025-09-05 16:19:31 +09:00
|
|
|
|
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-08 16:46:53 +09:00
|
|
|
|
// 선택된 첫 번째 컬럼을 연결 컬럼으로 설정
|
|
|
|
|
|
fromColumn: firstTableColumns[0] || "",
|
|
|
|
|
|
toColumn: secondTableColumns[0] || "",
|
2025-09-05 18:00:18 +09:00
|
|
|
|
// 선택된 모든 컬럼 정보를 선택 순서대로 전달
|
|
|
|
|
|
selectedColumnsData: (() => {
|
|
|
|
|
|
const orderedData: { [key: string]: { displayName: string; columns: string[] } } = {};
|
2025-09-05 16:19:31 +09:00
|
|
|
|
// selectionOrder 순서대로 데이터 구성 (첫 번째 선택이 먼저)
|
2025-09-05 18:00:18 +09:00
|
|
|
|
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],
|
2025-09-05 16:19:31 +09:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return orderedData;
|
|
|
|
|
|
})(),
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
// 실제 테이블 노드 추가
|
|
|
|
|
|
const addTableNode = useCallback(
|
|
|
|
|
|
async (table: TableDefinition) => {
|
2025-09-05 16:19:31 +09:00
|
|
|
|
try {
|
2025-09-05 18:00:18 +09:00
|
|
|
|
const newNode: Node<TableNodeData> = {
|
|
|
|
|
|
id: `table-${table.tableName}`,
|
|
|
|
|
|
type: "tableNode",
|
2025-09-05 16:19:31 +09:00
|
|
|
|
position: { x: Math.random() * 300, y: Math.random() * 200 },
|
|
|
|
|
|
data: {
|
2025-09-05 18:00:18 +09:00
|
|
|
|
table: {
|
|
|
|
|
|
tableName: table.tableName,
|
|
|
|
|
|
displayName: table.displayName || table.tableName,
|
2025-09-10 11:27:05 +09:00
|
|
|
|
description: "", // 새로 추가된 노드는 description 없이 통일
|
|
|
|
|
|
columns: Array.isArray(table.columns)
|
|
|
|
|
|
? table.columns.map((col) => ({
|
|
|
|
|
|
name: col.columnName || "unknown",
|
|
|
|
|
|
type: col.dataType || "varchar", // 기존과 동일한 기본값 사용
|
|
|
|
|
|
description: col.description || "",
|
|
|
|
|
|
}))
|
|
|
|
|
|
: [],
|
2025-09-05 16:19:31 +09:00
|
|
|
|
},
|
2025-09-05 18:00:18 +09:00
|
|
|
|
onColumnClick: handleColumnClick,
|
|
|
|
|
|
selectedColumns: selectedColumns[table.tableName] || [],
|
2025-09-10 11:27:05 +09:00
|
|
|
|
connectedColumns: {}, // 새로 추가된 노드는 연결 정보 없음
|
2025-09-05 16:19:31 +09:00
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setNodes((nds) => nds.concat(newNode));
|
|
|
|
|
|
} catch (error) {
|
2025-09-05 18:00:18 +09:00
|
|
|
|
console.error("테이블 노드 추가 실패:", error);
|
|
|
|
|
|
toast.error("테이블 정보를 불러오는데 실패했습니다.");
|
2025-09-05 16:19:31 +09:00
|
|
|
|
}
|
2025-09-05 11:30:27 +09:00
|
|
|
|
},
|
2025-09-05 18:00:18 +09:00
|
|
|
|
[handleColumnClick, selectedColumns, setNodes],
|
2025-09-05 11:30:27 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-10 17:25:41 +09:00
|
|
|
|
// 기존 clearNodes 함수 제거 (중복 방지)
|
2025-09-05 11:30:27 +09:00
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
// 현재 추가된 테이블명 목록 가져오기
|
|
|
|
|
|
const getSelectedTableNames = useCallback(() => {
|
|
|
|
|
|
return nodes.filter((node) => node.id.startsWith("table-")).map((node) => node.data.table.tableName);
|
2025-09-05 16:19:31 +09:00
|
|
|
|
}, [nodes]);
|
|
|
|
|
|
|
|
|
|
|
|
// 연결 설정 확인
|
|
|
|
|
|
const handleConfirmConnection = useCallback(
|
2025-09-08 16:46:53 +09:00
|
|
|
|
(relationship: TableRelationship) => {
|
2025-09-05 16:19:31 +09:00
|
|
|
|
if (!pendingConnection) return;
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// 메모리 기반 관계 생성 (DB 저장 없이)
|
2025-09-09 12:00:58 +09:00
|
|
|
|
const fromTable = relationship.from_table_name;
|
|
|
|
|
|
const toTable = relationship.to_table_name;
|
2025-09-10 11:27:05 +09:00
|
|
|
|
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);
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// JSON 형태의 관계 객체 생성
|
|
|
|
|
|
const newRelationship: JsonRelationship = {
|
2025-09-10 17:25:41 +09:00
|
|
|
|
id: editingRelationshipId || generateUniqueId("rel", Date.now()), // 수정 모드면 기존 ID 사용
|
2025-09-10 18:30:22 +09:00
|
|
|
|
relationshipName: relationship.relationship_name, // 연결 이름 추가
|
2025-09-10 15:30:14 +09:00
|
|
|
|
fromTable,
|
|
|
|
|
|
toTable,
|
|
|
|
|
|
fromColumns,
|
|
|
|
|
|
toColumns,
|
|
|
|
|
|
relationshipType: relationship.relationship_type,
|
|
|
|
|
|
connectionType: relationship.connection_type,
|
|
|
|
|
|
settings: relationship.settings || {},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-10 17:25:41 +09:00
|
|
|
|
// 수정 모드인 경우 기존 관계를 교체
|
|
|
|
|
|
if (editingRelationshipId) {
|
|
|
|
|
|
setTempRelationships((prev) => prev.filter((rel) => rel.id !== editingRelationshipId));
|
|
|
|
|
|
setEdges((prev) => prev.filter((edge) => edge.data?.relationshipId !== editingRelationshipId));
|
|
|
|
|
|
setEditingRelationshipId(null); // 수정 모드 해제
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// 메모리에 관계 추가
|
|
|
|
|
|
setTempRelationships((prev) => [...prev, newRelationship]);
|
|
|
|
|
|
setHasUnsavedChanges(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 캔버스에 엣지 즉시 표시
|
2025-09-10 11:27:05 +09:00
|
|
|
|
const newEdge: Edge = {
|
2025-09-10 15:30:14 +09:00
|
|
|
|
id: generateUniqueId("edge", Date.now()),
|
2025-09-10 11:27:05 +09:00
|
|
|
|
source: pendingConnection.fromNode.id,
|
|
|
|
|
|
target: pendingConnection.toNode.id,
|
|
|
|
|
|
type: "smoothstep",
|
|
|
|
|
|
animated: false,
|
|
|
|
|
|
style: {
|
|
|
|
|
|
stroke: "#3b82f6",
|
|
|
|
|
|
strokeWidth: 2,
|
|
|
|
|
|
strokeDasharray: "none",
|
|
|
|
|
|
},
|
|
|
|
|
|
data: {
|
2025-09-10 15:30:14 +09:00
|
|
|
|
relationshipId: newRelationship.id,
|
2025-09-10 18:30:22 +09:00
|
|
|
|
relationshipName: newRelationship.relationshipName,
|
2025-09-10 11:27:05 +09:00
|
|
|
|
relationshipType: relationship.relationship_type,
|
|
|
|
|
|
connectionType: relationship.connection_type,
|
2025-09-10 15:30:14 +09:00
|
|
|
|
fromTable,
|
|
|
|
|
|
toTable,
|
|
|
|
|
|
fromColumns,
|
|
|
|
|
|
toColumns,
|
2025-09-10 11:27:05 +09:00
|
|
|
|
details: {
|
|
|
|
|
|
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
|
2025-09-09 12:00:58 +09:00
|
|
|
|
relationshipType: relationship.relationship_type,
|
|
|
|
|
|
connectionType: relationship.connection_type,
|
|
|
|
|
|
},
|
2025-09-10 11:27:05 +09:00
|
|
|
|
},
|
|
|
|
|
|
};
|
2025-09-05 16:19:31 +09:00
|
|
|
|
|
2025-09-10 11:27:05 +09:00
|
|
|
|
setEdges((eds) => [...eds, newEdge]);
|
2025-09-05 16:19:31 +09:00
|
|
|
|
setPendingConnection(null);
|
|
|
|
|
|
|
2025-09-10 11:27:05 +09:00
|
|
|
|
// 관계 생성 후 선택된 컬럼들 초기화
|
|
|
|
|
|
setSelectedColumns({});
|
|
|
|
|
|
setSelectionOrder([]);
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
console.log("메모리에 관계 생성 완료:", newRelationship);
|
|
|
|
|
|
toast.success("관계가 생성되었습니다. 저장 버튼을 눌러 관계도를 저장하세요.");
|
2025-09-05 16:19:31 +09:00
|
|
|
|
},
|
2025-09-10 15:30:14 +09:00
|
|
|
|
[pendingConnection, setEdges],
|
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);
|
2025-09-11 11:32:00 +09:00
|
|
|
|
// 편집 모드 취소 시 선택된 컬럼도 초기화
|
|
|
|
|
|
setSelectedColumns({});
|
2025-09-10 17:25:41 +09:00
|
|
|
|
}
|
|
|
|
|
|
}, [editingRelationshipId]);
|
2025-09-05 16:19:31 +09:00
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// 관계도 저장 함수
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
2025-09-10 17:48:55 +09:00
|
|
|
|
// 현재 노드 위치 추출
|
|
|
|
|
|
const nodePositions: NodePositions = {};
|
|
|
|
|
|
nodes.forEach((node) => {
|
|
|
|
|
|
if (node.data?.table?.tableName) {
|
|
|
|
|
|
nodePositions[node.data.table.tableName] = {
|
|
|
|
|
|
x: node.position.x,
|
|
|
|
|
|
y: node.position.y,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// 저장 요청 데이터 생성
|
|
|
|
|
|
const createRequest: CreateDiagramRequest = {
|
|
|
|
|
|
diagram_name: diagramName,
|
|
|
|
|
|
relationships: {
|
|
|
|
|
|
relationships: tempRelationships,
|
|
|
|
|
|
tables: connectedTables,
|
|
|
|
|
|
},
|
2025-09-10 17:48:55 +09:00
|
|
|
|
node_positions: nodePositions,
|
2025-09-10 15:30:14 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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(() => {
|
2025-09-10 17:25:41 +09:00
|
|
|
|
// 관계가 0개여도 저장 가능하도록 수정
|
2025-09-10 15:30:14 +09:00
|
|
|
|
setShowSaveModal(true);
|
2025-09-10 17:25:41 +09:00
|
|
|
|
}, []);
|
2025-09-10 15:30:14 +09:00
|
|
|
|
|
|
|
|
|
|
// 저장 모달 닫기
|
|
|
|
|
|
const handleCloseSaveModal = useCallback(() => {
|
|
|
|
|
|
if (!isSaving) {
|
|
|
|
|
|
setShowSaveModal(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isSaving]);
|
|
|
|
|
|
|
2025-09-10 17:25:41 +09:00
|
|
|
|
// 고립된 노드 제거 함수
|
|
|
|
|
|
const removeOrphanedNodes = useCallback(
|
|
|
|
|
|
(updatedRelationships: JsonRelationship[], showMessage = true) => {
|
|
|
|
|
|
setNodes((currentNodes) => {
|
|
|
|
|
|
// 현재 관계에서 사용되는 테이블들 추출
|
|
|
|
|
|
const usedTables = new Set<string>();
|
|
|
|
|
|
updatedRelationships.forEach((rel) => {
|
|
|
|
|
|
usedTables.add(rel.fromTable);
|
|
|
|
|
|
usedTables.add(rel.toTable);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 사용되지 않는 노드들 찾기
|
|
|
|
|
|
const orphanedNodes = currentNodes.filter((node) => {
|
|
|
|
|
|
const tableName = node.data.table.tableName;
|
|
|
|
|
|
return !usedTables.has(tableName);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 연결된 노드들만 유지
|
|
|
|
|
|
const connectedNodes = currentNodes.filter((node) => {
|
|
|
|
|
|
const tableName = node.data.table.tableName;
|
|
|
|
|
|
return usedTables.has(tableName);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (orphanedNodes.length > 0 && showMessage) {
|
|
|
|
|
|
const orphanedTableNames = orphanedNodes.map((node) => node.data.table.displayName).join(", ");
|
|
|
|
|
|
toast(`${orphanedNodes.length}개의 연결되지 않은 테이블 노드가 제거되었습니다: ${orphanedTableNames}`, {
|
|
|
|
|
|
duration: 4000,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return connectedNodes;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
[setNodes],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 엣지 삭제 핸들러
|
|
|
|
|
|
const handleDeleteEdge = useCallback(() => {
|
|
|
|
|
|
if (!selectedEdgeForEdit) return;
|
|
|
|
|
|
|
|
|
|
|
|
const edgeData = selectedEdgeForEdit.data as {
|
|
|
|
|
|
relationshipId: string;
|
|
|
|
|
|
fromTable: string;
|
|
|
|
|
|
toTable: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// tempRelationships에서 해당 관계 제거
|
|
|
|
|
|
const updatedRelationships = tempRelationships.filter((rel) => rel.id !== edgeData.relationshipId);
|
|
|
|
|
|
setTempRelationships(updatedRelationships);
|
|
|
|
|
|
|
|
|
|
|
|
// 엣지 제거
|
|
|
|
|
|
setEdges((prev) => prev.filter((edge) => edge.id !== selectedEdgeForEdit.id));
|
|
|
|
|
|
|
|
|
|
|
|
// 고립된 노드 제거
|
|
|
|
|
|
removeOrphanedNodes(updatedRelationships);
|
|
|
|
|
|
|
|
|
|
|
|
// 상태 초기화
|
|
|
|
|
|
setShowEdgeActions(false);
|
|
|
|
|
|
setSelectedEdgeForEdit(null);
|
|
|
|
|
|
setSelectedEdgeInfo(null);
|
|
|
|
|
|
setSelectedColumns({});
|
|
|
|
|
|
setHasUnsavedChanges(true);
|
|
|
|
|
|
|
|
|
|
|
|
toast.success("관계가 삭제되었습니다.");
|
|
|
|
|
|
}, [selectedEdgeForEdit, tempRelationships, setEdges, removeOrphanedNodes]);
|
|
|
|
|
|
|
|
|
|
|
|
// 엣지 수정 핸들러 (수정 모드 전환)
|
|
|
|
|
|
const handleEditEdge = useCallback(() => {
|
|
|
|
|
|
if (!selectedEdgeForEdit) return;
|
|
|
|
|
|
|
|
|
|
|
|
const edgeData = selectedEdgeForEdit.data as {
|
|
|
|
|
|
relationshipId: string;
|
|
|
|
|
|
relationshipName: string;
|
|
|
|
|
|
fromTable: string;
|
|
|
|
|
|
toTable: string;
|
|
|
|
|
|
fromColumns: string[];
|
|
|
|
|
|
toColumns: string[];
|
|
|
|
|
|
relationshipType: string;
|
|
|
|
|
|
connectionType: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 기존 관계 찾기
|
|
|
|
|
|
const existingRelationship = tempRelationships.find((rel) => rel.id === edgeData.relationshipId);
|
|
|
|
|
|
if (!existingRelationship) {
|
|
|
|
|
|
toast.error("수정할 관계를 찾을 수 없습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 수정 모드로 전환 (관계는 제거하지 않음)
|
|
|
|
|
|
setEditingRelationshipId(edgeData.relationshipId);
|
|
|
|
|
|
|
|
|
|
|
|
// 기존 관계를 기반으로 연결 정보 구성
|
|
|
|
|
|
const fromNode = nodes.find((node) => node.data.table.tableName === edgeData.fromTable);
|
|
|
|
|
|
const toNode = nodes.find((node) => node.data.table.tableName === edgeData.toTable);
|
|
|
|
|
|
|
|
|
|
|
|
if (!fromNode || !toNode) {
|
|
|
|
|
|
toast.error("연결된 테이블을 찾을 수 없습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const connectionInfo = {
|
|
|
|
|
|
fromNode: {
|
|
|
|
|
|
id: fromNode.id,
|
|
|
|
|
|
tableName: fromNode.data.table.tableName,
|
|
|
|
|
|
displayName: fromNode.data.table.displayName,
|
|
|
|
|
|
},
|
|
|
|
|
|
toNode: {
|
|
|
|
|
|
id: toNode.id,
|
|
|
|
|
|
tableName: toNode.data.table.tableName,
|
|
|
|
|
|
displayName: toNode.data.table.displayName,
|
|
|
|
|
|
},
|
|
|
|
|
|
selectedColumnsData: {
|
|
|
|
|
|
[edgeData.fromTable]: {
|
|
|
|
|
|
displayName: fromNode.data.table.displayName,
|
|
|
|
|
|
columns: edgeData.fromColumns,
|
|
|
|
|
|
},
|
|
|
|
|
|
[edgeData.toTable]: {
|
|
|
|
|
|
displayName: toNode.data.table.displayName,
|
|
|
|
|
|
columns: edgeData.toColumns,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-09-11 10:45:16 +09:00
|
|
|
|
// 기존 관계 정보 추가 (연결 이름 유지를 위해)
|
|
|
|
|
|
existingRelationship: {
|
|
|
|
|
|
relationshipName: existingRelationship.relationshipName,
|
|
|
|
|
|
relationshipType: existingRelationship.relationshipType,
|
|
|
|
|
|
connectionType: existingRelationship.connectionType,
|
|
|
|
|
|
settings: existingRelationship.settings,
|
|
|
|
|
|
},
|
2025-09-10 17:25:41 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ConnectionSetupModal을 위한 연결 정보 설정
|
|
|
|
|
|
setPendingConnection(connectionInfo);
|
|
|
|
|
|
|
|
|
|
|
|
// 상태 초기화
|
|
|
|
|
|
setShowEdgeActions(false);
|
|
|
|
|
|
setSelectedEdgeForEdit(null);
|
|
|
|
|
|
setSelectedEdgeInfo(null);
|
|
|
|
|
|
|
|
|
|
|
|
toast("관계 수정 모드입니다. 원하는 대로 설정을 변경하고 확인을 눌러주세요.", {
|
|
|
|
|
|
duration: 3000,
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [selectedEdgeForEdit, tempRelationships, nodes]);
|
|
|
|
|
|
|
|
|
|
|
|
// 전체 삭제 핸들러
|
|
|
|
|
|
const clearNodes = useCallback(() => {
|
|
|
|
|
|
setNodes([]);
|
|
|
|
|
|
setEdges([]);
|
|
|
|
|
|
setTempRelationships([]);
|
|
|
|
|
|
setSelectedColumns({});
|
|
|
|
|
|
setSelectedNodes([]);
|
|
|
|
|
|
setPendingConnection(null);
|
|
|
|
|
|
setSelectedEdgeInfo(null);
|
|
|
|
|
|
setShowEdgeActions(false);
|
|
|
|
|
|
setSelectedEdgeForEdit(null);
|
|
|
|
|
|
setHasUnsavedChanges(true);
|
|
|
|
|
|
|
|
|
|
|
|
toast.success("모든 테이블과 관계가 삭제되었습니다.");
|
|
|
|
|
|
}, [setNodes, setEdges]);
|
|
|
|
|
|
|
2025-09-05 11:30:27 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="data-flow-designer h-screen bg-gray-100">
|
|
|
|
|
|
<div className="flex h-full">
|
|
|
|
|
|
{/* 사이드바 */}
|
|
|
|
|
|
<div className="w-80 border-r border-gray-200 bg-white shadow-lg">
|
|
|
|
|
|
<div className="p-6">
|
2025-09-05 18:00:18 +09:00
|
|
|
|
<h2 className="mb-6 text-xl font-bold text-gray-800">테이블 간 데이터 관계 설정</h2>
|
2025-09-05 11:30:27 +09:00
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
{/* 테이블 선택기 */}
|
|
|
|
|
|
<TableSelector
|
2025-09-05 16:19:31 +09:00
|
|
|
|
companyCode={companyCode}
|
2025-09-05 18:00:18 +09:00
|
|
|
|
onTableAdd={addTableNode}
|
|
|
|
|
|
selectedTables={getSelectedTableNames()}
|
2025-09-05 16:19:31 +09:00
|
|
|
|
/>
|
|
|
|
|
|
|
2025-09-05 11:30:27 +09:00
|
|
|
|
{/* 컨트롤 버튼들 */}
|
|
|
|
|
|
<div className="space-y-3">
|
2025-09-10 17:25:41 +09:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => removeOrphanedNodes(tempRelationships)}
|
|
|
|
|
|
className="w-full rounded-lg bg-orange-500 p-3 font-medium text-white transition-colors hover:bg-orange-600"
|
|
|
|
|
|
disabled={nodes.length === 0}
|
|
|
|
|
|
>
|
|
|
|
|
|
🧹 고립된 노드 정리
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
2025-09-05 11:30:27 +09:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={clearNodes}
|
|
|
|
|
|
className="w-full rounded-lg bg-red-500 p-3 font-medium text-white transition-colors hover:bg-red-600"
|
|
|
|
|
|
>
|
2025-09-10 17:25:41 +09:00
|
|
|
|
🗑️ 전체 삭제
|
2025-09-05 11:30:27 +09:00
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
2025-09-10 15:30:14 +09:00
|
|
|
|
onClick={handleOpenSaveModal}
|
2025-09-10 17:25:41 +09:00
|
|
|
|
className={`w-full rounded-lg bg-green-500 p-3 font-medium text-white transition-colors hover:bg-green-600 ${
|
|
|
|
|
|
hasUnsavedChanges ? "animate-pulse" : ""
|
|
|
|
|
|
}`}
|
2025-09-05 11:30:27 +09:00
|
|
|
|
>
|
2025-09-10 15:30:14 +09:00
|
|
|
|
💾 관계도 저장 {tempRelationships.length > 0 && `(${tempRelationships.length})`}
|
2025-09-05 11:30:27 +09:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 통계 정보 */}
|
|
|
|
|
|
<div className="mt-6 rounded-lg bg-gray-50 p-4">
|
|
|
|
|
|
<div className="mb-2 text-sm font-semibold text-gray-700">통계</div>
|
|
|
|
|
|
<div className="space-y-1 text-sm text-gray-600">
|
|
|
|
|
|
<div className="flex justify-between">
|
2025-09-05 18:00:18 +09:00
|
|
|
|
<span>테이블 노드:</span>
|
2025-09-05 11:30:27 +09:00
|
|
|
|
<span className="font-medium">{nodes.length}개</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex justify-between">
|
|
|
|
|
|
<span>연결:</span>
|
|
|
|
|
|
<span className="font-medium">{edges.length}개</span>
|
|
|
|
|
|
</div>
|
2025-09-10 15:30:14 +09:00
|
|
|
|
<div className="flex justify-between">
|
|
|
|
|
|
<span>메모리 관계:</span>
|
|
|
|
|
|
<span className="font-medium text-orange-600">{tempRelationships.length}개</span>
|
|
|
|
|
|
</div>
|
2025-09-09 18:42:01 +09:00
|
|
|
|
<div className="flex justify-between">
|
|
|
|
|
|
<span>관계도 ID:</span>
|
|
|
|
|
|
<span className="font-medium">{currentDiagramId || "미설정"}</span>
|
|
|
|
|
|
</div>
|
2025-09-10 15:30:14 +09:00
|
|
|
|
{hasUnsavedChanges && (
|
|
|
|
|
|
<div className="mt-2 text-xs font-medium text-orange-600">⚠️ 저장되지 않은 변경사항이 있습니다</div>
|
|
|
|
|
|
)}
|
2025-09-05 11:30:27 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
{/* 선택된 컬럼 정보 */}
|
2025-09-05 11:30:27 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 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-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]}
|
|
|
|
|
|
selectionOnDrag={true}
|
|
|
|
|
|
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-05 11:30:27 +09:00
|
|
|
|
</ReactFlow>
|
|
|
|
|
|
|
2025-09-11 11:32:00 +09:00
|
|
|
|
{/* 선택된 컬럼 팝업 - 캔버스 좌측 상단 고정 (새 관계 생성 시에만 표시) */}
|
|
|
|
|
|
{Object.keys(selectedColumns).length > 0 &&
|
|
|
|
|
|
!showEdgeActions &&
|
|
|
|
|
|
!pendingConnection &&
|
|
|
|
|
|
!editingRelationshipId && (
|
|
|
|
|
|
<div className="pointer-events-auto absolute top-4 left-4 z-40 w-80 rounded-xl border border-blue-200 bg-white shadow-lg">
|
|
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
|
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100">
|
|
|
|
|
|
<span className="text-sm text-blue-600">📋</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">선택된 컬럼</div>
|
|
|
|
|
|
<div className="text-xs text-gray-500">{Object.keys(selectedColumns).length}개 테이블</div>
|
|
|
|
|
|
</div>
|
2025-09-10 18:35:55 +09:00
|
|
|
|
</div>
|
2025-09-11 11:32:00 +09:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setSelectedColumns({});
|
|
|
|
|
|
setSelectionOrder([]);
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
|
|
|
|
|
>
|
|
|
|
|
|
✕
|
|
|
|
|
|
</button>
|
2025-09-10 18:35:55 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-11 11:32:00 +09:00
|
|
|
|
{/* 컨텐츠 */}
|
|
|
|
|
|
<div className="max-h-80 overflow-y-auto p-3">
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{[...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 (
|
|
|
|
|
|
<div key={`selected-${tableName}-${index}`}>
|
|
|
|
|
|
{/* 테이블 정보 */}
|
|
|
|
|
|
<div className="rounded-lg bg-blue-50 p-2">
|
|
|
|
|
|
<div className="mb-1 text-xs font-medium text-blue-700">{displayName}</div>
|
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
|
{columns.map((column, columnIndex) => (
|
|
|
|
|
|
<span
|
|
|
|
|
|
key={`${tableName}-${column}-${columnIndex}`}
|
|
|
|
|
|
className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800"
|
|
|
|
|
|
title={column}
|
|
|
|
|
|
>
|
|
|
|
|
|
{column}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2025-09-10 18:35:55 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-11 11:32:00 +09:00
|
|
|
|
{/* 화살표 */}
|
|
|
|
|
|
{index === 0 && filteredOrder.length > 1 && (
|
|
|
|
|
|
<div className="flex justify-center py-1">
|
|
|
|
|
|
<div className="text-sm text-gray-400">↓</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
2025-09-10 18:35:55 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-11 11:32:00 +09:00
|
|
|
|
{/* 액션 버튼 */}
|
|
|
|
|
|
<div className="flex gap-2 border-t border-blue-100 p-3">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={openConnectionModal}
|
|
|
|
|
|
disabled={!canCreateConnection()}
|
|
|
|
|
|
className={`flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-medium transition-colors ${
|
|
|
|
|
|
canCreateConnection()
|
|
|
|
|
|
? "bg-blue-500 text-white hover:bg-blue-600"
|
|
|
|
|
|
: "cursor-not-allowed bg-gray-300 text-gray-500"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>🔗</span>
|
|
|
|
|
|
<span>연결 설정</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setSelectedColumns({});
|
|
|
|
|
|
setSelectionOrder([]);
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-gray-600 hover:bg-gray-300"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>🗑️</span>
|
|
|
|
|
|
<span>초기화</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-09-10 18:35:55 +09:00
|
|
|
|
</div>
|
2025-09-11 11:32:00 +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
|
|
|
|
|
|
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 15:30:14 +09:00
|
|
|
|
|
2025-09-10 18:30:22 +09:00
|
|
|
|
{/* 엣지 정보 및 액션 버튼 */}
|
|
|
|
|
|
{showEdgeActions && selectedEdgeForEdit && selectedEdgeInfo && (
|
2025-09-10 17:25:41 +09:00
|
|
|
|
<div
|
2025-09-11 11:12:17 +09:00
|
|
|
|
className="fixed z-50 rounded-xl border border-gray-200 bg-white shadow-2xl"
|
2025-09-10 17:25:41 +09:00
|
|
|
|
style={{
|
2025-09-10 18:30:22 +09:00
|
|
|
|
left: edgeActionPosition.x - 160,
|
|
|
|
|
|
top: edgeActionPosition.y - 100,
|
|
|
|
|
|
minWidth: "320px",
|
2025-09-11 11:12:17 +09:00
|
|
|
|
maxWidth: "380px",
|
2025-09-10 17:25:41 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-10 18:30:22 +09:00
|
|
|
|
{/* 헤더 */}
|
2025-09-11 11:12:17 +09:00
|
|
|
|
<div className="flex items-center justify-between rounded-t-xl border-b border-gray-200 bg-blue-600 p-4">
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
|
|
|
|
|
|
<span className="text-sm text-white">🔗</span>
|
2025-09-10 18:30:22 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2025-09-11 11:12:17 +09:00
|
|
|
|
<div className="text-sm font-bold text-white">{selectedEdgeInfo.relationshipName}</div>
|
|
|
|
|
|
<div className="text-xs text-blue-100">데이터 관계 정보</div>
|
2025-09-10 18:30:22 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setSelectedEdgeInfo(null);
|
|
|
|
|
|
setShowEdgeActions(false);
|
|
|
|
|
|
setSelectedEdgeForEdit(null);
|
|
|
|
|
|
setSelectedColumns({});
|
|
|
|
|
|
}}
|
2025-09-11 11:12:17 +09:00
|
|
|
|
className="flex h-6 w-6 items-center justify-center rounded-full text-white/80 transition-all hover:bg-white/20 hover:text-white"
|
2025-09-10 18:30:22 +09:00
|
|
|
|
>
|
2025-09-11 11:12:17 +09:00
|
|
|
|
<span className="text-sm">✕</span>
|
2025-09-10 18:30:22 +09:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-11 11:12:17 +09:00
|
|
|
|
{/* 관계 정보 요약 */}
|
|
|
|
|
|
<div className="border-b border-gray-100 bg-gray-50 p-3">
|
|
|
|
|
|
<div className="flex items-center justify-center gap-4">
|
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
|
<div className="text-xs font-medium tracking-wide text-gray-500 uppercase">관계 유형</div>
|
|
|
|
|
|
<div className="mt-1 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-semibold text-blue-800">
|
|
|
|
|
|
{selectedEdgeInfo.relationshipType}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-gray-300">|</div>
|
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
|
<div className="text-xs font-medium tracking-wide text-gray-500 uppercase">연결 유형</div>
|
|
|
|
|
|
<div className="mt-1 inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-semibold text-indigo-800">
|
|
|
|
|
|
{selectedEdgeInfo.connectionType}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-10 18:30:22 +09:00
|
|
|
|
{/* 연결 정보 */}
|
|
|
|
|
|
<div className="space-y-3 p-4">
|
|
|
|
|
|
{/* From 테이블 */}
|
2025-09-11 11:12:17 +09:00
|
|
|
|
<div className="rounded-lg border-l-4 border-emerald-400 bg-emerald-50 p-3">
|
|
|
|
|
|
<div className="mb-2 text-xs font-bold tracking-wide text-emerald-700 uppercase">FROM</div>
|
|
|
|
|
|
<div className="mb-2 text-base font-bold text-gray-800">{selectedEdgeInfo.fromTable}</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{selectedEdgeInfo.fromColumns.map((column, index) => (
|
|
|
|
|
|
<span
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
className="inline-flex items-center rounded-md bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800 ring-1 ring-emerald-200"
|
|
|
|
|
|
>
|
|
|
|
|
|
{column}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2025-09-10 18:30:22 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-11 11:12:17 +09:00
|
|
|
|
{/* 관계 화살표 */}
|
2025-09-10 18:30:22 +09:00
|
|
|
|
<div className="flex justify-center">
|
2025-09-11 11:12:17 +09:00
|
|
|
|
<span className="text-l text-gray-600">→</span>
|
2025-09-10 18:30:22 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* To 테이블 */}
|
2025-09-11 11:12:17 +09:00
|
|
|
|
<div className="rounded-lg border-l-4 border-blue-400 bg-blue-50 p-3">
|
|
|
|
|
|
<div className="mb-2 text-xs font-bold tracking-wide text-blue-700 uppercase">TO</div>
|
|
|
|
|
|
<div className="mb-2 text-base font-bold text-gray-800">{selectedEdgeInfo.toTable}</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{selectedEdgeInfo.toColumns.map((column, index) => (
|
|
|
|
|
|
<span
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
className="inline-flex items-center rounded-md bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 ring-1 ring-blue-200"
|
|
|
|
|
|
>
|
|
|
|
|
|
{column}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2025-09-10 18:30:22 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 액션 버튼 */}
|
2025-09-11 11:12:17 +09:00
|
|
|
|
<div className="flex gap-2 border-t border-gray-200 bg-gray-50 p-3">
|
2025-09-10 18:30:22 +09:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleEditEdge}
|
2025-09-11 11:12:17 +09:00
|
|
|
|
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-blue-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-all hover:bg-blue-700 hover:shadow-md"
|
2025-09-10 18:30:22 +09:00
|
|
|
|
>
|
|
|
|
|
|
<span>수정</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleDeleteEdge}
|
2025-09-11 11:12:17 +09:00
|
|
|
|
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-red-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-all hover:bg-red-700 hover:shadow-md"
|
2025-09-10 18:30:22 +09:00
|
|
|
|
>
|
|
|
|
|
|
<span>삭제</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-09-10 17:25:41 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
{/* 관계도 저장 모달 */}
|
|
|
|
|
|
<SaveDiagramModal
|
|
|
|
|
|
isOpen={showSaveModal}
|
|
|
|
|
|
onClose={handleCloseSaveModal}
|
|
|
|
|
|
onSave={handleSaveDiagram}
|
|
|
|
|
|
relationships={tempRelationships}
|
|
|
|
|
|
defaultName={
|
|
|
|
|
|
diagramId && diagramId > 0 && currentDiagramName
|
|
|
|
|
|
? currentDiagramName // 편집 모드: 기존 관계도 이름
|
|
|
|
|
|
: `관계도 ${new Date().toLocaleDateString()}` // 신규 생성 모드: 새로운 이름
|
|
|
|
|
|
}
|
|
|
|
|
|
isLoading={isSaving}
|
|
|
|
|
|
/>
|
2025-09-05 11:30:27 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|