196 lines
5.3 KiB
TypeScript
196 lines
5.3 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 newNode: any = {
|
||
|
|
id: `node_${Date.now()}`,
|
||
|
|
type,
|
||
|
|
position,
|
||
|
|
data: {
|
||
|
|
displayName: `새 ${type} 노드`,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
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>
|
||
|
|
);
|
||
|
|
}
|