238 lines
8.5 KiB
TypeScript
238 lines
8.5 KiB
TypeScript
"use client";
|
||
|
||
import React, { useEffect, useState } from "react";
|
||
import { useParams } from "next/navigation";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Loader2 } from "lucide-react";
|
||
import { screenApi } from "@/lib/api/screen";
|
||
import { ScreenDefinition, LayoutData } from "@/types/screen";
|
||
import { useRouter } from "next/navigation";
|
||
import { toast } from "sonner";
|
||
import { initializeComponents } from "@/lib/registry/components";
|
||
import { EditModal } from "@/components/screen/EditModal";
|
||
import { ResponsiveLayoutEngine } from "@/components/screen/ResponsiveLayoutEngine";
|
||
import { useBreakpoint } from "@/hooks/useBreakpoint";
|
||
|
||
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);
|
||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||
|
||
// 화면 너비에 따라 Y좌표 유지 여부 결정
|
||
const [preserveYPosition, setPreserveYPosition] = useState(true);
|
||
|
||
const breakpoint = useBreakpoint();
|
||
|
||
// 편집 모달 상태
|
||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||
const [editModalConfig, setEditModalConfig] = useState<{
|
||
screenId?: number;
|
||
modalSize?: "sm" | "md" | "lg" | "xl" | "full";
|
||
editData?: Record<string, unknown>;
|
||
onSave?: () => void;
|
||
modalTitle?: string;
|
||
modalDescription?: string;
|
||
}>({});
|
||
|
||
useEffect(() => {
|
||
const initComponents = async () => {
|
||
try {
|
||
console.log("🚀 할당된 화면에서 컴포넌트 시스템 초기화 시작...");
|
||
await initializeComponents();
|
||
console.log("✅ 할당된 화면에서 컴포넌트 시스템 초기화 완료");
|
||
} catch (error) {
|
||
console.error("❌ 할당된 화면에서 컴포넌트 시스템 초기화 실패:", error);
|
||
}
|
||
};
|
||
|
||
initComponents();
|
||
}, []);
|
||
|
||
// 편집 모달 이벤트 리스너 등록
|
||
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,
|
||
});
|
||
setEditModalOpen(true);
|
||
};
|
||
|
||
// @ts-expect-error - CustomEvent type
|
||
window.addEventListener("openEditModal", handleOpenEditModal);
|
||
|
||
return () => {
|
||
// @ts-expect-error - CustomEvent type
|
||
window.removeEventListener("openEditModal", handleOpenEditModal);
|
||
};
|
||
}, []);
|
||
|
||
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,
|
||
components: [],
|
||
gridSettings: {
|
||
columns: 12,
|
||
gap: 16,
|
||
padding: 16,
|
||
enabled: true,
|
||
size: 8,
|
||
color: "#e0e0e0",
|
||
opacity: 0.5,
|
||
snapToGrid: true,
|
||
},
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error("화면 로드 실패:", error);
|
||
setError("화면을 불러오는데 실패했습니다.");
|
||
toast.error("화면을 불러오는데 실패했습니다.");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (screenId) {
|
||
loadScreen();
|
||
}
|
||
}, [screenId]);
|
||
|
||
// 윈도우 크기 변경 감지 - layout이 로드된 후에만 실행
|
||
useEffect(() => {
|
||
if (!layout) return;
|
||
|
||
const screenWidth = layout?.screenResolution?.width || 1200;
|
||
|
||
const handleResize = () => {
|
||
const shouldPreserve = window.innerWidth >= screenWidth - 100;
|
||
setPreserveYPosition(shouldPreserve);
|
||
};
|
||
|
||
window.addEventListener("resize", handleResize);
|
||
// 초기 값도 설정
|
||
handleResize();
|
||
|
||
return () => window.removeEventListener("resize", handleResize);
|
||
}, [layout]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br from-gray-50 to-slate-100">
|
||
<div className="rounded-xl border border-gray-200/60 bg-white p-8 text-center shadow-lg">
|
||
<Loader2 className="mx-auto h-10 w-10 animate-spin text-blue-600" />
|
||
<p className="mt-4 font-medium text-gray-700">화면을 불러오는 중...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error || !screen) {
|
||
return (
|
||
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br from-gray-50 to-slate-100">
|
||
<div className="max-w-md rounded-xl border border-gray-200/60 bg-white p-8 text-center shadow-lg">
|
||
<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>
|
||
</div>
|
||
<h2 className="mb-3 text-xl font-bold text-gray-900">화면을 찾을 수 없습니다</h2>
|
||
<p className="mb-6 leading-relaxed text-gray-600">{error || "요청하신 화면이 존재하지 않습니다."}</p>
|
||
<Button onClick={() => router.back()} variant="outline" className="rounded-lg">
|
||
이전으로 돌아가기
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 화면 해상도 정보가 있으면 해당 크기로, 없으면 기본 크기 사용
|
||
const screenWidth = layout?.screenResolution?.width || 1200;
|
||
|
||
return (
|
||
<div className="h-full w-full bg-white">
|
||
<div style={{ padding: "16px 0" }}>
|
||
{/* 항상 반응형 모드로 렌더링 */}
|
||
{layout && layout.components.length > 0 ? (
|
||
<ResponsiveLayoutEngine
|
||
components={layout?.components || []}
|
||
breakpoint={breakpoint}
|
||
containerWidth={window.innerWidth}
|
||
screenWidth={screenWidth}
|
||
preserveYPosition={preserveYPosition}
|
||
isDesignMode={false}
|
||
formData={formData}
|
||
onFormDataChange={(fieldName: string, value: unknown) => {
|
||
console.log("📝 page.tsx formData 업데이트:", fieldName, value);
|
||
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
||
}}
|
||
screenInfo={{ id: screenId, tableName: screen?.tableName }}
|
||
/>
|
||
) : (
|
||
// 빈 화면일 때
|
||
<div className="flex items-center justify-center bg-white" style={{ minHeight: "600px" }}>
|
||
<div className="text-center">
|
||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white shadow-sm">
|
||
<span className="text-2xl">📄</span>
|
||
</div>
|
||
<h2 className="mb-2 text-xl font-semibold text-gray-900">화면이 비어있습니다</h2>
|
||
<p className="text-gray-600">이 화면에는 아직 설계된 컴포넌트가 없습니다.</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 편집 모달 */}
|
||
<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}
|
||
onDataChange={(changedFormData) => {
|
||
console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData);
|
||
// 변경된 데이터를 메인 폼에 반영
|
||
setFormData((prev) => {
|
||
const updatedFormData = {
|
||
...prev,
|
||
...changedFormData, // 변경된 필드들만 업데이트
|
||
};
|
||
console.log("📊 메인 폼 데이터 업데이트:", updatedFormData);
|
||
return updatedFormData;
|
||
});
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|