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

333 lines
14 KiB
TypeScript
Raw Normal View History

2025-09-01 18:42:59 +09:00
"use client";
import { useEffect, useState } from "react";
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";
2025-09-09 14:29:04 +09:00
import { InteractiveScreenViewer } from "@/components/screen/InteractiveScreenViewerDynamic";
2025-09-10 14:09:32 +09:00
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
import { DynamicWebTypeRenderer } from "@/lib/registry";
2025-09-01 18:42:59 +09:00
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-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);
const [formData, setFormData] = useState<Record<string, any>>({});
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-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({
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 (
2025-09-04 17:01:07 +09:00
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-white">
2025-09-01 18:42:59 +09:00
<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 (
2025-09-04 17:01:07 +09:00
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-white">
2025-09-01 18:42:59 +09:00
<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>
);
}
2025-09-04 17:01:07 +09:00
// 화면 해상도 정보가 있으면 해당 크기로, 없으면 기본 크기 사용
const screenWidth = layout?.screenResolution?.width || 1200;
const screenHeight = layout?.screenResolution?.height || 800;
2025-09-01 18:42:59 +09:00
return (
2025-09-04 17:01:07 +09:00
<div className="h-full w-full overflow-auto bg-white">
{layout && layout.components.length > 0 ? (
2025-09-04 17:01:07 +09:00
// 캔버스 컴포넌트들을 정확한 해상도로 표시
<div
2025-09-10 14:09:32 +09:00
className="relative bg-white"
2025-09-04 17:01:07 +09:00
style={{
width: `${screenWidth}px`,
height: `${screenHeight}px`,
minWidth: `${screenWidth}px`,
minHeight: `${screenHeight}px`,
2025-09-10 14:09:32 +09:00
margin: "0", // mx-auto 제거하여 사이드바 오프셋 방지
2025-09-04 17:01:07 +09:00
}}
>
{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",
}}
2025-09-01 18:42:59 +09:00
>
{/* 그룹 제목 */}
{(component as any).title && (
<div className="mb-2 text-sm font-medium text-blue-700">{(component as any).title}</div>
2025-09-01 18:42:59 +09:00
)}
{/* 그룹 내 자식 컴포넌트들 렌더링 */}
{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) => {
2025-09-12 14:24:25 +09:00
console.log("📝 폼 데이터 변경:", { fieldName, value });
setFormData((prev) => {
const newFormData = {
...prev,
[fieldName]: value,
};
console.log("📊 전체 폼 데이터:", newFormData);
return newFormData;
});
}}
screenInfo={{
id: screenId,
tableName: screen?.tableName,
2025-09-01 18:42:59 +09:00
}}
/>
</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`,
2025-09-10 14:09:32 +09:00
width: `${component.size.width}px`,
height: `${component.size.height}px`,
zIndex: component.position.z || 1,
}}
2025-09-10 14:09:32 +09:00
onMouseEnter={() => {
console.log("🎯 할당된 화면 컴포넌트:", {
id: component.id,
type: component.type,
position: component.position,
size: component.size,
styleWidth: component.style?.width,
styleHeight: component.style?.height,
finalWidth: `${component.size.width}px`,
finalHeight: `${component.size.height}px`,
});
}}
>
2025-09-10 14:09:32 +09:00
{/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */}
{component.type !== "widget" ? (
<DynamicComponentRenderer
component={component}
isInteractive={true}
formData={formData}
onFormDataChange={(fieldName, value) => {
setFormData((prev) => ({
...prev,
[fieldName]: value,
}));
}}
2025-09-12 14:24:25 +09:00
screenId={screenId}
tableName={screen?.tableName}
onRefresh={() => {
console.log("화면 새로고침 요청");
}}
onClose={() => {
console.log("화면 닫기 요청");
}}
2025-09-10 14:09:32 +09:00
/>
) : (
<DynamicWebTypeRenderer
webType={component.webType || "text"}
config={component.webTypeConfig}
2025-09-18 10:05:50 +09:00
props={{
component: component,
value: formData[component.columnName || component.id] || "",
onChange: (value: any) => {
const fieldName = component.columnName || component.id;
setFormData((prev) => ({
...prev,
[fieldName]: value,
}));
},
onFormDataChange: (fieldName, value) => {
console.log(`🎯 page.tsx onFormDataChange 호출: ${fieldName} = "${value}"`);
console.log(`📋 현재 formData:`, formData);
setFormData((prev) => {
const newFormData = {
...prev,
[fieldName]: value,
};
console.log(`📝 업데이트된 formData:`, newFormData);
return newFormData;
});
},
isInteractive: true,
formData: formData,
readonly: component.readonly,
required: component.required,
placeholder: component.placeholder,
className: "w-full h-full",
2025-09-10 14:09:32 +09:00
}}
/>
)}
</div>
2025-09-01 18:42:59 +09:00
</div>
);
})}
</div>
) : (
// 빈 화면일 때도 깔끔하게 표시
2025-09-04 17:01:07 +09:00
<div
className="mx-auto flex items-center justify-center bg-gray-50"
style={{
width: `${screenWidth}px`,
height: `${screenHeight}px`,
minWidth: `${screenWidth}px`,
minHeight: `${screenHeight}px`,
}}
>
<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>
2025-09-01 18:42:59 +09:00
</div>
<h2 className="mb-2 text-xl font-semibold text-gray-900"> </h2>
<p className="text-gray-600"> .</p>
2025-09-01 18:42:59 +09:00
</div>
</div>
)}
2025-09-01 18:42:59 +09:00
</div>
);
}