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

818 lines
31 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useEffect, useState, useMemo } from "react";
import { useParams, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Loader2 } 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";
function ScreenViewPage() {
const { showConfirmDialog, previewResult, handleConfirm, closeDialog, isLoading: scheduleLoading } = useScheduleGenerator();
const params = useParams();
const searchParams = useSearchParams();
const router = useRouter();
const screenId = parseInt(params.screenId as string);
// URL 쿼리에서 menuObjid 가져오기 (메뉴 스코프)
const menuObjid = 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<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, unknown>>({});
// 테이블에서 선택된 행 데이터 (버튼 액션에 전달)
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
// 테이블 정렬 정보 (엑셀 다운로드용)
const [tableSortBy, setTableSortBy] = useState<string | undefined>();
const [tableSortOrder, setTableSortOrder] = useState<"asc" | "desc">("asc");
const [tableColumnOrder, setTableColumnOrder] = useState<string[] | undefined>();
// 플로우에서 선택된 데이터 (버튼 액션에 전달)
const [flowSelectedData, setFlowSelectedData] = useState<any[]>([]);
const [flowSelectedStepId, setFlowSelectedStepId] = useState<number | null>(null);
// 테이블 새로고침 키
const [tableRefreshKey, setTableRefreshKey] = useState(0);
// 플로우 새로고침 키
const [flowRefreshKey, setFlowRefreshKey] = useState(0);
// 조건부 컨테이너 높이 추적 (컴포넌트 ID → 높이)
const [conditionalContainerHeights, setConditionalContainerHeights] = useState<Record<string, number>>({});
// 레이어 시스템 지원
const [conditionalLayers, setConditionalLayers] = useState<LayerDefinition[]>([]);
// 조건부 영역(Zone) 목록
const [zones, setZones] = useState<import("@/types/screen-management").ConditionalZone[]>([]);
// 데이터 전달에 의해 강제 활성화된 레이어 ID 목록
const [forceActivatedLayerIds, setForceActivatedLayerIds] = useState<string[]>([]);
// 편집 모달 상태
const [editModalOpen, setEditModalOpen] = useState(false);
const [editModalConfig, setEditModalConfig] = useState<{
screenId?: number;
modalSize?: "sm" | "md" | "lg" | "xl" | "full";
editData?: Record<string, unknown>;
onSave?: () => void;
modalTitle?: string;
modalDescription?: string;
}>({});
// 레이아웃 준비 완료 상태
const [layoutReady, setLayoutReady] = useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
useEffect(() => {
const initComponents = async () => {
try {
await initializeComponents();
} catch (error) {
console.error("❌ 할당된 화면에서 컴포넌트 시스템 초기화 실패:", error);
}
};
initComponents();
}, []);
// 편집 모달 이벤트 리스너 등록
useEffect(() => {
const handleOpenEditModal = (event: CustomEvent) => {
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);
};
}, []);
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<string, any> = {};
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<string>();
layout.components.forEach((component) => {
const conditional = (component as any).conditional;
if (conditional?.enabled && conditional.field) {
conditionFields.add(conditional.field);
}
});
const values: Record<string, any> = {};
conditionFields.forEach((field) => {
values[field] = (formData as Record<string, any>)[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<string, any>, layout.components);
if (!conditionalResult.visible || conditionalResult.disabled) {
const fieldName = (component as any).columnName || component.id;
const currentValue = (formData as Record<string, any>)[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<string, any>) => ({
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 (
<div className="from-muted to-muted/50 flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br">
<div className="border-border bg-background rounded-xl border p-8 text-center shadow-lg">
<Loader2 className="text-primary mx-auto h-10 w-10 animate-spin" />
<p className="text-foreground mt-4 font-medium"> ...</p>
</div>
</div>
);
}
if (error || !screen) {
return (
<div className="from-muted to-muted/50 flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br">
<div className="border-border bg-background max-w-md rounded-xl border p-8 text-center shadow-lg">
<div className="from-destructive/20 to-warning/20 mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br shadow-sm">
<span className="text-3xl"></span>
</div>
<h2 className="text-foreground mb-3 text-xl font-bold"> </h2>
<p className="text-muted-foreground mb-6 leading-relaxed">{error || "요청하신 화면이 존재하지 않습니다."}</p>
<Button onClick={() => router.back()} variant="outline" className="rounded-lg">
</Button>
</div>
</div>
);
}
return (
<ScreenPreviewProvider isPreviewMode={false}>
<ActiveTabProvider>
<TableOptionsProvider>
<div
ref={containerRef}
className={`bg-background h-full w-full ${isPreviewMode ? "overflow-hidden p-0" : "overflow-auto p-3"}`}
>
{/* 레이아웃 준비 중 로딩 표시 */}
{!layoutReady && (
<div className="from-muted to-muted/50 flex h-full w-full items-center justify-center bg-gradient-to-br">
<div className="border-border bg-background rounded-xl border p-8 text-center shadow-lg">
<Loader2 className="text-primary mx-auto h-8 w-8 animate-spin" />
<p className="text-foreground mt-4 text-sm font-medium"> ...</p>
</div>
</div>
)}
{/* 반응형 그리드 렌더링 */}
{layoutReady && layout && layout.components.length > 0 ? (
<ScreenMultiLangProvider components={layout.components} companyCode={companyCode}>
{/* 기본 레이어: ResponsiveGridRenderer로 렌더링 */}
<ResponsiveGridRenderer
components={layout.components}
canvasWidth={screenWidth}
canvasHeight={screenHeight}
renderComponent={(component) => {
// 조건부 표시 평가
const conditional = (component as any).conditional;
let conditionalDisabled = false;
if (conditional?.enabled) {
const conditionalResult = evaluateConditional(
conditional,
formData as Record<string, any>,
layout?.components || [],
);
if (!conditionalResult.visible) {
return null;
}
conditionalDisabled = conditionalResult.disabled;
}
return (
<RealtimePreview
{...buildRealtimePreviewProps(component, {
conditionalDisabled,
onHeightChange: (componentId: string, newHeight: number) => {
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 (
<RealtimePreview
key={child.id}
{...buildRealtimePreviewProps(relativeChildComponent)}
/>
);
})}
</RealtimePreview>
);
}}
/>
{/* 조건부 레이어 (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 (
<div
key={`conditional-layer-${layer.id}`}
data-conditional-layer="true"
style={{
position: "absolute",
left: region ? `${region.x}px` : "0px",
top: region ? `${region.y}px` : "0px",
width: region ? `${region.width}px` : "100%",
height: region ? `${region.height}px` : "auto",
zIndex: layer.zIndex || 20,
overflow: "hidden",
transition: "none",
}}
>
<ResponsiveGridRenderer
components={layer.components.filter((comp) => !comp.parentId)}
canvasWidth={region?.width || screenWidth}
canvasHeight={region?.height || screenHeight}
renderComponent={(comp) => (
<RealtimePreview
key={comp.id}
{...buildRealtimePreviewProps(comp)}
/>
)}
/>
</div>
);
})}
</ScreenMultiLangProvider>
) : (
// 빈 화면일 때
layoutReady && (
<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>
</div>
<h2 className="text-foreground mb-2 text-xl font-semibold"> </h2>
<p className="text-muted-foreground"> .</p>
</div>
</div>
)
)}
{/* 편집 모달 */}
<EditModal
isOpen={editModalOpen}
onClose={() => {
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,
}));
}}
/>
{/* 스케줄 생성 확인 다이얼로그 */}
<ScheduleConfirmDialog
open={showConfirmDialog}
onOpenChange={(open) => !open && closeDialog()}
preview={previewResult}
onConfirm={() => handleConfirm(true)}
onCancel={closeDialog}
isLoading={scheduleLoading}
/>
</div>
</TableOptionsProvider>
</ActiveTabProvider>
</ScreenPreviewProvider>
);
}
// 실제 컴포넌트를 Provider로 감싸기
function ScreenViewPageWrapper() {
return (
<TableSearchWidgetHeightProvider>
<ScreenContextProvider>
<SplitPanelProvider>
<ScreenViewPage />
</SplitPanelProvider>
</ScreenContextProvider>
</TableSearchWidgetHeightProvider>
);
}
export default ScreenViewPageWrapper;