"use client"; import { useState, useEffect, useCallback, useMemo } from "react"; import { ReactFlow, Node, Edge, Position, MarkerType, Background, Controls, MiniMap, useNodesState, useEdgesState, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { cn } from "@/lib/utils"; import { Monitor, Layers, ArrowRight, Loader2 } from "lucide-react"; import { ScreenDefinition } from "@/types/screen"; import { screenApi } from "@/lib/api/screen"; // ============================================================ // 타입 정의 // ============================================================ interface PopScreenFlowViewProps { screen: ScreenDefinition | null; className?: string; onSubScreenSelect?: (subScreenId: string) => void; } interface PopLayoutData { version?: string; sections?: any[]; mainScreen?: { id: string; name: string; }; subScreens?: SubScreen[]; flow?: FlowConnection[]; } interface SubScreen { id: string; name: string; type: "modal" | "drawer" | "fullscreen"; triggerFrom?: string; // 어느 화면/버튼에서 트리거되는지 } interface FlowConnection { from: string; to: string; trigger?: string; label?: string; } // ============================================================ // 커스텀 노드 컴포넌트 // ============================================================ interface ScreenNodeData { label: string; type: "main" | "modal" | "drawer" | "fullscreen"; isMain?: boolean; } function ScreenNode({ data }: { data: ScreenNodeData }) { const isMain = data.type === "main" || data.isMain; return (
{isMain ? ( ) : ( )} {isMain ? "메인 화면" : data.type === "modal" ? "모달" : data.type === "drawer" ? "드로어" : "전체화면"}
{data.label}
); } const nodeTypes = { screenNode: ScreenNode, }; // ============================================================ // 메인 컴포넌트 // ============================================================ export function PopScreenFlowView({ screen, className, onSubScreenSelect }: PopScreenFlowViewProps) { const [loading, setLoading] = useState(false); const [layoutData, setLayoutData] = useState(null); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); // 레이아웃 데이터 로드 useEffect(() => { if (!screen) { setLayoutData(null); setNodes([]); setEdges([]); return; } const loadLayout = async () => { try { setLoading(true); const layout = await screenApi.getLayoutPop(screen.screenId); if (layout && layout.version === "pop-1.0") { setLayoutData(layout); } else { setLayoutData(null); } } catch (error) { console.error("레이아웃 로드 실패:", error); setLayoutData(null); } finally { setLoading(false); } }; loadLayout(); }, [screen]); // 레이아웃 데이터에서 노드/엣지 생성 useEffect(() => { if (!layoutData || !screen) { return; } const newNodes: Node[] = []; const newEdges: Edge[] = []; // 메인 화면 노드 const mainNodeId = "main"; newNodes.push({ id: mainNodeId, type: "screenNode", position: { x: 50, y: 100 }, data: { label: screen.screenName, type: "main", isMain: true, }, sourcePosition: Position.Right, targetPosition: Position.Left, }); // 하위 화면 노드들 const subScreens = layoutData.subScreens || []; const horizontalGap = 200; const verticalGap = 100; subScreens.forEach((subScreen, index) => { // 세로로 나열, 여러 개일 경우 열 분리 const col = Math.floor(index / 3); const row = index % 3; newNodes.push({ id: subScreen.id, type: "screenNode", position: { x: 300 + col * horizontalGap, y: 50 + row * verticalGap, }, data: { label: subScreen.name, type: subScreen.type || "modal", }, sourcePosition: Position.Right, targetPosition: Position.Left, }); }); // 플로우 연결 (flow 배열 또는 triggerFrom 기반) const flows = layoutData.flow || []; if (flows.length > 0) { // 명시적 flow 배열 사용 flows.forEach((flow, index) => { newEdges.push({ id: `edge-${index}`, source: flow.from, target: flow.to, type: "smoothstep", animated: true, label: flow.label || flow.trigger, markerEnd: { type: MarkerType.ArrowClosed, color: "#888", }, style: { stroke: "#888", strokeWidth: 2 }, }); }); } else { // triggerFrom 기반으로 엣지 생성 (기본: 메인 → 서브) subScreens.forEach((subScreen, index) => { const sourceId = subScreen.triggerFrom || mainNodeId; newEdges.push({ id: `edge-${index}`, source: sourceId, target: subScreen.id, type: "smoothstep", animated: true, markerEnd: { type: MarkerType.ArrowClosed, color: "#888", }, style: { stroke: "#888", strokeWidth: 2 }, }); }); } setNodes(newNodes); setEdges(newEdges); }, [layoutData, screen, setNodes, setEdges]); // 노드 클릭 핸들러 const onNodeClick = useCallback( (_: React.MouseEvent, node: Node) => { if (node.id !== "main" && onSubScreenSelect) { onSubScreenSelect(node.id); } }, [onSubScreenSelect] ); // 레이아웃 또는 하위 화면이 없는 경우 const hasSubScreens = layoutData?.subScreens && layoutData.subScreens.length > 0; if (!screen) { return (

화면 흐름

화면을 선택하면 흐름이 표시됩니다.

); } if (loading) { return (

화면 흐름

); } if (!layoutData) { return (

화면 흐름

POP 레이아웃이 없습니다.

); } return (

화면 흐름

{screen.screenName}
{!hasSubScreens && ( 하위 화면 없음 )}
{hasSubScreens ? ( (node.data?.isMain ? "#3b82f6" : "#9ca3af")} maskColor="rgba(0, 0, 0, 0.1)" className="!bg-muted/50" /> ) : ( // 하위 화면이 없으면 간단한 단일 노드 표시
{screen.screenName}

이 화면에 연결된 하위 화면(모달)이 없습니다.
화면 설정에서 하위 화면을 추가할 수 있습니다.

)}
); }