/** * PopViewerWithModals - 뷰어 모드에서 모달 렌더링 래퍼 * * PopRenderer를 감싸서: * 1. __pop_modal_open__ 이벤트 구독 → Dialog 열기 * 2. __pop_modal_close__ 이벤트 구독 → Dialog 닫기 * 3. 모달 스택 관리 (중첩 모달 지원) * * 모달 내부는 또 다른 PopRenderer로 렌더링 (독립 그리드). */ "use client"; import { useState, useCallback, useEffect, useMemo } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import PopRenderer from "../designer/renderers/PopRenderer"; import type { PopLayoutDataV5, PopModalDefinition, GridMode } from "../designer/types/pop-layout"; import { detectGridMode, resolveModalWidth } from "../designer/types/pop-layout"; import { usePopEvent } from "@/hooks/pop/usePopEvent"; import { useConnectionResolver } from "@/hooks/pop/useConnectionResolver"; // ======================================== // 타입 // ======================================== interface PopViewerWithModalsProps { /** 전체 레이아웃 (모달 정의 포함) */ layout: PopLayoutDataV5; /** 뷰포트 너비 */ viewportWidth: number; /** 화면 ID (이벤트 버스용) */ screenId: string; /** 현재 그리드 모드 (PopRenderer 전달용) */ currentMode?: GridMode; /** Gap 오버라이드 */ overrideGap?: number; /** Padding 오버라이드 */ overridePadding?: number; } /** 열린 모달 상태 */ interface OpenModal { definition: PopModalDefinition; returnTo?: string; } // ======================================== // 메인 컴포넌트 // ======================================== export default function PopViewerWithModals({ layout, viewportWidth, screenId, currentMode, overrideGap, overridePadding, }: PopViewerWithModalsProps) { const [modalStack, setModalStack] = useState([]); const { subscribe, publish } = usePopEvent(screenId); // 연결 해석기: layout에 정의된 connections를 이벤트 라우팅으로 변환 const stableConnections = useMemo( () => layout.dataFlow?.connections ?? [], [layout.dataFlow?.connections] ); const componentTypes = useMemo(() => { const map = new Map(); if (layout.components) { for (const comp of Object.values(layout.components)) { map.set(comp.id, comp.type); } } return map; }, [layout.components]); useConnectionResolver({ screenId, connections: stableConnections, componentTypes, }); // 모달 열기/닫기 이벤트 구독 useEffect(() => { const unsubOpen = subscribe("__pop_modal_open__", (payload: unknown) => { const data = payload as { modalId?: string; title?: string; mode?: string; returnTo?: string; }; if (data?.modalId) { const modalDef = layout.modals?.find(m => m.id === data.modalId); if (modalDef) { setModalStack(prev => [...prev, { definition: modalDef, returnTo: data.returnTo, }]); } } }); const unsubClose = subscribe("__pop_modal_close__", (payload: unknown) => { const data = payload as { selectedRow?: Record } | undefined; setModalStack(prev => { if (prev.length === 0) return prev; const topModal = prev[prev.length - 1]; // 결과 데이터가 있고, 반환 대상이 지정된 경우 결과 이벤트 발행 if (data?.selectedRow && topModal.returnTo) { publish("__pop_modal_result__", { selectedRow: data.selectedRow, returnTo: topModal.returnTo, }); } return prev.slice(0, -1); }); }); return () => { unsubOpen(); unsubClose(); }; }, [subscribe, publish, layout.modals]); // 최상위 모달만 닫기 (X 버튼, overlay 클릭, ESC) const handleCloseTopModal = useCallback(() => { setModalStack(prev => prev.slice(0, -1)); }, []); return ( <> {/* 메인 화면 렌더링 */} {/* 모달 스택 렌더링 */} {modalStack.map((modal, index) => { const { definition } = modal; const isTopModal = index === modalStack.length - 1; const closeOnOverlay = definition.frameConfig?.closeOnOverlay !== false; const closeOnEsc = definition.frameConfig?.closeOnEsc !== false; const modalLayout: PopLayoutDataV5 = { ...layout, gridConfig: definition.gridConfig, components: definition.components, overrides: definition.overrides, }; const detectedMode = currentMode || detectGridMode(viewportWidth); const modalWidth = resolveModalWidth(definition.sizeConfig, detectedMode, viewportWidth); const isFull = modalWidth >= viewportWidth; const rendererWidth = isFull ? viewportWidth : modalWidth - 32; return ( { if (!open && isTopModal) handleCloseTopModal(); }} > { // 최상위 모달이 아니면 overlay 클릭 무시 (하위 모달이 먼저 닫히는 것 방지) if (!isTopModal || !closeOnOverlay) e.preventDefault(); }} onEscapeKeyDown={(e) => { if (!isTopModal || !closeOnEsc) e.preventDefault(); }} > {definition.title}
); })} ); }