2026-02-23 13:54:49 +09:00
|
|
|
/**
|
|
|
|
|
* 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";
|
2026-02-23 18:45:21 +09:00
|
|
|
import { useConnectionResolver } from "@/hooks/pop/useConnectionResolver";
|
2026-02-23 13:54:49 +09:00
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 타입
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
interface PopViewerWithModalsProps {
|
|
|
|
|
/** 전체 레이아웃 (모달 정의 포함) */
|
|
|
|
|
layout: PopLayoutDataV5;
|
|
|
|
|
/** 뷰포트 너비 */
|
|
|
|
|
viewportWidth: number;
|
|
|
|
|
/** 화면 ID (이벤트 버스용) */
|
|
|
|
|
screenId: string;
|
|
|
|
|
/** 현재 그리드 모드 (PopRenderer 전달용) */
|
|
|
|
|
currentMode?: GridMode;
|
|
|
|
|
/** Gap 오버라이드 */
|
|
|
|
|
overrideGap?: number;
|
|
|
|
|
/** Padding 오버라이드 */
|
|
|
|
|
overridePadding?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 열린 모달 상태 */
|
|
|
|
|
interface OpenModal {
|
|
|
|
|
definition: PopModalDefinition;
|
2026-02-24 12:52:29 +09:00
|
|
|
returnTo?: string;
|
2026-02-23 13:54:49 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 메인 컴포넌트
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
export default function PopViewerWithModals({
|
|
|
|
|
layout,
|
|
|
|
|
viewportWidth,
|
|
|
|
|
screenId,
|
|
|
|
|
currentMode,
|
|
|
|
|
overrideGap,
|
|
|
|
|
overridePadding,
|
|
|
|
|
}: PopViewerWithModalsProps) {
|
|
|
|
|
const [modalStack, setModalStack] = useState<OpenModal[]>([]);
|
2026-02-24 12:52:29 +09:00
|
|
|
const { subscribe, publish } = usePopEvent(screenId);
|
2026-02-23 13:54:49 +09:00
|
|
|
|
2026-02-23 18:45:21 +09:00
|
|
|
// 연결 해석기: layout에 정의된 connections를 이벤트 라우팅으로 변환
|
|
|
|
|
useConnectionResolver({
|
|
|
|
|
screenId,
|
|
|
|
|
connections: layout.dataFlow?.connections || [],
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-24 12:52:29 +09:00
|
|
|
// 모달 열기/닫기 이벤트 구독
|
2026-02-23 13:54:49 +09:00
|
|
|
useEffect(() => {
|
|
|
|
|
const unsubOpen = subscribe("__pop_modal_open__", (payload: unknown) => {
|
|
|
|
|
const data = payload as {
|
|
|
|
|
modalId?: string;
|
|
|
|
|
title?: string;
|
|
|
|
|
mode?: string;
|
2026-02-24 12:52:29 +09:00
|
|
|
returnTo?: string;
|
2026-02-23 13:54:49 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (data?.modalId) {
|
|
|
|
|
const modalDef = layout.modals?.find(m => m.id === data.modalId);
|
|
|
|
|
if (modalDef) {
|
2026-02-24 12:52:29 +09:00
|
|
|
setModalStack(prev => [...prev, {
|
|
|
|
|
definition: modalDef,
|
|
|
|
|
returnTo: data.returnTo,
|
|
|
|
|
}]);
|
2026-02-23 13:54:49 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-24 12:52:29 +09:00
|
|
|
const unsubClose = subscribe("__pop_modal_close__", (payload: unknown) => {
|
|
|
|
|
const data = payload as { selectedRow?: Record<string, unknown> } | 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);
|
|
|
|
|
});
|
2026-02-23 13:54:49 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
unsubOpen();
|
|
|
|
|
unsubClose();
|
|
|
|
|
};
|
2026-02-24 12:52:29 +09:00
|
|
|
}, [subscribe, publish, layout.modals]);
|
2026-02-23 13:54:49 +09:00
|
|
|
|
2026-02-23 14:03:55 +09:00
|
|
|
// 최상위 모달만 닫기 (X 버튼, overlay 클릭, ESC)
|
|
|
|
|
const handleCloseTopModal = useCallback(() => {
|
|
|
|
|
setModalStack(prev => prev.slice(0, -1));
|
2026-02-23 13:54:49 +09:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{/* 메인 화면 렌더링 */}
|
|
|
|
|
<PopRenderer
|
|
|
|
|
layout={layout}
|
|
|
|
|
viewportWidth={viewportWidth}
|
|
|
|
|
currentScreenId={Number(screenId) || undefined}
|
|
|
|
|
currentMode={currentMode}
|
|
|
|
|
isDesignMode={false}
|
|
|
|
|
overrideGap={overrideGap}
|
|
|
|
|
overridePadding={overridePadding}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 모달 스택 렌더링 */}
|
|
|
|
|
{modalStack.map((modal, index) => {
|
|
|
|
|
const { definition } = modal;
|
2026-02-23 14:03:55 +09:00
|
|
|
const isTopModal = index === modalStack.length - 1;
|
2026-02-23 13:54:49 +09:00
|
|
|
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 (
|
|
|
|
|
<Dialog
|
|
|
|
|
key={`${definition.id}-${index}`}
|
|
|
|
|
open={true}
|
|
|
|
|
onOpenChange={(open) => {
|
2026-02-23 14:03:55 +09:00
|
|
|
if (!open && isTopModal) handleCloseTopModal();
|
2026-02-23 13:54:49 +09:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<DialogContent
|
|
|
|
|
className={isFull
|
|
|
|
|
? "h-dvh max-h-dvh w-screen max-w-[100vw] overflow-auto rounded-none border-none p-0"
|
|
|
|
|
: "max-h-[90vh] overflow-auto p-0"
|
|
|
|
|
}
|
|
|
|
|
style={isFull ? undefined : {
|
|
|
|
|
maxWidth: `${modalWidth}px`,
|
|
|
|
|
width: `${modalWidth}px`,
|
|
|
|
|
}}
|
|
|
|
|
onInteractOutside={(e) => {
|
2026-02-23 14:03:55 +09:00
|
|
|
// 최상위 모달이 아니면 overlay 클릭 무시 (하위 모달이 먼저 닫히는 것 방지)
|
|
|
|
|
if (!isTopModal || !closeOnOverlay) e.preventDefault();
|
2026-02-23 13:54:49 +09:00
|
|
|
}}
|
|
|
|
|
onEscapeKeyDown={(e) => {
|
2026-02-23 14:03:55 +09:00
|
|
|
if (!isTopModal || !closeOnEsc) e.preventDefault();
|
2026-02-23 13:54:49 +09:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<DialogHeader className={isFull ? "px-4 pt-3 pb-2" : "px-4 pt-4 pb-2"}>
|
|
|
|
|
<DialogTitle className="text-base">
|
|
|
|
|
{definition.title}
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<div className={isFull ? "flex-1 overflow-auto" : "px-4 pb-4"}>
|
|
|
|
|
<PopRenderer
|
|
|
|
|
layout={modalLayout}
|
|
|
|
|
viewportWidth={rendererWidth}
|
|
|
|
|
currentScreenId={Number(screenId) || undefined}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|