230 lines
9.2 KiB
TypeScript
230 lines
9.2 KiB
TypeScript
"use client";
|
||
|
||
import { 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 { InteractiveScreenViewer } from "@/components/screen/InteractiveScreenViewer";
|
||
import { useRouter } from "next/navigation";
|
||
import { toast } from "sonner";
|
||
|
||
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, any>>({});
|
||
|
||
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({
|
||
components: [],
|
||
gridSettings: { columns: 12, gap: 16, padding: 16 },
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error("화면 로드 실패:", error);
|
||
setError("화면을 불러오는데 실패했습니다.");
|
||
toast.error("화면을 불러오는데 실패했습니다.");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (screenId) {
|
||
loadScreen();
|
||
}
|
||
}, [screenId]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex h-screen w-screen items-center justify-center bg-white">
|
||
<div className="text-center">
|
||
<Loader2 className="mx-auto h-8 w-8 animate-spin text-blue-600" />
|
||
<p className="mt-2 text-gray-600">화면을 불러오는 중...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error || !screen) {
|
||
return (
|
||
<div className="flex h-screen w-screen items-center justify-center bg-white">
|
||
<div className="text-center">
|
||
<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-semibold text-gray-900">화면을 찾을 수 없습니다</h2>
|
||
<p className="mb-4 text-gray-600">{error || "요청하신 화면이 존재하지 않습니다."}</p>
|
||
<Button onClick={() => router.back()} variant="outline">
|
||
이전으로 돌아가기
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="h-screen w-screen bg-white">
|
||
{layout && layout.components.length > 0 ? (
|
||
// 캔버스 컴포넌트들만 표시 - 전체 화면 사용
|
||
<div className="relative h-full w-full">
|
||
{layout.components
|
||
.filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함)
|
||
.map((component) => {
|
||
// 그룹 컴포넌트인 경우 특별 처리
|
||
if (component.type === "group") {
|
||
const groupChildren = layout.components.filter((child) => child.parentId === component.id);
|
||
|
||
return (
|
||
<div
|
||
key={component.id}
|
||
style={{
|
||
position: "absolute",
|
||
left: `${component.position.x}px`,
|
||
top: `${component.position.y}px`,
|
||
width: `${component.size.width}px`,
|
||
height: `${component.size.height}px`,
|
||
zIndex: component.position.z || 1,
|
||
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.1)",
|
||
border: (component as any).border || "2px dashed #3b82f6",
|
||
borderRadius: (component as any).borderRadius || "8px",
|
||
padding: "16px",
|
||
}}
|
||
>
|
||
{/* 그룹 제목 */}
|
||
{(component as any).title && (
|
||
<div className="mb-2 text-sm font-medium text-blue-700">{(component as any).title}</div>
|
||
)}
|
||
|
||
{/* 그룹 내 자식 컴포넌트들 렌더링 */}
|
||
{groupChildren.map((child) => (
|
||
<div
|
||
key={child.id}
|
||
style={{
|
||
position: "absolute",
|
||
left: `${child.position.x}px`,
|
||
top: `${child.position.y}px`,
|
||
width: `${child.size.width}px`,
|
||
height: `${child.size.height}px`,
|
||
zIndex: child.position.z || 1,
|
||
}}
|
||
>
|
||
<InteractiveScreenViewer
|
||
component={child}
|
||
allComponents={layout.components}
|
||
formData={formData}
|
||
onFormDataChange={(fieldName, value) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
[fieldName]: value,
|
||
}));
|
||
}}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 라벨 표시 여부 계산
|
||
const templateTypes = ["datatable"];
|
||
const shouldShowLabel =
|
||
component.style?.labelDisplay !== false &&
|
||
(component.label || component.style?.labelText) &&
|
||
!templateTypes.includes(component.type);
|
||
|
||
const labelText = component.style?.labelText || component.label || "";
|
||
const labelStyle = {
|
||
fontSize: component.style?.labelFontSize || "14px",
|
||
color: component.style?.labelColor || "#374151",
|
||
fontWeight: component.style?.labelFontWeight || "500",
|
||
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
||
padding: component.style?.labelPadding || "0",
|
||
borderRadius: component.style?.labelBorderRadius || "0",
|
||
marginBottom: component.style?.labelMarginBottom || "4px",
|
||
};
|
||
|
||
// 일반 컴포넌트 렌더링
|
||
return (
|
||
<div key={component.id}>
|
||
{/* 라벨을 외부에 별도로 렌더링 */}
|
||
{shouldShowLabel && (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: `${component.position.x}px`,
|
||
top: `${component.position.y - 25}px`, // 컴포넌트 위쪽에 라벨 배치
|
||
zIndex: (component.position.z || 1) + 1,
|
||
...labelStyle,
|
||
}}
|
||
>
|
||
{labelText}
|
||
{component.required && <span style={{ color: "#f97316", marginLeft: "2px" }}>*</span>}
|
||
</div>
|
||
)}
|
||
|
||
{/* 실제 컴포넌트 */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: `${component.position.x}px`,
|
||
top: `${component.position.y}px`,
|
||
width: component.style?.width || `${component.size.width}px`,
|
||
height: component.style?.height || `${component.size.height}px`,
|
||
zIndex: component.position.z || 1,
|
||
}}
|
||
>
|
||
<InteractiveScreenViewer
|
||
component={component}
|
||
allComponents={layout.components}
|
||
formData={formData}
|
||
onFormDataChange={(fieldName, value) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
[fieldName]: value,
|
||
}));
|
||
}}
|
||
hideLabel={true} // 라벨 숨김 플래그 전달
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
// 빈 화면일 때도 깔끔하게 표시
|
||
<div className="flex h-full items-center justify-center bg-gray-50">
|
||
<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>
|
||
);
|
||
}
|