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

363 lines
15 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useEffect, useState, useMemo } from "react";
import { useParams, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Loader2, ArrowLeft, Smartphone, Tablet, Monitor, RotateCcw } from "lucide-react";
import { screenApi } from "@/lib/api/screen";
import { ScreenDefinition, LayoutData, ComponentData } from "@/types/screen";
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";
import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter";
// POP 디바이스 타입
type DeviceType = "mobile" | "tablet";
// 디바이스별 크기
const DEVICE_SIZES = {
mobile: { width: 375, height: 812, label: "모바일" },
tablet: { width: 768, height: 1024, label: "태블릿" },
};
function PopScreenViewPage() {
const params = useParams();
const searchParams = useSearchParams();
const router = useRouter();
const screenId = parseInt(params.screenId as string);
// URL 쿼리에서 디바이스 타입 가져오기 (기본: tablet)
const deviceParam = searchParams.get("device") as DeviceType | null;
const [deviceType, setDeviceType] = useState<DeviceType>(deviceParam || "tablet");
// 프리뷰 모드 (디자이너에서 열렸을 때)
const isPreviewMode = searchParams.get("preview") === "true";
// 사용자 정보
const { user, userName, companyCode } = useAuth();
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
const [layout, setLayout] = useState<LayoutData | null>(null);
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);
// 컴포넌트 초기화
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);
// POP 레이아웃 로드 (screen_layouts_pop 테이블에서)
// POP 레이아웃은 sections[] 구조 사용 (데스크톱의 components[]와 다름)
try {
const popLayout = await screenApi.getLayoutPop(screenId);
if (popLayout && popLayout.sections && popLayout.sections.length > 0) {
// POP 레이아웃 (sections 구조) - 그대로 저장
console.log("POP 레이아웃 로드:", popLayout.sections?.length || 0, "개 섹션");
setLayout(popLayout as any); // sections 구조 그대로 사용
} else if (popLayout && popLayout.components && popLayout.components.length > 0) {
// 이전 형식 (components 구조) - 호환성 유지
console.log("POP 레이아웃 로드 (이전 형식):", popLayout.components?.length || 0, "개 컴포넌트");
setLayout(popLayout as LayoutData);
} else {
// POP 레이아웃이 비어있으면 빈 레이아웃
console.log("POP 레이아웃 없음, 빈 화면 표시");
setLayout({
screenId,
sections: [],
components: [],
gridSettings: {
columns: 12,
gap: 8,
padding: 16,
enabled: true,
size: 8,
color: "#e0e0e0",
opacity: 0.5,
snapToGrid: true,
},
} as any);
}
} catch (layoutError) {
console.warn("POP 레이아웃 로드 실패:", layoutError);
setLayout({
screenId,
sections: [],
components: [],
gridSettings: {
columns: 12,
gap: 8,
padding: 16,
enabled: true,
size: 8,
color: "#e0e0e0",
opacity: 0.5,
snapToGrid: true,
},
} as any);
}
} catch (error) {
console.error("POP 화면 로드 실패:", error);
setError("화면을 불러오는데 실패했습니다.");
toast.error("화면을 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
};
if (screenId) {
loadScreen();
}
}, [screenId]);
// 현재 디바이스 크기
const currentDevice = DEVICE_SIZES[deviceType];
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="min-h-screen bg-gray-100">
{/* 상단 툴바 (프리뷰 모드에서만) */}
{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>
</div>
{/* 디바이스 전환 버튼 */}
<div className="flex items-center gap-1 bg-gray-100 rounded-lg p-1">
<Button
variant={deviceType === "mobile" ? "default" : "ghost"}
size="sm"
onClick={() => setDeviceType("mobile")}
className="gap-1"
>
<Smartphone className="h-4 w-4" />
</Button>
<Button
variant={deviceType === "tablet" ? "default" : "ghost"}
size="sm"
onClick={() => setDeviceType("tablet")}
className="gap-1"
>
<Tablet className="h-4 w-4" />
릿
</Button>
</div>
<Button variant="ghost" size="sm" onClick={() => window.location.reload()}>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
</div>
)}
{/* POP 화면 컨텐츠 */}
<div className={`flex justify-center ${isPreviewMode ? "py-4" : "py-0"}`}>
<div
className={`bg-white ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-hidden border-8 border-gray-800" : ""}`}
style={{
width: isPreviewMode ? currentDevice.width : "100%",
minHeight: isPreviewMode ? currentDevice.height : "100vh",
maxWidth: isPreviewMode ? currentDevice.width : "100%",
}}
>
{/* POP 레이아웃: sections 구조 렌더링 */}
{layout && (layout as any).sections && (layout as any).sections.length > 0 ? (
<div className="w-full min-h-full p-2">
{/* 그리드 레이아웃으로 섹션 배치 */}
<div
className="grid gap-1"
style={{
gridTemplateColumns: `repeat(${(layout as any).canvasGrid?.columns || 24}, 1fr)`,
}}
>
{(layout as any).sections.map((section: any) => (
<div
key={section.id}
className="bg-gray-50 border border-gray-200 rounded-lg p-2"
style={{
gridColumn: `${section.grid?.col || 1} / span ${section.grid?.colSpan || 6}`,
gridRow: `${section.grid?.row || 1} / span ${section.grid?.rowSpan || 4}`,
minHeight: `${(section.grid?.rowSpan || 4) * 20}px`,
}}
>
{/* 섹션 라벨 */}
{section.label && (
<div className="text-xs font-medium text-gray-500 mb-1">
{section.label}
</div>
)}
{/* 섹션 내 컴포넌트들 */}
{section.components && section.components.length > 0 ? (
<div className="space-y-1">
{section.components.map((comp: any) => (
<div
key={comp.id}
className="bg-white border border-gray-100 rounded p-2 text-sm"
>
{/* TODO: POP 전용 컴포넌트 렌더러 구현 필요 */}
<span className="text-gray-600">
{comp.label || comp.type || comp.id}
</span>
</div>
))}
</div>
) : (
<div className="text-xs text-gray-400 text-center py-2">
</div>
)}
</div>
))}
</div>
</div>
) : layout && layout.components && layout.components.length > 0 ? (
// 이전 형식 (components 구조) - 호환성 유지
<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>
);
}
// Provider 래퍼
export default function PopScreenViewPageWrapper() {
return (
<TableSearchWidgetHeightProvider>
<ScreenContextProvider>
<SplitPanelProvider>
<PopScreenViewPage />
</SplitPanelProvider>
</ScreenContextProvider>
</TableSearchWidgetHeightProvider>
);
}