"use client"; import React, { useEffect, useState, useMemo } from "react"; import { useParams, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Loader2, FileQuestion, AlertTriangle } from "lucide-react"; import { screenApi } from "@/lib/api/screen"; import { ScreenDefinition, LayoutData, ComponentData } from "@/types/screen"; import { LayerDefinition } from "@/types/screen-management"; import { useRouter } from "next/navigation"; import { showErrorToast } from "@/lib/utils/toastUtils"; import { initializeComponents } from "@/lib/registry/components"; import { EditModal } from "@/components/screen/EditModal"; import { RealtimePreview } from "@/components/screen/RealtimePreviewDynamic"; import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; import { useAuth } from "@/hooks/useAuth"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext"; import { ScreenContextProvider } from "@/contexts/ScreenContext"; import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; import { evaluateConditional } from "@/lib/utils/conditionalEvaluator"; import { ScreenMultiLangProvider } from "@/contexts/ScreenMultiLangContext"; import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter"; import { useScheduleGenerator, ScheduleConfirmDialog } from "@/lib/v2-core/services/ScheduleGeneratorService"; import { ResponsiveGridRenderer } from "@/components/screen/ResponsiveGridRenderer"; import { useTabId } from "@/contexts/TabIdContext"; import { useTabStore } from "@/stores/tabStore"; export interface ScreenViewPageProps { screenIdProp?: number; menuObjidProp?: number; } function ScreenViewPage({ screenIdProp, menuObjidProp }: ScreenViewPageProps = {}) { // 스케줄 자동 생성 서비스 활성화 const { showConfirmDialog, previewResult, handleConfirm, closeDialog, isLoading: scheduleLoading, } = useScheduleGenerator(); const params = useParams(); const searchParams = useSearchParams(); const router = useRouter(); const screenId = screenIdProp ?? parseInt(params.screenId as string); // props 우선, 없으면 URL 쿼리에서 menuObjid 가져오기 const menuObjid = menuObjidProp ?? (searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined); // URL 쿼리에서 프리뷰용 company_code 가져오기 const previewCompanyCode = searchParams.get("company_code"); // 프리뷰 모드 감지 (iframe에서 로드될 때) const isPreviewMode = searchParams.get("preview") === "true"; const { user, userName, companyCode: authCompanyCode } = useAuth(); // 프리뷰 모드에서는 URL 파라미터의 company_code 우선 사용 const companyCode = previewCompanyCode || authCompanyCode; const [screen, setScreen] = useState(null); const [layout, setLayout] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [formData, setFormData] = useState>({}); // 테이블에서 선택된 행 데이터 (버튼 액션에 전달) const [selectedRowsData, setSelectedRowsData] = useState([]); // 테이블 정렬 정보 (엑셀 다운로드용) const [tableSortBy, setTableSortBy] = useState(); const [tableSortOrder, setTableSortOrder] = useState<"asc" | "desc">("asc"); const [tableColumnOrder, setTableColumnOrder] = useState(); // 플로우에서 선택된 데이터 (버튼 액션에 전달) const [flowSelectedData, setFlowSelectedData] = useState([]); const [flowSelectedStepId, setFlowSelectedStepId] = useState(null); // 테이블 새로고침 키 const [tableRefreshKey, setTableRefreshKey] = useState(0); // 플로우 새로고침 키 const [flowRefreshKey, setFlowRefreshKey] = useState(0); // 조건부 컨테이너 높이 추적 (컴포넌트 ID → 높이) const [conditionalContainerHeights, setConditionalContainerHeights] = useState>({}); // 레이어 시스템 지원 const [conditionalLayers, setConditionalLayers] = useState([]); // 조건부 영역(Zone) 목록 const [zones, setZones] = useState([]); // 데이터 전달에 의해 강제 활성화된 레이어 ID 목록 const [forceActivatedLayerIds, setForceActivatedLayerIds] = useState([]); // 편집 모달 상태 const [editModalOpen, setEditModalOpen] = useState(false); const [editModalConfig, setEditModalConfig] = useState<{ screenId?: number; modalSize?: "sm" | "md" | "lg" | "xl" | "full"; editData?: Record; onSave?: () => void; modalTitle?: string; modalDescription?: string; }>({}); // 레이아웃 준비 완료 상태 const [layoutReady, setLayoutReady] = useState(false); const containerRef = React.useRef(null); useEffect(() => { const initComponents = async () => { try { await initializeComponents(); } catch (error) { console.error("❌ 할당된 화면에서 컴포넌트 시스템 초기화 실패:", error); } }; initComponents(); }, []); // 편집 모달 이벤트 리스너 등록 (활성 탭에서만 처리) const tabId = useTabId(); useEffect(() => { const handleOpenEditModal = (event: CustomEvent) => { const state = useTabStore.getState(); const currentActiveTabId = state[state.mode].activeTabId; if (tabId && tabId !== currentActiveTabId) return; 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); }; }, [tabId]); useEffect(() => { const loadScreen = async () => { try { setLoading(true); setLayoutReady(false); setError(null); const screenData = await screenApi.getScreen(screenId); setScreen(screenData); // 레이아웃 로드 (V2 우선, Zod 기반 기본값 병합) try { const v2Response = await screenApi.getLayoutV2(screenId); if (v2Response && isValidV2Layout(v2Response)) { const convertedLayout = convertV2ToLegacy(v2Response); if (convertedLayout) { setLayout({ ...convertedLayout, screenResolution: v2Response.screenResolution || convertedLayout.screenResolution, } as LayoutData); } else { throw new Error("V2 레이아웃 변환 실패"); } } else { const layoutData = await screenApi.getLayout(screenId); if (layoutData?.components?.length > 0) { setLayout(layoutData); } else { console.warn("[ScreenViewPage] getLayout 실패, getLayerLayout(1) fallback:", screenId); const baseLayerData = await screenApi.getLayerLayout(screenId, 1); if (baseLayerData && isValidV2Layout(baseLayerData)) { const converted = convertV2ToLegacy(baseLayerData); if (converted) { setLayout({ ...converted, screenResolution: baseLayerData.screenResolution || converted.screenResolution, } as LayoutData); } else { setLayout(layoutData); } } else { 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("화면을 불러오는데 실패했습니다."); showErrorToast("화면을 불러오는 데 실패했습니다", error, { guidance: "화면 설정을 확인하거나 잠시 후 다시 시도해 주세요.", }); } finally { setLoading(false); setLayoutReady(true); } }; if (screenId) { loadScreen(); } }, [screenId]); // 조건부 레이어 + Zone 로드 useEffect(() => { const loadConditionalLayersAndZones = async () => { if (!screenId || !layout) return; try { // Zone 로드 const loadedZones = await screenApi.getScreenZones(screenId); setZones(loadedZones); // 모든 레이어 목록 조회 const allLayers = await screenApi.getScreenLayers(screenId); const nonBaseLayers = allLayers.filter((l: any) => l.layer_id > 1); if (nonBaseLayers.length === 0) { setConditionalLayers([]); return; } // 각 레이어의 레이아웃 데이터 로드 const layerDefinitions: LayerDefinition[] = []; for (const layerInfo of nonBaseLayers) { try { const layerData = await screenApi.getLayerLayout(screenId, layerInfo.layer_id); const condConfig = layerInfo.condition_config || layerData?.conditionConfig || {}; let layerComponents: any[] = []; const rawComponents = layerData?.components; if (rawComponents && Array.isArray(rawComponents) && rawComponents.length > 0) { const tempV2 = { version: "2.0" as const, components: rawComponents, gridSettings: layerData.gridSettings, screenResolution: layerData.screenResolution, }; if (isValidV2Layout(tempV2)) { const converted = convertV2ToLegacy(tempV2); if (converted) { layerComponents = converted.components || []; } } } const zoneId = condConfig.zone_id; const conditionValue = condConfig.condition_value; const zone = zoneId ? loadedZones.find((z: any) => z.zone_id === zoneId) : null; const layerDef: LayerDefinition = { id: String(layerInfo.layer_id), name: layerInfo.layer_name || `레이어 ${layerInfo.layer_id}`, type: "conditional", zIndex: layerInfo.layer_id * 10, isVisible: false, isLocked: false, condition: zone ? { targetComponentId: zone.trigger_component_id || "", operator: (zone.trigger_operator as "eq" | "neq" | "in") || "eq", value: conditionValue, } : condConfig.targetComponentId ? { targetComponentId: condConfig.targetComponentId, operator: condConfig.operator || "eq", value: condConfig.value, } : undefined, zoneId: zoneId || undefined, conditionValue: conditionValue || undefined, displayRegion: zone ? { x: zone.x, y: zone.y, width: zone.width, height: zone.height } : condConfig.displayRegion || undefined, components: layerComponents, }; layerDefinitions.push(layerDef); } catch (layerError) { console.warn(`레이어 ${layerInfo.layer_id} 로드 실패:`, layerError); } } setConditionalLayers(layerDefinitions); } catch (error) { console.error("레이어/Zone 로드 실패:", error); } }; loadConditionalLayersAndZones(); }, [screenId, layout]); // 조건부 레이어 조건 평가 (formData 변경 시 동기적으로 즉시 계산) const activeLayerIds = useMemo(() => { if (conditionalLayers.length === 0 || !layout) return [] as string[]; const allComponents = layout.components || []; const newActiveIds: string[] = []; conditionalLayers.forEach((layer) => { if (layer.condition) { const { targetComponentId, operator, value } = layer.condition; if (!targetComponentId) return; const targetComponent = allComponents.find((c) => c.id === targetComponentId); const fieldKey = (targetComponent as any)?.columnName || (targetComponent as any)?.componentConfig?.columnName || targetComponentId; const targetValue = formData[fieldKey]; let isMatch = false; switch (operator) { case "eq": isMatch = String(targetValue ?? "") === String(value ?? ""); break; case "neq": isMatch = String(targetValue ?? "") !== String(value ?? ""); break; case "in": if (Array.isArray(value)) { isMatch = value.some((v) => String(v) === String(targetValue ?? "")); } else if (typeof value === "string" && value.includes(",")) { isMatch = value .split(",") .map((v) => v.trim()) .includes(String(targetValue ?? "")); } break; } if (isMatch) { newActiveIds.push(layer.id); } } }); for (const forcedId of forceActivatedLayerIds) { if (!newActiveIds.includes(forcedId)) { newActiveIds.push(forcedId); } } return newActiveIds; }, [formData, conditionalLayers, layout, forceActivatedLayerIds]); // 데이터 전달에 의한 레이어 강제 활성화 이벤트 리스너 useEffect(() => { const handleActivateLayer = (e: Event) => { const { componentId, targetLayerId } = (e as CustomEvent).detail || {}; if (!componentId && !targetLayerId) return; if (targetLayerId) { setForceActivatedLayerIds((prev) => (prev.includes(targetLayerId) ? prev : [...prev, targetLayerId])); return; } for (const layer of conditionalLayers) { const found = layer.components.some((comp) => comp.id === componentId); if (found) { setForceActivatedLayerIds((prev) => (prev.includes(layer.id) ? prev : [...prev, layer.id])); return; } } }; window.addEventListener("activateLayerForComponent", handleActivateLayer); return () => { window.removeEventListener("activateLayerForComponent", handleActivateLayer); }; }, [conditionalLayers]); // 메인 테이블 데이터 자동 로드 (단일 레코드 폼) useEffect(() => { const loadMainTableData = async () => { if (!screen || !layout || !layout.components || !companyCode) { return; } const mainTableName = screen.tableName; if (!mainTableName) { return; } // 테이블 위젯이 있으면 자동 로드 건너뜀 (테이블 행 선택으로 데이터 로드) const hasTableWidget = layout.components.some( (comp: any) => comp.componentType === "table-list" || comp.componentType === "v2-table-list" || comp.widgetType === "table", ); if (hasTableWidget) { return; } const inputComponents = layout.components.filter((comp: any) => { const compType = comp.componentType || comp.widgetType; const isInputType = compType?.includes("input") || compType?.includes("select") || compType?.includes("textarea") || compType?.includes("v2-input") || compType?.includes("v2-select") || compType?.includes("v2-media") || compType?.includes("file-upload"); const hasColumnName = !!(comp as any).columnName; return isInputType && hasColumnName; }); if (inputComponents.length === 0) { return; } try { const { tableTypeApi } = await import("@/lib/api/screen"); const result = await tableTypeApi.getTableRecord(mainTableName, "company_code", companyCode, "*"); if (result && result.record) { const newFormData: Record = {}; inputComponents.forEach((comp: any) => { const columnName = comp.columnName; if (columnName && result.record[columnName] !== undefined) { newFormData[columnName] = result.record[columnName]; } }); if (Object.keys(newFormData).length > 0) { setFormData((prev) => ({ ...prev, ...newFormData, })); } } } catch (error) { console.log("메인 테이블 자동 로드 실패 (정상일 수 있음):", error); } }; loadMainTableData(); }, [screen, layout, companyCode]); // 개별 autoFill 처리 useEffect(() => { const initAutoFill = async () => { if (!layout || !layout.components || !user) { return; } for (const comp of layout.components) { if (comp.type === "widget" || comp.type === "component") { const widget = comp as any; const fieldName = widget.columnName || widget.id; if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) { const autoFillConfig = widget.autoFill || (comp as any).autoFill; const currentValue = formData[fieldName]; if (currentValue === undefined || currentValue === "") { const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig; const userValue = user?.[userField as keyof typeof user]; if (userValue && sourceTable && filterColumn && displayColumn) { try { const { tableTypeApi } = await import("@/lib/api/screen"); const result = await tableTypeApi.getTableRecord(sourceTable, filterColumn, userValue, displayColumn); setFormData((prev) => ({ ...prev, [fieldName]: result.value, })); } catch (error) { console.error(`autoFill 조회 실패: ${fieldName}`, error); } } } } } } }; initAutoFill(); }, [layout, user]); // 조건부 비활성화/숨김 시 해당 필드 값 초기화 const conditionalFieldValues = useMemo(() => { if (!layout?.components) return ""; const conditionFields = new Set(); layout.components.forEach((component) => { const conditional = (component as any).conditional; if (conditional?.enabled && conditional.field) { conditionFields.add(conditional.field); } }); const values: Record = {}; conditionFields.forEach((field) => { values[field] = (formData as Record)[field]; }); return JSON.stringify(values); }, [layout?.components, formData]); useEffect(() => { if (!layout?.components) return; const fieldsToReset: string[] = []; layout.components.forEach((component) => { const conditional = (component as any).conditional; if (!conditional?.enabled) return; const conditionalResult = evaluateConditional(conditional, formData as Record, layout.components); if (!conditionalResult.visible || conditionalResult.disabled) { const fieldName = (component as any).columnName || component.id; const currentValue = (formData as Record)[fieldName]; if (currentValue !== undefined && currentValue !== "" && currentValue !== null) { fieldsToReset.push(fieldName); } } }); if (fieldsToReset.length > 0) { setFormData((prev) => { const updated = { ...prev }; fieldsToReset.forEach((fieldName) => { updated[fieldName] = ""; }); return updated; }); } }, [conditionalFieldValues, layout?.components]); // 화면 해상도 정보 const screenWidth = layout?.screenResolution?.width || 1200; const screenHeight = layout?.screenResolution?.height || 800; // RealtimePreview에 전달할 공통 props 빌더 const buildRealtimePreviewProps = (component: ComponentData, extraProps?: Record) => ({ component, isSelected: false, isDesignMode: false, onClick: () => {}, menuObjid, screenId, tableName: screen?.tableName, userId: user?.userId, userName, companyCode, selectedRowsData, sortBy: tableSortBy, sortOrder: tableSortOrder, columnOrder: tableColumnOrder, flowSelectedData, flowSelectedStepId, onFlowSelectedDataChange: (selectedData: any[], stepId: number | null) => { setFlowSelectedData(selectedData); setFlowSelectedStepId(stepId); }, refreshKey: tableRefreshKey, onRefresh: () => { setTableRefreshKey((prev) => prev + 1); setSelectedRowsData([]); }, flowRefreshKey, onFlowRefresh: () => { setFlowRefreshKey((prev) => prev + 1); setFlowSelectedData([]); setFlowSelectedStepId(null); }, formData, onFormDataChange: (fieldName: string, value: any) => { setFormData((prev) => ({ ...prev, [fieldName]: value })); }, onSelectedRowsChange: (_: any[], selectedData: any[]) => { setSelectedRowsData(selectedData); }, ...extraProps, }); if (loading) { return (

화면을 불러오는 중...

); } if (error || !screen) { return (

화면을 찾을 수 없습니다

{error || "요청하신 화면이 존재하지 않습니다."}

); } return (
{/* 레이아웃 준비 중 로딩 표시 */} {!layoutReady && (

화면 준비 중...

)} {/* 반응형 그리드 렌더링 */} {layoutReady && layout && layout.components.length > 0 ? ( {/* 기본 레이어: ResponsiveGridRenderer로 렌더링 */} { // 조건부 표시 평가 const conditional = (component as any).conditional; let conditionalDisabled = false; if (conditional?.enabled) { const conditionalResult = evaluateConditional( conditional, formData as Record, layout?.components || [], ); if (!conditionalResult.visible) { return null; } conditionalDisabled = conditionalResult.disabled; } return ( { setConditionalContainerHeights((prev) => ({ ...prev, [componentId]: newHeight, })); }, })} > {/* 자식 컴포넌트들 (그룹/컨테이너/영역) */} {(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 ( ); })} ); }} /> {/* 조건부 레이어 (Zone 기반) */} {conditionalLayers.map((layer) => { const isActive = activeLayerIds.includes(layer.id); if (!isActive || !layer.components || layer.components.length === 0) return null; const zone = layer.zoneId ? zones.find((z) => z.zone_id === layer.zoneId) : null; const region = zone ? { x: zone.x, y: zone.y, width: zone.width, height: zone.height } : layer.displayRegion; return (
!comp.parentId)} canvasWidth={region?.width || screenWidth} canvasHeight={region?.height || screenHeight} renderComponent={(comp) => ( )} />
); })}
) : ( // 빈 화면일 때 layoutReady && (

화면이 비어있습니다

이 화면에는 아직 설계된 컴포넌트가 없습니다.

) )} {/* 편집 모달 */} { setEditModalOpen(false); setEditModalConfig({}); }} screenId={editModalConfig.screenId} modalSize={editModalConfig.modalSize} editData={editModalConfig.editData} onSave={editModalConfig.onSave} modalTitle={editModalConfig.modalTitle} modalDescription={editModalConfig.modalDescription} onDataChange={(changedFormData) => { setFormData((prev) => ({ ...prev, ...changedFormData, })); }} /> {/* 스케줄 생성 확인 다이얼로그 */} !open && closeDialog()} preview={previewResult} onConfirm={() => handleConfirm(true)} onCancel={closeDialog} isLoading={scheduleLoading} />
); } // 실제 컴포넌트를 Provider로 감싸기 function ScreenViewPageWrapper({ screenIdProp, menuObjidProp }: ScreenViewPageProps = {}) { return ( ); } export { ScreenViewPageWrapper }; export default ScreenViewPageWrapper;