생성된 관계도 확인

This commit is contained in:
hyeonsu
2025-09-09 11:35:05 +09:00
parent 989c118ad2
commit 7260ad733b
8 changed files with 1018 additions and 107 deletions

View File

@@ -17,7 +17,7 @@ import "@xyflow/react/dist/style.css";
import { TableNode } from "./TableNode";
import { TableSelector } from "./TableSelector";
import { ConnectionSetupModal } from "./ConnectionSetupModal";
import { TableDefinition, TableRelationship, DataFlowAPI } from "@/lib/api/dataflow";
import { TableDefinition, TableRelationship, DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow";
// 고유 ID 생성 함수
const generateUniqueId = (prefix: string, relationshipId?: number): string => {
@@ -52,11 +52,18 @@ const edgeTypes = {};
interface DataFlowDesignerProps {
companyCode: string;
onSave?: (relationships: TableRelationship[]) => void;
selectedDiagram?: DataFlowDiagram | null;
onBackToList?: () => void;
}
// TableRelationship 타입은 dataflow.ts에서 import
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode, onSave }) => {
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
companyCode,
onSave,
selectedDiagram,
onBackToList,
}) => {
const [nodes, setNodes, onNodesChange] = useNodesState<Node<TableNodeData>>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const [selectedColumns, setSelectedColumns] = useState<{
@@ -113,56 +120,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
return () => window.removeEventListener("keydown", handleKeyDown);
}, [selectedNodes, setNodes]);
// 기존 관계 로드
const loadExistingRelationships = useCallback(async () => {
try {
const existingRelationships = await DataFlowAPI.getRelationshipsByCompany(companyCode);
setRelationships(existingRelationships);
// 기존 관계를 엣지로 변환하여 표시
const existingEdges = existingRelationships.map((rel) => ({
id: generateUniqueId("edge", rel.relationshipId),
source: `table-${rel.fromTableName}`,
target: `table-${rel.toTableName}`,
sourceHandle: "right",
targetHandle: "left",
type: "default",
data: {
relationshipId: rel.relationshipId,
relationshipType: rel.relationshipType,
connectionType: rel.connectionType,
label: rel.relationshipName,
fromColumn: rel.fromColumnName,
toColumn: rel.toColumnName,
},
}));
setEdges(existingEdges);
} catch (error) {
console.error("기존 관계 로드 실패:", error);
toast.error("기존 관계를 불러오는데 실패했습니다.");
}
}, [companyCode, setEdges]);
// 컴포넌트 마운트 시 기존 관계 로드
useEffect(() => {
if (companyCode) {
loadExistingRelationships();
}
}, [companyCode, loadExistingRelationships]);
// 노드 선택 변경 핸들러
const onSelectionChange = useCallback(({ nodes }: { nodes: Node<TableNodeData>[] }) => {
const selectedNodeIds = nodes.map((node) => node.id);
setSelectedNodes(selectedNodeIds);
}, []);
// 빈 onConnect 함수 (드래그 연결 비활성화)
const onConnect = useCallback(() => {
// 드래그로 연결하는 것을 방지
return;
}, []);
// 컬럼 클릭 처리 (토글 방식, 최대 2개 테이블만 허용)
const handleColumnClick = useCallback((tableName: string, columnName: string) => {
setSelectedColumns((prev) => {
@@ -184,39 +141,191 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
}
return { ...prev, [tableName]: newColumns };
} else {
// 선택 추가 - 새로운 테이블이고 이미 2개 테이블이 선택되어 있으면 거부
if (!prev[tableName] && selectedTables.length >= 2) {
// 토스트 중복 방지를 위한 ref 사용
if (!toastShownRef.current) {
toastShownRef.current = true;
setTimeout(() => {
toast.error("최대 2개의 테이블에서만 컬럼을 선택할 수 있습니다.", {
duration: 3000,
position: "top-center",
});
// 3초 후 플래그 리셋
setTimeout(() => {
toastShownRef.current = false;
}, 3000);
}, 0);
}
// 선택
if (selectedTables.length >= 2 && !selectedTables.includes(tableName)) {
toast.error("최대 2개 테이블까지만 선택할 수 있습니다.");
return prev;
}
// 새로운 테이블이면 선택 순서에 추가, 기존 테이블이면 맨 뒤로 이동 (다음 렌더링에서)
const newColumns = [...currentColumns, columnName];
const newSelection = { ...prev, [tableName]: newColumns };
// 선택 순서 업데이트 (다음 렌더링에서)
setTimeout(() => {
setSelectionOrder((order) => {
// 기존에 있던 테이블이면 제거 후 맨 뒤에 추가 (순서 갱신)
const filteredOrder = order.filter((name) => name !== tableName);
return [...filteredOrder, tableName];
if (!order.includes(tableName)) {
return [...order, tableName];
}
return order;
});
}, 0);
return { ...prev, [tableName]: [...currentColumns, columnName] };
return newSelection;
}
});
}, []);
// 선택된 관계도의 관계 로드
const loadSelectedDiagramRelationships = useCallback(async () => {
if (!selectedDiagram) return;
try {
console.log("🔍 관계도 로드 시작:", selectedDiagram.diagramName);
toast.loading("관계도를 불러오는 중...", { id: "load-diagram" });
const diagramRelationships = await DataFlowAPI.getDiagramRelationships(selectedDiagram.diagramName);
console.log("📋 관계도 관계 데이터:", diagramRelationships);
console.log("📋 첫 번째 관계 상세:", diagramRelationships[0]);
console.log(
"📋 관계 객체 키들:",
diagramRelationships[0] ? Object.keys(diagramRelationships[0]) : "배열이 비어있음",
);
setRelationships(diagramRelationships);
// 관계도의 모든 테이블 추출
const tableNames = new Set<string>();
diagramRelationships.forEach((rel) => {
tableNames.add(rel.from_table_name);
tableNames.add(rel.to_table_name);
});
console.log("📊 추출된 테이블 이름들:", Array.from(tableNames));
// 테이블 정보 로드
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);
console.log(`📋 테이블 ${tableName}의 컬럼 수:`, columns.length);
tableDefinitions.push({
tableName: foundTable.tableName,
displayName: foundTable.displayName,
description: foundTable.description,
columns: columns,
});
} else {
console.warn(`⚠️ 테이블 ${tableName}을 찾을 수 없습니다`);
}
}
// 테이블을 노드로 변환 (자동 레이아웃)
const tableNodes = tableDefinitions.map((table, index) => {
const x = (index % 3) * 400 + 100; // 3열 배치
const y = Math.floor(index / 3) * 300 + 100;
return {
id: `table-${table.tableName}`,
type: "tableNode",
position: { x, y },
data: {
table: {
tableName: table.tableName,
displayName: table.displayName,
description: table.description || "",
columns: table.columns.map((col) => ({
name: col.columnName,
type: col.dataType || "varchar",
description: col.description || "",
})),
},
onColumnClick: handleColumnClick,
selectedColumns: selectedColumns[table.tableName] || [],
} as TableNodeData,
};
});
console.log("🎨 생성된 테이블 노드 수:", tableNodes.length);
console.log("📍 테이블 노드 상세:", tableNodes);
setNodes(tableNodes);
// 관계를 엣지로 변환하여 표시
const relationshipEdges = diagramRelationships.map((rel) => ({
id: generateUniqueId("edge", rel.relationship_id),
source: `table-${rel.from_table_name}`,
target: `table-${rel.to_table_name}`,
sourceHandle: "right",
targetHandle: "left",
type: "default",
data: {
relationshipId: rel.relationship_id,
relationshipType: rel.relationship_type,
connectionType: rel.connection_type,
label: rel.relationship_name,
fromColumn: rel.from_column_name,
toColumn: rel.to_column_name,
},
}));
console.log("🔗 생성된 관계 에지 수:", relationshipEdges.length);
console.log("📍 관계 에지 상세:", relationshipEdges);
setEdges(relationshipEdges);
toast.success(`"${selectedDiagram.diagramName}" 관계도를 불러왔습니다.`, { id: "load-diagram" });
} catch (error) {
console.error("선택된 관계도 로드 실패:", error);
toast.error("관계도를 불러오는데 실패했습니다.", { id: "load-diagram" });
}
}, [selectedDiagram, companyCode, setNodes, setEdges, selectedColumns, handleColumnClick]);
// 기존 관계 로드 (새 관계도 생성 시)
const loadExistingRelationships = useCallback(async () => {
if (selectedDiagram) return; // 선택된 관계도가 있으면 실행하지 않음
try {
const existingRelationships = await DataFlowAPI.getRelationshipsByCompany(companyCode);
setRelationships(existingRelationships);
// 기존 관계를 엣지로 변환하여 표시
const existingEdges = existingRelationships.map((rel) => ({
id: generateUniqueId("edge", rel.relationship_id),
source: `table-${rel.from_table_name}`,
target: `table-${rel.to_table_name}`,
sourceHandle: "right",
targetHandle: "left",
type: "default",
data: {
relationshipId: rel.relationship_id,
relationshipType: rel.relationship_type,
connectionType: rel.connection_type,
label: rel.relationship_name,
fromColumn: rel.from_column_name,
toColumn: rel.to_column_name,
},
}));
setEdges(existingEdges);
} catch (error) {
console.error("기존 관계 로드 실패:", error);
toast.error("기존 관계를 불러오는데 실패했습니다.");
}
}, [companyCode, setEdges, selectedDiagram]);
// 컴포넌트 마운트 시 관계 로드
useEffect(() => {
if (companyCode) {
if (selectedDiagram) {
loadSelectedDiagramRelationships();
} else {
loadExistingRelationships();
}
}
}, [companyCode, selectedDiagram, loadExistingRelationships, loadSelectedDiagramRelationships]);
// 노드 선택 변경 핸들러
const onSelectionChange = useCallback(({ nodes }: { nodes: Node<TableNodeData>[] }) => {
const selectedNodeIds = nodes.map((node) => node.id);
setSelectedNodes(selectedNodeIds);
}, []);
// 빈 onConnect 함수 (드래그 연결 비활성화)
const onConnect = useCallback(() => {
// 드래그로 연결하는 것을 방지
return;
}, []);
// 선택된 컬럼이 변경될 때마다 기존 노드들 업데이트 및 selectionOrder 정리
useEffect(() => {
setNodes((prevNodes) =>
@@ -380,19 +489,19 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
if (!pendingConnection) return;
const newEdge = {
id: generateUniqueId("edge", relationship.relationshipId),
id: generateUniqueId("edge", relationship.relationship_id),
source: pendingConnection.fromNode.id,
target: pendingConnection.toNode.id,
sourceHandle: "right",
targetHandle: "left",
type: "default",
data: {
relationshipId: relationship.relationshipId,
relationshipType: relationship.relationshipType,
connectionType: relationship.connectionType,
label: relationship.relationshipName,
fromColumn: relationship.fromColumnName,
toColumn: relationship.toColumnName,
relationshipId: relationship.relationship_id,
relationshipType: relationship.relationship_type,
connectionType: relationship.connection_type,
label: relationship.relationship_name,
fromColumn: relationship.from_column_name,
toColumn: relationship.to_column_name,
},
};