From 0a8413ee8ce8d4e243bc08795eff86c3ce034307 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Wed, 10 Sep 2025 11:27:05 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=A3=EC=A7=80=20=ED=98=B8=EB=B2=84=20?= =?UTF-8?q?=EB=93=B1=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dataflow/DataFlowDesigner.tsx | 275 +++++++++++++----- frontend/components/dataflow/TableNode.tsx | 34 +-- frontend/lib/api/dataflow.ts | 12 +- 3 files changed, 210 insertions(+), 111 deletions(-) diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx index 8a14149b..a2fe059d 100644 --- a/frontend/components/dataflow/DataFlowDesigner.tsx +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -89,6 +89,7 @@ export const DataFlowDesigner: React.FC = ({ } | null>(null); const [relationships, setRelationships] = useState([]); // eslint-disable-line @typescript-eslint/no-unused-vars const [currentDiagramId, setCurrentDiagramId] = useState(null); // 현재 화면의 diagram_id + const [selectedEdgeInfo, setSelectedEdgeInfo] = useState(null); // 선택된 엣지 정보 const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars // 키보드 이벤트 핸들러 (Del 키로 선택된 노드 삭제) @@ -220,12 +221,13 @@ export const DataFlowDesigner: React.FC = ({ if (foundTable) { // 각 테이블의 컬럼 정보를 별도로 가져옴 const columns = await DataFlowAPI.getTableColumns(tableName); - console.log(`📋 테이블 ${tableName}의 컬럼 수:`, columns.length); + const safeColumns = Array.isArray(columns) ? columns : []; + console.log(`📋 테이블 ${tableName}의 컬럼 수:`, safeColumns.length); tableDefinitions.push({ tableName: foundTable.tableName, displayName: foundTable.displayName, description: foundTable.description, - columns: columns, + columns: safeColumns, }); } else { console.warn(`⚠️ 테이블 ${tableName}을 찾을 수 없습니다`); @@ -275,6 +277,8 @@ export const DataFlowDesigner: React.FC = ({ }); }); + console.log("🔌 연결된 컬럼 정보:", connectedColumnsInfo); + // 테이블을 노드로 변환 (자동 레이아웃) const tableNodes = tableDefinitions.map((table, index) => { const x = (index % 3) * 400 + 100; // 3열 배치 @@ -288,15 +292,17 @@ export const DataFlowDesigner: React.FC = ({ 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 || "", - })), + description: "", // 기존 로드된 노드도 description 없이 통일 + columns: Array.isArray(table.columns) + ? table.columns.map((col) => ({ + name: col.columnName, + type: col.dataType || "varchar", + description: col.description || "", + })) + : [], }, onColumnClick: handleColumnClick, - selectedColumns: selectedColumns[table.tableName] || [], + selectedColumns: [], // 관계도 로드 시에는 빈 상태로 시작 connectedColumns: connectedColumnsInfo[table.tableName] || {}, } as TableNodeData, }; @@ -306,7 +312,7 @@ export const DataFlowDesigner: React.FC = ({ console.log("📍 테이블 노드 상세:", tableNodes); setNodes(tableNodes); - // 관계를 엣지로 변환하여 표시 (컬럼별 연결) + // 관계를 엣지로 변환하여 표시 (테이블 간 번들 연결) const relationshipEdges: Edge[] = []; diagramRelationships.forEach((rel) => { @@ -326,42 +332,40 @@ export const DataFlowDesigner: React.FC = ({ .map((col) => col.trim()) .filter((col) => col); - // 각 from 컬럼을 각 to 컬럼에 연결 (1:1 매핑이거나 many:many인 경우) if (fromColumns.length === 0 || toColumns.length === 0) { console.warn("⚠️ 컬럼 정보가 없습니다:", { fromColumns, toColumns }); return; } - 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.diagram_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, + // 테이블 간 하나의 번들 엣지 생성 (컬럼별 개별 엣지 대신) + relationshipEdges.push({ + id: generateUniqueId("edge", rel.diagram_id), + source: `table-${fromTable}`, + target: `table-${toTable}`, + type: "smoothstep", + animated: false, + style: { + stroke: "#3b82f6", + strokeWidth: 2, + strokeDasharray: "none", + }, + data: { + relationshipId: rel.relationship_id, + relationshipName: rel.relationship_name, + relationshipType: rel.relationship_type, + connectionType: rel.connection_type, + fromTable: fromTable, + toTable: toTable, + fromColumns: fromColumns, + toColumns: toColumns, + // 클릭 시 표시할 상세 정보 + details: { + connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`, relationshipType: rel.relationship_type, connectionType: rel.connection_type, - fromTable: fromTable, - toTable: toTable, - fromColumn: fromColumn, - toColumn: toColumn, }, - }); - } + }, + }); }); console.log("🔗 생성된 관계 에지 수:", relationshipEdges.length); @@ -372,7 +376,7 @@ export const DataFlowDesigner: React.FC = ({ console.error("선택된 관계도 로드 실패:", error); toast.error("관계도를 불러오는데 실패했습니다.", { id: "load-diagram" }); } - }, [diagramId, relationshipId, setNodes, setEdges, selectedColumns, handleColumnClick]); + }, [diagramId, relationshipId, setNodes, setEdges, handleColumnClick]); // 기존 관계 로드 (새 관계도 생성 시) const loadExistingRelationships = useCallback(async () => { @@ -448,12 +452,80 @@ export const DataFlowDesigner: React.FC = ({ setSelectedNodes(selectedNodeIds); }, []); + // 캔버스 클릭 시 엣지 정보 섹션 닫기 + const onPaneClick = useCallback(() => { + if (selectedEdgeInfo) { + setSelectedEdgeInfo(null); + } + }, [selectedEdgeInfo]); + // 빈 onConnect 함수 (드래그 연결 비활성화) const onConnect = useCallback(() => { // 드래그로 연결하는 것을 방지 return; }, []); + // 엣지 클릭 시 연결 정보 표시 + const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { + event.stopPropagation(); + const edgeData = edge.data as any; + if (edgeData) { + 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}`, + }); + } + }, []); + + // 엣지 마우스 엔터 시 색상 변경 + 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], + ); + // 선택된 컬럼이 변경될 때마다 기존 노드들 업데이트 및 selectionOrder 정리 useEffect(() => { setNodes((prevNodes) => @@ -548,16 +620,18 @@ export const DataFlowDesigner: React.FC = ({ table: { tableName: table.tableName, displayName: table.displayName || table.tableName, - description: table.description || "", - columns: table.columns.map((col) => ({ - name: col.columnName || "unknown", - type: col.dataType || col.dbType || "UNKNOWN", - description: - col.columnLabel || col.displayName || col.description || col.columnName || "No description", - })), + description: "", // 새로 추가된 노드는 description 없이 통일 + columns: Array.isArray(table.columns) + ? table.columns.map((col) => ({ + name: col.columnName || "unknown", + type: col.dataType || "varchar", // 기존과 동일한 기본값 사용 + description: col.description || "", + })) + : [], }, onColumnClick: handleColumnClick, selectedColumns: selectedColumns[table.tableName] || [], + connectedColumns: {}, // 새로 추가된 노드는 연결 정보 없음 }, }; @@ -617,45 +691,48 @@ export const DataFlowDesigner: React.FC = ({ (relationship: TableRelationship) => { if (!pendingConnection) return; - // 컬럼별 에지 생성 - 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()); + 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); - 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.diagram_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, + const newEdge: Edge = { + id: generateUniqueId("edge", relationship.diagram_id), + source: pendingConnection.fromNode.id, + target: pendingConnection.toNode.id, + type: "smoothstep", + animated: false, + style: { + stroke: "#3b82f6", + strokeWidth: 2, + strokeDasharray: "none", + }, + data: { + relationshipId: relationship.relationship_id, + relationshipName: relationship.relationship_name, + relationshipType: relationship.relationship_type, + connectionType: relationship.connection_type, + fromTable: fromTable, + toTable: toTable, + fromColumns: fromColumns, + toColumns: toColumns, + // 클릭 시 표시할 상세 정보 + details: { + connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`, relationshipType: relationship.relationship_type, connectionType: relationship.connection_type, - fromTable: fromTable, - toTable: toTable, - fromColumn: fromColumn, - toColumn: toColumn, }, - }); - } + }, + }; - setEdges((eds) => [...eds, ...newEdges]); + setEdges((eds) => [...eds, newEdge]); setRelationships((prev) => [...prev, relationship]); setPendingConnection(null); @@ -664,6 +741,10 @@ export const DataFlowDesigner: React.FC = ({ setCurrentDiagramId(relationship.diagram_id); } + // 관계 생성 후 선택된 컬럼들 초기화 + setSelectedColumns({}); + setSelectionOrder([]); + console.log("관계 생성 완료:", relationship); // 관계 생성 완료 후 자동으로 목록 새로고침을 위한 콜백 (선택적) // 렌더링 중 상태 업데이트 방지를 위해 제거 @@ -805,18 +886,56 @@ export const DataFlowDesigner: React.FC = ({ )} + + {/* 선택된 엣지 정보 */} + {selectedEdgeInfo && ( +
+
+
+
🔗 연결 정보
+ +
+
+
+
연결
+
+ {selectedEdgeInfo.fromTable}({selectedEdgeInfo.fromColumns.join(", ")}) →{" "} + {selectedEdgeInfo.toTable}({selectedEdgeInfo.toColumns.join(", ")}) +
+
+
+
+
관계 유형
+
{selectedEdgeInfo.relationshipType}
+
+
+
연결 유형
+
{selectedEdgeInfo.connectionType}
+
+
+
+
+
+ )} {/* React Flow 캔버스 */}
= ({ data }) => { - {/* 테이블 헤더 */} -
+ {/* 테이블 헤더 - 통일된 디자인 */} +

{table.displayName}

{table.description &&

{table.description}

}
@@ -52,14 +52,6 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
{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 (
= ({ data }) => { }`} onClick={() => onColumnClick(table.tableName, column.name)} > - {/* Target Handle (왼쪽) - 세련된 디자인 */} - {showTargetHandle && ( - - )} - - {/* Source Handle (오른쪽) - 세련된 디자인 */} - {showSourceHandle && ( - - )} + {/* 핸들 제거됨 - 컬럼 클릭으로만 연결 생성 */}
{column.name} diff --git a/frontend/lib/api/dataflow.ts b/frontend/lib/api/dataflow.ts index 8b5f5d45..d7182108 100644 --- a/frontend/lib/api/dataflow.ts +++ b/frontend/lib/api/dataflow.ts @@ -139,13 +139,21 @@ export class DataFlowAPI { */ static async getTableColumns(tableName: string): Promise { try { - const response = await apiClient.get>(`/table-management/tables/${tableName}/columns`); + const response = await apiClient.get< + ApiResponse<{ + columns: ColumnInfo[]; + page: number; + total: number; + totalPages: number; + }> + >(`/table-management/tables/${tableName}/columns`); if (!response.data.success) { throw new Error(response.data.message || "컬럼 정보 조회에 실패했습니다."); } - return response.data.data || []; + // 페이지네이션된 응답에서 columns 배열만 추출 + return response.data.data?.columns || []; } catch (error) { console.error("컬럼 정보 조회 오류:", error); throw error;