ERP-node/frontend/app/(pop)/pop/screens/[screenId]/page.tsx

319 lines
12 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useEffect, useState } from "react";
import { useParams, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Loader2, ArrowLeft, Smartphone, Tablet, RotateCcw, RotateCw } from "lucide-react";
import { screenApi } from "@/lib/api/screen";
import { ScreenDefinition } from "@/types/screen";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
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 {
PopLayoutDataV5,
GridMode,
isV5Layout,
createEmptyPopLayoutV5,
} from "@/components/pop/designer/types/pop-layout";
import PopRenderer from "@/components/pop/designer/renderers/PopRenderer";
import {
useResponsiveModeWithOverride,
type DeviceType,
} from "@/hooks/useDeviceOrientation";
// 디바이스별 크기 (프리뷰 모드용)
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): GridMode => {
if (device === "tablet") {
return isLandscape ? "tablet_landscape" : "tablet_portrait";
}
return isLandscape ? "mobile_landscape" : "mobile_portrait";
};
// ========================================
// 메인 컴포넌트 (v5 그리드 시스템 전용)
// ========================================
function PopScreenViewPage() {
const params = useParams();
const searchParams = useSearchParams();
const router = useRouter();
const screenId = parseInt(params.screenId as string);
const isPreviewMode = searchParams.get("preview") === "true";
// 반응형 모드 감지 (화면 크기에 따라 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 = getModeKey(deviceType, isLandscape);
const { user } = useAuth();
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
const [layout, setLayout] = useState<PopLayoutDataV5>(createEmptyPopLayoutV5());
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 뷰포트 너비 (클라이언트 사이드에서만 계산, 최대 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);
}, []);
// 화면 및 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);
if (popLayout && isV5Layout(popLayout)) {
// v5 레이아웃 로드
setLayout(popLayout);
const componentCount = Object.keys(popLayout.components).length;
console.log(`[POP] v5 레이아웃 로드됨: ${componentCount}개 컴포넌트`);
} else if (popLayout) {
// 다른 버전 레이아웃은 빈 v5로 처리
console.log("[POP] 레거시 레이아웃 감지, 빈 레이아웃으로 시작합니다:", popLayout.version);
setLayout(createEmptyPopLayoutV5());
} else {
console.log("[POP] 레이아웃 없음");
setLayout(createEmptyPopLayoutV5());
}
} catch (layoutError) {
console.warn("[POP] 레이아웃 로드 실패:", layoutError);
setLayout(createEmptyPopLayoutV5());
}
} catch (error) {
console.error("[POP] 화면 로드 실패:", error);
setError("화면을 불러오는데 실패했습니다.");
toast.error("화면을 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
};
if (screenId) {
loadScreen();
}
}, [screenId]);
const currentDevice = DEVICE_SIZES[deviceType][isLandscape ? "landscape" : "portrait"];
const hasComponents = Object.keys(layout.components).length > 0;
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>
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
{/* 상단 툴바 (프리뷰 모드에서만) */}
{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>
<span className="text-xs text-gray-400">
({currentModeKey.replace("_", " ")})
</span>
</div>
<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>
{/* 자동 감지 모드 버튼 */}
<Button
variant={isAutoDetect ? "default" : "outline"}
size="sm"
onClick={() => {
setDevice(undefined);
setOrientation(undefined);
}}
className="gap-1"
>
</Button>
</div>
<Button variant="ghost" size="sm" onClick={() => window.location.reload()}>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
</div>
)}
{/* POP 화면 컨텐츠 */}
<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>
)}
<div
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}
>
{/* v5 그리드 렌더러 */}
{hasComponents ? (
<div
className="mx-auto h-full"
style={{ maxWidth: 1366 }}
>
<PopRenderer
layout={layout}
viewportWidth={isPreviewMode ? currentDevice.width : viewportWidth}
currentMode={currentModeKey}
isDesignMode={false}
/>
</div>
) : (
// 빈 화면
<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>
);
}
// Provider 래퍼
export default function PopScreenViewPageWrapper() {
return (
<TableSearchWidgetHeightProvider>
<ScreenContextProvider>
<SplitPanelProvider>
<PopScreenViewPage />
</SplitPanelProvider>
</ScreenContextProvider>
</TableSearchWidgetHeightProvider>
);
}