Fix modal label display issues and DOM node removal errors

- Hide rounded background labels in modal (계약구분, 국내/해외, 기본 버튼)
- Add try-catch blocks for DOM operations to prevent removeChild errors
- Fix event listener registration/removal in RealtimePreview, FileUpload, FileComponentConfigPanel
- Improve error handling for CustomEvent dispatching
This commit is contained in:
leeheejin 2025-09-29 19:32:20 +09:00
parent d0d37d9e29
commit d861eb5196
7 changed files with 142 additions and 112 deletions

View File

@ -245,6 +245,7 @@ export const EditModal: React.FC<EditModalProps> = ({
maxHeight: "95vh",
zIndex: 9999, // 모든 컴포넌트보다 위에 표시
}}
data-radix-portal="true"
>
<DialogHeader className="sr-only">
<DialogTitle></DialogTitle>
@ -271,16 +272,17 @@ export const EditModal: React.FC<EditModalProps> = ({
>
{/* 화면 컴포넌트들 원본 레이아웃 유지하여 렌더링 */}
<div className="relative" style={{ minHeight: "300px" }}>
{components.map((component) => (
{components.map((component, index) => (
<div
key={component.id}
className="rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 p-4 shadow-sm transition-all duration-200 hover:shadow-md"
style={{
position: "absolute",
top: component.position?.y || 0,
left: component.position?.x || 0,
width: component.size?.width || 200,
height: component.size?.height || 40,
zIndex: 1,
zIndex: component.position?.z || (1000 + index), // 모달 내부에서 충분히 높은 z-index
}}
>
{/* 위젯 컴포넌트는 InteractiveScreenViewer 사용 (라벨 표시를 위해) */}
@ -288,7 +290,7 @@ export const EditModal: React.FC<EditModalProps> = ({
<InteractiveScreenViewer
component={component}
allComponents={components}
hideLabel={false} // 라벨 표시 활성화
hideLabel={true} // 라벨 숨김 (원래 화면과 동일하게)
formData={formData}
onFormDataChange={(fieldName, value) => {
console.log("📝 폼 데이터 변경:", fieldName, value);
@ -312,7 +314,7 @@ export const EditModal: React.FC<EditModalProps> = ({
...component,
style: {
...component.style,
labelDisplay: true, // 수정 모달에서는 라벨 강제 표시
labelDisplay: false, // 라벨 숨김 (원래 화면과 동일하게)
},
}}
screenId={screenId}

View File

@ -1726,19 +1726,19 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
return (
<>
<div className="h-full w-full rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 p-4 shadow-sm transition-all duration-200 hover:shadow-md">
{/* 라벨이 있는 경우 표시 (데이터 테이블 제외) */}
{shouldShowLabel && (
<div className="block mb-3" style={labelStyle}>
<div className="inline-flex items-center bg-gray-100 px-3 py-1 rounded-lg text-sm font-semibold">
{labelText}
{component.required && <span style={{ color: "#f97316", marginLeft: "4px" }}>*</span>}
<div className="h-full w-full">
{/* 라벨이 있는 경우 표시 (데이터 테이블 제외) */}
{shouldShowLabel && (
<div className="block mb-3" style={labelStyle}>
<div className="inline-flex items-center bg-gray-100 px-3 py-1 rounded-lg text-sm font-semibold">
{labelText}
{component.required && <span style={{ color: "#f97316", marginLeft: "4px" }}>*</span>}
</div>
</div>
</div>
)}
)}
{/* 실제 위젯 */}
<div className="flex-1 rounded-lg overflow-hidden">{renderInteractiveWidget(component)}</div>
<div className="h-full w-full">{renderInteractiveWidget(component)}</div>
</div>
{/* 개선된 검증 패널 (선택적 표시) */}

View File

@ -532,19 +532,11 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
return (
<>
<div className="absolute" style={componentStyle}>
<div className="h-full w-full rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 p-4 shadow-sm transition-all duration-200 hover:shadow-md">
{/* 라벨 표시 - 컴포넌트 내부에서 라벨을 처리하므로 외부에서는 표시하지 않음 */}
{!hideLabel && component.label && component.style?.labelDisplay === false && (
<div className="mb-3">
<div className="inline-flex items-center bg-gray-100 px-3 py-1 rounded-lg text-sm font-semibold text-gray-700">
{component.label}
{(component as WidgetComponent).required && <span className="ml-1 text-orange-500">*</span>}
</div>
</div>
)}
<div className="h-full w-full">
{/* 라벨 숨김 - 모달에서는 라벨을 표시하지 않음 */}
{/* 위젯 렌더링 */}
<div className="flex-1 rounded-lg overflow-hidden">{renderInteractiveWidget(component)}</div>
<div className="h-full w-full">{renderInteractiveWidget(component)}</div>
</div>
</div>

View File

@ -292,15 +292,23 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
};
if (typeof window !== 'undefined') {
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
// 전역 강제 업데이트 함수 등록
if (!(window as any).forceRealtimePreviewUpdate) {
(window as any).forceRealtimePreviewUpdate = forceUpdate;
try {
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
// 전역 강제 업데이트 함수 등록
if (!(window as any).forceRealtimePreviewUpdate) {
(window as any).forceRealtimePreviewUpdate = forceUpdate;
}
} catch (error) {
console.warn("RealtimePreview 이벤트 리스너 등록 실패:", error);
}
return () => {
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
try {
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
} catch (error) {
console.warn("RealtimePreview 이벤트 리스너 제거 실패:", error);
}
};
}
}, [component.id, fileUpdateTrigger]);

View File

@ -600,55 +600,71 @@ export const FileComponentConfigPanel: React.FC<FileComponentConfigPanelProps> =
// 🎯 RealtimePreview 동기화를 위한 전역 이벤트 발생
if (typeof window !== 'undefined') {
const eventDetail = {
componentId: component.id,
files: updatedFiles,
fileCount: updatedFiles.length,
action: 'delete',
timestamp: timestamp,
source: 'designMode' // 🎯 화면설계 모드에서 온 이벤트임을 표시
};
console.log("🚀🚀🚀 FileComponentConfigPanel 삭제 이벤트 발생:", eventDetail);
const event = new CustomEvent('globalFileStateChanged', {
detail: eventDetail
});
window.dispatchEvent(event);
console.log("✅✅✅ globalFileStateChanged 삭제 이벤트 발생 완료");
// 추가 지연 이벤트들
setTimeout(() => {
console.log("🔄 추가 삭제 이벤트 발생 (지연 100ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true }
}));
}, 100);
setTimeout(() => {
console.log("🔄 추가 삭제 이벤트 발생 (지연 300ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true, attempt: 2 }
}));
}, 300);
try {
const eventDetail = {
componentId: component.id,
files: updatedFiles,
fileCount: updatedFiles.length,
action: 'delete',
timestamp: timestamp,
source: 'designMode' // 🎯 화면설계 모드에서 온 이벤트임을 표시
};
console.log("🚀🚀🚀 FileComponentConfigPanel 삭제 이벤트 발생:", eventDetail);
const event = new CustomEvent('globalFileStateChanged', {
detail: eventDetail
});
window.dispatchEvent(event);
console.log("✅✅✅ globalFileStateChanged 삭제 이벤트 발생 완료");
// 추가 지연 이벤트들
setTimeout(() => {
try {
console.log("🔄 추가 삭제 이벤트 발생 (지연 100ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true }
}));
} catch (error) {
console.warn("FileComponentConfigPanel 지연 이벤트 발생 실패:", error);
}
}, 100);
setTimeout(() => {
try {
console.log("🔄 추가 삭제 이벤트 발생 (지연 300ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true, attempt: 2 }
}));
} catch (error) {
console.warn("FileComponentConfigPanel 지연 이벤트 발생 실패:", error);
}
}, 300);
} catch (error) {
console.warn("FileComponentConfigPanel 이벤트 발생 실패:", error);
}
// 그리드 파일 상태 새로고침 이벤트도 유지
const tableName = currentTableName || 'screen_files';
const recordId = component.id;
const columnName = component.columnName || component.id || 'file_attachment';
const targetObjid = `${tableName}:${recordId}:${columnName}`;
const refreshEvent = new CustomEvent('refreshFileStatus', {
detail: {
tableName: tableName,
recordId: recordId,
columnName: columnName,
targetObjid: targetObjid,
fileCount: updatedFiles.length
}
});
window.dispatchEvent(refreshEvent);
try {
const tableName = currentTableName || 'screen_files';
const recordId = component.id;
const columnName = component.columnName || component.id || 'file_attachment';
const targetObjid = `${tableName}:${recordId}:${columnName}`;
const refreshEvent = new CustomEvent('refreshFileStatus', {
detail: {
tableName: tableName,
recordId: recordId,
columnName: columnName,
targetObjid: targetObjid,
fileCount: updatedFiles.length
}
});
window.dispatchEvent(refreshEvent);
} catch (error) {
console.warn("FileComponentConfigPanel refreshFileStatus 이벤트 발생 실패:", error);
}
console.log("🔄 FileComponentConfigPanel 파일 삭제 후 그리드 새로고침:", {
tableName,
recordId,

View File

@ -605,38 +605,50 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
// 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생
if (typeof window !== 'undefined') {
const eventDetail = {
componentId: component.id,
files: filteredFiles,
fileCount: filteredFiles.length,
action: 'delete',
timestamp: Date.now(),
source: 'realScreen' // 실제 화면에서 온 이벤트임을 표시
};
console.log("🚀🚀🚀 FileUpload 위젯 삭제 이벤트 발생:", eventDetail);
const event = new CustomEvent('globalFileStateChanged', {
detail: eventDetail
});
window.dispatchEvent(event);
console.log("✅✅✅ FileUpload 위젯 → 화면설계 모드 동기화 이벤트 발생 완료");
// 추가 지연 이벤트들
setTimeout(() => {
console.log("🔄 FileUpload 위젯 추가 삭제 이벤트 발생 (지연 100ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true }
}));
}, 100);
setTimeout(() => {
console.log("🔄 FileUpload 위젯 추가 삭제 이벤트 발생 (지연 300ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true, attempt: 2 }
}));
}, 300);
try {
const eventDetail = {
componentId: component.id,
files: filteredFiles,
fileCount: filteredFiles.length,
action: 'delete',
timestamp: Date.now(),
source: 'realScreen' // 실제 화면에서 온 이벤트임을 표시
};
console.log("🚀🚀🚀 FileUpload 위젯 삭제 이벤트 발생:", eventDetail);
const event = new CustomEvent('globalFileStateChanged', {
detail: eventDetail
});
window.dispatchEvent(event);
console.log("✅✅✅ FileUpload 위젯 → 화면설계 모드 동기화 이벤트 발생 완료");
// 추가 지연 이벤트들
setTimeout(() => {
try {
console.log("🔄 FileUpload 위젯 추가 삭제 이벤트 발생 (지연 100ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true }
}));
} catch (error) {
console.warn("FileUpload 지연 이벤트 발생 실패:", error);
}
}, 100);
setTimeout(() => {
try {
console.log("🔄 FileUpload 위젯 추가 삭제 이벤트 발생 (지연 300ms)");
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
detail: { ...eventDetail, delayed: true, attempt: 2 }
}));
} catch (error) {
console.warn("FileUpload 지연 이벤트 발생 실패:", error);
}
}, 300);
} catch (error) {
console.warn("FileUpload 이벤트 발생 실패:", error);
}
}
onUpdateComponent({

View File

@ -51,11 +51,11 @@ function SelectContent({
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Portal container={document.querySelector('[data-radix-portal]') || document.body}>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-[99999] max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-[10000] max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,