2026-02-02 15:15:01 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
import React, { useEffect, useState } from "react";
|
2026-02-02 15:15:01 +09:00
|
|
|
import { useParams, useSearchParams } from "next/navigation";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2026-02-03 19:11:03 +09:00
|
|
|
import { Loader2, ArrowLeft, Smartphone, Tablet, RotateCcw, RotateCw } from "lucide-react";
|
2026-02-02 15:15:01 +09:00
|
|
|
import { screenApi } from "@/lib/api/screen";
|
2026-02-03 19:11:03 +09:00
|
|
|
import { ScreenDefinition, LayoutData } from "@/types/screen";
|
2026-02-02 15:15:01 +09:00
|
|
|
import { useRouter } from "next/navigation";
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
import { initializeComponents } from "@/lib/registry/components";
|
|
|
|
|
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
|
|
|
|
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
|
|
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
|
|
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
|
|
|
|
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
|
|
|
|
import { ScreenContextProvider } from "@/contexts/ScreenContext";
|
|
|
|
|
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
|
|
|
|
import { ActiveTabProvider } from "@/contexts/ActiveTabContext";
|
|
|
|
|
import { ScreenMultiLangProvider } from "@/contexts/ScreenMultiLangContext";
|
2026-02-03 19:11:03 +09:00
|
|
|
import {
|
|
|
|
|
PopLayoutDataV3,
|
2026-02-04 14:14:48 +09:00
|
|
|
PopLayoutDataV4,
|
2026-02-03 19:11:03 +09:00
|
|
|
PopLayoutModeKey,
|
|
|
|
|
ensureV3Layout,
|
|
|
|
|
isV3Layout,
|
2026-02-04 14:14:48 +09:00
|
|
|
isV4Layout,
|
2026-02-03 19:11:03 +09:00
|
|
|
} from "@/components/pop/designer/types/pop-layout";
|
|
|
|
|
import {
|
|
|
|
|
PopLayoutRenderer,
|
|
|
|
|
hasBaseLayout,
|
|
|
|
|
getEffectiveModeLayout,
|
|
|
|
|
} from "@/components/pop/designer/renderers";
|
2026-02-04 14:14:48 +09:00
|
|
|
import { PopFlexRenderer } from "@/components/pop/designer/renderers/PopFlexRenderer";
|
2026-02-03 19:11:03 +09:00
|
|
|
import {
|
|
|
|
|
useResponsiveMode,
|
|
|
|
|
useResponsiveModeWithOverride,
|
|
|
|
|
type DeviceType,
|
|
|
|
|
} from "@/hooks/useDeviceOrientation";
|
2026-02-02 15:15:01 +09:00
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
// 디바이스별 크기 (프리뷰 모드용)
|
|
|
|
|
const DEVICE_SIZES: Record<DeviceType, Record<"landscape" | "portrait", { width: number; height: number; label: string }>> = {
|
|
|
|
|
mobile: {
|
|
|
|
|
landscape: { width: 667, height: 375, label: "모바일 가로" },
|
|
|
|
|
portrait: { width: 375, height: 667, label: "모바일 세로" },
|
|
|
|
|
},
|
|
|
|
|
tablet: {
|
|
|
|
|
landscape: { width: 1024, height: 768, label: "태블릿 가로" },
|
|
|
|
|
portrait: { width: 768, height: 1024, label: "태블릿 세로" },
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 헬퍼 함수
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
const getModeKey = (device: DeviceType, isLandscape: boolean): PopLayoutModeKey => {
|
|
|
|
|
if (device === "tablet") {
|
|
|
|
|
return isLandscape ? "tablet_landscape" : "tablet_portrait";
|
|
|
|
|
}
|
|
|
|
|
return isLandscape ? "mobile_landscape" : "mobile_portrait";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// v3.0 레이아웃인지 확인
|
|
|
|
|
const isPopLayoutV3 = (layout: any): layout is PopLayoutDataV3 => {
|
|
|
|
|
return layout && layout.version === "pop-3.0" && layout.layouts && layout.components;
|
|
|
|
|
};
|
2026-02-02 15:15:01 +09:00
|
|
|
|
2026-02-04 14:14:48 +09:00
|
|
|
// v4.0 레이아웃인지 확인
|
|
|
|
|
const isPopLayoutV4 = (layout: any): layout is PopLayoutDataV4 => {
|
|
|
|
|
return layout && layout.version === "pop-4.0" && layout.root && layout.components;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// v1/v2/v3/v4 레이아웃인지 확인
|
2026-02-03 19:11:03 +09:00
|
|
|
const isPopLayout = (layout: any): boolean => {
|
|
|
|
|
return layout && (
|
|
|
|
|
layout.version === "pop-1.0" ||
|
|
|
|
|
layout.version === "pop-2.0" ||
|
2026-02-04 14:14:48 +09:00
|
|
|
layout.version === "pop-3.0" ||
|
|
|
|
|
layout.version === "pop-4.0"
|
2026-02-03 19:11:03 +09:00
|
|
|
);
|
2026-02-02 15:15:01 +09:00
|
|
|
};
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
// ========================================
|
|
|
|
|
// 메인 컴포넌트
|
|
|
|
|
// ========================================
|
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
function PopScreenViewPage() {
|
|
|
|
|
const params = useParams();
|
|
|
|
|
const searchParams = useSearchParams();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const screenId = parseInt(params.screenId as string);
|
|
|
|
|
|
|
|
|
|
const isPreviewMode = searchParams.get("preview") === "true";
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
// 반응형 모드 감지 (화면 크기에 따라 tablet/mobile, landscape/portrait 자동 전환)
|
|
|
|
|
// 프리뷰 모드에서는 수동 전환 가능
|
|
|
|
|
const { mode, setDevice, setOrientation, isAutoDetect } = useResponsiveModeWithOverride(
|
|
|
|
|
isPreviewMode ? "tablet" : undefined,
|
|
|
|
|
isPreviewMode ? true : undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 현재 모드 정보
|
|
|
|
|
const deviceType = mode.device;
|
|
|
|
|
const isLandscape = mode.isLandscape;
|
|
|
|
|
const currentModeKey = mode.modeKey;
|
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
const { user, userName, companyCode } = useAuth();
|
|
|
|
|
|
|
|
|
|
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
|
|
|
|
|
const [layout, setLayout] = useState<LayoutData | null>(null);
|
2026-02-03 19:11:03 +09:00
|
|
|
const [popLayoutV3, setPopLayoutV3] = useState<PopLayoutDataV3 | null>(null);
|
2026-02-04 14:14:48 +09:00
|
|
|
const [popLayoutV4, setPopLayoutV4] = useState<PopLayoutDataV4 | null>(null);
|
2026-02-02 15:15:01 +09:00
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
|
|
|
|
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
|
|
|
|
|
const [tableRefreshKey, setTableRefreshKey] = useState(0);
|
|
|
|
|
|
2026-02-04 14:14:48 +09:00
|
|
|
// 뷰포트 너비 (클라이언트 사이드에서만 계산, 최대 1366px)
|
|
|
|
|
const [viewportWidth, setViewportWidth] = useState(1024); // 기본값: 태블릿 가로
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const updateViewportWidth = () => {
|
|
|
|
|
setViewportWidth(Math.min(window.innerWidth, 1366));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
updateViewportWidth();
|
|
|
|
|
window.addEventListener("resize", updateViewportWidth);
|
|
|
|
|
return () => window.removeEventListener("resize", updateViewportWidth);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
// 컴포넌트 초기화
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const initComponents = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await initializeComponents();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("POP 화면 컴포넌트 초기화 실패:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
initComponents();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 화면 및 POP 레이아웃 로드
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const loadScreen = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
const screenData = await screenApi.getScreen(screenId);
|
|
|
|
|
setScreen(screenData);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const popLayout = await screenApi.getLayoutPop(screenId);
|
|
|
|
|
|
2026-02-04 14:14:48 +09:00
|
|
|
if (popLayout && isPopLayoutV4(popLayout)) {
|
|
|
|
|
// v4 레이아웃
|
|
|
|
|
setPopLayoutV4(popLayout);
|
|
|
|
|
setPopLayoutV3(null);
|
|
|
|
|
const componentCount = Object.keys(popLayout.components).length;
|
|
|
|
|
console.log(`[POP] v4 레이아웃 로드됨: ${componentCount}개 컴포넌트`);
|
|
|
|
|
} else if (popLayout && isPopLayout(popLayout)) {
|
2026-02-03 19:11:03 +09:00
|
|
|
// v1/v2/v3 → v3로 변환
|
|
|
|
|
const v3Layout = ensureV3Layout(popLayout);
|
|
|
|
|
setPopLayoutV3(v3Layout);
|
2026-02-04 14:14:48 +09:00
|
|
|
setPopLayoutV4(null);
|
2026-02-03 19:11:03 +09:00
|
|
|
|
|
|
|
|
const componentCount = Object.keys(v3Layout.components).length;
|
|
|
|
|
console.log(`[POP] v3 레이아웃 로드됨: ${componentCount}개 컴포넌트`);
|
|
|
|
|
|
|
|
|
|
if (!isV3Layout(popLayout)) {
|
|
|
|
|
console.log("[POP] v1/v2 → v3 자동 마이그레이션 완료");
|
|
|
|
|
}
|
|
|
|
|
} else if (popLayout && popLayout.components && Array.isArray(popLayout.components) && popLayout.components.length > 0) {
|
|
|
|
|
// 이전 형식 (레거시 components 구조)
|
|
|
|
|
console.log("[POP] 레거시 레이아웃 로드:", popLayout.components.length, "개 컴포넌트");
|
2026-02-02 15:15:01 +09:00
|
|
|
setLayout(popLayout as LayoutData);
|
|
|
|
|
} else {
|
2026-02-03 19:11:03 +09:00
|
|
|
console.log("[POP] 레이아웃 없음");
|
|
|
|
|
setPopLayoutV3(null);
|
2026-02-04 14:14:48 +09:00
|
|
|
setPopLayoutV4(null);
|
2026-02-03 19:11:03 +09:00
|
|
|
setLayout(null);
|
2026-02-02 15:15:01 +09:00
|
|
|
}
|
|
|
|
|
} catch (layoutError) {
|
2026-02-03 19:11:03 +09:00
|
|
|
console.warn("[POP] 레이아웃 로드 실패:", layoutError);
|
|
|
|
|
setPopLayoutV3(null);
|
2026-02-04 14:14:48 +09:00
|
|
|
setPopLayoutV4(null);
|
2026-02-03 19:11:03 +09:00
|
|
|
setLayout(null);
|
2026-02-02 15:15:01 +09:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-02-03 19:11:03 +09:00
|
|
|
console.error("[POP] 화면 로드 실패:", error);
|
2026-02-02 15:15:01 +09:00
|
|
|
setError("화면을 불러오는데 실패했습니다.");
|
|
|
|
|
toast.error("화면을 불러오는데 실패했습니다.");
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (screenId) {
|
|
|
|
|
loadScreen();
|
|
|
|
|
}
|
|
|
|
|
}, [screenId]);
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
const currentDevice = DEVICE_SIZES[deviceType][isLandscape ? "landscape" : "portrait"];
|
2026-02-02 15:15:01 +09:00
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-screen w-full items-center justify-center bg-gray-100">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<Loader2 className="mx-auto h-10 w-10 animate-spin text-blue-500" />
|
|
|
|
|
<p className="mt-4 text-gray-600">POP 화면 로딩 중...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error || !screen) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-screen w-full items-center justify-center bg-gray-100">
|
|
|
|
|
<div className="text-center max-w-md p-6">
|
|
|
|
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
|
|
|
|
|
<span className="text-2xl">!</span>
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="mb-2 text-xl font-bold text-gray-800">화면을 찾을 수 없습니다</h2>
|
|
|
|
|
<p className="mb-4 text-gray-600">{error || "요청하신 POP 화면이 존재하지 않습니다."}</p>
|
|
|
|
|
<Button onClick={() => router.back()} variant="outline">
|
|
|
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
|
|
|
돌아가기
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ScreenPreviewProvider isPreviewMode={isPreviewMode}>
|
|
|
|
|
<ActiveTabProvider>
|
|
|
|
|
<TableOptionsProvider>
|
2026-02-03 19:11:03 +09:00
|
|
|
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
|
2026-02-02 15:15:01 +09:00
|
|
|
{/* 상단 툴바 (프리뷰 모드에서만) */}
|
|
|
|
|
{isPreviewMode && (
|
|
|
|
|
<div className="sticky top-0 z-50 bg-white border-b shadow-sm">
|
|
|
|
|
<div className="flex items-center justify-between px-4 py-2">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={() => window.close()}>
|
|
|
|
|
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
|
|
|
닫기
|
|
|
|
|
</Button>
|
|
|
|
|
<span className="text-sm font-medium">{screen.screenName}</span>
|
2026-02-03 19:11:03 +09:00
|
|
|
<span className="text-xs text-gray-400">
|
|
|
|
|
({currentModeKey.replace("_", " ")})
|
|
|
|
|
</span>
|
2026-02-02 15:15:01 +09:00
|
|
|
</div>
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<div className="flex items-center gap-1 bg-gray-100 rounded-lg p-1">
|
|
|
|
|
<Button
|
|
|
|
|
variant={deviceType === "mobile" ? "default" : "ghost"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setDevice("mobile")}
|
|
|
|
|
className="gap-1"
|
|
|
|
|
>
|
|
|
|
|
<Smartphone className="h-4 w-4" />
|
|
|
|
|
모바일
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant={deviceType === "tablet" ? "default" : "ghost"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setDevice("tablet")}
|
|
|
|
|
className="gap-1"
|
|
|
|
|
>
|
|
|
|
|
<Tablet className="h-4 w-4" />
|
|
|
|
|
태블릿
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-1 bg-gray-100 rounded-lg p-1">
|
|
|
|
|
<Button
|
|
|
|
|
variant={isLandscape ? "default" : "ghost"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setOrientation(true)}
|
|
|
|
|
className="gap-1"
|
|
|
|
|
>
|
|
|
|
|
<RotateCw className="h-4 w-4" />
|
|
|
|
|
가로
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant={!isLandscape ? "default" : "ghost"}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setOrientation(false)}
|
|
|
|
|
className="gap-1"
|
|
|
|
|
>
|
|
|
|
|
<RotateCcw className="h-4 w-4" />
|
|
|
|
|
세로
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 자동 감지 모드 버튼 */}
|
2026-02-02 15:15:01 +09:00
|
|
|
<Button
|
2026-02-03 19:11:03 +09:00
|
|
|
variant={isAutoDetect ? "default" : "outline"}
|
2026-02-02 15:15:01 +09:00
|
|
|
size="sm"
|
2026-02-03 19:11:03 +09:00
|
|
|
onClick={() => {
|
|
|
|
|
setDevice(undefined);
|
|
|
|
|
setOrientation(undefined);
|
|
|
|
|
}}
|
2026-02-02 15:15:01 +09:00
|
|
|
className="gap-1"
|
|
|
|
|
>
|
2026-02-03 19:11:03 +09:00
|
|
|
자동
|
2026-02-02 15:15:01 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={() => window.location.reload()}>
|
|
|
|
|
<RotateCcw className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* POP 화면 컨텐츠 */}
|
2026-02-03 19:11:03 +09:00
|
|
|
<div className={`flex-1 flex flex-col ${isPreviewMode ? "py-4 overflow-auto items-center" : ""}`}>
|
|
|
|
|
{/* 현재 모드 표시 (일반 모드) */}
|
|
|
|
|
{!isPreviewMode && (
|
|
|
|
|
<div className="absolute top-2 right-2 z-10 bg-black/50 text-white text-xs px-2 py-1 rounded">
|
|
|
|
|
{currentModeKey.replace("_", " ")}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
<div
|
2026-02-03 19:11:03 +09:00
|
|
|
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-hidden border-8 border-gray-800" : "w-full h-full"}`}
|
|
|
|
|
style={isPreviewMode ? {
|
|
|
|
|
width: currentDevice.width,
|
|
|
|
|
height: currentDevice.height,
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
} : undefined}
|
2026-02-02 15:15:01 +09:00
|
|
|
>
|
2026-02-04 14:14:48 +09:00
|
|
|
{/* POP 레이아웃 v4.0 렌더링 */}
|
|
|
|
|
{popLayoutV4 ? (
|
|
|
|
|
<div
|
|
|
|
|
className="mx-auto h-full"
|
|
|
|
|
style={{ maxWidth: 1366 }}
|
|
|
|
|
>
|
|
|
|
|
<PopFlexRenderer
|
|
|
|
|
layout={popLayoutV4}
|
|
|
|
|
viewportWidth={isPreviewMode ? currentDevice.width : viewportWidth}
|
|
|
|
|
isDesignMode={false}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
) : popLayoutV3 ? (
|
|
|
|
|
/* POP 레이아웃 v3.0 렌더링 */
|
2026-02-03 19:11:03 +09:00
|
|
|
<PopLayoutV3Renderer
|
|
|
|
|
layout={popLayoutV3}
|
|
|
|
|
modeKey={currentModeKey}
|
|
|
|
|
/>
|
2026-02-02 18:01:05 +09:00
|
|
|
) : layout && layout.components && layout.components.length > 0 ? (
|
2026-02-03 19:11:03 +09:00
|
|
|
// 레거시 형식 (components 구조) - 호환성 유지
|
2026-02-02 15:15:01 +09:00
|
|
|
<ScreenMultiLangProvider components={layout.components} companyCode={companyCode}>
|
|
|
|
|
<div className="relative w-full min-h-full p-4">
|
|
|
|
|
{layout.components
|
|
|
|
|
.filter((component) => !component.parentId)
|
|
|
|
|
.map((component) => (
|
|
|
|
|
<div
|
|
|
|
|
key={component.id}
|
|
|
|
|
style={{
|
|
|
|
|
position: component.position ? "absolute" : "relative",
|
|
|
|
|
left: component.position?.x || 0,
|
|
|
|
|
top: component.position?.y || 0,
|
|
|
|
|
width: component.size?.width || "100%",
|
|
|
|
|
height: component.size?.height || "auto",
|
|
|
|
|
zIndex: component.position?.z || 1,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<DynamicComponentRenderer
|
|
|
|
|
component={component}
|
|
|
|
|
isDesignMode={false}
|
|
|
|
|
isInteractive={true}
|
|
|
|
|
formData={formData}
|
|
|
|
|
onDataflowComplete={() => { }}
|
|
|
|
|
screenId={screenId}
|
|
|
|
|
tableName={screen?.tableName}
|
|
|
|
|
userId={user?.userId}
|
|
|
|
|
userName={userName}
|
|
|
|
|
companyCode={companyCode}
|
|
|
|
|
selectedRowsData={selectedRowsData}
|
|
|
|
|
onSelectedRowsChange={(_, selectedData) => {
|
|
|
|
|
setSelectedRowsData(selectedData);
|
|
|
|
|
}}
|
|
|
|
|
refreshKey={tableRefreshKey}
|
|
|
|
|
onRefresh={() => {
|
|
|
|
|
setTableRefreshKey((prev) => prev + 1);
|
|
|
|
|
setSelectedRowsData([]);
|
|
|
|
|
}}
|
|
|
|
|
onFormDataChange={(fieldName, value) => {
|
|
|
|
|
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</ScreenMultiLangProvider>
|
|
|
|
|
) : (
|
|
|
|
|
// 빈 화면
|
|
|
|
|
<div className="flex flex-col items-center justify-center min-h-[400px] p-8 text-center">
|
|
|
|
|
<div className="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
|
|
|
|
<Smartphone className="h-8 w-8 text-gray-400" />
|
|
|
|
|
</div>
|
|
|
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-2">
|
|
|
|
|
화면이 비어있습니다
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-sm text-gray-500 max-w-xs">
|
|
|
|
|
POP 화면 디자이너에서 컴포넌트를 추가하여 화면을 구성하세요.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</TableOptionsProvider>
|
|
|
|
|
</ActiveTabProvider>
|
|
|
|
|
</ScreenPreviewProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 19:11:03 +09:00
|
|
|
// ========================================
|
|
|
|
|
// POP 레이아웃 v3.0 렌더러
|
|
|
|
|
// ========================================
|
|
|
|
|
interface PopLayoutV3RendererProps {
|
|
|
|
|
layout: PopLayoutDataV3;
|
|
|
|
|
modeKey: PopLayoutModeKey;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function PopLayoutV3Renderer({ layout, modeKey }: PopLayoutV3RendererProps) {
|
|
|
|
|
// 태블릿 가로 모드가 기준으로 설정되어 있는지 확인
|
|
|
|
|
if (!hasBaseLayout(layout)) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center min-h-[400px] p-8 text-center">
|
|
|
|
|
<div className="w-16 h-16 rounded-full bg-yellow-100 flex items-center justify-center mb-4">
|
|
|
|
|
<span className="text-2xl">!</span>
|
|
|
|
|
</div>
|
|
|
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-2">
|
|
|
|
|
화면이 설정되지 않았습니다
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-sm text-gray-500 max-w-xs">
|
|
|
|
|
POP 화면 디자이너에서 태블릿 가로 모드 레이아웃을 먼저 설정해주세요.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 현재 모드에 맞는 레이아웃 가져오기
|
|
|
|
|
const { modeLayout, isConverted, sourceModeKey } = getEffectiveModeLayout(layout, modeKey);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="w-full h-full flex flex-col">
|
|
|
|
|
{isConverted && (
|
|
|
|
|
<div className="mx-2 mt-2 px-2 py-1 bg-yellow-50 border border-yellow-200 rounded text-xs text-yellow-700 shrink-0">
|
|
|
|
|
{sourceModeKey} 기준 자동 변환됨
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<PopLayoutRenderer
|
|
|
|
|
layout={layout}
|
|
|
|
|
modeKey={modeKey}
|
|
|
|
|
customModeLayout={isConverted ? modeLayout : undefined}
|
|
|
|
|
isDesignMode={false}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
style={{ height: "100%" }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 15:15:01 +09:00
|
|
|
// Provider 래퍼
|
|
|
|
|
export default function PopScreenViewPageWrapper() {
|
|
|
|
|
return (
|
|
|
|
|
<TableSearchWidgetHeightProvider>
|
|
|
|
|
<ScreenContextProvider>
|
|
|
|
|
<SplitPanelProvider>
|
|
|
|
|
<PopScreenViewPage />
|
|
|
|
|
</SplitPanelProvider>
|
|
|
|
|
</ScreenContextProvider>
|
|
|
|
|
</TableSearchWidgetHeightProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|