2026-02-23 13:54:49 +09:00
|
|
|
/**
|
|
|
|
|
* PopViewerWithModals - 뷰어 모드에서 모달 렌더링 래퍼
|
|
|
|
|
*
|
|
|
|
|
* PopRenderer를 감싸서:
|
|
|
|
|
* 1. __pop_modal_open__ 이벤트 구독 → Dialog 열기
|
|
|
|
|
* 2. __pop_modal_close__ 이벤트 구독 → Dialog 닫기
|
|
|
|
|
* 3. 모달 스택 관리 (중첩 모달 지원)
|
|
|
|
|
*
|
|
|
|
|
* 모달 내부는 또 다른 PopRenderer로 렌더링 (독립 그리드).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
"use client";
|
|
|
|
|
|
2026-02-24 13:05:16 +09:00
|
|
|
import { useState, useCallback, useEffect, useMemo } from "react";
|
2026-03-03 17:13:01 +09:00
|
|
|
import { useRouter } from "next/navigation";
|
2026-02-23 13:54:49 +09:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
import PopRenderer from "../designer/renderers/PopRenderer";
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
import type { PopLayoutData, PopModalDefinition, GridMode } from "../designer/types/pop-layout";
|
2026-02-23 13:54:49 +09:00
|
|
|
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 {
|
|
|
|
|
/** 전체 레이아웃 (모달 정의 포함) */
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
layout: PopLayoutData;
|
2026-02-23 13:54:49 +09:00
|
|
|
/** 뷰포트 너비 */
|
|
|
|
|
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) {
|
2026-03-03 17:13:01 +09:00
|
|
|
const router = useRouter();
|
2026-02-23 13:54:49 +09:00
|
|
|
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를 이벤트 라우팅으로 변환
|
2026-02-24 13:05:16 +09:00
|
|
|
const stableConnections = useMemo(
|
|
|
|
|
() => layout.dataFlow?.connections ?? [],
|
|
|
|
|
[layout.dataFlow?.connections]
|
|
|
|
|
);
|
2026-03-03 15:30:07 +09:00
|
|
|
|
|
|
|
|
const componentTypes = useMemo(() => {
|
|
|
|
|
const map = new Map<string, string>();
|
|
|
|
|
if (layout.components) {
|
|
|
|
|
for (const comp of Object.values(layout.components)) {
|
|
|
|
|
map.set(comp.id, comp.type);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return map;
|
|
|
|
|
}, [layout.components]);
|
|
|
|
|
|
2026-02-23 18:45:21 +09:00
|
|
|
useConnectionResolver({
|
|
|
|
|
screenId,
|
2026-02-24 13:05:16 +09:00
|
|
|
connections: stableConnections,
|
2026-03-03 15:30:07 +09:00
|
|
|
componentTypes,
|
2026-02-23 18:45:21 +09:00
|
|
|
});
|
|
|
|
|
|
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
|
|
|
});
|
|
|
|
|
|
2026-03-03 17:13:01 +09:00
|
|
|
const unsubNavigate = subscribe("__pop_navigate__", (payload: unknown) => {
|
|
|
|
|
const data = payload as {
|
|
|
|
|
screenId?: string;
|
|
|
|
|
params?: Record<string, string>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!data?.screenId) return;
|
|
|
|
|
|
|
|
|
|
if (data.screenId === "back") {
|
|
|
|
|
router.back();
|
|
|
|
|
} else {
|
|
|
|
|
const query = data.params
|
|
|
|
|
? "?" + new URLSearchParams(data.params).toString()
|
|
|
|
|
: "";
|
|
|
|
|
window.location.href = `/pop/screens/${data.screenId}${query}`;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-23 13:54:49 +09:00
|
|
|
return () => {
|
|
|
|
|
unsubOpen();
|
|
|
|
|
unsubClose();
|
2026-03-03 17:13:01 +09:00
|
|
|
unsubNavigate();
|
2026-02-23 13:54:49 +09:00
|
|
|
};
|
2026-03-03 17:13:01 +09:00
|
|
|
}, [subscribe, publish, layout.modals, router]);
|
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;
|
|
|
|
|
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
const modalLayout: PopLayoutData = {
|
2026-02-23 13:54:49 +09:00
|
|
|
...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>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|