"use client"; /** * 플로우 편집기 페이지 * - React Flow 기반 비주얼 플로우 편집 * - 단계 추가/수정/삭제 * - 단계 연결 생성/삭제 * - 조건 설정 */ import { useState, useEffect, useCallback } from "react"; import { useParams, useRouter } from "next/navigation"; import ReactFlow, { Node, Edge, addEdge, Connection, useNodesState, useEdgesState, Background, Controls, MiniMap, Panel, } from "reactflow"; import "reactflow/dist/style.css"; import { ArrowLeft, Plus, Save, Play, Settings, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { useToast } from "@/hooks/use-toast"; import { getFlowDefinition, getFlowSteps, getFlowConnections, createFlowStep, updateFlowStep, deleteFlowStep, createFlowConnection, deleteFlowConnection, getAllStepCounts, } from "@/lib/api/flow"; import { FlowDefinition, FlowStep, FlowStepConnection, FlowNodeData } from "@/types/flow"; import { FlowNodeComponent } from "@/components/flow/FlowNodeComponent"; import { FlowStepPanel } from "@/components/flow/FlowStepPanel"; import { FlowConditionBuilder } from "@/components/flow/FlowConditionBuilder"; // 커스텀 노드 타입 등록 const nodeTypes = { flowStep: FlowNodeComponent, }; export default function FlowEditorPage() { const params = useParams(); const router = useRouter(); const { toast } = useToast(); const flowId = Number(params.id); // 상태 const [flowDefinition, setFlowDefinition] = useState(null); const [steps, setSteps] = useState([]); const [connections, setConnections] = useState([]); const [selectedStep, setSelectedStep] = useState(null); const [stepCounts, setStepCounts] = useState>({}); const [loading, setLoading] = useState(true); // React Flow 상태 const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); // 플로우 데이터 로드 const loadFlowData = async () => { setLoading(true); try { // 플로우 정의 로드 const flowRes = await getFlowDefinition(flowId); if (flowRes.success && flowRes.data) { setFlowDefinition(flowRes.data); } // 단계 로드 const stepsRes = await getFlowSteps(flowId); if (stepsRes.success && stepsRes.data) { setSteps(stepsRes.data); } // 연결 로드 const connectionsRes = await getFlowConnections(flowId); if (connectionsRes.success && connectionsRes.data) { setConnections(connectionsRes.data); } // 데이터 카운트 로드 const countsRes = await getAllStepCounts(flowId); if (countsRes.success && countsRes.data) { const counts: Record = {}; countsRes.data.forEach((item) => { counts[item.stepId] = item.count; }); setStepCounts(counts); } } catch (error: any) { toast({ title: "로딩 실패", description: error.message, variant: "destructive", }); } finally { setLoading(false); } }; useEffect(() => { loadFlowData(); }, [flowId]); // React Flow 노드/엣지 변환 useEffect(() => { if (steps.length === 0) return; // 노드 생성 const newNodes: Node[] = steps.map((step) => ({ id: String(step.id), type: "flowStep", position: { x: step.positionX, y: step.positionY }, data: { id: step.id, label: step.stepName, stepOrder: step.stepOrder, tableName: step.tableName, count: stepCounts[step.id] || 0, condition: step.conditionJson, }, })); // 엣지 생성 const newEdges: Edge[] = connections.map((conn) => ({ id: String(conn.id), source: String(conn.fromStepId), target: String(conn.toStepId), label: conn.label, type: "smoothstep", animated: true, })); setNodes(newNodes); setEdges(newEdges); }, [steps, connections, stepCounts]); // 노드 추가 const handleAddStep = async () => { const newStepOrder = steps.length + 1; const newStep = { stepName: `단계 ${newStepOrder}`, stepOrder: newStepOrder, color: "#3B82F6", positionX: 100 + newStepOrder * 250, positionY: 100, }; try { const response = await createFlowStep(flowId, newStep); if (response.success && response.data) { toast({ title: "단계 추가", description: "새로운 단계가 추가되었습니다.", }); loadFlowData(); } } catch (error: any) { toast({ title: "추가 실패", description: error.message, variant: "destructive", }); } }; // 노드 위치 업데이트 const handleNodeDragStop = useCallback( async (event: any, node: Node) => { const step = steps.find((s) => s.id === Number(node.id)); if (!step) return; try { await updateFlowStep(step.id, { positionX: Math.round(node.position.x), positionY: Math.round(node.position.y), }); } catch (error: any) { console.error("위치 업데이트 실패:", error); } }, [steps], ); // 연결 생성 const handleConnect = useCallback( async (connection: Connection) => { if (!connection.source || !connection.target) return; try { const response = await createFlowConnection({ flowDefinitionId: flowId, fromStepId: Number(connection.source), toStepId: Number(connection.target), }); if (response.success) { toast({ title: "연결 생성", description: "단계가 연결되었습니다.", }); loadFlowData(); } } catch (error: any) { toast({ title: "연결 실패", description: error.message, variant: "destructive", }); } }, [flowId], ); // 노드 클릭 const handleNodeClick = useCallback( (event: React.MouseEvent, node: Node) => { const step = steps.find((s) => s.id === Number(node.id)); if (step) { setSelectedStep(step); } }, [steps], ); if (loading) { return (

로딩 중...

); } if (!flowDefinition) { return (

플로우를 찾을 수 없습니다.

); } return (
{/* 헤더 */}

{flowDefinition.name}

테이블: {flowDefinition.tableName}

{/* 편집기 */}
총 단계: {steps.length}개
연결: {connections.length}개
{/* 사이드 패널 */} {selectedStep && ( setSelectedStep(null)} onUpdate={loadFlowData} /> )}
); }