diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 5b7b68d5..ba0f721f 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -117,10 +117,10 @@ export default function ScreenViewPage() { if (loading) { return ( -
-
- -

화면을 불러오는 중...

+
+
+ +

화면을 불러오는 중...

); @@ -128,14 +128,14 @@ export default function ScreenViewPage() { if (error || !screen) { return ( -
-
-
- ⚠️ +
+
+
+ ⚠️
-

화면을 찾을 수 없습니다

-

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

-
@@ -148,17 +148,17 @@ export default function ScreenViewPage() { const screenHeight = layout?.screenResolution?.height || 800; return ( -
+
{layout && layout.components.length > 0 ? ( // 캔버스 컴포넌트들을 정확한 해상도로 표시
{layout.components @@ -178,15 +178,16 @@ export default function ScreenViewPage() { width: `${component.size.width}px`, height: `${component.size.height}px`, zIndex: component.position.z || 1, - backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.1)", - border: (component as any).border || "2px dashed #3b82f6", - borderRadius: (component as any).borderRadius || "8px", - padding: "16px", + backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.05)", + border: (component as any).border || "1px solid rgba(59, 130, 246, 0.2)", + borderRadius: (component as any).borderRadius || "12px", + padding: "20px", + boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", }} > {/* 그룹 제목 */} {(component as any).title && ( -
{(component as any).title}
+
{(component as any).title}
)} {/* 그룹 내 자식 컴포넌트들 렌더링 */} diff --git a/frontend/components/screen/FloatingPanel.tsx b/frontend/components/screen/FloatingPanel.tsx index e6d3241e..ca6f8a76 100644 --- a/frontend/components/screen/FloatingPanel.tsx +++ b/frontend/components/screen/FloatingPanel.tsx @@ -227,7 +227,7 @@ export const FloatingPanel: React.FC = ({
= ({
= ({

{title}

-
@@ -282,7 +282,7 @@ export const FloatingPanel: React.FC = ({ {/* 리사이즈 핸들 */} {resizable && !autoHeight && (
-
+
)}
diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index eaa3d6d7..9ca617e3 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -37,6 +37,7 @@ import { RotateCw, Folder, FolderOpen, + Grid, } from "lucide-react"; import { tableTypeApi } from "@/lib/api/screen"; import { commonCodeApi } from "@/lib/api/commonCode"; @@ -1721,7 +1722,7 @@ export const InteractiveDataTable: React.FC = ({ }; return ( -
+
{/* 헤더 */}
@@ -1811,7 +1812,8 @@ export const InteractiveDataTable: React.FC = ({
{visibleColumns.length > 0 ? ( <> - +
+
{/* 체크박스 컬럼 (삭제 기능이 활성화된 경우) */} @@ -1826,7 +1828,7 @@ export const InteractiveDataTable: React.FC = ({ {visibleColumns.map((column: DataTableColumn) => ( {column.label} @@ -1850,7 +1852,7 @@ export const InteractiveDataTable: React.FC = ({ ) : data.length > 0 ? ( data.map((row, rowIndex) => ( - + {/* 체크박스 셀 (삭제 기능이 활성화된 경우) */} {component.enableDelete && ( @@ -1861,7 +1863,7 @@ export const InteractiveDataTable: React.FC = ({ )} {visibleColumns.map((column: DataTableColumn) => ( - + {formatCellValue(row[column.columnName], column, row)} ))} @@ -1884,10 +1886,11 @@ export const InteractiveDataTable: React.FC = ({ )}
+
{/* 페이지네이션 */} {component.pagination?.enabled && totalPages > 1 && ( -
+
{component.pagination.showPageInfo && (
diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index bbaa42d3..2b2e2b52 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -1726,17 +1726,19 @@ export const InteractiveScreenViewer: React.FC = ( return ( <> -
+
{/* 라벨이 있는 경우 표시 (데이터 테이블 제외) */} {shouldShowLabel && ( -
- {labelText} - {component.required && *} +
+
+ {labelText} + {component.required && *} +
)} {/* 실제 위젯 */} -
{renderInteractiveWidget(component)}
+
{renderInteractiveWidget(component)}
{/* 개선된 검증 패널 (선택적 표시) */} diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 02ea6fb7..e182afa2 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -440,13 +440,18 @@ export const InteractiveScreenViewerDynamic: React.FC { - console.log("📝 파일 업로드 완료:", data); + console.log("📝 실제 화면 파일 업로드 완료:", data); if (onFormDataChange) { Object.entries(data).forEach(([key, value]) => { onFormDataChange(key, value); @@ -454,11 +459,57 @@ export const InteractiveScreenViewerDynamic: React.FC { - console.log("🔄 파일 컴포넌트 업데이트:", updates); - // 파일 업로드 완료 시 formData 업데이터 + console.log("🔄🔄🔄 실제 화면 파일 컴포넌트 업데이트:", { + componentId: comp.id, + hasUploadedFiles: !!updates.uploadedFiles, + filesCount: updates.uploadedFiles?.length || 0, + hasLastFileUpdate: !!updates.lastFileUpdate, + updates + }); + + // 파일 업로드/삭제 완료 시 formData 업데이터 if (updates.uploadedFiles && onFormDataChange) { onFormDataChange(fieldName, updates.uploadedFiles); } + + // 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생 (업로드/삭제 모두) + if (updates.uploadedFiles !== undefined && typeof window !== 'undefined') { + // 업로드인지 삭제인지 판단 (lastFileUpdate가 있으면 변경사항 있음) + const action = updates.lastFileUpdate ? 'update' : 'sync'; + + const eventDetail = { + componentId: comp.id, + files: updates.uploadedFiles, + fileCount: updates.uploadedFiles.length, + action: action, + timestamp: updates.lastFileUpdate || Date.now(), + source: 'realScreen' // 실제 화면에서 온 이벤트임을 표시 + }; + + console.log("🚀🚀🚀 실제 화면 파일 변경 이벤트 발생:", eventDetail); + + const event = new CustomEvent('globalFileStateChanged', { + detail: eventDetail + }); + window.dispatchEvent(event); + + console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 이벤트 발생 완료"); + + // 추가 지연 이벤트들 (화면설계 모드가 열려있을 때를 대비) + setTimeout(() => { + console.log("🔄 실제 화면 추가 이벤트 발생 (지연 100ms)"); + window.dispatchEvent(new CustomEvent('globalFileStateChanged', { + detail: { ...eventDetail, delayed: true } + })); + }, 100); + + setTimeout(() => { + console.log("🔄 실제 화면 추가 이벤트 발생 (지연 500ms)"); + window.dispatchEvent(new CustomEvent('globalFileStateChanged', { + detail: { ...eventDetail, delayed: true, attempt: 2 } + })); + }, 500); + } }} />
@@ -481,19 +532,19 @@ export const InteractiveScreenViewerDynamic: React.FC
-
+
{/* 라벨 표시 - 컴포넌트 내부에서 라벨을 처리하므로 외부에서는 표시하지 않음 */} {!hideLabel && component.label && component.style?.labelDisplay === false && ( -
-
diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index f58f3ab7..9ed0c2e9 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -127,31 +127,15 @@ const WidgetRenderer: React.FC<{ component: ComponentData }> = ({ component }) = className: `w-full h-full ${borderClass}`, }; - // 파일 컴포넌트 강제 체크 + // 파일 컴포넌트는 별도 로직에서 처리하므로 여기서는 제외 if (isFileComponent(widget)) { - console.log("🎯 RealtimePreview - 파일 컴포넌트 강제 감지:", { + console.log("🎯 RealtimePreview - 파일 컴포넌트 감지 (별도 처리):", { componentId: widget.id, widgetType: widgetType, isFileComponent: true }); - try { - return ( - - ); - } catch (error) { - console.error(`파일 웹타입 렌더링 실패:`, error); - return
파일 컴포넌트 (렌더링 오류)
; - } + return
파일 컴포넌트 (별도 렌더링)
; } // 동적 웹타입 렌더링 사용 @@ -242,24 +226,84 @@ export const RealtimePreviewDynamic: React.FC = ({ // 전역 파일 상태 변경 감지 (해당 컴포넌트만) useEffect(() => { const handleGlobalFileStateChange = (event: CustomEvent) => { + console.log("🎯🎯🎯 RealtimePreview 이벤트 수신:", { + eventComponentId: event.detail.componentId, + currentComponentId: component.id, + isMatch: event.detail.componentId === component.id, + filesCount: event.detail.files?.length || 0, + action: event.detail.action, + delayed: event.detail.delayed || false, + attempt: event.detail.attempt || 1, + eventDetail: event.detail + }); + if (event.detail.componentId === component.id) { - console.log("🔄 RealtimePreview 파일 상태 변경 감지:", { + console.log("✅✅✅ RealtimePreview 파일 상태 변경 감지 - 리렌더링 시작:", { componentId: component.id, filesCount: event.detail.files?.length || 0, - action: event.detail.action + action: event.detail.action, + oldTrigger: fileUpdateTrigger, + delayed: event.detail.delayed || false, + attempt: event.detail.attempt || 1 + }); + setFileUpdateTrigger(prev => { + const newTrigger = prev + 1; + console.log("🔄🔄🔄 fileUpdateTrigger 업데이트:", { + old: prev, + new: newTrigger, + componentId: component.id, + attempt: event.detail.attempt || 1 + }); + return newTrigger; + }); + } else { + console.log("❌ 컴포넌트 ID 불일치:", { + eventComponentId: event.detail.componentId, + currentComponentId: component.id + }); + } + }; + + // 강제 업데이트 함수 등록 + const forceUpdate = (componentId: string, files: any[]) => { + console.log("🔥🔥🔥 RealtimePreview 강제 업데이트 호출:", { + targetComponentId: componentId, + currentComponentId: component.id, + isMatch: componentId === component.id, + filesCount: files.length + }); + + if (componentId === component.id) { + console.log("✅✅✅ RealtimePreview 강제 업데이트 적용:", { + componentId: component.id, + filesCount: files.length, + oldTrigger: fileUpdateTrigger + }); + setFileUpdateTrigger(prev => { + const newTrigger = prev + 1; + console.log("🔄🔄🔄 강제 fileUpdateTrigger 업데이트:", { + old: prev, + new: newTrigger, + componentId: component.id + }); + return newTrigger; }); - setFileUpdateTrigger(prev => prev + 1); } }; if (typeof window !== 'undefined') { window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); + // 전역 강제 업데이트 함수 등록 + if (!(window as any).forceRealtimePreviewUpdate) { + (window as any).forceRealtimePreviewUpdate = forceUpdate; + } + return () => { window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); }; } - }, [component.id]); + }, [component.id, fileUpdateTrigger]); // 컴포넌트 스타일 계산 const componentStyle = { @@ -350,8 +394,8 @@ export const RealtimePreviewDynamic: React.FC = ({
)} - {/* 위젯 타입 - 동적 렌더링 */} - {type === "widget" && ( + {/* 위젯 타입 - 동적 렌더링 (파일 컴포넌트 제외) */} + {type === "widget" && !isFileComponent(component) && (
@@ -383,7 +427,7 @@ export const RealtimePreviewDynamic: React.FC = ({ }); return ( -
+
{currentFiles.length > 0 ? (
diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 6dbd9236..f54af1f3 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -754,33 +754,61 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD if (response.success && response.componentFiles) { console.log("📁 복원할 파일 데이터:", response.componentFiles); - // 각 컴포넌트별로 파일 복원 - Object.entries(response.componentFiles).forEach(([componentId, files]) => { - if (Array.isArray(files) && files.length > 0) { - // 전역 상태에 파일 저장 + // 각 컴포넌트별로 파일 복원 (전역 상태와 localStorage 우선 적용) + Object.entries(response.componentFiles).forEach(([componentId, serverFiles]) => { + if (Array.isArray(serverFiles) && serverFiles.length > 0) { + // 🎯 전역 상태와 localStorage에서 현재 파일 상태 확인 const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {}; - globalFileState[componentId] = files; + const currentGlobalFiles = globalFileState[componentId] || []; + + let currentLocalStorageFiles: any[] = []; + if (typeof window !== 'undefined') { + try { + const storedFiles = localStorage.getItem(`fileComponent_${componentId}_files`); + if (storedFiles) { + currentLocalStorageFiles = JSON.parse(storedFiles); + } + } catch (e) { + console.warn("localStorage 파일 파싱 실패:", e); + } + } + + // 🎯 우선순위: 전역 상태 > localStorage > 서버 데이터 + let finalFiles = serverFiles; + if (currentGlobalFiles.length > 0) { + finalFiles = currentGlobalFiles; + console.log(`📂 컴포넌트 ${componentId} 전역 상태 우선 적용:`, finalFiles.length, "개"); + } else if (currentLocalStorageFiles.length > 0) { + finalFiles = currentLocalStorageFiles; + console.log(`📂 컴포넌트 ${componentId} localStorage 우선 적용:`, finalFiles.length, "개"); + } else { + console.log(`📂 컴포넌트 ${componentId} 서버 데이터 적용:`, finalFiles.length, "개"); + } + + // 전역 상태에 파일 저장 + globalFileState[componentId] = finalFiles; if (typeof window !== 'undefined') { (window as any).globalFileState = globalFileState; } // localStorage에도 백업 if (typeof window !== 'undefined') { - localStorage.setItem(`fileComponent_${componentId}_files`, JSON.stringify(files)); + localStorage.setItem(`fileComponent_${componentId}_files`, JSON.stringify(finalFiles)); } - - console.log(`📂 컴포넌트 ${componentId} 파일 복원:`, files.length, "개"); } }); - // 레이아웃의 컴포넌트들에 파일 정보 적용 + // 레이아웃의 컴포넌트들에 파일 정보 적용 (전역 상태 우선) setLayout(prevLayout => { const updatedComponents = prevLayout.components.map(comp => { - const componentFiles = response.componentFiles[comp.id]; - if (componentFiles && componentFiles.length > 0) { + // 🎯 전역 상태에서 최신 파일 정보 가져오기 + const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {}; + const finalFiles = globalFileState[comp.id] || []; + + if (finalFiles.length > 0) { return { ...comp, - uploadedFiles: componentFiles, + uploadedFiles: finalFiles, lastFileUpdate: Date.now() }; } @@ -3368,7 +3396,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } return ( -
+
{/* 상단 툴바 */} {/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */} -
+
{/* 해상도 정보 표시 - 적당한 여백 */}
diff --git a/frontend/components/screen/panels/ComponentsPanel.tsx b/frontend/components/screen/panels/ComponentsPanel.tsx index a779d484..747bf9eb 100644 --- a/frontend/components/screen/panels/ComponentsPanel.tsx +++ b/frontend/components/screen/panels/ComponentsPanel.tsx @@ -125,126 +125,145 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) { }; return ( - - - -
- - 컴포넌트 ({componentsByCategory.all.length}) +
+
+ {/* 헤더 */} +
+
+

컴포넌트

+

{componentsByCategory.all.length}개의 사용 가능한 컴포넌트

- - +
{/* 검색창 */} -
- +
+ setSearchQuery(e.target.value)} - className="pl-8" + className="pl-10 border-0 bg-white/80 backdrop-blur-sm shadow-sm focus:bg-white transition-colors" />
- +
- +
setSelectedCategory(value as ComponentCategory | "all")} > {/* 카테고리 탭 (input 카테고리 제외) */} - - + + 전체 - + 표시 - + 액션 - + 레이아웃 - + - 유틸 + 유틸리티 {/* 컴포넌트 목록 */} -
- +
+ {filteredComponents.length > 0 ? ( -
+
{filteredComponents.map((component) => (
handleDragStart(e, component)} - className="hover:bg-accent flex cursor-grab items-center rounded-lg border p-3 transition-colors active:cursor-grabbing" + onDragStart={(e) => { + handleDragStart(e, component); + // 드래그 시작 시 시각적 피드백 + e.currentTarget.style.opacity = '0.5'; + e.currentTarget.style.transform = 'rotate(-3deg) scale(0.95)'; + }} + onDragEnd={(e) => { + // 드래그 종료 시 원래 상태로 복원 + e.currentTarget.style.opacity = '1'; + e.currentTarget.style.transform = 'none'; + }} + className="group cursor-grab rounded-lg border border-gray-200/40 bg-white/90 backdrop-blur-sm p-5 shadow-sm transition-all duration-300 hover:bg-white hover:shadow-lg hover:shadow-purple-500/15 hover:scale-[1.02] hover:border-purple-300/60 hover:-translate-y-1 active:cursor-grabbing active:scale-[0.98] active:translate-y-0" title={component.description} > -
-
-

{component.name}

-
- {/* 카테고리 뱃지 */} - - {getCategoryIcon(component.category)} - {component.category} - - - {/* 새 컴포넌트 뱃지 */} - +
+
+ {getCategoryIcon(component.category)} +
+
+
+

{component.name}

+ 신규
-
-

{component.description}

+

{component.description}

- {/* 웹타입 및 크기 정보 */} -
- 웹타입: {component.webType} - - {component.defaultSize.width}×{component.defaultSize.height} - -
- - {/* 태그 */} - {component.tags && component.tags.length > 0 && ( -
- {component.tags.slice(0, 3).map((tag, index) => ( - - {tag} - - ))} - {component.tags.length > 3 && ( - - +{component.tags.length - 3} - - )} +
+
+ + {component.defaultSize.width}×{component.defaultSize.height} + +
+ + {component.category} +
- )} + + {/* 태그 */} + {component.tags && component.tags.length > 0 && ( +
+ {component.tags.slice(0, 2).map((tag, index) => ( + + {tag} + + ))} + {component.tags.length > 2 && ( + + +{component.tags.length - 2} + + )} +
+ )} +
))}
) : ( -
- -

- {searchQuery - ? `"${searchQuery}"에 대한 검색 결과가 없습니다.` - : "이 카테고리에 컴포넌트가 없습니다."} -

+
+
+ +

+ {searchQuery + ? `"${searchQuery}"에 대한 컴포넌트를 찾을 수 없습니다` + : "이 카테고리에 컴포넌트가 없습니다"} +

+

검색어나 필터를 조정해보세요

+
)} @@ -252,31 +271,40 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) { {/* 통계 정보 */} -
+
-
{filteredComponents.length}
-
표시된 컴포넌트
+
{filteredComponents.length}
+
필터됨
-
{allComponents.length}
-
전체 컴포넌트
+
{allComponents.length}
+
전체
{/* 개발 정보 (개발 모드에서만) */} {process.env.NODE_ENV === "development" && ( -
-
-
🔧 레지스트리 기반 시스템
-
⚡ Hot Reload 지원
-
🛡️ 완전한 타입 안전성
+
+
+
+ + 레지스트리 기반 시스템 +
+
+ + Hot Reload 지원 +
+
+ + 완전한 타입 안전성 +
)} - - +
+
); } diff --git a/frontend/components/screen/panels/FileComponentConfigPanel.tsx b/frontend/components/screen/panels/FileComponentConfigPanel.tsx index b07f7d37..91057fdb 100644 --- a/frontend/components/screen/panels/FileComponentConfigPanel.tsx +++ b/frontend/components/screen/panels/FileComponentConfigPanel.tsx @@ -28,6 +28,13 @@ export const FileComponentConfigPanel: React.FC = currentTable, currentTableName, }) => { + console.log("🎨🎨🎨 FileComponentConfigPanel 렌더링:", { + componentId: component?.id, + componentType: component?.type, + hasOnUpdateProperty: !!onUpdateProperty, + currentTable, + currentTableName + }); // fileConfig가 없는 경우 초기화 React.useEffect(() => { if (!component.fileConfig) { @@ -112,13 +119,18 @@ export const FileComponentConfigPanel: React.FC = const componentFiles = component.uploadedFiles || []; const globalFiles = getGlobalFileState()[component.id] || []; - // localStorage 백업에서 복원 (영구 저장된 파일 + 임시 파일) + // localStorage 백업에서 복원 (영구 저장된 파일 + 임시 파일 + FileUploadComponent 백업) const backupKey = `fileComponent_${component.id}_files`; const tempBackupKey = `fileComponent_${component.id}_files_temp`; + const fileUploadBackupKey = `fileUpload_${component.id}`; // 🎯 실제 화면과 동기화 + const backupFiles = localStorage.getItem(backupKey); const tempBackupFiles = localStorage.getItem(tempBackupKey); + const fileUploadBackupFiles = localStorage.getItem(fileUploadBackupKey); // 🎯 실제 화면 백업 + let parsedBackupFiles: FileInfo[] = []; let parsedTempFiles: FileInfo[] = []; + let parsedFileUploadFiles: FileInfo[] = []; // 🎯 실제 화면 파일 if (backupFiles) { try { @@ -136,8 +148,18 @@ export const FileComponentConfigPanel: React.FC = } } - // 우선순위: 전역 상태 > localStorage > 임시 파일 > 컴포넌트 속성 + // 🎯 실제 화면 FileUploadComponent 백업 파싱 + if (fileUploadBackupFiles) { + try { + parsedFileUploadFiles = JSON.parse(fileUploadBackupFiles); + } catch (error) { + console.error("FileUploadComponent 백업 파일 파싱 실패:", error); + } + } + + // 🎯 우선순위: 전역 상태 > FileUploadComponent 백업 > localStorage > 임시 파일 > 컴포넌트 속성 const finalFiles = globalFiles.length > 0 ? globalFiles : + parsedFileUploadFiles.length > 0 ? parsedFileUploadFiles : // 🎯 실제 화면 우선 parsedBackupFiles.length > 0 ? parsedBackupFiles : parsedTempFiles.length > 0 ? parsedTempFiles : componentFiles; @@ -148,8 +170,12 @@ export const FileComponentConfigPanel: React.FC = globalFiles: globalFiles.length, backupFiles: parsedBackupFiles.length, tempFiles: parsedTempFiles.length, + fileUploadFiles: parsedFileUploadFiles.length, // 🎯 실제 화면 파일 수 finalFiles: finalFiles.length, - source: globalFiles.length > 0 ? 'global' : parsedBackupFiles.length > 0 ? 'localStorage' : parsedTempFiles.length > 0 ? 'temp' : 'component' + source: globalFiles.length > 0 ? 'global' : + parsedFileUploadFiles.length > 0 ? 'fileUploadComponent' : // 🎯 실제 화면 소스 + parsedBackupFiles.length > 0 ? 'localStorage' : + parsedTempFiles.length > 0 ? 'temp' : 'component' }); return finalFiles; @@ -190,7 +216,17 @@ export const FileComponentConfigPanel: React.FC = // 파일 업로드 처리 const handleFileUpload = useCallback(async (files: FileList | File[]) => { - if (!files || files.length === 0) return; + console.log("🚀🚀🚀 FileComponentConfigPanel 파일 업로드 시작:", { + filesCount: files?.length || 0, + componentId: component?.id, + componentType: component?.type, + hasOnUpdateProperty: !!onUpdateProperty + }); + + if (!files || files.length === 0) { + console.log("❌ 파일이 없음"); + return; + } const fileArray = Array.from(files); const validFiles: File[] = []; @@ -291,23 +327,49 @@ export const FileComponentConfigPanel: React.FC = setUploading(true); toast.loading(`${filesToUpload.length}개 파일 업로드 중...`); - // 그리드와 연동되는 targetObjid 생성 (화면 복원 시스템과 통일) - const tableName = 'screen_files'; - const screenId = (window as any).__CURRENT_SCREEN_ID__ || 'unknown'; // 현재 화면 ID + // 🎯 여러 방법으로 screenId 확인 + let screenId = (window as any).__CURRENT_SCREEN_ID__; + + // 1차: 전역 변수에서 가져오기 + if (!screenId) { + // 2차: URL에서 추출 시도 + if (typeof window !== 'undefined' && window.location.pathname.includes('/screens/')) { + const pathScreenId = window.location.pathname.split('/screens/')[1]; + if (pathScreenId && !isNaN(parseInt(pathScreenId))) { + screenId = parseInt(pathScreenId); + } + } + } + + // 3차: 기본값 설정 + if (!screenId) { + screenId = 40; // 기본 화면 ID (디자인 모드용) + console.warn("⚠️ screenId를 찾을 수 없어 기본값(40) 사용"); + } + const componentId = component.id; const fieldName = component.columnName || component.id || 'file_attachment'; - const targetObjid = `${tableName}:${screenId}:${componentId}:${fieldName}`; + + console.log("📋 파일 업로드 기본 정보:", { + screenId, + screenIdSource: (window as any).__CURRENT_SCREEN_ID__ ? 'global' : 'url_or_default', + componentId, + fieldName, + docType: localInputs.docType, + docTypeName: localInputs.docTypeName, + currentPath: typeof window !== 'undefined' ? window.location.pathname : 'unknown' + }); const response = await uploadFiles({ files: filesToUpload, - tableName: tableName, - fieldName: fieldName, - recordId: `screen_${screenId}:${componentId}`, // 템플릿 파일 형태 + // 🎯 백엔드 API가 기대하는 정확한 형식으로 설정 + autoLink: true, // 자동 연결 활성화 + linkedTable: 'screen_files', // 연결 테이블 + recordId: screenId, // 레코드 ID + columnName: fieldName, // 컬럼명 + isVirtualFileColumn: true, // 가상 파일 컬럼 docType: localInputs.docType, docTypeName: localInputs.docTypeName, - targetObjid: targetObjid, // 그리드 연동을 위한 targetObjid - columnName: fieldName, - isVirtualFileColumn: true, // 가상 파일 컬럼으로 처리 }); console.log("📤 파일 업로드 응답:", response); @@ -360,15 +422,61 @@ export const FileComponentConfigPanel: React.FC = // 전역 파일 상태 변경 이벤트 발생 (RealtimePreview 업데이트용) if (typeof window !== 'undefined') { + const eventDetail = { + componentId: component.id, + files: updatedFiles, + fileCount: updatedFiles.length, + action: 'upload', + timestamp: Date.now(), + source: 'designMode' // 🎯 화면설계 모드에서 온 이벤트임을 표시 + }; + + console.log("🚀🚀🚀 FileComponentConfigPanel 이벤트 발생:", eventDetail); + console.log("🔍 현재 컴포넌트 ID:", component.id); + console.log("🔍 업로드된 파일 수:", updatedFiles.length); + console.log("🔍 파일 목록:", updatedFiles.map(f => f.name)); + const event = new CustomEvent('globalFileStateChanged', { - detail: { - componentId: component.id, - files: updatedFiles, - action: 'upload', - timestamp: Date.now() - } + detail: eventDetail }); + + // 이벤트 리스너가 있는지 확인 + const listenerCount = window.getEventListeners ? + window.getEventListeners(window)?.globalFileStateChanged?.length || 0 : + 'unknown'; + console.log("🔍 globalFileStateChanged 리스너 수:", listenerCount); + window.dispatchEvent(event); + + console.log("✅✅✅ globalFileStateChanged 이벤트 발생 완료"); + + // 강제로 모든 RealtimePreview 컴포넌트에게 알림 (여러 번) + 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); + + setTimeout(() => { + console.log("🔄 추가 이벤트 발생 (지연 500ms)"); + window.dispatchEvent(new CustomEvent('globalFileStateChanged', { + detail: { ...eventDetail, delayed: true, attempt: 3 } + })); + }, 500); + + // 직접 전역 상태 강제 업데이트 + console.log("🔄 전역 상태 강제 업데이트 시도"); + if ((window as any).forceRealtimePreviewUpdate) { + (window as any).forceRealtimePreviewUpdate(component.id, updatedFiles); + } } console.log("🔄 FileComponentConfigPanel 자동 저장:", { @@ -417,10 +525,18 @@ export const FileComponentConfigPanel: React.FC = } else { throw new Error(response.message || '파일 업로드에 실패했습니다.'); } - } catch (error) { - console.error('❌ 파일 업로드 오류:', error); + } catch (error: any) { + console.error('❌ 파일 업로드 오류:', { + error, + errorMessage: error?.message, + errorResponse: error?.response?.data, + errorStatus: error?.response?.status, + componentId: component?.id, + screenId, + fieldName + }); toast.dismiss(); - toast.error('파일 업로드에 실패했습니다.'); + toast.error(`파일 업로드에 실패했습니다: ${error?.message || '알 수 없는 오류'}`); } finally { console.log("🏁 파일 업로드 완료, 로딩 상태 해제"); setUploading(false); @@ -444,8 +560,17 @@ export const FileComponentConfigPanel: React.FC = // 파일 삭제 처리 const handleFileDelete = useCallback(async (fileId: string) => { + console.log("🗑️🗑️🗑️ FileComponentConfigPanel 파일 삭제 시작:", { + fileId, + componentId: component?.id, + currentFilesCount: uploadedFiles.length, + hasOnUpdateProperty: !!onUpdateProperty + }); + try { + console.log("📡 deleteFile API 호출 시작..."); await deleteFile(fileId, 'temp_record'); + console.log("✅ deleteFile API 호출 성공"); const updatedFiles = uploadedFiles.filter(file => file.objid !== fileId && file.id !== fileId); setUploadedFiles(updatedFiles); @@ -473,8 +598,42 @@ export const FileComponentConfigPanel: React.FC = timestamp: timestamp }); - // 그리드 파일 상태 새로고침 이벤트 발생 - if (typeof window !== 'undefined') { + // 🎯 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); + + // 그리드 파일 상태 새로고침 이벤트도 유지 const tableName = currentTableName || 'screen_files'; const recordId = component.id; const columnName = component.columnName || component.id || 'file_attachment'; @@ -557,12 +716,22 @@ export const FileComponentConfigPanel: React.FC = e.preventDefault(); setDragOver(false); const files = e.dataTransfer.files; + console.log("📂 드래그앤드롭 이벤트:", { + filesCount: files.length, + files: files.length > 0 ? Array.from(files).map(f => f.name) : [], + componentId: component?.id + }); if (files.length > 0) { handleFileUpload(files); } - }, [handleFileUpload]); + }, [handleFileUpload, component?.id]); const handleFileSelect = useCallback((e: React.ChangeEvent) => { + console.log("📁 파일 선택 이벤트:", { + filesCount: e.target.files?.length || 0, + files: e.target.files ? Array.from(e.target.files).map(f => f.name) : [] + }); + const files = e.target.files; if (files && files.length > 0) { handleFileUpload(files); @@ -667,20 +836,49 @@ export const FileComponentConfigPanel: React.FC = // 전역 파일 상태 변경 감지 (화면 복원 포함) useEffect(() => { const handleGlobalFileStateChange = (event: CustomEvent) => { - const { componentId, files, fileCount, isRestore } = event.detail; + const { componentId, files, fileCount, isRestore, source } = event.detail; if (componentId === component.id) { console.log("🌐 FileComponentConfigPanel 전역 상태 변경 감지:", { componentId, fileCount, isRestore: !!isRestore, + source: source || 'unknown', files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName })) }); if (files && Array.isArray(files)) { setUploadedFiles(files); - if (isRestore) { + // 🎯 실제 화면에서 온 이벤트이거나 화면 복원인 경우 컴포넌트 속성도 업데이트 + if (isRestore || source === 'realScreen') { + console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 적용:", { + componentId, + fileCount: files.length, + source: source || 'restore' + }); + + onUpdateProperty(component.id, "uploadedFiles", files); + onUpdateProperty(component.id, "lastFileUpdate", Date.now()); + + // localStorage 백업도 업데이트 + try { + const backupKey = `fileComponent_${component.id}_files`; + localStorage.setItem(backupKey, JSON.stringify(files)); + console.log("💾 실제 화면 동기화 후 localStorage 백업 업데이트:", { + componentId: component.id, + fileCount: files.length + }); + } catch (e) { + console.warn("localStorage 백업 업데이트 실패:", e); + } + + // 전역 상태 업데이트 + setGlobalFileState(prev => ({ + ...prev, + [component.id]: files + })); + } else if (isRestore) { console.log("✅ 파일 컴포넌트 설정 패널 데이터 복원 완료:", { componentId, restoredFileCount: files.length @@ -697,7 +895,7 @@ export const FileComponentConfigPanel: React.FC = window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); }; } - }, [component.id]); + }, [component.id, onUpdateProperty]); // 미리 정의된 문서 타입들 const docTypeOptions = [ @@ -893,18 +1091,33 @@ export const FileComponentConfigPanel: React.FC = {/* 파일 업로드 영역 */}

파일 업로드

- - + +
!uploading && document.getElementById('file-input-config')?.click()} + onClick={() => { + console.log("🖱️ 파일 업로드 영역 클릭:", { + uploading, + inputElement: document.getElementById('file-input-config'), + componentId: component?.id + }); + if (!uploading) { + const input = document.getElementById('file-input-config'); + if (input) { + console.log("✅ 파일 input 클릭 실행"); + input.click(); + } else { + console.log("❌ 파일 input 요소를 찾을 수 없음"); + } + } + }} > +
{/* 헤더 */}
diff --git a/frontend/components/screen/panels/TemplatesPanel.tsx b/frontend/components/screen/panels/TemplatesPanel.tsx index 95e4d078..464450d5 100644 --- a/frontend/components/screen/panels/TemplatesPanel.tsx +++ b/frontend/components/screen/panels/TemplatesPanel.tsx @@ -487,16 +487,22 @@ export const TemplatesPanel: React.FC = ({ onDragStart }) = }); return ( -
+
+ {/* 헤더 */} +
+

템플릿

+

캔버스로 드래그하여 화면을 구성하세요

+
+ {/* 검색 */} -
+
setSearchTerm(e.target.value)} - className="pl-10" + className="pl-10 border-0 bg-white/80 backdrop-blur-sm shadow-sm focus:bg-white transition-colors" />
@@ -508,7 +514,13 @@ export const TemplatesPanel: React.FC = ({ onDragStart }) = variant={selectedCategory === category.id ? "default" : "outline"} size="sm" onClick={() => setSelectedCategory(category.id)} - className="flex items-center space-x-1" + className={` + flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all + ${selectedCategory === category.id + ? 'bg-blue-600 text-white shadow-sm hover:bg-blue-700' + : 'bg-white/60 text-gray-600 border-gray-200/60 hover:bg-white hover:text-gray-900 hover:border-gray-300' + } + `} > {category.icon} {category.name} @@ -517,23 +529,21 @@ export const TemplatesPanel: React.FC = ({ onDragStart }) =
- - {/* 새로고침 버튼 */} {error && ( -
+
템플릿 로딩 실패, 기본 템플릿 사용 중
-
)} {/* 템플릿 목록 */} -
+
{isLoading ? (
@@ -541,9 +551,10 @@ export const TemplatesPanel: React.FC = ({ onDragStart }) =
) : filteredTemplates.length === 0 ? (
-
- -

검색 결과가 없습니다

+
+ +

템플릿을 찾을 수 없습니다

+

검색어나 필터를 조정해보세요

) : ( @@ -551,27 +562,40 @@ export const TemplatesPanel: React.FC = ({ onDragStart }) =
onDragStart(e, template)} - className="group cursor-move rounded-lg border border-gray-200 bg-white p-3 shadow-sm transition-all hover:border-blue-300 hover:shadow-md" + onDragStart={(e) => { + onDragStart(e, template); + // 드래그 시작 시 시각적 피드백 + e.currentTarget.style.opacity = '0.6'; + e.currentTarget.style.transform = 'rotate(2deg) scale(0.98)'; + }} + onDragEnd={(e) => { + // 드래그 종료 시 원래 상태로 복원 + e.currentTarget.style.opacity = '1'; + e.currentTarget.style.transform = 'none'; + }} + className="group cursor-grab rounded-lg border border-gray-200/40 bg-white/90 backdrop-blur-sm p-5 shadow-sm transition-all duration-300 hover:bg-white hover:shadow-lg hover:shadow-blue-500/15 hover:scale-[1.02] hover:border-blue-300/60 hover:-translate-y-1 active:cursor-grabbing active:scale-[0.98] active:translate-y-0" > -
-
+
+
{template.icon}
-
-

{template.name}

- - {template.components.length}개 +
+

{template.name}

+ + {template.components.length}
-

{template.description}

-
- - {template.defaultSize.width}×{template.defaultSize.height} +

{template.description}

+
+
+ + {template.defaultSize.width}×{template.defaultSize.height} + +
+ + {template.category} - - {template.category}
@@ -581,12 +605,14 @@ export const TemplatesPanel: React.FC = ({ onDragStart }) =
{/* 도움말 */} -
-
- -
-

사용 방법

-

템플릿을 캔버스로 드래그하여 빠르게 화면을 구성하세요.

+
+
+
+ +
+
+

사용 방법

+

템플릿을 캔버스로 드래그하여 빠르게 화면을 구성하세요.

diff --git a/frontend/components/screen/widgets/FileUpload.tsx b/frontend/components/screen/widgets/FileUpload.tsx index 00f18cdc..2bddb598 100644 --- a/frontend/components/screen/widgets/FileUpload.tsx +++ b/frontend/components/screen/widgets/FileUpload.tsx @@ -504,6 +504,26 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf [component.id]: updatedFiles })); + // RealtimePreview 동기화를 위한 추가 이벤트 발생 + if (typeof window !== 'undefined') { + const eventDetail = { + componentId: component.id, + files: updatedFiles, + fileCount: updatedFiles.length, + action: 'upload', + timestamp: Date.now() + }; + + console.log("🚀 FileUpload 위젯 이벤트 발생:", eventDetail); + + const event = new CustomEvent('globalFileStateChanged', { + detail: eventDetail + }); + window.dispatchEvent(event); + + console.log("✅ FileUpload globalFileStateChanged 이벤트 발생 완료"); + } + // 컴포넌트 업데이트 (옵셔널) if (onUpdateComponent) { onUpdateComponent({ @@ -583,6 +603,42 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf [component.id]: filteredFiles })); + // 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생 + 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); + } + onUpdateComponent({ uploadedFiles: filteredFiles, }); @@ -635,8 +691,8 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
{/* 드래그 앤 드롭 영역 */}

또는 클릭하여 파일을 선택하세요

- diff --git a/frontend/components/screen/widgets/types/FileWidget.tsx b/frontend/components/screen/widgets/types/FileWidget.tsx index be88d9e8..4a36305b 100644 --- a/frontend/components/screen/widgets/types/FileWidget.tsx +++ b/frontend/components/screen/widgets/types/FileWidget.tsx @@ -134,7 +134,7 @@ export const FileWidget: React.FC = ({ component, value,
{/* 파일 업로드 영역 */}
= ({ ); } - // 컨테이너 스타일 (원래 카드 레이아웃과 완전히 동일) + // 컨테이너 스타일 - 통일된 디자인 시스템 적용 const containerStyle: React.CSSProperties = { display: "grid", gridTemplateColumns: `repeat(${componentConfig.cardsPerRow || 3}, 1fr)`, // 기본값 3 (한 행당 카드 수) gridAutoRows: "min-content", // 자동 행 생성으로 모든 데이터 표시 - gap: `${componentConfig.cardSpacing || 16}px`, - padding: "16px", + gap: `${componentConfig.cardSpacing || 32}px`, // 간격 대폭 증가로 여유로운 느낌 + padding: "32px", // 패딩 대폭 증가 width: "100%", height: "100%", - background: "transparent", + background: "linear-gradient(to br, #f8fafc, #f1f5f9)", // 부드러운 그라데이션 배경 overflow: "auto", + borderRadius: "12px", // 컨테이너 자체도 라운드 처리 }; - // 카드 스타일 (원래 카드 레이아웃과 완전히 동일) + // 카드 스타일 - 통일된 디자인 시스템 적용 const cardStyle: React.CSSProperties = { backgroundColor: "white", - border: "1px solid #e5e7eb", - borderRadius: "8px", - padding: "16px", - boxShadow: "0 1px 3px 0 rgba(0, 0, 0, 0.1)", - transition: "all 0.2s ease-in-out", + border: "1px solid #e2e8f0", // 더 부드러운 보더 색상 + borderRadius: "12px", // 통일된 라운드 처리 + padding: "24px", // 더 여유로운 패딩 + boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", // 더 깊은 그림자 + transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", // 부드러운 트랜지션 overflow: "hidden", display: "flex", flexDirection: "column", position: "relative", - minHeight: "200px", + minHeight: "240px", // 최소 높이 더 증가 cursor: isDesignMode ? "pointer" : "default", + // 호버 효과를 위한 추가 스타일 + "&:hover": { + transform: "translateY(-2px)", + boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)", + } }; // 텍스트 자르기 함수 @@ -386,53 +392,53 @@ export const CardDisplayComponent: React.FC = ({
handleCardClick(data)} > - {/* 카드 이미지 */} + {/* 카드 이미지 - 통일된 디자인 */} {componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && ( -
-
- 👤 +
+
+ 👤
)} - {/* 카드 타이틀 */} + {/* 카드 타이틀 - 통일된 디자인 */} {componentConfig.cardStyle?.showTitle && ( -
-

{titleValue}

+
+

{titleValue}

)} - {/* 카드 서브타이틀 */} + {/* 카드 서브타이틀 - 통일된 디자인 */} {componentConfig.cardStyle?.showSubtitle && ( -
-

{subtitleValue}

+
+

{subtitleValue}

)} - {/* 카드 설명 */} + {/* 카드 설명 - 통일된 디자인 */} {componentConfig.cardStyle?.showDescription && ( -
-

+

+

{truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)}

)} - {/* 추가 표시 컬럼들 */} + {/* 추가 표시 컬럼들 - 통일된 디자인 */} {componentConfig.columnMapping?.displayColumns && componentConfig.columnMapping.displayColumns.length > 0 && ( -
+
{componentConfig.columnMapping.displayColumns.map((columnName, idx) => { const value = getColumnValue(data, columnName); if (!value) return null; return ( -
- {getColumnLabel(columnName)}: - {value} +
+ {getColumnLabel(columnName)}: + {value}
); })} diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index 999bc85b..86bb406d 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -131,15 +131,90 @@ const FileUploadComponent: React.FC = ({ } }, [component.id]); // component.id가 변경될 때만 실행 + // 🎯 화면설계 모드에서 실제 화면으로의 실시간 동기화 이벤트 리스너 + useEffect(() => { + const handleDesignModeFileChange = (event: CustomEvent) => { + console.log("🎯🎯🎯 FileUploadComponent 화면설계 모드 파일 변경 이벤트 수신:", { + eventComponentId: event.detail.componentId, + currentComponentId: component.id, + isMatch: event.detail.componentId === component.id, + filesCount: event.detail.files?.length || 0, + action: event.detail.action, + source: event.detail.source, + eventDetail: event.detail + }); + + // 현재 컴포넌트와 일치하고 화면설계 모드에서 온 이벤트인 경우 + if (event.detail.componentId === component.id && event.detail.source === 'designMode') { + console.log("✅✅✅ 화면설계 모드 → 실제 화면 파일 동기화 시작:", { + componentId: component.id, + filesCount: event.detail.files?.length || 0, + action: event.detail.action + }); + + // 파일 상태 업데이트 + const newFiles = event.detail.files || []; + setUploadedFiles(newFiles); + + // localStorage 백업 업데이트 + try { + const backupKey = `fileUpload_${component.id}`; + localStorage.setItem(backupKey, JSON.stringify(newFiles)); + console.log("💾 화면설계 모드 동기화 후 localStorage 백업 업데이트:", { + componentId: component.id, + fileCount: newFiles.length + }); + } catch (e) { + console.warn("localStorage 백업 업데이트 실패:", e); + } + + // 전역 상태 업데이트 + if (typeof window !== 'undefined') { + (window as any).globalFileState = { + ...(window as any).globalFileState, + [component.id]: newFiles + }; + } + + // onUpdate 콜백 호출 (부모 컴포넌트에 알림) + if (onUpdate) { + onUpdate({ + uploadedFiles: newFiles, + lastFileUpdate: event.detail.timestamp + }); + } + + console.log("🎉🎉🎉 화면설계 모드 → 실제 화면 동기화 완료:", { + componentId: component.id, + finalFileCount: newFiles.length + }); + } + }; + + if (typeof window !== 'undefined') { + window.addEventListener('globalFileStateChanged', handleDesignModeFileChange as EventListener); + + return () => { + window.removeEventListener('globalFileStateChanged', handleDesignModeFileChange as EventListener); + }; + } + }, [component.id, onUpdate]); + // 템플릿 파일과 데이터 파일을 조회하는 함수 const loadComponentFiles = useCallback(async () => { if (!component?.id) return; try { - const screenId = formData?.screenId || (typeof window !== 'undefined' && window.location.pathname.includes('/screens/') + let screenId = formData?.screenId || (typeof window !== 'undefined' && window.location.pathname.includes('/screens/') ? parseInt(window.location.pathname.split('/screens/')[1]) : null); + // 디자인 모드인 경우 기본 화면 ID 사용 + if (!screenId && isDesignMode) { + screenId = 40; // 기본 화면 ID + console.log("📂 디자인 모드: 기본 화면 ID 사용 (40)"); + } + if (!screenId) { console.log("📂 화면 ID 없음, 기존 파일 로직 사용"); return false; // 기존 로직 사용 @@ -474,14 +549,18 @@ const FileUploadComponent: React.FC = ({ } const uploadData = { - tableName: tableName, - fieldName: columnName, - recordId: recordId || `temp_${component.id}`, + // 🎯 formData에서 백엔드 API 설정 가져오기 + autoLink: formData?.autoLink || true, + linkedTable: formData?.linkedTable || tableName, + recordId: formData?.recordId || recordId || `temp_${component.id}`, + columnName: formData?.columnName || columnName, + isVirtualFileColumn: formData?.isVirtualFileColumn || true, docType: component.fileConfig?.docType || 'DOCUMENT', docTypeName: component.fileConfig?.docTypeName || '일반 문서', + // 호환성을 위한 기존 필드들 + tableName: tableName, + fieldName: columnName, targetObjid: targetObjid, // InteractiveDataTable 호환을 위한 targetObjid 추가 - columnName: columnName, // 가상 파일 컬럼 지원 - isVirtualFileColumn: true, // 가상 파일 컬럼으로 처리 }; console.log("📤 파일 업로드 시작:", { @@ -715,7 +794,9 @@ const FileUploadComponent: React.FC = ({ componentId: component.id, files: updatedFiles, fileCount: updatedFiles.length, - timestamp: Date.now() + timestamp: Date.now(), + source: 'realScreen', // 🎯 실제 화면에서 온 이벤트임을 표시 + action: 'delete' } }); window.dispatchEvent(syncEvent); diff --git a/frontend/lib/registry/components/file-upload/FileViewerModal.tsx b/frontend/lib/registry/components/file-upload/FileViewerModal.tsx index b3f02982..d563e065 100644 --- a/frontend/lib/registry/components/file-upload/FileViewerModal.tsx +++ b/frontend/lib/registry/components/file-upload/FileViewerModal.tsx @@ -516,7 +516,7 @@ export const FileViewerModal: React.FC = ({ return ( {}}> - +
diff --git a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx index b4be9bcb..1b88672e 100644 --- a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx @@ -46,7 +46,7 @@ export const SingleTableWithSticky: React.FC = ({ return (
= ({ boxSizing: "border-box", }} > - - + + {visibleColumns.map((column, colIndex) => { // 왼쪽 고정 컬럼들의 누적 너비 계산 const leftFixedWidth = visibleColumns @@ -84,13 +84,13 @@ export const SingleTableWithSticky: React.FC = ({ key={column.columnName} className={cn( column.columnName === "__checkbox__" - ? "h-10 border-b px-4 py-2 text-center align-middle" - : "h-10 cursor-pointer border-b px-4 py-2 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none", + ? "h-12 border-0 px-4 py-3 text-center align-middle" + : "h-12 cursor-pointer border-0 px-4 py-3 text-left align-middle font-semibold whitespace-nowrap text-slate-700 select-none transition-all duration-200", `text-${column.align}`, - column.sortable && "hover:bg-gray-50", + column.sortable && "hover:bg-blue-50/50 hover:text-blue-700", // 고정 컬럼 스타일 - column.fixed === "left" && "sticky z-10 border-r bg-white shadow-sm", - column.fixed === "right" && "sticky z-10 border-l bg-white shadow-sm", + column.fixed === "left" && "sticky z-10 border-r border-gray-200/60 bg-gradient-to-r from-slate-50 to-blue-50/30 shadow-sm", + column.fixed === "right" && "sticky z-10 border-l border-gray-200/60 bg-gradient-to-r from-slate-50 to-blue-50/30 shadow-sm", // 숨김 컬럼 스타일 (디자인 모드에서만) isDesignMode && column.hidden && "bg-gray-100/50 opacity-40", )} @@ -118,15 +118,15 @@ export const SingleTableWithSticky: React.FC = ({ {columnLabels[column.columnName] || column.displayName || column.columnName} {column.sortable && ( - + {sortColumn === column.columnName ? ( sortDirection === "asc" ? ( - + ) : ( - + ) ) : ( - + )} )} @@ -142,8 +142,16 @@ export const SingleTableWithSticky: React.FC = ({ {data.length === 0 ? ( - - 데이터가 없습니다 + +
+
+ + + +
+ 데이터가 없습니다 + 조건을 변경하여 다시 검색해보세요 +
) : ( @@ -151,11 +159,11 @@ export const SingleTableWithSticky: React.FC = ({ handleRowClick(row)} > {visibleColumns.map((column, colIndex) => { @@ -177,15 +185,15 @@ export const SingleTableWithSticky: React.FC = ({