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 0b787b4c4c
7 changed files with 142 additions and 112 deletions

View File

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

View File

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

View File

@ -532,19 +532,11 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
return ( return (
<> <>
<div className="absolute" style={componentStyle}> <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"> <div className="h-full w-full">
{/* 라벨 표시 - 컴포넌트 내부에서 라벨을 처리하므로 외부에서는 표시하지 않음 */} {/* 라벨 숨김 - 모달에서는 라벨을 표시하지 않음 */}
{!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="flex-1 rounded-lg overflow-hidden">{renderInteractiveWidget(component)}</div> <div className="h-full w-full">{renderInteractiveWidget(component)}</div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -51,11 +51,11 @@ function SelectContent({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) { }: React.ComponentProps<typeof SelectPrimitive.Content>) {
return ( return (
<SelectPrimitive.Portal> <SelectPrimitive.Portal container={document.querySelector('[data-radix-portal]') || document.body}>
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot="select-content"
className={cn( 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" && 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", "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, className,