레이아웃 수정

This commit is contained in:
hyeonsu 2025-09-05 18:21:28 +09:00
parent f74442dce5
commit b02e9610ea
3 changed files with 103 additions and 42 deletions

View File

@ -68,6 +68,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
[tableName: string]: string[];
}>({});
const [selectionOrder, setSelectionOrder] = useState<string[]>([]);
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
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<DataFlowDesignerProps> = ({ 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<TableNodeData>[] }) => {
const selectedNodeIds = nodes.map((node) => node.id);
setSelectedNodes(selectedNodeIds);
}, []);
// 빈 onConnect 함수 (드래그 연결 비활성화)
const onConnect = useCallback(() => {
// 드래그로 연결하는 것을 방지
@ -284,6 +325,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
setEdges([]);
setSelectedColumns({});
setSelectionOrder([]);
setSelectedNodes([]);
}, [setNodes, setEdges]);
// 현재 추가된 테이블명 목록 가져오기
@ -456,6 +498,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onSelectionChange={onSelectionChange}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
@ -485,7 +528,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
<div className="text-center text-gray-500">
<div className="mb-2 text-2xl">📊</div>
<div className="mb-1 text-lg font-medium"> </div>
<div className="text-sm"> </div>
<div className="text-sm">
<div> </div>
<div className="mt-1 text-xs text-gray-400"> Del </div>
</div>
</div>
</div>
)}

View File

@ -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 (
<div className="relative min-h-[200px] min-w-[280px] rounded-lg border-2 border-gray-300 bg-white shadow-lg">
<div
className="relative flex min-w-[280px] flex-col overflow-hidden rounded-lg border-2 border-gray-300 bg-white shadow-lg"
style={{ height: `${calculatedHeight}px`, minHeight: `${calculatedHeight}px` }}
>
{/* NodeResizer for resizing functionality */}
<NodeResizer
color="#ff0071"
isVisible={selected}
minWidth={280}
minHeight={200}
minHeight={calculatedHeight}
keepAspectRatio={false}
handleStyle={{
width: 8,
height: 8,
@ -53,7 +77,7 @@ export const TableNode: React.FC<{ data: TableNodeData; selected?: boolean }> =
{/* 컬럼 목록 */}
<div
className="max-h-[300px] overflow-y-auto p-2"
className={`flex-1 p-2 ${needsScroll ? "overflow-y-auto" : "overflow-hidden"}`}
onMouseEnter={onScrollAreaEnter}
onMouseLeave={onScrollAreaLeave}
>

View File

@ -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<TableSelectorProps> = ({ companyCode, onTab
</div>
</div>
) : (
filteredTables.map((table) => (
<Card
key={table.tableName}
className={`transition-all hover:shadow-md ${
isTableSelected(table.tableName) ? "border-blue-500 bg-blue-50" : "hover:border-gray-300"
}`}
>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium">{table.displayName}</CardTitle>
<div className="flex items-center gap-2">
<Badge variant="default" className="text-xs">
{table.columnCount}
</Badge>
<Button
onClick={() => handleAddTable(table)}
disabled={isTableSelected(table.tableName)}
size="sm"
className="h-7 px-2"
>
<Plus className="h-3 w-3" />
{isTableSelected(table.tableName) ? "추가됨" : "추가"}
</Button>
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs text-gray-600">
<Database className="h-3 w-3" />
<span className="font-mono">{table.tableName}</span>
filteredTables.map((table) => {
const isSelected = isTableSelected(table.tableName);
return (
<Card
key={table.tableName}
className={`cursor-pointer transition-all hover:shadow-md ${
isSelected ? "cursor-not-allowed border-blue-500 bg-blue-50 opacity-60" : "hover:border-gray-300"
}`}
onDoubleClick={() => !isSelected && handleAddTable(table)}
>
<CardHeader className="pb-2">
<div>
<CardTitle className="text-sm font-medium">{table.displayName}</CardTitle>
<div className="mt-1 text-xs text-gray-500">{table.columnCount} </div>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs text-gray-600">
<Database className="h-3 w-3" />
<span className="font-mono">{table.tableName}</span>
{isSelected && <span className="font-medium text-blue-600">()</span>}
</div>
{table.description && <p className="line-clamp-2 text-xs text-gray-500">{table.description}</p>}
</div>
</CardContent>
</Card>
))
{table.description && <p className="line-clamp-2 text-xs text-gray-500">{table.description}</p>}
</div>
</CardContent>
</Card>
);
})
)}
</div>