/** * PopViewerWithModals - 뷰어 모드에서 모달 렌더링 래퍼 * * PopRenderer를 감싸서: * 1. __pop_modal_open__ 이벤트 구독 → Dialog 열기 * 2. __pop_modal_close__ 이벤트 구독 → Dialog 닫기 * 3. 모달 스택 관리 (중첩 모달 지원) * * 모달 내부는 또 다른 PopRenderer로 렌더링 (독립 그리드). */ "use client"; import { useState, useCallback, useEffect } 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"; // ======================================== // 타입 // ======================================== interface PopViewerWithModalsProps { /** 전체 레이아웃 (모달 정의 포함) */ layout: PopLayoutDataV5; /** 뷰포트 너비 */ viewportWidth: number; /** 화면 ID (이벤트 버스용) */ screenId: string; /** 현재 그리드 모드 (PopRenderer 전달용) */ currentMode?: GridMode; /** Gap 오버라이드 */ overrideGap?: number; /** Padding 오버라이드 */ overridePadding?: number; } /** 열린 모달 상태 */ interface OpenModal { definition: PopModalDefinition; } // ======================================== // 메인 컴포넌트 // ======================================== export default function PopViewerWithModals({ layout, viewportWidth, screenId, currentMode, overrideGap, overridePadding, }: PopViewerWithModalsProps) { const [modalStack, setModalStack] = useState([]); const { subscribe } = usePopEvent(screenId); // 모달 열기 이벤트 구독 useEffect(() => { const unsubOpen = subscribe("__pop_modal_open__", (payload: unknown) => { const data = payload as { modalId?: string; title?: string; mode?: string; }; // fullscreen 모달: layout.modals에서 정의 찾기 if (data?.modalId) { const modalDef = layout.modals?.find(m => m.id === data.modalId); if (modalDef) { setModalStack(prev => [...prev, { definition: modalDef }]); } } }); const unsubClose = subscribe("__pop_modal_close__", () => { // 가장 최근 모달 닫기 setModalStack(prev => prev.slice(0, -1)); }); return () => { unsubOpen(); unsubClose(); }; }, [subscribe, layout.modals]); // 특정 인덱스의 모달 닫기 const handleCloseModal = useCallback((index: number) => { setModalStack(prev => prev.slice(0, index)); }, []); return ( <> {/* 메인 화면 렌더링 */} {/* 모달 스택 렌더링 */} {modalStack.map((modal, index) => { const { definition } = modal; const closeOnOverlay = definition.frameConfig?.closeOnOverlay !== false; const closeOnEsc = definition.frameConfig?.closeOnEsc !== false; // 모달의 layout 구성 (모달 자체를 하나의 레이아웃으로) const modalLayout: PopLayoutDataV5 = { ...layout, gridConfig: definition.gridConfig, components: definition.components, overrides: definition.overrides, }; // sizeConfig 기반 모달 너비 계산 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) handleCloseModal(index); }} > { if (!closeOnOverlay) e.preventDefault(); }} onEscapeKeyDown={(e) => { if (!closeOnEsc) e.preventDefault(); }} > {definition.title}
); })} ); }