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

246 lines
9.0 KiB
TypeScript
Raw Normal View History

2025-09-01 18:42:59 +09:00
"use client";
2025-10-16 18:16:57 +09:00
import React, { useEffect, useState } from "react";
2025-09-01 18:42:59 +09:00
import { useParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
2025-09-01 18:42:59 +09:00
import { screenApi } from "@/lib/api/screen";
import { ScreenDefinition, LayoutData } from "@/types/screen";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
2025-09-12 14:24:25 +09:00
import { initializeComponents } from "@/lib/registry/components";
2025-09-18 18:49:30 +09:00
import { EditModal } from "@/components/screen/EditModal";
2025-10-22 17:19:47 +09:00
import { RealtimePreview } from "@/components/screen/RealtimePreviewDynamic";
2025-09-01 18:42:59 +09:00
export default function ScreenViewPage() {
const params = useParams();
const router = useRouter();
const screenId = parseInt(params.screenId as string);
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);
2025-10-22 17:19:47 +09:00
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [formData, setFormData] = useState<Record<string, unknown>>({});
2025-09-01 18:42:59 +09:00
2025-09-18 18:49:30 +09:00
// 편집 모달 상태
const [editModalOpen, setEditModalOpen] = useState(false);
const [editModalConfig, setEditModalConfig] = useState<{
screenId?: number;
modalSize?: "sm" | "md" | "lg" | "xl" | "full";
editData?: Record<string, unknown>;
2025-09-18 18:49:30 +09:00
onSave?: () => void;
modalTitle?: string;
modalDescription?: string;
2025-09-18 18:49:30 +09:00
}>({});
2025-09-12 14:24:25 +09:00
useEffect(() => {
const initComponents = async () => {
try {
console.log("🚀 할당된 화면에서 컴포넌트 시스템 초기화 시작...");
await initializeComponents();
console.log("✅ 할당된 화면에서 컴포넌트 시스템 초기화 완료");
} catch (error) {
console.error("❌ 할당된 화면에서 컴포넌트 시스템 초기화 실패:", error);
}
};
initComponents();
}, []);
2025-09-18 18:49:30 +09:00
// 편집 모달 이벤트 리스너 등록
useEffect(() => {
const handleOpenEditModal = (event: CustomEvent) => {
console.log("🎭 편집 모달 열기 이벤트 수신:", event.detail);
setEditModalConfig({
screenId: event.detail.screenId,
modalSize: event.detail.modalSize,
editData: event.detail.editData,
onSave: event.detail.onSave,
modalTitle: event.detail.modalTitle,
modalDescription: event.detail.modalDescription,
2025-09-18 18:49:30 +09:00
});
setEditModalOpen(true);
};
// @ts-expect-error - CustomEvent type
2025-09-18 18:49:30 +09:00
window.addEventListener("openEditModal", handleOpenEditModal);
return () => {
// @ts-expect-error - CustomEvent type
2025-09-18 18:49:30 +09:00
window.removeEventListener("openEditModal", handleOpenEditModal);
};
}, []);
2025-09-01 18:42:59 +09:00
useEffect(() => {
const loadScreen = async () => {
try {
setLoading(true);
setError(null);
// 화면 정보 로드
const screenData = await screenApi.getScreen(screenId);
setScreen(screenData);
// 레이아웃 로드
try {
const layoutData = await screenApi.getLayout(screenId);
setLayout(layoutData);
} catch (layoutError) {
console.warn("레이아웃 로드 실패, 빈 레이아웃 사용:", layoutError);
setLayout({
screenId,
2025-09-01 18:42:59 +09:00
components: [],
gridSettings: {
columns: 12,
gap: 16,
padding: 16,
enabled: true,
size: 8,
color: "#e0e0e0",
opacity: 0.5,
snapToGrid: true,
},
2025-09-01 18:42:59 +09:00
});
}
} catch (error) {
console.error("화면 로드 실패:", error);
setError("화면을 불러오는데 실패했습니다.");
toast.error("화면을 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
};
if (screenId) {
loadScreen();
}
}, [screenId]);
if (loading) {
return (
2025-09-29 17:21:47 +09:00
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br from-gray-50 to-slate-100">
2025-10-14 11:48:04 +09:00
<div className="rounded-xl border border-gray-200/60 bg-white p-8 text-center shadow-lg">
2025-09-29 17:21:47 +09:00
<Loader2 className="mx-auto h-10 w-10 animate-spin text-blue-600" />
2025-10-14 11:48:04 +09:00
<p className="mt-4 font-medium text-gray-700"> ...</p>
2025-09-01 18:42:59 +09:00
</div>
</div>
);
}
if (error || !screen) {
return (
2025-09-29 17:21:47 +09:00
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br from-gray-50 to-slate-100">
2025-10-14 11:48:04 +09:00
<div className="max-w-md rounded-xl border border-gray-200/60 bg-white p-8 text-center shadow-lg">
2025-09-29 17:21:47 +09:00
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-red-100 to-orange-100 shadow-sm">
<span className="text-3xl"></span>
2025-09-01 18:42:59 +09:00
</div>
2025-09-29 17:21:47 +09:00
<h2 className="mb-3 text-xl font-bold text-gray-900"> </h2>
2025-10-14 11:48:04 +09:00
<p className="mb-6 leading-relaxed text-gray-600">{error || "요청하신 화면이 존재하지 않습니다."}</p>
2025-09-29 17:21:47 +09:00
<Button onClick={() => router.back()} variant="outline" className="rounded-lg">
2025-09-01 18:42:59 +09:00
</Button>
</div>
</div>
);
}
2025-09-04 17:01:07 +09:00
// 화면 해상도 정보가 있으면 해당 크기로, 없으면 기본 크기 사용
const screenWidth = layout?.screenResolution?.width || 1200;
2025-10-22 17:19:47 +09:00
const screenHeight = layout?.screenResolution?.height || 800;
2025-09-04 17:01:07 +09:00
2025-09-01 18:42:59 +09:00
return (
2025-10-22 17:19:47 +09:00
<div className="bg-background h-full w-full">
{/* 절대 위치 기반 렌더링 */}
{layout && layout.components.length > 0 ? (
<div
className="bg-background relative mx-auto"
style={{
width: screenWidth,
minHeight: screenHeight,
}}
>
{/* 최상위 컴포넌트들 렌더링 */}
{layout.components
.filter((component) => !component.parentId)
.map((component) => (
<RealtimePreview
key={component.id}
component={component}
isSelected={false}
isDesignMode={false}
onClick={() => {}}
>
{/* 자식 컴포넌트들 */}
{(component.type === "group" || component.type === "container" || component.type === "area") &&
layout.components
.filter((child) => child.parentId === component.id)
.map((child) => {
// 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정
const relativeChildComponent = {
...child,
position: {
x: child.position.x - component.position.x,
y: child.position.y - component.position.y,
z: child.position.z || 1,
},
};
return (
<RealtimePreview
key={child.id}
component={relativeChildComponent}
isSelected={false}
isDesignMode={false}
onClick={() => {}}
/>
);
})}
</RealtimePreview>
))}
</div>
) : (
// 빈 화면일 때
<div className="bg-background flex items-center justify-center" style={{ minHeight: screenHeight }}>
<div className="text-center">
<div className="bg-muted mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full shadow-sm">
<span className="text-2xl">📄</span>
2025-09-01 18:42:59 +09:00
</div>
2025-10-22 17:19:47 +09:00
<h2 className="text-foreground mb-2 text-xl font-semibold"> </h2>
<p className="text-muted-foreground"> .</p>
2025-09-01 18:42:59 +09:00
</div>
2025-10-22 17:19:47 +09:00
</div>
)}
2025-09-18 18:49:30 +09:00
{/* 편집 모달 */}
<EditModal
isOpen={editModalOpen}
onClose={() => {
setEditModalOpen(false);
setEditModalConfig({});
}}
screenId={editModalConfig.screenId}
modalSize={editModalConfig.modalSize}
editData={editModalConfig.editData}
onSave={editModalConfig.onSave}
modalTitle={editModalConfig.modalTitle}
modalDescription={editModalConfig.modalDescription}
2025-09-18 18:49:30 +09:00
onDataChange={(changedFormData) => {
console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData);
// 변경된 데이터를 메인 폼에 반영
setFormData((prev) => {
const updatedFormData = {
...prev,
...changedFormData, // 변경된 필드들만 업데이트
};
console.log("📊 메인 폼 데이터 업데이트:", updatedFormData);
return updatedFormData;
});
}}
/>
</div>
2025-09-01 18:42:59 +09:00
);
}