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";
|
2025-09-03 11:55:38 +09:00
|
|
|
|
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-18 18:49:30 +09:00
|
|
|
|
import { EditModal } from "@/components/screen/EditModal";
|
2025-09-29 13:29:03 +09:00
|
|
|
|
import { isFileComponent, getComponentWebType } from "@/lib/utils/componentTypeUtils";
|
2025-09-23 15:25:55 +09:00
|
|
|
|
// import { ResponsiveScreenContainer } from "@/components/screen/ResponsiveScreenContainer"; // 컨테이너 제거
|
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-18 18:49:30 +09:00
|
|
|
|
// 테이블 선택된 행 상태 (화면 레벨에서 관리)
|
|
|
|
|
|
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
|
|
|
|
|
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블 새로고침을 위한 키 상태
|
|
|
|
|
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
|
|
|
|
|
|
|
|
|
|
// 편집 모달 상태
|
|
|
|
|
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
|
|
|
|
|
const [editModalConfig, setEditModalConfig] = useState<{
|
|
|
|
|
|
screenId?: number;
|
|
|
|
|
|
modalSize?: "sm" | "md" | "lg" | "xl" | "full";
|
|
|
|
|
|
editData?: any;
|
|
|
|
|
|
onSave?: () => void;
|
|
|
|
|
|
}>({});
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
});
|
|
|
|
|
|
setEditModalOpen(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
window.addEventListener("openEditModal", handleOpenEditModal);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
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({
|
|
|
|
|
|
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-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">
|
|
|
|
|
|
<div className="text-center bg-white rounded-xl border border-gray-200/60 shadow-lg p-8">
|
|
|
|
|
|
<Loader2 className="mx-auto h-10 w-10 animate-spin text-blue-600" />
|
|
|
|
|
|
<p className="mt-4 text-gray-700 font-medium">화면을 불러오는 중...</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">
|
|
|
|
|
|
<div className="text-center bg-white rounded-xl border border-gray-200/60 shadow-lg p-8 max-w-md">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<p className="mb-6 text-gray-600 leading-relaxed">{error || "요청하신 화면이 존재하지 않습니다."}</p>
|
|
|
|
|
|
<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;
|
|
|
|
|
|
const screenHeight = layout?.screenResolution?.height || 800;
|
|
|
|
|
|
|
2025-09-01 18:42:59 +09:00
|
|
|
|
return (
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="h-full w-full overflow-auto bg-gradient-to-br from-gray-50 to-slate-100 p-10">
|
2025-09-03 11:55:38 +09:00
|
|
|
|
{layout && layout.components.length > 0 ? (
|
2025-09-04 17:01:07 +09:00
|
|
|
|
// 캔버스 컴포넌트들을 정확한 해상도로 표시
|
|
|
|
|
|
<div
|
2025-09-29 17:21:47 +09:00
|
|
|
|
className="relative bg-white rounded-xl border border-gray-200/60 shadow-lg shadow-gray-900/5 mx-auto"
|
2025-09-04 17:01:07 +09:00
|
|
|
|
style={{
|
|
|
|
|
|
width: `${screenWidth}px`,
|
|
|
|
|
|
height: `${screenHeight}px`,
|
|
|
|
|
|
minWidth: `${screenWidth}px`,
|
|
|
|
|
|
minHeight: `${screenHeight}px`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-23 15:30:02 +09:00
|
|
|
|
{layout.components
|
|
|
|
|
|
.filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함)
|
|
|
|
|
|
.map((component) => {
|
|
|
|
|
|
// 그룹 컴포넌트인 경우 특별 처리
|
|
|
|
|
|
if (component.type === "group") {
|
|
|
|
|
|
const groupChildren = layout.components.filter((child) => child.parentId === component.id);
|
2025-09-03 11:55:38 +09:00
|
|
|
|
|
2025-09-23 15:25:55 +09:00
|
|
|
|
return (
|
2025-09-23 15:30:02 +09:00
|
|
|
|
<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,
|
2025-09-29 17:21:47 +09:00
|
|
|
|
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.05)",
|
|
|
|
|
|
border: (component as any).border || "1px solid rgba(59, 130, 246, 0.2)",
|
|
|
|
|
|
borderRadius: (component as any).borderRadius || "12px",
|
|
|
|
|
|
padding: "20px",
|
|
|
|
|
|
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
|
2025-09-23 15:30:02 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 그룹 제목 */}
|
|
|
|
|
|
{(component as any).title && (
|
2025-09-29 17:21:47 +09:00
|
|
|
|
<div className="mb-3 text-sm font-semibold text-blue-700 bg-blue-50 px-3 py-1 rounded-lg inline-block">{(component as any).title}</div>
|
2025-09-23 15:30:02 +09:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 그룹 내 자식 컴포넌트들 렌더링 */}
|
|
|
|
|
|
{groupChildren.map((child) => (
|
2025-09-03 11:55:38 +09:00
|
|
|
|
<div
|
2025-09-23 15:30:02 +09:00
|
|
|
|
key={child.id}
|
2025-09-03 11:55:38 +09:00
|
|
|
|
style={{
|
|
|
|
|
|
position: "absolute",
|
2025-09-23 15:30:02 +09:00
|
|
|
|
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,
|
2025-09-03 11:55:38 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-23 15:30:02 +09:00
|
|
|
|
<InteractiveScreenViewer
|
|
|
|
|
|
component={child}
|
|
|
|
|
|
allComponents={layout.components}
|
|
|
|
|
|
formData={formData}
|
|
|
|
|
|
onFormDataChange={(fieldName, value) => {
|
|
|
|
|
|
console.log("📝 폼 데이터 변경:", { fieldName, value });
|
|
|
|
|
|
setFormData((prev) => {
|
|
|
|
|
|
const newFormData = {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[fieldName]: value,
|
|
|
|
|
|
};
|
|
|
|
|
|
console.log("📊 전체 폼 데이터:", newFormData);
|
|
|
|
|
|
return newFormData;
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
screenInfo={{
|
|
|
|
|
|
id: screenId,
|
|
|
|
|
|
tableName: screen?.tableName,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2025-09-03 11:55:38 +09:00
|
|
|
|
</div>
|
2025-09-23 15:30:02 +09:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-09-04 11:33:52 +09:00
|
|
|
|
|
2025-09-23 15:30:02 +09:00
|
|
|
|
// 라벨 표시 여부 계산
|
|
|
|
|
|
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",
|
2025-10-02 14:34:15 +09:00
|
|
|
|
color: component.style?.labelColor || "#212121",
|
2025-09-23 15:30:02 +09:00
|
|
|
|
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 && (
|
2025-09-04 11:33:52 +09:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: "absolute",
|
|
|
|
|
|
left: `${component.position.x}px`,
|
2025-09-23 15:30:02 +09:00
|
|
|
|
top: `${component.position.y - 25}px`, // 컴포넌트 위쪽에 라벨 배치
|
|
|
|
|
|
zIndex: (component.position.z || 1) + 1,
|
|
|
|
|
|
...labelStyle,
|
2025-09-04 11:33:52 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-23 15:30:02 +09:00
|
|
|
|
{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.size.width}px`,
|
|
|
|
|
|
height: `${component.size.height}px`,
|
|
|
|
|
|
zIndex: component.position.z || 1,
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseEnter={() => {
|
2025-09-25 18:54:25 +09:00
|
|
|
|
// 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-23 15:30:02 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */}
|
|
|
|
|
|
{component.type !== "widget" ? (
|
|
|
|
|
|
<DynamicComponentRenderer
|
|
|
|
|
|
component={component}
|
|
|
|
|
|
isInteractive={true}
|
|
|
|
|
|
formData={formData}
|
|
|
|
|
|
onFormDataChange={(fieldName, value) => {
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[fieldName]: value,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}}
|
|
|
|
|
|
screenId={screenId}
|
|
|
|
|
|
tableName={screen?.tableName}
|
|
|
|
|
|
onRefresh={() => {
|
|
|
|
|
|
console.log("화면 새로고침 요청");
|
|
|
|
|
|
// 테이블 컴포넌트 강제 새로고침을 위한 키 업데이트
|
|
|
|
|
|
setRefreshKey((prev) => prev + 1);
|
|
|
|
|
|
// 선택된 행 상태도 초기화
|
|
|
|
|
|
setSelectedRows([]);
|
|
|
|
|
|
setSelectedRowsData([]);
|
|
|
|
|
|
}}
|
|
|
|
|
|
onClose={() => {
|
|
|
|
|
|
console.log("화면 닫기 요청");
|
|
|
|
|
|
}}
|
|
|
|
|
|
// 테이블 선택된 행 정보 전달
|
|
|
|
|
|
selectedRows={selectedRows}
|
|
|
|
|
|
selectedRowsData={selectedRowsData}
|
|
|
|
|
|
onSelectedRowsChange={(newSelectedRows, newSelectedRowsData) => {
|
|
|
|
|
|
setSelectedRows(newSelectedRows);
|
|
|
|
|
|
setSelectedRowsData(newSelectedRowsData);
|
|
|
|
|
|
}}
|
|
|
|
|
|
// 테이블 새로고침 키 전달
|
|
|
|
|
|
refreshKey={refreshKey}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<DynamicWebTypeRenderer
|
2025-09-29 13:29:03 +09:00
|
|
|
|
webType={(() => {
|
|
|
|
|
|
// 유틸리티 함수로 파일 컴포넌트 감지
|
|
|
|
|
|
if (isFileComponent(component)) {
|
|
|
|
|
|
console.log(`🎯 page.tsx - 파일 컴포넌트 감지 → webType: "file"`, {
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
componentType: component.type,
|
|
|
|
|
|
originalWebType: component.webType
|
|
|
|
|
|
});
|
|
|
|
|
|
return "file";
|
|
|
|
|
|
}
|
|
|
|
|
|
// 다른 컴포넌트는 유틸리티 함수로 webType 결정
|
|
|
|
|
|
return getComponentWebType(component) || "text";
|
|
|
|
|
|
})()}
|
2025-09-23 15:30:02 +09:00
|
|
|
|
config={component.webTypeConfig}
|
|
|
|
|
|
props={{
|
|
|
|
|
|
component: component,
|
|
|
|
|
|
value: formData[component.columnName || component.id] || "",
|
|
|
|
|
|
onChange: (value: any) => {
|
|
|
|
|
|
const fieldName = component.columnName || component.id;
|
2025-09-18 10:05:50 +09:00
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[fieldName]: value,
|
|
|
|
|
|
}));
|
2025-09-23 15:30:02 +09:00
|
|
|
|
},
|
|
|
|
|
|
onFormDataChange: (fieldName, value) => {
|
|
|
|
|
|
console.log(`🎯 page.tsx onFormDataChange 호출: ${fieldName} = "${value}"`);
|
2025-09-29 17:24:06 +09:00
|
|
|
|
console.log("📋 현재 formData:", formData);
|
2025-09-23 15:30:02 +09:00
|
|
|
|
setFormData((prev) => {
|
|
|
|
|
|
const newFormData = {
|
2025-09-18 10:05:50 +09:00
|
|
|
|
...prev,
|
|
|
|
|
|
[fieldName]: value,
|
2025-09-23 15:30:02 +09:00
|
|
|
|
};
|
2025-09-29 17:24:06 +09:00
|
|
|
|
console.log("📝 업데이트된 formData:", newFormData);
|
2025-09-23 15:30:02 +09:00
|
|
|
|
return newFormData;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
isInteractive: true,
|
|
|
|
|
|
formData: formData,
|
|
|
|
|
|
readonly: component.readonly,
|
|
|
|
|
|
required: component.required,
|
|
|
|
|
|
placeholder: component.placeholder,
|
|
|
|
|
|
className: "w-full h-full",
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-09-04 11:33:52 +09:00
|
|
|
|
</div>
|
2025-09-23 15:30:02 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
2025-09-03 11:55:38 +09:00
|
|
|
|
) : (
|
|
|
|
|
|
// 빈 화면일 때도 깔끔하게 표시
|
2025-09-04 17:01:07 +09:00
|
|
|
|
<div
|
2025-10-02 14:34:15 +09:00
|
|
|
|
className="mx-auto flex items-center justify-center bg-white rounded-xl border border-gray-200/60 shadow-lg shadow-gray-900/5"
|
2025-09-04 17:01:07 +09:00
|
|
|
|
style={{
|
|
|
|
|
|
width: `${screenWidth}px`,
|
|
|
|
|
|
height: `${screenHeight}px`,
|
|
|
|
|
|
minWidth: `${screenWidth}px`,
|
|
|
|
|
|
minHeight: `${screenHeight}px`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-03 11:55:38 +09:00
|
|
|
|
<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>
|
2025-09-03 11:55:38 +09:00
|
|
|
|
<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>
|
2025-09-03 11:55:38 +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}
|
|
|
|
|
|
onDataChange={(changedFormData) => {
|
|
|
|
|
|
console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData);
|
|
|
|
|
|
// 변경된 데이터를 메인 폼에 반영
|
|
|
|
|
|
setFormData((prev) => {
|
|
|
|
|
|
const updatedFormData = {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
...changedFormData, // 변경된 필드들만 업데이트
|
|
|
|
|
|
};
|
|
|
|
|
|
console.log("📊 메인 폼 데이터 업데이트:", updatedFormData);
|
|
|
|
|
|
return updatedFormData;
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2025-09-23 15:21:50 +09:00
|
|
|
|
</div>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
);
|
|
|
|
|
|
}
|