레이아웃 수정
This commit is contained in:
parent
f74442dce5
commit
b02e9610ea
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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,30 +108,20 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
filteredTables.map((table) => (
|
||||
filteredTables.map((table) => {
|
||||
const isSelected = isTableSelected(table.tableName);
|
||||
return (
|
||||
<Card
|
||||
key={table.tableName}
|
||||
className={`transition-all hover:shadow-md ${
|
||||
isTableSelected(table.tableName) ? "border-blue-500 bg-blue-50" : "hover:border-gray-300"
|
||||
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 className="flex items-center justify-between">
|
||||
<div>
|
||||
<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 className="mt-1 text-xs text-gray-500">{table.columnCount}개 컬럼</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
|
|
@ -140,13 +129,15 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
<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>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue