레이아웃 수정
This commit is contained in:
parent
f74442dce5
commit
b02e9610ea
|
|
@ -68,6 +68,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
||||||
[tableName: string]: string[];
|
[tableName: string]: string[];
|
||||||
}>({});
|
}>({});
|
||||||
const [selectionOrder, setSelectionOrder] = useState<string[]>([]);
|
const [selectionOrder, setSelectionOrder] = useState<string[]>([]);
|
||||||
|
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
|
||||||
const [pendingConnection, setPendingConnection] = useState<{
|
const [pendingConnection, setPendingConnection] = useState<{
|
||||||
fromNode: { id: string; tableName: string; displayName: string };
|
fromNode: { id: string; tableName: string; displayName: string };
|
||||||
toNode: { 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);
|
} | null>(null);
|
||||||
const toastShownRef = useRef(false);
|
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 함수 (드래그 연결 비활성화)
|
// 빈 onConnect 함수 (드래그 연결 비활성화)
|
||||||
const onConnect = useCallback(() => {
|
const onConnect = useCallback(() => {
|
||||||
// 드래그로 연결하는 것을 방지
|
// 드래그로 연결하는 것을 방지
|
||||||
|
|
@ -284,6 +325,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
||||||
setEdges([]);
|
setEdges([]);
|
||||||
setSelectedColumns({});
|
setSelectedColumns({});
|
||||||
setSelectionOrder([]);
|
setSelectionOrder([]);
|
||||||
|
setSelectedNodes([]);
|
||||||
}, [setNodes, setEdges]);
|
}, [setNodes, setEdges]);
|
||||||
|
|
||||||
// 현재 추가된 테이블명 목록 가져오기
|
// 현재 추가된 테이블명 목록 가져오기
|
||||||
|
|
@ -456,6 +498,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
|
onSelectionChange={onSelectionChange}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
fitView
|
fitView
|
||||||
|
|
@ -485,7 +528,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode,
|
||||||
<div className="text-center text-gray-500">
|
<div className="text-center text-gray-500">
|
||||||
<div className="mb-2 text-2xl">📊</div>
|
<div className="mb-2 text-2xl">📊</div>
|
||||||
<div className="mb-1 text-lg font-medium">테이블 간 데이터 관계 설정을 시작하세요</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,38 @@ interface TableNodeData {
|
||||||
export const TableNode: React.FC<{ data: TableNodeData; selected?: boolean }> = ({ data, selected }) => {
|
export const TableNode: React.FC<{ data: TableNodeData; selected?: boolean }> = ({ data, selected }) => {
|
||||||
const { table, onColumnClick, onScrollAreaEnter, onScrollAreaLeave, selectedColumns = [] } = data;
|
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 (
|
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 for resizing functionality */}
|
||||||
<NodeResizer
|
<NodeResizer
|
||||||
color="#ff0071"
|
color="#ff0071"
|
||||||
isVisible={selected}
|
isVisible={selected}
|
||||||
minWidth={280}
|
minWidth={280}
|
||||||
minHeight={200}
|
minHeight={calculatedHeight}
|
||||||
|
keepAspectRatio={false}
|
||||||
handleStyle={{
|
handleStyle={{
|
||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
|
|
@ -53,7 +77,7 @@ export const TableNode: React.FC<{ data: TableNodeData; selected?: boolean }> =
|
||||||
|
|
||||||
{/* 컬럼 목록 */}
|
{/* 컬럼 목록 */}
|
||||||
<div
|
<div
|
||||||
className="max-h-[300px] overflow-y-auto p-2"
|
className={`flex-1 p-2 ${needsScroll ? "overflow-y-auto" : "overflow-hidden"}`}
|
||||||
onMouseEnter={onScrollAreaEnter}
|
onMouseEnter={onScrollAreaEnter}
|
||||||
onMouseLeave={onScrollAreaLeave}
|
onMouseLeave={onScrollAreaLeave}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ import React, { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Search, Database } from "lucide-react";
|
||||||
import { Search, Plus, Database } from "lucide-react";
|
|
||||||
import { DataFlowAPI, TableDefinition, TableInfo } from "@/lib/api/dataflow";
|
import { DataFlowAPI, TableDefinition, TableInfo } from "@/lib/api/dataflow";
|
||||||
|
|
||||||
interface TableSelectorProps {
|
interface TableSelectorProps {
|
||||||
|
|
@ -109,30 +108,20 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredTables.map((table) => (
|
filteredTables.map((table) => {
|
||||||
|
const isSelected = isTableSelected(table.tableName);
|
||||||
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={table.tableName}
|
key={table.tableName}
|
||||||
className={`transition-all hover:shadow-md ${
|
className={`cursor-pointer transition-all hover:shadow-md ${
|
||||||
isTableSelected(table.tableName) ? "border-blue-500 bg-blue-50" : "hover:border-gray-300"
|
isSelected ? "cursor-not-allowed border-blue-500 bg-blue-50 opacity-60" : "hover:border-gray-300"
|
||||||
}`}
|
}`}
|
||||||
|
onDoubleClick={() => !isSelected && handleAddTable(table)}
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex items-center justify-between">
|
<div>
|
||||||
<CardTitle className="text-sm font-medium">{table.displayName}</CardTitle>
|
<CardTitle className="text-sm font-medium">{table.displayName}</CardTitle>
|
||||||
<div className="flex items-center gap-2">
|
<div className="mt-1 text-xs text-gray-500">{table.columnCount}개 컬럼</div>
|
||||||
<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>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<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">
|
<div className="flex items-center gap-2 text-xs text-gray-600">
|
||||||
<Database className="h-3 w-3" />
|
<Database className="h-3 w-3" />
|
||||||
<span className="font-mono">{table.tableName}</span>
|
<span className="font-mono">{table.tableName}</span>
|
||||||
|
{isSelected && <span className="font-medium text-blue-600">(추가됨)</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{table.description && <p className="line-clamp-2 text-xs text-gray-500">{table.description}</p>}
|
{table.description && <p className="line-clamp-2 text-xs text-gray-500">{table.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue