"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 (
);
}
return (
화면 흐름
{screen.screenName}
{!hasSubScreens && (
하위 화면 없음
)}
{hasSubScreens ? (
(node.data?.isMain ? "#3b82f6" : "#9ca3af")}
maskColor="rgba(0, 0, 0, 0.1)"
className="!bg-muted/50"
/>
) : (
// 하위 화면이 없으면 간단한 단일 노드 표시
{screen.screenName}
이 화면에 연결된 하위 화면(모달)이 없습니다.
화면 설정에서 하위 화면을 추가할 수 있습니다.
)}
);
}