ERP-node/frontend/components/dataflow/node-editor/FlowEditor.tsx

214 lines
5.9 KiB
TypeScript

"use client";
/**
* 노드 기반 플로우 에디터 메인 컴포넌트
*/
import { useCallback, useRef } from "react";
import ReactFlow, { Background, Controls, MiniMap, Panel, ReactFlowProvider, useReactFlow } from "reactflow";
import "reactflow/dist/style.css";
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
import { NodePalette } from "./sidebar/NodePalette";
import { PropertiesPanel } from "./panels/PropertiesPanel";
import { FlowToolbar } from "./FlowToolbar";
import { TableSourceNode } from "./nodes/TableSourceNode";
import { ExternalDBSourceNode } from "./nodes/ExternalDBSourceNode";
import { ConditionNode } from "./nodes/ConditionNode";
import { FieldMappingNode } from "./nodes/FieldMappingNode";
import { InsertActionNode } from "./nodes/InsertActionNode";
import { UpdateActionNode } from "./nodes/UpdateActionNode";
import { DeleteActionNode } from "./nodes/DeleteActionNode";
import { UpsertActionNode } from "./nodes/UpsertActionNode";
import { DataTransformNode } from "./nodes/DataTransformNode";
import { RestAPISourceNode } from "./nodes/RestAPISourceNode";
import { CommentNode } from "./nodes/CommentNode";
import { LogNode } from "./nodes/LogNode";
// 노드 타입들
const nodeTypes = {
// 데이터 소스
tableSource: TableSourceNode,
externalDBSource: ExternalDBSourceNode,
restAPISource: RestAPISourceNode,
// 변환/조건
condition: ConditionNode,
fieldMapping: FieldMappingNode,
dataTransform: DataTransformNode,
// 액션
insertAction: InsertActionNode,
updateAction: UpdateActionNode,
deleteAction: DeleteActionNode,
upsertAction: UpsertActionNode,
// 유틸리티
comment: CommentNode,
log: LogNode,
};
/**
* FlowEditor 내부 컴포넌트
*/
function FlowEditorInner() {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { screenToFlowPosition } = useReactFlow();
const {
nodes,
edges,
onNodesChange,
onEdgesChange,
onConnect,
addNode,
showPropertiesPanel,
selectNodes,
selectedNodes,
removeNodes,
} = useFlowEditorStore();
/**
* 노드 선택 변경 핸들러
*/
const onSelectionChange = useCallback(
({ nodes: selectedNodes }: { nodes: any[] }) => {
const selectedIds = selectedNodes.map((node) => node.id);
selectNodes(selectedIds);
console.log("🔍 선택된 노드:", selectedIds);
},
[selectNodes],
);
/**
* 키보드 이벤트 핸들러 (Delete/Backspace 키로 노드 삭제)
*/
const onKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if ((event.key === "Delete" || event.key === "Backspace") && selectedNodes.length > 0) {
event.preventDefault();
console.log("🗑️ 선택된 노드 삭제:", selectedNodes);
removeNodes(selectedNodes);
}
},
[selectedNodes, removeNodes],
);
/**
* 드래그 앤 드롭 핸들러
*/
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}, []);
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData("application/reactflow");
if (!type) return;
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
// 🔥 노드 타입별 기본 데이터 설정
const defaultData: any = {
displayName: `${type} 노드`,
};
// 액션 노드의 경우 targetType 기본값 설정
if (["insertAction", "updateAction", "deleteAction", "upsertAction"].includes(type)) {
defaultData.targetType = "internal"; // 기본값: 내부 DB
defaultData.fieldMappings = [];
defaultData.options = {};
if (type === "updateAction" || type === "deleteAction") {
defaultData.whereConditions = [];
}
if (type === "upsertAction") {
defaultData.conflictKeys = [];
}
}
const newNode: any = {
id: `node_${Date.now()}`,
type,
position,
data: defaultData,
};
addNode(newNode);
},
[screenToFlowPosition, addNode],
);
return (
<div className="flex h-full w-full">
{/* 좌측 노드 팔레트 */}
<div className="w-[250px] border-r bg-white">
<NodePalette />
</div>
{/* 중앙 캔버스 */}
<div className="relative flex-1" ref={reactFlowWrapper} onKeyDown={onKeyDown} tabIndex={0}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onSelectionChange={onSelectionChange}
onDragOver={onDragOver}
onDrop={onDrop}
nodeTypes={nodeTypes}
fitView
className="bg-gray-50"
deleteKeyCode={["Delete", "Backspace"]}
>
{/* 배경 그리드 */}
<Background gap={16} size={1} color="#E5E7EB" />
{/* 컨트롤 버튼 */}
<Controls className="bg-white shadow-md" />
{/* 미니맵 */}
<MiniMap
className="bg-white shadow-md"
nodeColor={(node) => {
// 노드 타입별 색상 (추후 구현)
return "#3B82F6";
}}
maskColor="rgba(0, 0, 0, 0.1)"
/>
{/* 상단 툴바 */}
<Panel position="top-center" className="pointer-events-auto">
<FlowToolbar />
</Panel>
</ReactFlow>
</div>
{/* 우측 속성 패널 */}
{showPropertiesPanel && (
<div className="w-[350px] border-l bg-white">
<PropertiesPanel />
</div>
)}
</div>
);
}
/**
* FlowEditor 메인 컴포넌트 (Provider로 감싸기)
*/
export function FlowEditor() {
return (
<div className="h-[calc(100vh-200px)] min-h-[700px] w-full">
<ReactFlowProvider>
<FlowEditorInner />
</ReactFlowProvider>
</div>
);
}