ERP-node/frontend/components/screen/InteractiveScreenViewerDyna...

765 lines
27 KiB
TypeScript
Raw Normal View History

2025-09-09 14:29:04 +09:00
"use client";
import React, { useState, useCallback, useEffect } from "react";
2025-09-09 14:29:04 +09:00
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader } from "@/components/ui/resizable-dialog";
import { DialogTitle, DialogHeader } from "@/components/ui/dialog";
2025-09-09 14:29:04 +09:00
import { useAuth } from "@/hooks/useAuth";
import { uploadFilesAndCreateData } from "@/lib/api/file";
import { toast } from "sonner";
import { ComponentData, WidgetComponent, DataTableComponent, FileComponent, ButtonTypeConfig } from "@/types/screen";
import { FileUploadComponent } from "@/lib/registry/components/file-upload/FileUploadComponent";
2025-09-09 14:29:04 +09:00
import { InteractiveDataTable } from "./InteractiveDataTable";
import { DynamicWebTypeRenderer } from "@/lib/registry";
2025-09-10 14:09:32 +09:00
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
2025-09-29 13:29:03 +09:00
import { isFileComponent, isDataTableComponent, isButtonComponent } from "@/lib/utils/componentTypeUtils";
2025-10-24 10:37:02 +09:00
import { FlowButtonGroup } from "./widgets/FlowButtonGroup";
import { FlowVisibilityConfig } from "@/types/control-management";
import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
2025-10-28 15:39:22 +09:00
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
2025-09-10 14:09:32 +09:00
// 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록
import "@/lib/registry/components/ButtonRenderer";
import "@/lib/registry/components/CardRenderer";
import "@/lib/registry/components/DashboardRenderer";
import "@/lib/registry/components/WidgetRenderer";
2025-09-09 14:29:04 +09:00
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
import { useParams } from "next/navigation";
import { screenApi } from "@/lib/api/screen";
interface InteractiveScreenViewerProps {
component: ComponentData;
allComponents: ComponentData[];
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
hideLabel?: boolean;
screenInfo?: {
id: number;
tableName?: string;
};
onSave?: () => Promise<void>;
2025-09-09 14:29:04 +09:00
}
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
component,
allComponents,
formData: externalFormData,
onFormDataChange,
hideLabel = false,
screenInfo,
onSave,
2025-09-09 14:29:04 +09:00
}) => {
2025-10-28 15:39:22 +09:00
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
2025-09-09 14:29:04 +09:00
const { userName, user } = useAuth();
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
2025-10-23 13:15:52 +09:00
// 테이블에서 선택된 행 데이터 (버튼 액션에 전달)
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
// 플로우에서 선택된 데이터 (버튼 액션에 전달)
const [flowSelectedData, setFlowSelectedData] = useState<any[]>([]);
const [flowSelectedStepId, setFlowSelectedStepId] = useState<number | null>(null);
2025-09-09 14:29:04 +09:00
// 팝업 화면 상태
const [popupScreen, setPopupScreen] = useState<{
screenId: number;
title: string;
size: string;
} | null>(null);
// 팝업 화면 레이아웃 상태
const [popupLayout, setPopupLayout] = useState<ComponentData[]>([]);
const [popupLoading, setPopupLoading] = useState(false);
const [popupScreenResolution, setPopupScreenResolution] = useState<{ width: number; height: number } | null>(null);
const [popupScreenInfo, setPopupScreenInfo] = useState<{ id: number; tableName?: string } | null>(null);
// 팝업 전용 formData 상태
const [popupFormData, setPopupFormData] = useState<Record<string, any>>({});
// formData 결정 (외부에서 전달받은 것이 있으면 우선 사용)
const formData = externalFormData || localFormData;
2025-11-04 14:33:39 +09:00
// formData 업데이트 함수
const updateFormData = useCallback(
(fieldName: string, value: any) => {
if (onFormDataChange) {
onFormDataChange(fieldName, value);
} else {
setLocalFormData((prev) => ({
...prev,
[fieldName]: value,
}));
}
},
[onFormDataChange],
);
2025-09-09 14:29:04 +09:00
// 자동값 생성 함수
const generateAutoValue = useCallback(
(autoValueType: string): string => {
const now = new Date();
switch (autoValueType) {
case "current_datetime":
return now.toISOString().slice(0, 19).replace("T", " ");
case "current_date":
return now.toISOString().slice(0, 10);
case "current_time":
return now.toTimeString().slice(0, 8);
case "current_user":
return userName || "사용자";
case "uuid":
return crypto.randomUUID();
case "sequence":
return `SEQ_${Date.now()}`;
default:
return "";
}
},
[userName],
);
// 🆕 Enter 키로 다음 필드 이동
useEffect(() => {
const handleEnterKey = (e: KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
const target = e.target as HTMLElement;
2025-11-06 17:32:29 +09:00
// 한글 조합 중이면 무시 (한글 입력 문제 방지)
if ((e as any).isComposing || e.keyCode === 229) {
return;
}
// textarea는 제외 (여러 줄 입력)
if (target.tagName === "TEXTAREA") {
return;
}
// input, select 등의 폼 요소에서만 작동
if (
target.tagName === "INPUT" ||
target.tagName === "SELECT" ||
target.getAttribute("role") === "combobox"
) {
e.preventDefault();
// 모든 포커스 가능한 요소 찾기
const focusableElements = document.querySelectorAll<HTMLElement>(
'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [role="combobox"]:not([disabled])'
);
// 화면에 보이는 순서(Y 좌표 → X 좌표)대로 정렬
const focusableArray = Array.from(focusableElements).sort((a, b) => {
const rectA = a.getBoundingClientRect();
const rectB = b.getBoundingClientRect();
// Y 좌표 차이가 10px 이상이면 Y 좌표로 정렬 (위에서 아래로)
if (Math.abs(rectA.top - rectB.top) > 10) {
return rectA.top - rectB.top;
}
// 같은 줄이면 X 좌표로 정렬 (왼쪽에서 오른쪽으로)
return rectA.left - rectB.left;
});
const currentIndex = focusableArray.indexOf(target);
if (currentIndex !== -1 && currentIndex < focusableArray.length - 1) {
// 다음 요소로 포커스 이동
const nextElement = focusableArray[currentIndex + 1];
nextElement.focus();
2025-11-06 17:32:29 +09:00
// select() 제거: 한글 입력 시 이전 필드의 마지막 글자가 복사되는 버그 방지
}
}
}
};
document.addEventListener("keydown", handleEnterKey);
return () => {
document.removeEventListener("keydown", handleEnterKey);
};
}, []);
2025-11-04 14:33:39 +09:00
// 🆕 autoFill 자동 입력 초기화
React.useEffect(() => {
const initAutoInputFields = async () => {
for (const comp of allComponents) {
// type: "component" 또는 type: "widget" 모두 처리
if (comp.type === 'widget' || comp.type === 'component') {
const widget = comp as any;
const fieldName = widget.columnName || widget.id;
// autoFill 처리 (테이블 조회 기반 자동 입력)
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];
if (userValue && sourceTable && filterColumn && displayColumn) {
try {
const { tableTypeApi } = await import("@/lib/api/screen");
const result = await tableTypeApi.getTableRecord(
sourceTable,
filterColumn,
userValue,
displayColumn
);
updateFormData(fieldName, result.value);
} catch (error) {
console.error(`autoFill 조회 실패: ${fieldName}`, error);
}
}
}
}
}
}
};
initAutoInputFields();
}, [allComponents, user]);
2025-09-09 14:29:04 +09:00
// 팝업 화면 레이아웃 로드
React.useEffect(() => {
if (popupScreen?.screenId) {
loadPopupScreen(popupScreen.screenId);
}
}, [popupScreen?.screenId]);
const loadPopupScreen = async (screenId: number) => {
try {
setPopupLoading(true);
const response = await screenApi.getScreenLayout(screenId);
if (response.success && response.data) {
const screenData = response.data;
setPopupLayout(screenData.components || []);
setPopupScreenResolution({
width: screenData.screenResolution?.width || 1200,
height: screenData.screenResolution?.height || 800,
});
setPopupScreenInfo({
id: screenData.id,
tableName: screenData.tableName,
});
} else {
toast.error("팝업 화면을 불러올 수 없습니다.");
setPopupScreen(null);
}
} catch (error) {
// console.error("팝업 화면 로드 오류:", error);
2025-09-09 14:29:04 +09:00
toast.error("팝업 화면 로드 중 오류가 발생했습니다.");
setPopupScreen(null);
} finally {
setPopupLoading(false);
}
};
// 폼 데이터 변경 핸들러
const handleFormDataChange = (fieldName: string | any, value?: any) => {
// 일반 필드 변경
2025-09-09 14:29:04 +09:00
if (onFormDataChange) {
onFormDataChange(fieldName, value);
} else {
setLocalFormData((prev) => ({ ...prev, [fieldName]: value }));
}
};
// 동적 대화형 위젯 렌더링
const renderInteractiveWidget = (comp: ComponentData) => {
// 데이터 테이블 컴포넌트 처리
2025-09-29 13:29:03 +09:00
if (isDataTableComponent(comp)) {
2025-09-09 14:29:04 +09:00
return (
<InteractiveDataTable
component={comp as DataTableComponent}
className="h-full w-full"
style={{
width: "100%",
height: "100%",
}}
onRefresh={() => {
// 테이블 자체에서 loadData를 호출하므로 여기서는 빈 함수
console.log("🔄 InteractiveDataTable 새로고침 트리거됨 (Dynamic)");
}}
2025-09-09 14:29:04 +09:00
/>
);
}
// 파일 컴포넌트 처리
2025-09-29 13:29:03 +09:00
if (isFileComponent(comp)) {
2025-09-09 14:29:04 +09:00
return renderFileComponent(comp as FileComponent);
}
// 버튼 컴포넌트 또는 위젯이 아닌 경우 DynamicComponentRenderer 사용
2025-09-09 14:29:04 +09:00
if (comp.type !== "widget") {
2025-09-10 14:09:32 +09:00
return (
<DynamicComponentRenderer
component={comp}
isInteractive={true}
formData={formData}
onFormDataChange={handleFormDataChange}
2025-09-12 14:24:25 +09:00
screenId={screenInfo?.id}
tableName={screenInfo?.tableName}
userId={user?.userId} // ✅ 사용자 ID 전달
userName={user?.userName} // ✅ 사용자 이름 전달
companyCode={user?.companyCode} // ✅ 회사 코드 전달
2025-10-23 13:15:52 +09:00
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(selectedRows, selectedData) => {
console.log("🔍 테이블에서 선택된 행 데이터:", selectedData);
setSelectedRowsData(selectedData);
}}
flowSelectedData={flowSelectedData}
flowSelectedStepId={flowSelectedStepId}
onFlowSelectedDataChange={(selectedData, stepId) => {
console.log("🔍 플로우에서 선택된 데이터:", { selectedData, stepId });
setFlowSelectedData(selectedData);
setFlowSelectedStepId(stepId);
}}
2025-09-12 14:24:25 +09:00
onRefresh={() => {
// 테이블 컴포넌트는 자체적으로 loadData 호출
2025-09-12 14:24:25 +09:00
}}
onClose={() => {
// buttonActions.ts가 이미 처리함
2025-09-12 14:24:25 +09:00
}}
2025-09-10 14:09:32 +09:00
/>
);
2025-09-09 14:29:04 +09:00
}
const widget = comp as WidgetComponent;
const { widgetType, label, placeholder, required, readonly, columnName } = widget;
const fieldName = columnName || comp.id;
const currentValue = formData[fieldName] || "";
// 스타일 적용
const applyStyles = (element: React.ReactElement) => {
if (!comp.style) return element;
2025-11-10 09:33:29 +09:00
// ✅ 격자 시스템 잔재 제거: style.width, style.height는 무시
// size.width, size.height가 부모 컨테이너에서 적용되므로
const { width, height, ...styleWithoutSize } = comp.style;
2025-09-09 14:29:04 +09:00
return React.cloneElement(element, {
style: {
...element.props.style,
2025-11-10 09:33:29 +09:00
...styleWithoutSize, // width/height 제외한 스타일만 적용
2025-09-09 14:29:04 +09:00
width: "100%",
height: "100%",
minHeight: "100%",
maxHeight: "100%",
boxSizing: "border-box",
},
});
};
// 동적 웹타입 렌더링 사용
if (widgetType) {
try {
const dynamicElement = (
<DynamicWebTypeRenderer
webType={widgetType}
props={{
component: widget,
value: currentValue,
onChange: (value: any) => handleFormDataChange(fieldName, value),
2025-09-18 10:05:50 +09:00
onFormDataChange: handleFormDataChange,
isInteractive: true,
2025-09-09 14:29:04 +09:00
readonly: readonly,
required: required,
placeholder: placeholder,
className: "w-full h-full",
}}
config={widget.webTypeConfig}
onEvent={(event: string, data: any) => {
// 이벤트 처리
// console.log(`Widget event: ${event}`, data);
2025-09-09 14:29:04 +09:00
}}
/>
);
return applyStyles(dynamicElement);
} catch (error) {
// console.error(`웹타입 "${widgetType}" 대화형 렌더링 실패:`, error);
2025-09-09 14:29:04 +09:00
// 오류 발생 시 폴백으로 기본 input 렌더링
const fallbackElement = (
<Input
type="text"
value={currentValue}
onChange={(e) => handleFormDataChange(fieldName, e.target.value)}
placeholder={`${widgetType} (렌더링 오류)`}
disabled={readonly}
required={required}
className="h-full w-full"
/>
);
return applyStyles(fallbackElement);
}
}
// 웹타입이 없는 경우 기본 input 렌더링 (하위 호환성)
const defaultElement = (
<Input
type="text"
value={currentValue}
onChange={(e) => handleFormDataChange(fieldName, e.target.value)}
placeholder={placeholder || "입력하세요"}
disabled={readonly}
required={required}
className="h-full w-full"
/>
);
return applyStyles(defaultElement);
};
// 버튼 렌더링
const renderButton = (comp: ComponentData) => {
const config = (comp as any).webTypeConfig as ButtonTypeConfig | undefined;
const { label } = comp;
// 버튼 액션 핸들러들
const handleSaveAction = async () => {
// EditModal에서 전달된 onSave가 있으면 우선 사용 (수정 모달)
if (onSave) {
try {
await onSave();
} catch (error) {
console.error("저장 오류:", error);
toast.error("저장 중 오류가 발생했습니다.");
}
return;
}
// 일반 저장 액션 (신규 생성)
2025-09-09 14:29:04 +09:00
if (!screenInfo?.tableName) {
toast.error("테이블명이 설정되지 않았습니다.");
return;
}
try {
const saveData: DynamicFormData = {
tableName: screenInfo.tableName,
data: formData,
};
// console.log("💾 저장 액션 실행:", saveData);
2025-09-09 14:29:04 +09:00
const response = await dynamicFormApi.saveData(saveData);
if (response.success) {
toast.success("데이터가 성공적으로 저장되었습니다.");
} else {
toast.error(response.message || "저장에 실패했습니다.");
}
} catch (error) {
// console.error("저장 오류:", error);
2025-09-09 14:29:04 +09:00
toast.error("저장 중 오류가 발생했습니다.");
}
};
const handleDeleteAction = async () => {
if (!config?.confirmationEnabled || window.confirm(config.confirmationMessage || "정말 삭제하시겠습니까?")) {
// console.log("🗑️ 삭제 액션 실행");
2025-09-09 14:29:04 +09:00
toast.success("삭제가 완료되었습니다.");
}
};
const handlePopupAction = () => {
if (config?.popupScreenId) {
setPopupScreen({
screenId: config.popupScreenId,
title: config.popupTitle || "팝업 화면",
size: config.popupSize || "medium",
});
}
};
const handleNavigateAction = () => {
const navigateType = config?.navigateType || "url";
if (navigateType === "screen" && config?.navigateScreenId) {
const screenPath = `/screens/${config.navigateScreenId}`;
if (config.navigateTarget === "_blank") {
window.open(screenPath, "_blank");
} else {
window.location.href = screenPath;
}
} else if (navigateType === "url" && config?.navigateUrl) {
if (config.navigateTarget === "_blank") {
window.open(config.navigateUrl, "_blank");
} else {
window.location.href = config.navigateUrl;
}
}
};
const handleCustomAction = async () => {
if (config?.customAction) {
try {
const result = eval(config.customAction);
if (result instanceof Promise) {
await result;
}
// console.log("⚡ 커스텀 액션 실행 완료");
2025-09-09 14:29:04 +09:00
} catch (error) {
throw new Error(`커스텀 액션 실행 실패: ${error.message}`);
}
}
};
const handleClick = async () => {
try {
const actionType = config?.actionType || "save";
switch (actionType) {
case "save":
await handleSaveAction();
break;
case "delete":
await handleDeleteAction();
break;
case "popup":
handlePopupAction();
break;
case "navigate":
handleNavigateAction();
break;
case "custom":
await handleCustomAction();
break;
default:
2025-10-28 15:39:22 +09:00
// console.log("🔘 기본 버튼 클릭");
2025-09-09 14:29:04 +09:00
}
} catch (error) {
// console.error("버튼 액션 오류:", error);
2025-09-09 14:29:04 +09:00
toast.error(error.message || "액션 실행 중 오류가 발생했습니다.");
}
};
return (
<Button
onClick={handleClick}
variant={(config?.variant as any) || "default"}
size={(config?.size as any) || "default"}
disabled={config?.disabled}
style={{
2025-11-06 17:32:29 +09:00
// 컴포넌트 스타일 적용
2025-09-09 14:29:04 +09:00
...comp.style,
2025-11-04 11:41:20 +09:00
// 설정값이 있으면 우선 적용
backgroundColor: config?.backgroundColor || comp.style?.backgroundColor,
color: config?.textColor || comp.style?.color,
2025-11-06 17:32:29 +09:00
// 부모 컨테이너 크기에 맞춤
width: '100%',
height: '100%',
2025-09-09 14:29:04 +09:00
}}
>
{label || "버튼"}
</Button>
);
};
// 파일 컴포넌트 렌더링
const renderFileComponent = (comp: FileComponent) => {
const { label, readonly } = comp;
const fieldName = comp.columnName || comp.id;
2025-09-29 13:29:03 +09:00
// 화면 ID 추출 (URL에서)
2025-10-28 15:39:22 +09:00
const screenId =
screenInfo?.screenId ||
(typeof window !== "undefined" && window.location.pathname.includes("/screens/")
? parseInt(window.location.pathname.split("/screens/")[1])
2025-09-29 13:29:03 +09:00
: null);
2025-09-09 14:29:04 +09:00
return (
<div className="h-full w-full">
{/* 실제 FileUploadComponent 사용 */}
<FileUploadComponent
2025-09-26 17:12:03 +09:00
component={comp}
componentConfig={{
...comp.fileConfig,
multiple: comp.fileConfig?.multiple !== false,
accept: comp.fileConfig?.accept || "*/*",
maxSize: (comp.fileConfig?.maxSize || 10) * 1024 * 1024, // MB to bytes
disabled: readonly,
}}
2025-09-26 17:12:03 +09:00
componentStyle={{
2025-10-28 15:39:22 +09:00
width: "100%",
height: "100%",
2025-09-26 17:12:03 +09:00
}}
className="h-full w-full"
isInteractive={true}
2025-09-26 17:12:03 +09:00
isDesignMode={false}
formData={{
2025-09-29 17:21:47 +09:00
screenId, // 🎯 화면 ID 전달
// 🎯 백엔드 API가 기대하는 정확한 형식으로 설정
autoLink: true, // 자동 연결 활성화
2025-10-28 15:39:22 +09:00
linkedTable: "screen_files", // 연결 테이블
2025-09-29 17:21:47 +09:00
recordId: screenId, // 레코드 ID
columnName: fieldName, // 컬럼명 (중요!)
isVirtualFileColumn: true, // 가상 파일 컬럼
id: formData.id,
2025-10-28 15:39:22 +09:00
...formData,
}}
2025-09-26 17:12:03 +09:00
onFormDataChange={(data) => {
// console.log("📝 실제 화면 파일 업로드 완료:", data);
2025-09-26 17:12:03 +09:00
if (onFormDataChange) {
Object.entries(data).forEach(([key, value]) => {
onFormDataChange(key, value);
});
}
}}
onUpdate={(updates) => {
2025-09-29 17:21:47 +09:00
console.log("🔄🔄🔄 실제 화면 파일 컴포넌트 업데이트:", {
componentId: comp.id,
hasUploadedFiles: !!updates.uploadedFiles,
filesCount: updates.uploadedFiles?.length || 0,
hasLastFileUpdate: !!updates.lastFileUpdate,
2025-10-28 15:39:22 +09:00
updates,
2025-09-29 17:21:47 +09:00
});
2025-10-28 15:39:22 +09:00
2025-09-29 17:21:47 +09:00
// 파일 업로드/삭제 완료 시 formData 업데이터
2025-09-26 17:12:03 +09:00
if (updates.uploadedFiles && onFormDataChange) {
onFormDataChange(fieldName, updates.uploadedFiles);
}
2025-10-28 15:39:22 +09:00
2025-09-29 17:21:47 +09:00
// 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생 (업로드/삭제 모두)
2025-10-28 15:39:22 +09:00
if (updates.uploadedFiles !== undefined && typeof window !== "undefined") {
2025-09-29 17:21:47 +09:00
// 업로드인지 삭제인지 판단 (lastFileUpdate가 있으면 변경사항 있음)
2025-10-28 15:39:22 +09:00
const action = updates.lastFileUpdate ? "update" : "sync";
2025-09-29 17:21:47 +09:00
const eventDetail = {
componentId: comp.id,
files: updates.uploadedFiles,
fileCount: updates.uploadedFiles.length,
action: action,
timestamp: updates.lastFileUpdate || Date.now(),
2025-10-28 15:39:22 +09:00
source: "realScreen", // 실제 화면에서 온 이벤트임을 표시
2025-09-29 17:21:47 +09:00
};
2025-10-28 15:39:22 +09:00
// console.log("🚀🚀🚀 실제 화면 파일 변경 이벤트 발생:", eventDetail);
2025-10-28 15:39:22 +09:00
const event = new CustomEvent("globalFileStateChanged", {
detail: eventDetail,
2025-09-29 17:21:47 +09:00
});
window.dispatchEvent(event);
2025-10-28 15:39:22 +09:00
// console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 이벤트 발생 완료");
2025-10-28 15:39:22 +09:00
2025-09-29 17:21:47 +09:00
// 추가 지연 이벤트들 (화면설계 모드가 열려있을 때를 대비)
setTimeout(() => {
// console.log("🔄 실제 화면 추가 이벤트 발생 (지연 100ms)");
2025-10-28 15:39:22 +09:00
window.dispatchEvent(
new CustomEvent("globalFileStateChanged", {
detail: { ...eventDetail, delayed: true },
}),
);
2025-09-29 17:21:47 +09:00
}, 100);
2025-10-28 15:39:22 +09:00
2025-09-29 17:21:47 +09:00
setTimeout(() => {
// console.log("🔄 실제 화면 추가 이벤트 발생 (지연 500ms)");
2025-10-28 15:39:22 +09:00
window.dispatchEvent(
new CustomEvent("globalFileStateChanged", {
detail: { ...eventDetail, delayed: true, attempt: 2 },
}),
);
2025-09-29 17:21:47 +09:00
}, 500);
}
}}
/>
2025-09-09 14:29:04 +09:00
</div>
);
};
// 메인 렌더링
const { type, position, size, style = {} } = component;
2025-11-10 09:33:29 +09:00
// ✅ 격자 시스템 잔재 제거: style.width, style.height 무시
const { width: styleWidth, height: styleHeight, ...styleWithoutSize } = style;
// TableSearchWidget의 경우 높이를 자동으로 설정
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
2025-09-09 14:29:04 +09:00
const componentStyle = {
position: "absolute" as const,
left: position?.x || 0,
top: position?.y || 0,
zIndex: position?.z || 1,
2025-11-10 09:33:29 +09:00
...styleWithoutSize, // width/height 제외한 스타일만 먼저 적용
width: size?.width || 200, // size의 픽셀 값이 최종 우선순위
height: isTableSearchWidget ? "auto" : (size?.height || 10),
minHeight: isTableSearchWidget ? "48px" : undefined,
2025-09-09 14:29:04 +09:00
};
return (
<>
<div className="absolute" style={componentStyle}>
{/* 라벨 숨김 - 모달에서는 라벨을 표시하지 않음 */}
{/* 위젯 렌더링 */}
{renderInteractiveWidget(component)}
2025-09-09 14:29:04 +09:00
</div>
{/* 팝업 화면 렌더링 */}
{popupScreen && (
<ResizableDialog open={!!popupScreen} onOpenChange={() => setPopupScreen(null)}>
<ResizableDialogContent
className="overflow-hidden p-0"
defaultWidth={popupScreen.size === "small" ? 600 : popupScreen.size === "large" ? 1400 : 1000}
defaultHeight={800}
minWidth={500}
minHeight={400}
maxWidth={1600}
maxHeight={1200}
modalId={`popup-screen-${popupScreen.screenId}`}
userId={user?.userId || "guest"}
2025-09-09 14:29:04 +09:00
>
<DialogHeader>
<DialogTitle>{popupScreen.title}</DialogTitle>
</DialogHeader>
{popupLoading ? (
<div className="flex items-center justify-center p-8">
<div className="text-gray-500"> ...</div>
</div>
) : (
<div
className="relative overflow-auto"
style={{
width: popupScreenResolution?.width || 1200,
height: popupScreenResolution?.height || 600,
maxWidth: "100%",
maxHeight: "70vh",
}}
>
{popupLayout.map((popupComponent) => (
<InteractiveScreenViewerDynamic
key={popupComponent.id}
component={popupComponent}
allComponents={popupLayout}
formData={popupFormData}
onFormDataChange={(fieldName, value) => {
setPopupFormData((prev) => ({ ...prev, [fieldName]: value }));
}}
screenInfo={popupScreenInfo}
/>
))}
</div>
)}
</ResizableDialogContent>
</ResizableDialog>
2025-09-09 14:29:04 +09:00
)}
</>
);
};
// 기존 InteractiveScreenViewer와의 호환성을 위한 export
export { InteractiveScreenViewerDynamic as InteractiveScreenViewer };
InteractiveScreenViewerDynamic.displayName = "InteractiveScreenViewerDynamic";