diff --git a/frontend/components/dataflow/ConnectionSetupModal.tsx b/frontend/components/dataflow/ConnectionSetupModal.tsx index 2e833c1e..1902eda4 100644 --- a/frontend/components/dataflow/ConnectionSetupModal.tsx +++ b/frontend/components/dataflow/ConnectionSetupModal.tsx @@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Badge } from "@/components/ui/badge"; import { Textarea } from "@/components/ui/textarea"; import { ArrowRight, Link, Key, Save, Globe, Plus } from "lucide-react"; -import { DataFlowAPI, TableRelationship } from "@/lib/api/dataflow"; +import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo } from "@/lib/api/dataflow"; import toast from "react-hot-toast"; // 연결 정보 타입 @@ -112,25 +112,57 @@ export const ConnectionSetupModal: React.FC = ({ bodyTemplate: "{}", }); + // 테이블 및 컬럼 선택을 위한 새로운 상태들 + const [availableTables, setAvailableTables] = useState([]); + const [selectedFromTable, setSelectedFromTable] = useState(""); + const [selectedToTable, setSelectedToTable] = useState(""); + const [fromTableColumns, setFromTableColumns] = useState([]); + const [toTableColumns, setToTableColumns] = useState([]); + const [selectedFromColumns, setSelectedFromColumns] = useState([]); + const [selectedToColumns, setSelectedToColumns] = useState([]); + + // 테이블 목록 로드 + useEffect(() => { + const loadTables = async () => { + try { + const tables = await DataFlowAPI.getTables(); + setAvailableTables(tables); + } catch (error) { + console.error("테이블 목록 로드 실패:", error); + toast.error("테이블 목록을 불러오는데 실패했습니다."); + } + }; + + if (isOpen) { + loadTables(); + } + }, [isOpen]); + // 모달이 열릴 때 기본값 설정 useEffect(() => { if (isOpen && connection) { - const fromTableName = connection.fromNode.displayName; - const toTableName = connection.toNode.displayName; + const fromTableName = connection.fromNode.tableName; + const toTableName = connection.toNode.tableName; + const fromDisplayName = connection.fromNode.displayName; + const toDisplayName = connection.toNode.displayName; + + // 테이블 선택 설정 + setSelectedFromTable(fromTableName); + setSelectedToTable(toTableName); setConfig({ - relationshipName: `${fromTableName} → ${toTableName}`, + relationshipName: `${fromDisplayName} → ${toDisplayName}`, relationshipType: "one-to-one", connectionType: "simple-key", fromColumnName: "", toColumnName: "", - description: `${fromTableName}과 ${toTableName} 간의 데이터 관계`, + description: `${fromDisplayName}과 ${toDisplayName} 간의 데이터 관계`, settings: {}, }); // 단순 키값 연결 기본값 설정 setSimpleKeySettings({ - notes: `${fromTableName}과 ${toTableName} 간의 키값 연결`, + notes: `${fromDisplayName}과 ${toDisplayName} 간의 키값 연결`, }); // 데이터 저장 기본값 설정 @@ -148,9 +180,67 @@ export const ConnectionSetupModal: React.FC = ({ headers: "{}", bodyTemplate: "{}", }); + + // 선택된 컬럼 정보가 있다면 설정 + if (connection.selectedColumnsData) { + const fromColumns = connection.selectedColumnsData[fromTableName]?.columns || []; + const toColumns = connection.selectedColumnsData[toTableName]?.columns || []; + + setSelectedFromColumns(fromColumns); + setSelectedToColumns(toColumns); + + setConfig((prev) => ({ + ...prev, + fromColumnName: fromColumns.join(", "), + toColumnName: toColumns.join(", "), + })); + } } }, [isOpen, connection]); + // From 테이블 선택 시 컬럼 로드 + useEffect(() => { + const loadFromColumns = async () => { + if (selectedFromTable) { + try { + const columns = await DataFlowAPI.getTableColumns(selectedFromTable); + setFromTableColumns(columns); + } catch (error) { + console.error("From 테이블 컬럼 로드 실패:", error); + toast.error("From 테이블 컬럼을 불러오는데 실패했습니다."); + } + } + }; + + loadFromColumns(); + }, [selectedFromTable]); + + // To 테이블 선택 시 컬럼 로드 + useEffect(() => { + const loadToColumns = async () => { + if (selectedToTable) { + try { + const columns = await DataFlowAPI.getTableColumns(selectedToTable); + setToTableColumns(columns); + } catch (error) { + console.error("To 테이블 컬럼 로드 실패:", error); + toast.error("To 테이블 컬럼을 불러오는데 실패했습니다."); + } + } + }; + + loadToColumns(); + }, [selectedToTable]); + + // 선택된 컬럼들이 변경될 때 config 업데이트 + useEffect(() => { + setConfig((prev) => ({ + ...prev, + fromColumnName: selectedFromColumns.join(", "), + toColumnName: selectedToColumns.join(", "), + })); + }, [selectedFromColumns, selectedToColumns]); + const handleConfirm = () => { if (!config.relationshipName || !connection) { toast.error("필수 정보를 모두 입력해주세요."); @@ -172,27 +262,23 @@ export const ConnectionSetupModal: React.FC = ({ break; } - // 선택된 컬럼들 추출 - const selectedColumnsData = connection.selectedColumnsData || {}; - const tableNames = Object.keys(selectedColumnsData); - const fromTable = tableNames[0]; - const toTable = tableNames[1]; - - const fromColumns = selectedColumnsData[fromTable]?.columns || []; - const toColumns = selectedColumnsData[toTable]?.columns || []; - - if (fromColumns.length === 0 || toColumns.length === 0) { - toast.error("선택된 컬럼이 없습니다."); + // 선택된 컬럼들 검증 + if (selectedFromColumns.length === 0 || selectedToColumns.length === 0) { + toast.error("선택된 컬럼이 없습니다. From과 To 테이블에서 각각 최소 1개 이상의 컬럼을 선택해주세요."); return; } + // 선택된 테이블과 컬럼 정보 사용 + const fromTableName = selectedFromTable || connection.fromNode.tableName; + const toTableName = selectedToTable || connection.toNode.tableName; + // 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달 const relationshipData: TableRelationship = { relationship_name: config.relationshipName, - from_table_name: connection.fromNode.tableName, - to_table_name: connection.toNode.tableName, - from_column_name: fromColumns.join(","), // 여러 컬럼을 콤마로 구분 - to_column_name: toColumns.join(","), // 여러 컬럼을 콤마로 구분 + from_table_name: fromTableName, + to_table_name: toTableName, + from_column_name: selectedFromColumns.join(","), // 여러 컬럼을 콤마로 구분 + to_column_name: selectedToColumns.join(","), // 여러 컬럼을 콤마로 구분 relationship_type: config.relationshipType as any, connection_type: config.connectionType as any, company_code: companyCode, @@ -200,15 +286,15 @@ export const ConnectionSetupModal: React.FC = ({ ...settings, description: config.description, multiColumnMapping: { - fromColumns: fromColumns, - toColumns: toColumns, - fromTable: selectedColumnsData[fromTable]?.displayName || fromTable, - toTable: selectedColumnsData[toTable]?.displayName || toTable, + fromColumns: selectedFromColumns, + toColumns: selectedToColumns, + fromTable: fromTableName, + toTable: toTableName, }, - isMultiColumn: fromColumns.length > 1 || toColumns.length > 1, + isMultiColumn: selectedFromColumns.length > 1 || selectedToColumns.length > 1, columnCount: { - from: fromColumns.length, - to: toColumns.length, + from: selectedFromColumns.length, + to: selectedToColumns.length, }, }, }; @@ -445,28 +531,128 @@ export const ConnectionSetupModal: React.FC = ({
- {/* 연결 정보 표시 */} -
-
연결 정보
-
- {fromTableData?.displayName || fromTable} - ({fromTable}) - - {toTableData?.displayName || toTable} - ({toTable}) + {/* 테이블 및 컬럼 선택 */} +
+
테이블 및 컬럼 선택
+ + {/* 테이블 선택 */} +
+
+ + +
+ +
+ + +
-
- {fromTableData?.columns.map((column, index) => ( - - {column} - - ))} - {toTableData?.columns.map((column, index) => ( - - {column} - - ))} + + {/* 컬럼 선택 */} +
+
+ +
+ {fromTableColumns.map((column) => ( + + ))} + {fromTableColumns.length === 0 && ( +
+ {selectedFromTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"} +
+ )} +
+
+ +
+ +
+ {toTableColumns.map((column) => ( + + ))} + {toTableColumns.length === 0 && ( +
+ {selectedToTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"} +
+ )} +
+
+ + {/* 선택된 컬럼 미리보기 */} + {(selectedFromColumns.length > 0 || selectedToColumns.length > 0) && ( +
+ {selectedFromTable} +
+ {selectedFromColumns.map((column) => ( + + {column} + + ))} +
+ + {selectedToTable} +
+ {selectedToColumns.map((column) => ( + + {column} + + ))} +
+
+ )}
{/* 기본 연결 설정 */} diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx index c14612ee..dbb7f615 100644 --- a/frontend/components/dataflow/DataFlowDesigner.tsx +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -121,6 +121,10 @@ export const DataFlowDesigner: React.FC = ({ const [showSaveModal, setShowSaveModal] = useState(false); // 저장 모달 표시 상태 const [isSaving, setIsSaving] = useState(false); // 저장 중 상태 const [currentDiagramName, setCurrentDiagramName] = useState(""); // 현재 편집 중인 관계도 이름 + const [selectedEdgeForEdit, setSelectedEdgeForEdit] = useState(null); // 수정/삭제할 엣지 + const [showEdgeActions, setShowEdgeActions] = useState(false); // 엣지 액션 버튼 표시 상태 + const [edgeActionPosition, setEdgeActionPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); // 액션 버튼 위치 + const [editingRelationshipId, setEditingRelationshipId] = useState(null); // 현재 수정 중인 관계 ID const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars // 편집 모드일 때 관계도 이름 로드 @@ -436,7 +440,13 @@ export const DataFlowDesigner: React.FC = ({ if (selectedEdgeInfo) { setSelectedEdgeInfo(null); } - }, [selectedEdgeInfo]); + if (showEdgeActions) { + setShowEdgeActions(false); + setSelectedEdgeForEdit(null); + } + // 컬럼 선택 해제 + setSelectedColumns({}); + }, [selectedEdgeInfo, showEdgeActions]); // 빈 onConnect 함수 (드래그 연결 비활성화) const onConnect = useCallback(() => { @@ -444,7 +454,7 @@ export const DataFlowDesigner: React.FC = ({ return; }, []); - // 엣지 클릭 시 연결 정보 표시 + // 엣지 클릭 시 연결 정보 표시 및 관련 컬럼 하이라이트 const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { event.stopPropagation(); const edgeData = edge.data as { @@ -462,7 +472,9 @@ export const DataFlowDesigner: React.FC = ({ connectionType: string; }; }; + if (edgeData) { + // 엣지 정보 설정 setSelectedEdgeInfo({ relationshipId: edgeData.relationshipId, relationshipName: edgeData.relationshipName || "관계", @@ -474,6 +486,26 @@ export const DataFlowDesigner: React.FC = ({ connectionType: edgeData.connectionType, connectionInfo: edgeData.details?.connectionInfo || `${edgeData.fromTable} → ${edgeData.toTable}`, }); + + // 관련 컬럼 하이라이트 + 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); } }, []); @@ -637,15 +669,7 @@ export const DataFlowDesigner: React.FC = ({ [handleColumnClick, selectedColumns, setNodes], ); - // 노드 전체 삭제 - const clearNodes = useCallback(() => { - setNodes([]); - setEdges([]); - setSelectedColumns({}); - setSelectionOrder([]); - setSelectedNodes([]); - setCurrentDiagramId(null); // 현재 diagram_id도 초기화 - }, [setNodes, setEdges]); + // 기존 clearNodes 함수 제거 (중복 방지) // 현재 추가된 테이블명 목록 가져오기 const getSelectedTableNames = useCallback(() => { @@ -671,7 +695,7 @@ export const DataFlowDesigner: React.FC = ({ // JSON 형태의 관계 객체 생성 const newRelationship: JsonRelationship = { - id: generateUniqueId("rel", Date.now()), + id: editingRelationshipId || generateUniqueId("rel", Date.now()), // 수정 모드면 기존 ID 사용 fromTable, toTable, fromColumns, @@ -681,6 +705,13 @@ export const DataFlowDesigner: React.FC = ({ settings: relationship.settings || {}, }; + // 수정 모드인 경우 기존 관계를 교체 + if (editingRelationshipId) { + setTempRelationships((prev) => prev.filter((rel) => rel.id !== editingRelationshipId)); + setEdges((prev) => prev.filter((edge) => edge.data?.relationshipId !== editingRelationshipId)); + setEditingRelationshipId(null); // 수정 모드 해제 + } + // 메모리에 관계 추가 setTempRelationships((prev) => [...prev, newRelationship]); setHasUnsavedChanges(true); @@ -730,7 +761,11 @@ export const DataFlowDesigner: React.FC = ({ // 연결 설정 취소 const handleCancelConnection = useCallback(() => { setPendingConnection(null); - }, []); + // 수정 모드였다면 해제 + if (editingRelationshipId) { + setEditingRelationshipId(null); + } + }, [editingRelationshipId]); // 관계도 저장 함수 const handleSaveDiagram = useCallback( @@ -796,12 +831,9 @@ export const DataFlowDesigner: React.FC = ({ // 저장 모달 열기 const handleOpenSaveModal = useCallback(() => { - if (tempRelationships.length === 0) { - toast.error("저장할 관계가 없습니다. 먼저 테이블을 연결해주세요."); - return; - } + // 관계가 0개여도 저장 가능하도록 수정 setShowSaveModal(true); - }, [tempRelationships.length]); + }, []); // 저장 모달 닫기 const handleCloseSaveModal = useCallback(() => { @@ -810,6 +842,158 @@ export const DataFlowDesigner: React.FC = ({ } }, [isSaving]); + // 고립된 노드 제거 함수 + const removeOrphanedNodes = useCallback( + (updatedRelationships: JsonRelationship[], showMessage = true) => { + setNodes((currentNodes) => { + // 현재 관계에서 사용되는 테이블들 추출 + const usedTables = new Set(); + 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, + }, + }, + }; + + // 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]); + return (
@@ -827,19 +1011,26 @@ export const DataFlowDesigner: React.FC = ({ {/* 컨트롤 버튼들 */}
+ + @@ -1033,6 +1224,30 @@ export const DataFlowDesigner: React.FC = ({ onCancel={handleCancelConnection} /> + {/* 엣지 액션 버튼 */} + {showEdgeActions && selectedEdgeForEdit && ( +
+ + +
+ )} + {/* 관계도 저장 모달 */}