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";
|
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";
|
|
|
|
|
|
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-23 15:06:00 +09:00
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
|
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
2025-09-01 18:42:59 +09:00
|
|
|
|
|
2025-10-23 13:15:52 +09:00
|
|
|
|
// 테이블에서 선택된 행 데이터 (버튼 액션에 전달)
|
|
|
|
|
|
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
|
|
|
|
|
|
|
2025-10-23 17:26:14 +09:00
|
|
|
|
// 플로우에서 선택된 데이터 (버튼 액션에 전달)
|
|
|
|
|
|
const [flowSelectedData, setFlowSelectedData] = useState<any[]>([]);
|
|
|
|
|
|
const [flowSelectedStepId, setFlowSelectedStepId] = useState<number | null>(null);
|
|
|
|
|
|
|
2025-10-23 13:15:52 +09:00
|
|
|
|
// 테이블 새로고침을 위한 키 (값이 변경되면 테이블이 리렌더링됨)
|
|
|
|
|
|
const [tableRefreshKey, setTableRefreshKey] = useState(0);
|
|
|
|
|
|
|
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";
|
2025-10-17 15:31:23 +09:00
|
|
|
|
editData?: Record<string, unknown>;
|
2025-09-18 18:49:30 +09:00
|
|
|
|
onSave?: () => void;
|
2025-10-01 17:41:30 +09:00
|
|
|
|
modalTitle?: string;
|
|
|
|
|
|
modalDescription?: string;
|
2025-09-18 18:49:30 +09:00
|
|
|
|
}>({});
|
|
|
|
|
|
|
2025-10-23 16:50:41 +09:00
|
|
|
|
// 자동 스케일 조정 (사용자 화면 크기에 맞춤)
|
|
|
|
|
|
const [scale, setScale] = useState(1);
|
|
|
|
|
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
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,
|
2025-10-01 17:41:30 +09:00
|
|
|
|
modalTitle: event.detail.modalTitle,
|
|
|
|
|
|
modalDescription: event.detail.modalDescription,
|
2025-09-18 18:49:30 +09:00
|
|
|
|
});
|
|
|
|
|
|
setEditModalOpen(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
|
// @ts-expect-error - CustomEvent type
|
2025-09-18 18:49:30 +09:00
|
|
|
|
window.addEventListener("openEditModal", handleOpenEditModal);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
2025-10-17 15:31:23 +09:00
|
|
|
|
// @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({
|
2025-10-17 15:31:23 +09:00
|
|
|
|
screenId,
|
2025-09-01 18:42:59 +09:00
|
|
|
|
components: [],
|
2025-10-17 15:31:23 +09:00
|
|
|
|
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]);
|
|
|
|
|
|
|
2025-10-23 16:50:41 +09:00
|
|
|
|
// 자동 스케일 조정 useEffect (항상 화면에 꽉 차게)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const updateScale = () => {
|
|
|
|
|
|
if (containerRef.current && layout) {
|
|
|
|
|
|
const screenWidth = layout?.screenResolution?.width || 1200;
|
|
|
|
|
|
const containerWidth = containerRef.current.offsetWidth;
|
|
|
|
|
|
const availableWidth = containerWidth - 32; // 좌우 패딩 16px * 2
|
|
|
|
|
|
|
|
|
|
|
|
// 항상 화면에 맞춰서 스케일 조정 (늘리거나 줄임)
|
|
|
|
|
|
const newScale = availableWidth / screenWidth;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("📏 스케일 계산 (화면 꽉 차게):", {
|
|
|
|
|
|
screenWidth,
|
|
|
|
|
|
containerWidth,
|
|
|
|
|
|
availableWidth,
|
|
|
|
|
|
scale: newScale,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setScale(newScale);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 초기 측정 (DOM이 완전히 렌더링된 후)
|
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
|
updateScale();
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("resize", updateScale);
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
|
window.removeEventListener("resize", updateScale);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [layout]);
|
|
|
|
|
|
|
2025-09-01 18:42:59 +09:00
|
|
|
|
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-23 16:50:41 +09:00
|
|
|
|
<div ref={containerRef} className="bg-background flex h-full w-full flex-col overflow-hidden">
|
2025-10-22 17:19:47 +09:00
|
|
|
|
{/* 절대 위치 기반 렌더링 */}
|
|
|
|
|
|
{layout && layout.components.length > 0 ? (
|
|
|
|
|
|
<div
|
2025-10-23 16:50:41 +09:00
|
|
|
|
className="bg-background relative flex-1"
|
2025-10-22 17:19:47 +09:00
|
|
|
|
style={{
|
|
|
|
|
|
width: screenWidth,
|
2025-10-23 16:50:41 +09:00
|
|
|
|
height: "100%",
|
|
|
|
|
|
transform: `scale(${scale})`,
|
|
|
|
|
|
transformOrigin: "top left",
|
|
|
|
|
|
overflow: "hidden",
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
flexDirection: "column",
|
2025-10-22 17:19:47 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 최상위 컴포넌트들 렌더링 */}
|
|
|
|
|
|
{layout.components
|
|
|
|
|
|
.filter((component) => !component.parentId)
|
|
|
|
|
|
.map((component) => (
|
|
|
|
|
|
<RealtimePreview
|
|
|
|
|
|
key={component.id}
|
|
|
|
|
|
component={component}
|
|
|
|
|
|
isSelected={false}
|
|
|
|
|
|
isDesignMode={false}
|
|
|
|
|
|
onClick={() => {}}
|
2025-10-23 13:15:52 +09:00
|
|
|
|
screenId={screenId}
|
|
|
|
|
|
tableName={screen?.tableName}
|
|
|
|
|
|
selectedRowsData={selectedRowsData}
|
|
|
|
|
|
onSelectedRowsChange={(_, selectedData) => {
|
|
|
|
|
|
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
|
|
|
|
|
|
setSelectedRowsData(selectedData);
|
|
|
|
|
|
}}
|
2025-10-23 17:26:14 +09:00
|
|
|
|
flowSelectedData={flowSelectedData}
|
|
|
|
|
|
flowSelectedStepId={flowSelectedStepId}
|
|
|
|
|
|
onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => {
|
|
|
|
|
|
console.log("🔍 [page.tsx] 플로우 선택된 데이터 받음:", {
|
|
|
|
|
|
dataCount: selectedData.length,
|
|
|
|
|
|
selectedData,
|
|
|
|
|
|
stepId,
|
|
|
|
|
|
});
|
|
|
|
|
|
setFlowSelectedData(selectedData);
|
|
|
|
|
|
setFlowSelectedStepId(stepId);
|
|
|
|
|
|
console.log("🔍 [page.tsx] 상태 업데이트 완료");
|
|
|
|
|
|
}}
|
2025-10-23 13:15:52 +09:00
|
|
|
|
refreshKey={tableRefreshKey}
|
|
|
|
|
|
onRefresh={() => {
|
|
|
|
|
|
console.log("🔄 테이블 새로고침 요청됨");
|
|
|
|
|
|
setTableRefreshKey((prev) => prev + 1);
|
|
|
|
|
|
setSelectedRowsData([]); // 선택 해제
|
|
|
|
|
|
}}
|
2025-10-23 15:06:00 +09:00
|
|
|
|
formData={formData}
|
|
|
|
|
|
onFormDataChange={(fieldName, value) => {
|
|
|
|
|
|
console.log("📝 폼 데이터 변경:", fieldName, "=", value);
|
|
|
|
|
|
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
|
|
|
|
|
}}
|
2025-10-22 17:19:47 +09:00
|
|
|
|
>
|
|
|
|
|
|
{/* 자식 컴포넌트들 */}
|
|
|
|
|
|
{(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={() => {}}
|
2025-10-23 13:15:52 +09:00
|
|
|
|
screenId={screenId}
|
|
|
|
|
|
tableName={screen?.tableName}
|
|
|
|
|
|
selectedRowsData={selectedRowsData}
|
|
|
|
|
|
onSelectedRowsChange={(_, selectedData) => {
|
|
|
|
|
|
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
|
|
|
|
|
|
setSelectedRowsData(selectedData);
|
|
|
|
|
|
}}
|
|
|
|
|
|
refreshKey={tableRefreshKey}
|
|
|
|
|
|
onRefresh={() => {
|
|
|
|
|
|
console.log("🔄 테이블 새로고침 요청됨 (자식)");
|
|
|
|
|
|
setTableRefreshKey((prev) => prev + 1);
|
|
|
|
|
|
setSelectedRowsData([]); // 선택 해제
|
|
|
|
|
|
}}
|
2025-10-23 15:06:00 +09:00
|
|
|
|
formData={formData}
|
|
|
|
|
|
onFormDataChange={(fieldName, value) => {
|
|
|
|
|
|
console.log("📝 폼 데이터 변경 (자식):", fieldName, "=", value);
|
|
|
|
|
|
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
|
|
|
|
|
}}
|
2025-10-22 17:19:47 +09:00
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</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}
|
2025-10-01 17:41:30 +09:00
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2025-09-23 15:21:50 +09:00
|
|
|
|
</div>
|
2025-09-01 18:42:59 +09:00
|
|
|
|
);
|
|
|
|
|
|
}
|