From b02e9610ea587744f2d4f9f993cb5d0d4ebfe681 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Fri, 5 Sep 2025 18:21:28 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dataflow/DataFlowDesigner.tsx | 48 ++++++++++++- frontend/components/dataflow/TableNode.tsx | 30 ++++++++- .../components/dataflow/TableSelector.tsx | 67 ++++++++----------- 3 files changed, 103 insertions(+), 42 deletions(-) diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx index 881676ee..d210ff8f 100644 --- a/frontend/components/dataflow/DataFlowDesigner.tsx +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -68,6 +68,7 @@ export const DataFlowDesigner: React.FC = ({ companyCode, [tableName: string]: string[]; }>({}); const [selectionOrder, setSelectionOrder] = useState([]); + const [selectedNodes, setSelectedNodes] = useState([]); const [pendingConnection, setPendingConnection] = useState<{ fromNode: { id: string; tableName: string; displayName: string }; toNode: { id: string; tableName: string; displayName: string }; @@ -82,6 +83,46 @@ export const DataFlowDesigner: React.FC = ({ companyCode, } | null>(null); const toastShownRef = useRef(false); + // 키보드 이벤트 핸들러 (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]); + + // 노드 선택 변경 핸들러 + const onSelectionChange = useCallback(({ nodes }: { nodes: Node[] }) => { + const selectedNodeIds = nodes.map((node) => node.id); + setSelectedNodes(selectedNodeIds); + }, []); + // 빈 onConnect 함수 (드래그 연결 비활성화) const onConnect = useCallback(() => { // 드래그로 연결하는 것을 방지 @@ -284,6 +325,7 @@ export const DataFlowDesigner: React.FC = ({ companyCode, setEdges([]); setSelectedColumns({}); setSelectionOrder([]); + setSelectedNodes([]); }, [setNodes, setEdges]); // 현재 추가된 테이블명 목록 가져오기 @@ -456,6 +498,7 @@ export const DataFlowDesigner: React.FC = ({ companyCode, onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} + onSelectionChange={onSelectionChange} nodeTypes={nodeTypes} edgeTypes={edgeTypes} fitView @@ -485,7 +528,10 @@ export const DataFlowDesigner: React.FC = ({ companyCode,
📊
테이블 간 데이터 관계 설정을 시작하세요
-
왼쪽 사이드바에서 테이블을 선택하여 추가하세요
+
+
왼쪽 사이드바에서 테이블을 더블클릭하여 추가하세요
+
테이블 선택 후 Del 키로 삭제 가능
+
)} diff --git a/frontend/components/dataflow/TableNode.tsx b/frontend/components/dataflow/TableNode.tsx index 3e2b7fa3..aa0077ef 100644 --- a/frontend/components/dataflow/TableNode.tsx +++ b/frontend/components/dataflow/TableNode.tsx @@ -28,14 +28,38 @@ interface TableNodeData { export const TableNode: React.FC<{ data: TableNodeData; selected?: boolean }> = ({ data, selected }) => { const { table, onColumnClick, onScrollAreaEnter, onScrollAreaLeave, selectedColumns = [] } = data; + // 컬럼 개수에 따른 높이 계산 + // 헤더: ~80px (제목 + 설명 + 패딩) + const headerHeight = table.description ? 80 : 65; + + // 컬럼 높이: 각 컬럼은 실제로 더 높음 (px-2 py-1 + 텍스트 2줄 + 설명 + space-y-1) + // 설명이 있는 컬럼: ~45px, 없는 컬럼: ~35px, 간격: ~4px + const avgColumnHeight = 45; // 여유있게 계산 + const idealColumnHeight = table.columns.length * avgColumnHeight; + + // 컨테이너 패딩 + const padding = 20; + + // 이상적인 높이 vs 최대 허용 높이 (너무 길면 스크롤) + const idealHeight = headerHeight + idealColumnHeight + padding; + const maxAllowedHeight = 800; // 최대 800px + const calculatedHeight = Math.max(200, Math.min(idealHeight, maxAllowedHeight)); + + // 스크롤이 필요한지 판단 + const needsScroll = idealHeight > maxAllowedHeight; + return ( -
+
{/* NodeResizer for resizing functionality */} = {/* 컬럼 목록 */}
diff --git a/frontend/components/dataflow/TableSelector.tsx b/frontend/components/dataflow/TableSelector.tsx index de1d9c67..86fc676f 100644 --- a/frontend/components/dataflow/TableSelector.tsx +++ b/frontend/components/dataflow/TableSelector.tsx @@ -4,8 +4,7 @@ import React, { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Search, Plus, Database } from "lucide-react"; +import { Search, Database } from "lucide-react"; import { DataFlowAPI, TableDefinition, TableInfo } from "@/lib/api/dataflow"; interface TableSelectorProps { @@ -109,44 +108,36 @@ export const TableSelector: React.FC = ({ companyCode, onTab
) : ( - filteredTables.map((table) => ( - - -
- {table.displayName} -
- - {table.columnCount}개 컬럼 - - -
-
-
- -
-
- - {table.tableName} + filteredTables.map((table) => { + const isSelected = isTableSelected(table.tableName); + return ( + !isSelected && handleAddTable(table)} + > + +
+ {table.displayName} +
{table.columnCount}개 컬럼
+
+ +
+
+ + {table.tableName} + {isSelected && (추가됨)} +
- {table.description &&

{table.description}

} -
-
-
- )) + {table.description &&

{table.description}

} +
+ + + ); + }) )}