ERP-node/frontend/components/dataflow/DataFlowDesigner.tsx

210 lines
6.8 KiB
TypeScript
Raw Normal View History

2025-09-05 11:30:27 +09:00
"use client";
import React, { useState, useCallback } from "react";
import {
ReactFlow,
Node,
Edge,
Controls,
Background,
MiniMap,
useNodesState,
useEdgesState,
addEdge,
Connection,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { ScreenNode } from "./ScreenNode";
import { CustomEdge } from "./CustomEdge";
// 노드 및 엣지 타입 정의
const nodeTypes = {
screenNode: ScreenNode,
};
const edgeTypes = {
customEdge: CustomEdge,
};
interface DataFlowDesignerProps {
companyCode: string;
onSave?: (relationships: any[]) => void;
}
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({ companyCode, onSave }) => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [selectedField, setSelectedField] = useState<{
screenId: string;
fieldName: string;
} | null>(null);
// 노드 연결 처리
const onConnect = useCallback(
(params: Connection) => {
const newEdge = {
...params,
type: "customEdge",
data: {
relationshipType: "one-to-one",
connectionType: "simple-key",
label: "1:1 연결",
},
};
setEdges((eds) => addEdge(newEdge, eds));
},
[setEdges],
);
// 필드 클릭 처리
const handleFieldClick = useCallback((screenId: string, fieldName: string) => {
setSelectedField({ screenId, fieldName });
}, []);
// 샘플 화면 노드 추가
const addSampleNode = useCallback(() => {
const newNode: Node = {
id: `screen-${Date.now()}`,
type: "screenNode",
position: { x: Math.random() * 300, y: Math.random() * 200 },
data: {
screen: {
screenId: `screen-${Date.now()}`,
screenName: `샘플 화면 ${nodes.length + 1}`,
screenCode: `SCREEN${nodes.length + 1}`,
tableName: `table_${nodes.length + 1}`,
fields: [
{ name: "id", type: "INTEGER", description: "고유 식별자" },
{ name: "name", type: "VARCHAR(100)", description: "이름" },
{ name: "code", type: "VARCHAR(50)", description: "코드" },
{ name: "created_date", type: "TIMESTAMP", description: "생성일시" },
],
},
onFieldClick: handleFieldClick,
},
};
setNodes((nds) => nds.concat(newNode));
}, [nodes.length, handleFieldClick, setNodes]);
// 노드 전체 삭제
const clearNodes = useCallback(() => {
setNodes([]);
setEdges([]);
}, [setNodes, setEdges]);
return (
<div className="data-flow-designer h-screen bg-gray-100">
<div className="flex h-full">
{/* 사이드바 */}
<div className="w-80 border-r border-gray-200 bg-white shadow-lg">
<div className="p-6">
<h2 className="mb-6 text-xl font-bold text-gray-800"> </h2>
{/* 회사 정보 */}
<div className="mb-6 rounded-lg bg-blue-50 p-4">
<div className="text-sm font-medium text-blue-600"> </div>
<div className="text-lg font-bold text-blue-800">{companyCode}</div>
</div>
{/* 컨트롤 버튼들 */}
<div className="space-y-3">
<button
onClick={addSampleNode}
className="w-full rounded-lg bg-blue-500 p-3 font-medium text-white transition-colors hover:bg-blue-600"
>
+
</button>
<button
onClick={clearNodes}
className="w-full rounded-lg bg-red-500 p-3 font-medium text-white transition-colors hover:bg-red-600"
>
</button>
<button
onClick={() => onSave && onSave([])}
className="w-full rounded-lg bg-green-500 p-3 font-medium text-white transition-colors hover:bg-green-600"
>
</button>
</div>
{/* 통계 정보 */}
<div className="mt-6 rounded-lg bg-gray-50 p-4">
<div className="mb-2 text-sm font-semibold text-gray-700"></div>
<div className="space-y-1 text-sm text-gray-600">
<div className="flex justify-between">
<span> :</span>
<span className="font-medium">{nodes.length}</span>
</div>
<div className="flex justify-between">
<span>:</span>
<span className="font-medium">{edges.length}</span>
</div>
</div>
</div>
{/* 선택된 필드 정보 */}
{selectedField && (
<div className="mt-6 rounded-lg border border-yellow-200 bg-yellow-50 p-4">
<div className="mb-2 text-sm font-semibold text-yellow-800"> </div>
<div className="text-sm text-yellow-700">
<div>: {selectedField.screenId}</div>
<div>: {selectedField.fieldName}</div>
</div>
<button
onClick={() => setSelectedField(null)}
className="mt-2 text-xs text-yellow-600 hover:text-yellow-800"
>
</button>
</div>
)}
</div>
</div>
{/* React Flow 캔버스 */}
<div className="relative flex-1">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
attributionPosition="bottom-left"
>
<Controls />
<MiniMap
nodeColor={(node) => {
switch (node.type) {
case "screenNode":
return "#3B82F6";
default:
return "#6B7280";
}
}}
/>
<Background variant="dots" gap={20} size={1} color="#E5E7EB" />
</ReactFlow>
{/* 안내 메시지 */}
{nodes.length === 0 && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<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>
</div>
)}
</div>
</div>
</div>
);
};