React Flow 기본 설정 완료
This commit is contained in:
209
frontend/components/dataflow/DataFlowDesigner.tsx
Normal file
209
frontend/components/dataflow/DataFlowDesigner.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
"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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user