From 3a24fd3ebd13934c618c41b3662d2ff240918569 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Tue, 9 Sep 2025 12:00:58 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=80=EA=B3=84=EB=8F=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dataflow/DataFlowDesigner.tsx | 210 +++++++++++++----- frontend/components/dataflow/DataFlowList.tsx | 37 +-- frontend/components/dataflow/TableNode.tsx | 46 +++- 3 files changed, 199 insertions(+), 94 deletions(-) diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx index 43b6c272..2c8f4c7a 100644 --- a/frontend/components/dataflow/DataFlowDesigner.tsx +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -62,7 +62,7 @@ export const DataFlowDesigner: React.FC = ({ companyCode, onSave, selectedDiagram, - onBackToList, + onBackToList, // eslint-disable-line @typescript-eslint/no-unused-vars }) => { const [nodes, setNodes, onNodesChange] = useNodesState>([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -84,7 +84,7 @@ export const DataFlowDesigner: React.FC = ({ }; } | null>(null); const [relationships, setRelationships] = useState([]); // eslint-disable-line @typescript-eslint/no-unused-vars - const toastShownRef = useRef(false); + const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars // 키보드 이벤트 핸들러 (Del 키로 선택된 노드 삭제) useEffect(() => { @@ -212,6 +212,38 @@ export const DataFlowDesigner: React.FC = ({ } } + // 연결된 컬럼 정보 계산 + const connectedColumnsInfo: { + [tableName: string]: { [columnName: string]: { direction: "source" | "target" | "both" } }; + } = {}; + + diagramRelationships.forEach((rel) => { + const fromTable = rel.from_table_name; + const toTable = rel.to_table_name; + const fromColumns = rel.from_column_name.split(",").map((col) => col.trim()); + const toColumns = rel.to_column_name.split(",").map((col) => col.trim()); + + // 소스 테이블의 컬럼들을 source로 표시 + if (!connectedColumnsInfo[fromTable]) connectedColumnsInfo[fromTable] = {}; + fromColumns.forEach((col) => { + if (connectedColumnsInfo[fromTable][col]) { + connectedColumnsInfo[fromTable][col].direction = "both"; + } else { + connectedColumnsInfo[fromTable][col] = { direction: "source" }; + } + }); + + // 타겟 테이블의 컬럼들을 target으로 표시 + if (!connectedColumnsInfo[toTable]) connectedColumnsInfo[toTable] = {}; + toColumns.forEach((col) => { + if (connectedColumnsInfo[toTable][col]) { + connectedColumnsInfo[toTable][col].direction = "both"; + } else { + connectedColumnsInfo[toTable][col] = { direction: "target" }; + } + }); + }); + // 테이블을 노드로 변환 (자동 레이아웃) const tableNodes = tableDefinitions.map((table, index) => { const x = (index % 3) * 400 + 100; // 3열 배치 @@ -234,6 +266,7 @@ export const DataFlowDesigner: React.FC = ({ }, onColumnClick: handleColumnClick, selectedColumns: selectedColumns[table.tableName] || [], + connectedColumns: connectedColumnsInfo[table.tableName] || {}, } as TableNodeData, }; }); @@ -242,23 +275,47 @@ export const DataFlowDesigner: React.FC = ({ 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, - }, - })); + // 관계를 엣지로 변환하여 표시 (컬럼별 연결) + const relationshipEdges: Edge[] = []; + + diagramRelationships.forEach((rel) => { + const fromTable = rel.from_table_name; + const toTable = rel.to_table_name; + const fromColumns = rel.from_column_name.split(",").map((col) => col.trim()); + const toColumns = rel.to_column_name.split(",").map((col) => col.trim()); + + // 각 from 컬럼을 각 to 컬럼에 연결 (1:1 매핑이거나 many:many인 경우) + const maxConnections = Math.max(fromColumns.length, toColumns.length); + + for (let i = 0; i < maxConnections; i++) { + const fromColumn = fromColumns[i] || fromColumns[0]; // 컬럼이 부족하면 첫 번째 컬럼 재사용 + const toColumn = toColumns[i] || toColumns[0]; // 컬럼이 부족하면 첫 번째 컬럼 재사용 + + relationshipEdges.push({ + id: generateUniqueId("edge", rel.relationship_id), + source: `table-${fromTable}`, + target: `table-${toTable}`, + sourceHandle: `${fromTable}-${fromColumn}-source`, + targetHandle: `${toTable}-${toColumn}-target`, + type: "smoothstep", + animated: false, + style: { + stroke: "#3b82f6", + strokeWidth: 1.5, + strokeDasharray: "none", + }, + data: { + relationshipId: rel.relationship_id, + relationshipType: rel.relationship_type, + connectionType: rel.connection_type, + fromTable: fromTable, + toTable: toTable, + fromColumn: fromColumn, + toColumn: toColumn, + }, + }); + } + }); console.log("🔗 생성된 관계 에지 수:", relationshipEdges.length); console.log("📍 관계 에지 상세:", relationshipEdges); @@ -268,7 +325,7 @@ export const DataFlowDesigner: React.FC = ({ console.error("선택된 관계도 로드 실패:", error); toast.error("관계도를 불러오는데 실패했습니다.", { id: "load-diagram" }); } - }, [selectedDiagram, companyCode, setNodes, setEdges, selectedColumns, handleColumnClick]); + }, [selectedDiagram, setNodes, setEdges, selectedColumns, handleColumnClick]); // 기존 관계 로드 (새 관계도 생성 시) const loadExistingRelationships = useCallback(async () => { @@ -278,23 +335,47 @@ export const DataFlowDesigner: React.FC = ({ 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, - }, - })); + // 기존 관계를 엣지로 변환하여 표시 (컬럼별 연결) + const existingEdges: Edge[] = []; + + existingRelationships.forEach((rel) => { + const fromTable = rel.from_table_name; + const toTable = rel.to_table_name; + const fromColumns = rel.from_column_name.split(",").map((col) => col.trim()); + const toColumns = rel.to_column_name.split(",").map((col) => col.trim()); + + // 각 from 컬럼을 각 to 컬럼에 연결 + const maxConnections = Math.max(fromColumns.length, toColumns.length); + + for (let i = 0; i < maxConnections; i++) { + const fromColumn = fromColumns[i] || fromColumns[0]; + const toColumn = toColumns[i] || toColumns[0]; + + existingEdges.push({ + id: generateUniqueId("edge", rel.relationship_id), + source: `table-${fromTable}`, + target: `table-${toTable}`, + sourceHandle: `${fromTable}-${fromColumn}-source`, + targetHandle: `${toTable}-${toColumn}-target`, + type: "smoothstep", + animated: false, + style: { + stroke: "#3b82f6", + strokeWidth: 1.5, + strokeDasharray: "none", + }, + data: { + relationshipId: rel.relationship_id, + relationshipType: rel.relationship_type, + connectionType: rel.connection_type, + fromTable: fromTable, + toTable: toTable, + fromColumn: fromColumn, + toColumn: toColumn, + }, + }); + } + }); setEdges(existingEdges); } catch (error) { @@ -488,24 +569,45 @@ export const DataFlowDesigner: React.FC = ({ (relationship: TableRelationship) => { if (!pendingConnection) return; - const newEdge = { - id: generateUniqueId("edge", relationship.relationship_id), - source: pendingConnection.fromNode.id, - target: pendingConnection.toNode.id, - sourceHandle: "right", - targetHandle: "left", - type: "default", - data: { - 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, - }, - }; + // 컬럼별 에지 생성 + const newEdges: Edge[] = []; + const fromTable = relationship.from_table_name; + const toTable = relationship.to_table_name; + const fromColumns = relationship.from_column_name.split(",").map((col) => col.trim()); + const toColumns = relationship.to_column_name.split(",").map((col) => col.trim()); - setEdges((eds) => [...eds, newEdge]); + const maxConnections = Math.max(fromColumns.length, toColumns.length); + + for (let i = 0; i < maxConnections; i++) { + const fromColumn = fromColumns[i] || fromColumns[0]; + const toColumn = toColumns[i] || toColumns[0]; + + newEdges.push({ + id: generateUniqueId("edge", relationship.relationship_id), + source: pendingConnection.fromNode.id, + target: pendingConnection.toNode.id, + sourceHandle: `${fromTable}-${fromColumn}-source`, + targetHandle: `${toTable}-${toColumn}-target`, + type: "smoothstep", + animated: false, + style: { + stroke: "#3b82f6", + strokeWidth: 1.5, + strokeDasharray: "none", + }, + data: { + relationshipId: relationship.relationship_id, + relationshipType: relationship.relationship_type, + connectionType: relationship.connection_type, + fromTable: fromTable, + toTable: toTable, + fromColumn: fromColumn, + toColumn: toColumn, + }, + }); + } + + setEdges((eds) => [...eds, ...newEdges]); setRelationships((prev) => [...prev, relationship]); setPendingConnection(null); diff --git a/frontend/components/dataflow/DataFlowList.tsx b/frontend/components/dataflow/DataFlowList.tsx index 2ea2e204..ba2afdb2 100644 --- a/frontend/components/dataflow/DataFlowList.tsx +++ b/frontend/components/dataflow/DataFlowList.tsx @@ -12,7 +12,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Network, Database, Calendar, User } from "lucide-react"; +import { MoreHorizontal, Trash2, Copy, Plus, Search, Network, Database, Calendar, User } from "lucide-react"; import { DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow"; import { toast } from "sonner"; @@ -58,31 +58,10 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig }; }, [currentPage, searchTerm]); - // 관계도 목록 다시 로드 - const reloadDiagrams = async () => { - try { - setLoading(true); - const response = await DataFlowAPI.getDataFlowDiagrams(currentPage, 20, searchTerm); - setDiagrams(response.diagrams || []); - setTotal(response.total || 0); - setTotalPages(Math.max(1, Math.ceil((response.total || 0) / 20))); - } catch (error) { - console.error("관계도 목록 조회 실패", error); - toast.error("관계도 목록을 불러오는데 실패했습니다."); - } finally { - setLoading(false); - } - }; - const handleDiagramSelect = (diagram: DataFlowDiagram) => { onDiagramSelect(diagram); }; - const handleEdit = (diagram: DataFlowDiagram) => { - // 편집 모달 열기 - console.log("편집:", diagram); - }; - const handleDelete = (diagram: DataFlowDiagram) => { if (confirm(`"${diagram.diagramName}" 관계도를 삭제하시겠습니까?`)) { // 삭제 API 호출 @@ -96,12 +75,6 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig toast.info("복사 기능은 아직 구현되지 않았습니다."); }; - const handleView = (diagram: DataFlowDiagram) => { - // 미리보기 모달 열기 - console.log("미리보기:", diagram); - toast.info("미리보기 기능은 아직 구현되지 않았습니다."); - }; - // 연결 타입에 따른 배지 색상 const getConnectionTypeBadge = (connectionType: string) => { switch (connectionType) { @@ -268,14 +241,6 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig 관계도 설계 - handleView(diagram)}> - - 미리보기 - - handleEdit(diagram)}> - - 편집 - handleCopy(diagram)}> 복사 diff --git a/frontend/components/dataflow/TableNode.tsx b/frontend/components/dataflow/TableNode.tsx index 4fa62fa2..41966853 100644 --- a/frontend/components/dataflow/TableNode.tsx +++ b/frontend/components/dataflow/TableNode.tsx @@ -22,10 +22,18 @@ interface TableNodeData { onScrollAreaEnter?: () => void; onScrollAreaLeave?: () => void; selectedColumns?: string[]; // 선택된 컬럼 목록 + connectedColumns?: { [columnName: string]: { direction: "source" | "target" | "both" } }; // 연결된 컬럼 정보 } export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { - const { table, onColumnClick, onScrollAreaEnter, onScrollAreaLeave, selectedColumns = [] } = data; + const { + table, + onColumnClick, + onScrollAreaEnter, + onScrollAreaLeave, + selectedColumns = [], + connectedColumns = {}, + } = data; return (
@@ -42,17 +50,47 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { {/* 컬럼 목록 */}
- {table.columns.map((column) => { + {table.columns.map((column, index) => { const isSelected = selectedColumns.includes(column.name); + const connectionInfo = connectedColumns[column.name]; + const isConnected = !!connectionInfo; + + // 연결된 컬럼에만 핸들 표시 + const showSourceHandle = + isConnected && (connectionInfo.direction === "source" || connectionInfo.direction === "both"); + const showTargetHandle = + isConnected && (connectionInfo.direction === "target" || connectionInfo.direction === "both"); return (
onColumnClick(table.tableName, column.name)} - className={`cursor-pointer rounded px-2 py-1 text-xs transition-colors ${ + className={`relative cursor-pointer rounded px-2 py-1 text-xs transition-colors ${ isSelected ? "bg-blue-100 text-blue-800 ring-2 ring-blue-500" : "text-gray-700 hover:bg-gray-100" }`} + onClick={() => onColumnClick(table.tableName, column.name)} > + {/* Target Handle (왼쪽) - 세련된 디자인 */} + {showTargetHandle && ( + + )} + + {/* Source Handle (오른쪽) - 세련된 디자인 */} + {showSourceHandle && ( + + )} +
{column.name} {column.type}