diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 9a22f572..e58690bc 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -32,6 +32,7 @@ import dataRoutes from "./routes/dataRoutes"; import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes"; import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes"; import multiConnectionRoutes from "./routes/multiConnectionRoutes"; +import screenFileRoutes from "./routes/screenFileRoutes"; import dbTypeCategoryRoutes from "./routes/dbTypeCategoryRoutes"; import ddlRoutes from "./routes/ddlRoutes"; import entityReferenceRoutes from "./routes/entityReferenceRoutes"; @@ -132,6 +133,7 @@ app.use("/api/data", dataRoutes); app.use("/api/test-button-dataflow", testButtonDataflowRoutes); app.use("/api/external-db-connections", externalDbConnectionRoutes); app.use("/api/multi-connection", multiConnectionRoutes); +app.use("/api/screen-files", screenFileRoutes); app.use("/api/db-type-categories", dbTypeCategoryRoutes); app.use("/api/ddl", ddlRoutes); app.use("/api/entity-reference", entityReferenceRoutes); diff --git a/backend-node/src/controllers/screenFileController.ts b/backend-node/src/controllers/screenFileController.ts new file mode 100644 index 00000000..95ca6816 --- /dev/null +++ b/backend-node/src/controllers/screenFileController.ts @@ -0,0 +1,145 @@ +import { Request, Response } from 'express'; +import { AuthenticatedRequest } from '../middleware/authMiddleware'; +import { PrismaClient } from '@prisma/client'; +import logger from '../utils/logger'; + +const prisma = new PrismaClient(); + +/** + * 화면 컴포넌트별 파일 정보 조회 및 복원 + */ +export const getScreenComponentFiles = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { screenId } = req.params; + + logger.info(`화면 컴포넌트 파일 조회 시작: screenId=${screenId}`); + + // screen_files: 접두사로 해당 화면의 모든 파일 조회 + const targetObjidPattern = `screen_files:${screenId}:%`; + + const files = await prisma.attach_file_info.findMany({ + where: { + target_objid: { + startsWith: `screen_files:${screenId}:` + }, + status: 'ACTIVE' + }, + orderBy: { + regdate: 'desc' + } + }); + + // 컴포넌트별로 파일 그룹화 + const componentFiles: { [componentId: string]: any[] } = {}; + + files.forEach(file => { + // target_objid 형식: screen_files:screenId:componentId:fieldName + const targetParts = file.target_objid?.split(':') || []; + if (targetParts.length >= 3) { + const componentId = targetParts[2]; + + if (!componentFiles[componentId]) { + componentFiles[componentId] = []; + } + + componentFiles[componentId].push({ + objid: file.objid.toString(), + savedFileName: file.saved_file_name, + realFileName: file.real_file_name, + fileSize: Number(file.file_size), + fileExt: file.file_ext, + filePath: file.file_path, + docType: file.doc_type, + docTypeName: file.doc_type_name, + targetObjid: file.target_objid, + parentTargetObjid: file.parent_target_objid, + writer: file.writer, + regdate: file.regdate?.toISOString(), + status: file.status + }); + } + }); + + logger.info(`화면 컴포넌트 파일 조회 완료: ${Object.keys(componentFiles).length}개 컴포넌트, 총 ${files.length}개 파일`); + + res.json({ + success: true, + componentFiles: componentFiles, + totalFiles: files.length, + componentCount: Object.keys(componentFiles).length + }); + + } catch (error) { + logger.error('화면 컴포넌트 파일 조회 오류:', error); + res.status(500).json({ + success: false, + message: '화면 컴포넌트 파일 조회 중 오류가 발생했습니다.', + error: error instanceof Error ? error.message : '알 수 없는 오류' + }); + } +}; + +/** + * 특정 컴포넌트의 파일 목록 조회 + */ +export const getComponentFiles = async ( + req: AuthenticatedRequest, + res: Response +): Promise => { + try { + const { screenId, componentId } = req.params; + + logger.info(`컴포넌트 파일 조회: screenId=${screenId}, componentId=${componentId}`); + + // target_objid 패턴: screen_files:screenId:componentId:* + const targetObjidPattern = `screen_files:${screenId}:${componentId}:`; + + const files = await prisma.attach_file_info.findMany({ + where: { + target_objid: { + startsWith: targetObjidPattern + }, + status: 'ACTIVE' + }, + orderBy: { + regdate: 'desc' + } + }); + + const fileList = files.map(file => ({ + objid: file.objid.toString(), + savedFileName: file.saved_file_name, + realFileName: file.real_file_name, + fileSize: Number(file.file_size), + fileExt: file.file_ext, + filePath: file.file_path, + docType: file.doc_type, + docTypeName: file.doc_type_name, + targetObjid: file.target_objid, + parentTargetObjid: file.parent_target_objid, + writer: file.writer, + regdate: file.regdate?.toISOString(), + status: file.status + })); + + logger.info(`컴포넌트 파일 조회 완료: ${fileList.length}개 파일`); + + res.json({ + success: true, + files: fileList, + componentId: componentId, + screenId: screenId + }); + + } catch (error) { + logger.error('컴포넌트 파일 조회 오류:', error); + res.status(500).json({ + success: false, + message: '컴포넌트 파일 조회 중 오류가 발생했습니다.', + error: error instanceof Error ? error.message : '알 수 없는 오류' + }); + } +}; diff --git a/backend-node/src/routes/screenFileRoutes.ts b/backend-node/src/routes/screenFileRoutes.ts new file mode 100644 index 00000000..c89b4ef6 --- /dev/null +++ b/backend-node/src/routes/screenFileRoutes.ts @@ -0,0 +1,13 @@ +import { Router } from 'express'; +import { authenticateToken } from '../middleware/authMiddleware'; +import { getScreenComponentFiles, getComponentFiles } from '../controllers/screenFileController'; + +const router = Router(); + +// 화면 컴포넌트별 파일 정보 조회 +router.get('/screens/:screenId/components/files', authenticateToken, getScreenComponentFiles); + +// 특정 컴포넌트의 파일 목록 조회 +router.get('/screens/:screenId/components/:componentId/files', authenticateToken, getComponentFiles); + +export default router; diff --git a/frontend/components/layout/Logo.tsx b/frontend/components/layout/Logo.tsx index e4264239..0cdb687c 100644 --- a/frontend/components/layout/Logo.tsx +++ b/frontend/components/layout/Logo.tsx @@ -1,4 +1,5 @@ import { LAYOUT_CONFIG } from "@/constants/layout"; +import Image from "next/image"; /** * 로고 컴포넌트 @@ -6,10 +7,17 @@ import { LAYOUT_CONFIG } from "@/constants/layout"; export function Logo() { return (
-
- P +
+ WACE 솔루션 로고
- {LAYOUT_CONFIG.COMPANY_NAME} + {/* {LAYOUT_CONFIG.COMPANY_NAME} */}
); } diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index f95dc025..eaa3d6d7 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -509,6 +509,48 @@ export const InteractiveDataTable: React.FC = ({ fetchCurrentUser(); }, []); + // 파일 상태 새로고침 이벤트 리스너 + useEffect(() => { + const handleRefreshFileStatus = async (event: CustomEvent) => { + const { tableName, recordId, columnName, targetObjid, fileCount } = event.detail; + + console.log("🔄 InteractiveDataTable 파일 상태 새로고침 이벤트 수신:", { + tableName, + recordId, + columnName, + targetObjid, + fileCount, + currentTableName: component.tableName + }); + + // 현재 테이블과 일치하는지 확인 + if (tableName === component.tableName) { + // 해당 행의 파일 상태 업데이트 + const columnKey = `${recordId}_${columnName}`; + setFileStatusMap(prev => ({ + ...prev, + [recordId]: { hasFiles: fileCount > 0, fileCount }, + [columnKey]: { hasFiles: fileCount > 0, fileCount } + })); + + console.log("✅ 파일 상태 업데이트 완료:", { + recordId, + columnKey, + hasFiles: fileCount > 0, + fileCount + }); + } + }; + + if (typeof window !== 'undefined') { + window.addEventListener('refreshFileStatus', handleRefreshFileStatus as EventListener); + + return () => { + window.removeEventListener('refreshFileStatus', handleRefreshFileStatus as EventListener); + }; + } + }, [component.tableName]); + // 테이블 컬럼 정보 로드 (웹 타입 정보 포함) useEffect(() => { const fetchTableColumns = async () => { diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 320203cf..7ea3db06 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -417,24 +417,40 @@ export const InteractiveScreenViewerDynamic: React.FC {/* 실제 FileUploadComponent 사용 */} { - console.log("📝 파일 업로드 완료:", { fieldName, value }); - handleFormDataChange(fieldName, value); + onFormDataChange={(data) => { + console.log("📝 파일 업로드 완료:", data); + if (onFormDataChange) { + Object.entries(data).forEach(([key, value]) => { + onFormDataChange(key, value); + }); + } + }} + onUpdate={(updates) => { + console.log("🔄 파일 컴포넌트 업데이트:", updates); + // 파일 업로드 완료 시 formData 업데이트 + if (updates.uploadedFiles && onFormDataChange) { + onFormDataChange(fieldName, updates.uploadedFiles); + } }} />
diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index dbe1f68e..d95f7916 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -41,6 +41,7 @@ import { toast } from "sonner"; import { MenuAssignmentModal } from "./MenuAssignmentModal"; import { FileAttachmentDetailModal } from "./FileAttachmentDetailModal"; import { initializeComponents } from "@/lib/registry/components"; +import { ScreenFileAPI } from "@/lib/api/screenFile"; import StyleEditor from "./StyleEditor"; import { RealtimePreview } from "./RealtimePreviewDynamic"; @@ -196,6 +197,85 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // 전역 파일 상태 변경 시 강제 리렌더링을 위한 상태 const [forceRenderTrigger, setForceRenderTrigger] = useState(0); + // 파일 컴포넌트 데이터 복원 함수 (실제 DB에서 조회) + const restoreFileComponentsData = useCallback(async (components: ComponentData[]) => { + if (!selectedScreen?.screenId) return; + + console.log("🔄 파일 컴포넌트 데이터 복원 시작:", components.length); + + try { + // 실제 DB에서 화면의 모든 파일 정보 조회 + const fileResponse = await ScreenFileAPI.getScreenComponentFiles(selectedScreen.screenId); + + if (!fileResponse.success) { + console.warn("⚠️ 파일 정보 조회 실패:", fileResponse); + return; + } + + const { componentFiles } = fileResponse; + + if (typeof window !== 'undefined') { + // 전역 파일 상태 초기화 + const globalFileState: {[key: string]: any[]} = {}; + let restoredCount = 0; + + // DB에서 조회한 파일 정보를 전역 상태로 복원 + Object.keys(componentFiles).forEach(componentId => { + const files = componentFiles[componentId]; + if (files && files.length > 0) { + globalFileState[componentId] = files; + restoredCount++; + + // localStorage에도 백업 + const backupKey = `fileComponent_${componentId}_files`; + localStorage.setItem(backupKey, JSON.stringify(files)); + + console.log("📁 DB에서 파일 컴포넌트 데이터 복원:", { + componentId: componentId, + fileCount: files.length, + files: files.map(f => ({ objid: f.objid, name: f.realFileName })) + }); + } + }); + + // 전역 상태 업데이트 + (window as any).globalFileState = globalFileState; + + // 모든 파일 컴포넌트에 복원 완료 이벤트 발생 + Object.keys(globalFileState).forEach(componentId => { + const files = globalFileState[componentId]; + const syncEvent = new CustomEvent('globalFileStateChanged', { + detail: { + componentId: componentId, + files: files, + fileCount: files.length, + timestamp: Date.now(), + isRestore: true + } + }); + window.dispatchEvent(syncEvent); + }); + + console.log("✅ DB 파일 컴포넌트 데이터 복원 완료:", { + totalComponents: components.length, + restoredFileComponents: restoredCount, + totalFiles: fileResponse.totalFiles, + globalFileState: Object.keys(globalFileState).map(id => ({ + id, + fileCount: globalFileState[id]?.length || 0 + })) + }); + + if (restoredCount > 0) { + toast.success(`${restoredCount}개 파일 컴포넌트 데이터가 DB에서 복원되었습니다. (총 ${fileResponse.totalFiles}개 파일)`); + } + } + } catch (error) { + console.error("❌ 파일 컴포넌트 데이터 복원 실패:", error); + toast.error("파일 데이터 복원 중 오류가 발생했습니다."); + } + }, [selectedScreen?.screenId]); + // 드래그 선택 상태 const [selectionDrag, setSelectionDrag] = useState({ isSelecting: false, @@ -722,6 +802,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // 화면 레이아웃 로드 useEffect(() => { if (selectedScreen?.screenId) { + // 현재 화면 ID를 전역 변수로 설정 (파일 업로드 시 사용) + if (typeof window !== 'undefined') { + (window as any).__CURRENT_SCREEN_ID__ = selectedScreen.screenId; + } + const loadLayout = async () => { try { const response = await screenApi.getLayout(selectedScreen.screenId); @@ -756,6 +841,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD setLayout(layoutWithDefaultGrid); setHistory([layoutWithDefaultGrid]); setHistoryIndex(0); + + // 파일 컴포넌트 데이터 복원 (비동기) + restoreFileComponentsData(layoutWithDefaultGrid.components); } } catch (error) { console.error("레이아웃 로드 실패:", error); diff --git a/frontend/components/screen/panels/FileComponentConfigPanel.tsx b/frontend/components/screen/panels/FileComponentConfigPanel.tsx index e65558b6..0d7059bc 100644 --- a/frontend/components/screen/panels/FileComponentConfigPanel.tsx +++ b/frontend/components/screen/panels/FileComponentConfigPanel.tsx @@ -238,18 +238,76 @@ export const FileComponentConfigPanel: React.FC = if (validFiles.length === 0) return; + // 중복 파일 체크 + const existingFiles = uploadedFiles; + const existingFileNames = existingFiles.map(f => f.realFileName.toLowerCase()); + const duplicates: string[] = []; + const uniqueFiles: File[] = []; + + console.log("🔍 중복 파일 체크:", { + uploadedFiles: existingFiles.length, + existingFileNames: existingFileNames, + newFiles: validFiles.map(f => f.name.toLowerCase()) + }); + + validFiles.forEach(file => { + const fileName = file.name.toLowerCase(); + if (existingFileNames.includes(fileName)) { + duplicates.push(file.name); + console.log("❌ 중복 파일 발견:", file.name); + } else { + uniqueFiles.push(file); + console.log("✅ 새로운 파일:", file.name); + } + }); + + console.log("🔍 중복 체크 결과:", { + duplicates: duplicates, + uniqueFiles: uniqueFiles.map(f => f.name) + }); + + if (duplicates.length > 0) { + toast.error(`중복된 파일이 있습니다: ${duplicates.join(', ')}`, { + description: "같은 이름의 파일이 이미 업로드되어 있습니다.", + duration: 4000 + }); + + if (uniqueFiles.length === 0) { + return; // 모든 파일이 중복이면 업로드 중단 + } + + // 일부만 중복인 경우 고유한 파일만 업로드 + toast.info(`${uniqueFiles.length}개의 새로운 파일만 업로드합니다.`); + } + + const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : validFiles; + try { - console.log("🔄 파일 업로드 시작:", { fileCount: validFiles.length, uploading }); + console.log("🔄 파일 업로드 시작:", { + originalFiles: validFiles.length, + filesToUpload: filesToUpload.length, + uploading + }); setUploading(true); - toast.loading(`${validFiles.length}개 파일 업로드 중...`); + toast.loading(`${filesToUpload.length}개 파일 업로드 중...`); + + // 그리드와 연동되는 targetObjid 생성 (화면 복원 시스템과 통일) + const tableName = 'screen_files'; + const screenId = (window as any).__CURRENT_SCREEN_ID__ || 'unknown'; // 현재 화면 ID + const componentId = component.id; + const fieldName = component.columnName || component.id || 'file_attachment'; + const targetObjid = `${tableName}:${screenId}:${componentId}:${fieldName}`; const response = await uploadFiles({ - files: validFiles, - tableName: currentTableName || 'screen_files', - fieldName: component.columnName || component.id || 'file_attachment', - recordId: component.id, + files: filesToUpload, + tableName: tableName, + fieldName: fieldName, + recordId: `${screenId}:${componentId}`, // 화면ID:컴포넌트ID 형태 docType: localInputs.docType, docTypeName: localInputs.docTypeName, + targetObjid: targetObjid, // 그리드 연동을 위한 targetObjid + columnName: fieldName, + isVirtualFileColumn: true, // 가상 파일 컬럼으로 처리 }); console.log("📤 파일 업로드 응답:", response); @@ -309,6 +367,27 @@ export const FileComponentConfigPanel: React.FC = localStorageBackup: localStorage.getItem(`fileComponent_${component.id}_files`) ? 'saved' : 'not saved' }); + // 그리드 파일 상태 새로고침 이벤트 발생 + if (typeof window !== 'undefined') { + const refreshEvent = new CustomEvent('refreshFileStatus', { + detail: { + tableName: tableName, + recordId: recordId, + columnName: columnName, + targetObjid: targetObjid, + fileCount: updatedFiles.length + } + }); + window.dispatchEvent(refreshEvent); + console.log("🔄 FileComponentConfigPanel 그리드 새로고침 이벤트 발생:", { + tableName, + recordId, + columnName, + targetObjid, + fileCount: updatedFiles.length + }); + } + toast.dismiss(); toast.success(`${validFiles.length}개 파일이 성공적으로 업로드되었습니다.`); console.log("✅ 파일 업로드 성공:", { @@ -375,6 +454,32 @@ export const FileComponentConfigPanel: React.FC = remainingFiles: updatedFiles.length, timestamp: timestamp }); + + // 그리드 파일 상태 새로고침 이벤트 발생 + if (typeof window !== 'undefined') { + 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); + console.log("🔄 FileComponentConfigPanel 파일 삭제 후 그리드 새로고침:", { + tableName, + recordId, + columnName, + targetObjid, + fileCount: updatedFiles.length + }); + } toast.success('파일이 삭제되었습니다.'); } catch (error) { @@ -541,6 +646,41 @@ export const FileComponentConfigPanel: React.FC = } }, [component.id]); // 컴포넌트 ID가 변경될 때만 초기화 + // 전역 파일 상태 변경 감지 (화면 복원 포함) + useEffect(() => { + const handleGlobalFileStateChange = (event: CustomEvent) => { + const { componentId, files, fileCount, isRestore } = event.detail; + + if (componentId === component.id) { + console.log("🌐 FileComponentConfigPanel 전역 상태 변경 감지:", { + componentId, + fileCount, + isRestore: !!isRestore, + files: files?.map((f: any) => ({ objid: f.objid, name: f.realFileName })) + }); + + if (files && Array.isArray(files)) { + setUploadedFiles(files); + + if (isRestore) { + console.log("✅ 파일 컴포넌트 설정 패널 데이터 복원 완료:", { + componentId, + restoredFileCount: files.length + }); + } + } + } + }; + + if (typeof window !== 'undefined') { + window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); + + return () => { + window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); + }; + } + }, [component.id]); + // 미리 정의된 문서 타입들 const docTypeOptions = [ { value: "CONTRACT", label: "계약서" }, diff --git a/frontend/lib/api/screenFile.ts b/frontend/lib/api/screenFile.ts new file mode 100644 index 00000000..f7cb3afa --- /dev/null +++ b/frontend/lib/api/screenFile.ts @@ -0,0 +1,49 @@ +import { apiClient } from './client'; + +export interface ScreenFileInfo { + objid: string; + savedFileName: string; + realFileName: string; + fileSize: number; + fileExt: string; + filePath: string; + docType: string; + docTypeName: string; + targetObjid: string; + parentTargetObjid?: string; + writer: string; + regdate: string; + status: string; +} + +export interface ScreenComponentFilesResponse { + success: boolean; + componentFiles: { [componentId: string]: ScreenFileInfo[] }; + totalFiles: number; + componentCount: number; +} + +export interface ComponentFilesResponse { + success: boolean; + files: ScreenFileInfo[]; + componentId: string; + screenId: string; +} + +export const ScreenFileAPI = { + /** + * 화면의 모든 컴포넌트별 파일 정보 조회 + */ + async getScreenComponentFiles(screenId: number): Promise { + const response = await apiClient.get(`/screen-files/screens/${screenId}/components/files`); + return response.data; + }, + + /** + * 특정 컴포넌트의 파일 목록 조회 + */ + async getComponentFiles(screenId: number, componentId: string): Promise { + const response = await apiClient.get(`/screen-files/screens/${screenId}/components/${componentId}/files`); + return response.data; + } +}; diff --git a/frontend/lib/registry/DynamicWebTypeRenderer.tsx b/frontend/lib/registry/DynamicWebTypeRenderer.tsx index 82d9154b..b49f5129 100644 --- a/frontend/lib/registry/DynamicWebTypeRenderer.tsx +++ b/frontend/lib/registry/DynamicWebTypeRenderer.tsx @@ -54,7 +54,7 @@ export const DynamicWebTypeRenderer: React.FC = ({ console.log(`DB 웹타입 정보:`, dbWebType); // FileWidget의 경우 FileUploadComponent 직접 사용 - if (dbWebType.component_name === "FileWidget" && webType === "file") { + if (dbWebType.component_name === "FileWidget" || webType === "file") { const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent"); console.log(`✅ FileWidget → FileUploadComponent 사용`); return ; @@ -75,6 +75,13 @@ export const DynamicWebTypeRenderer: React.FC = ({ if (webTypeDefinition) { console.log(`웹타입 "${webType}" → 레지스트리 컴포넌트 사용`); + // 파일 웹타입의 경우 FileUploadComponent 직접 사용 + if (webType === "file") { + const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent"); + console.log(`✅ 파일 웹타입 → FileUploadComponent 사용`); + return ; + } + // 웹타입이 비활성화된 경우 if (!webTypeDefinition.isActive) { console.warn(`웹타입 "${webType}"이 비활성화되어 있습니다.`); @@ -99,6 +106,14 @@ export const DynamicWebTypeRenderer: React.FC = ({ // 3순위: 웹타입명으로 자동 매핑 (폴백) try { console.warn(`웹타입 "${webType}" → 자동 매핑 폴백 사용`); + + // 파일 웹타입의 경우 FileUploadComponent 직접 사용 (최종 폴백) + if (webType === "file") { + const { FileUploadComponent } = require("@/lib/registry/components/file-upload/FileUploadComponent"); + console.log(`✅ 폴백: 파일 웹타입 → FileUploadComponent 사용`); + return ; + } + // const FallbackComponent = getWidgetComponentByWebType(webType); // return ; console.warn(`웹타입 "${webType}" 폴백 기능 임시 비활성화`); diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index dd46d0ea..efc0bedf 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -102,10 +102,19 @@ export const FileUploadComponent: React.FC = ({ const componentFiles = (component as any)?.uploadedFiles || []; const lastUpdate = (component as any)?.lastFileUpdate; + // 전역 상태에서 최신 파일 정보 가져오기 + const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {}; + const globalFiles = globalFileState[component.id] || []; + + // 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성) + const currentFiles = globalFiles.length > 0 ? globalFiles : componentFiles; + console.log("🔄 FileUploadComponent 파일 동기화:", { componentId: component.id, componentFiles: componentFiles.length, - currentFiles: uploadedFiles.length, + globalFiles: globalFiles.length, + currentFiles: currentFiles.length, + uploadedFiles: uploadedFiles.length, lastUpdate: lastUpdate }); @@ -113,7 +122,7 @@ export const FileUploadComponent: React.FC = ({ try { const backupKey = `fileUpload_${component.id}`; const backupFiles = localStorage.getItem(backupKey); - if (backupFiles && componentFiles.length === 0) { + if (backupFiles && currentFiles.length === 0) { const parsedFiles = JSON.parse(backupFiles); setUploadedFiles(parsedFiles); return; @@ -122,19 +131,66 @@ export const FileUploadComponent: React.FC = ({ console.warn("localStorage 백업 복원 실패:", e); } - // 컴포넌트 파일과 현재 파일 비교 - if (JSON.stringify(componentFiles) !== JSON.stringify(uploadedFiles)) { + // 최신 파일과 현재 파일 비교 + if (JSON.stringify(currentFiles) !== JSON.stringify(uploadedFiles)) { console.log("🔄 useEffect에서 파일 목록 변경 감지:", { - componentFiles: componentFiles.length, + currentFiles: currentFiles.length, uploadedFiles: uploadedFiles.length, - componentFilesData: componentFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })), + currentFilesData: currentFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })), uploadedFilesData: uploadedFiles.map(f => ({ objid: f.objid, name: f.realFileName })) }); - setUploadedFiles(componentFiles); + setUploadedFiles(currentFiles); setForceUpdate(prev => prev + 1); } }, [component.id, (component as any)?.uploadedFiles, (component as any)?.lastFileUpdate]); + // 전역 상태 변경 감지 (모든 파일 컴포넌트 동기화 + 화면 복원) + useEffect(() => { + const handleGlobalFileStateChange = (event: CustomEvent) => { + const { componentId, files, fileCount, timestamp, isRestore } = event.detail; + + console.log("🔄 FileUploadComponent 전역 상태 변경 감지:", { + currentComponentId: component.id, + eventComponentId: componentId, + isForThisComponent: componentId === component.id, + newFileCount: fileCount, + currentFileCount: uploadedFiles.length, + timestamp, + isRestore: !!isRestore + }); + + // 같은 컴포넌트 ID인 경우에만 업데이트 + if (componentId === component.id) { + const logMessage = isRestore ? "🔄 화면 복원으로 파일 상태 동기화" : "✅ 파일 상태 동기화 적용"; + console.log(logMessage, { + componentId: component.id, + 이전파일수: uploadedFiles.length, + 새파일수: files.length, + files: files.map((f: any) => ({ objid: f.objid, name: f.realFileName })) + }); + + setUploadedFiles(files); + setForceUpdate(prev => prev + 1); + + // localStorage 백업도 업데이트 + try { + const backupKey = `fileUpload_${component.id}`; + localStorage.setItem(backupKey, JSON.stringify(files)); + } catch (e) { + console.warn("localStorage 백업 실패:", e); + } + } + }; + + if (typeof window !== 'undefined') { + window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); + + return () => { + window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); + }; + } + }, [component.id, uploadedFiles.length]); + // 파일 업로드 설정 - componentConfig가 undefined일 수 있으므로 안전하게 처리 const safeComponentConfig = componentConfig || {}; const fileConfig = { @@ -163,24 +219,80 @@ export const FileUploadComponent: React.FC = ({ const handleFileUpload = useCallback(async (files: File[]) => { if (!files.length) return; + // 중복 파일 체크 + const existingFileNames = uploadedFiles.map(f => f.realFileName.toLowerCase()); + const duplicates: string[] = []; + const uniqueFiles: File[] = []; + + console.log("🔍 중복 파일 체크:", { + uploadedFiles: uploadedFiles.length, + existingFileNames: existingFileNames, + newFiles: files.map(f => f.name.toLowerCase()) + }); + + files.forEach(file => { + const fileName = file.name.toLowerCase(); + if (existingFileNames.includes(fileName)) { + duplicates.push(file.name); + console.log("❌ 중복 파일 발견:", file.name); + } else { + uniqueFiles.push(file); + console.log("✅ 새로운 파일:", file.name); + } + }); + + console.log("🔍 중복 체크 결과:", { + duplicates: duplicates, + uniqueFiles: uniqueFiles.map(f => f.name) + }); + + if (duplicates.length > 0) { + toast.error(`중복된 파일이 있습니다: ${duplicates.join(', ')}`, { + description: "같은 이름의 파일이 이미 업로드되어 있습니다.", + duration: 4000 + }); + + if (uniqueFiles.length === 0) { + return; // 모든 파일이 중복이면 업로드 중단 + } + + // 일부만 중복인 경우 고유한 파일만 업로드 + toast.info(`${uniqueFiles.length}개의 새로운 파일만 업로드합니다.`); + } + + const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files; setUploadStatus('uploading'); toast.loading("파일을 업로드하는 중...", { id: 'file-upload' }); try { + // targetObjid 생성 (InteractiveDataTable과 호환) + const tableName = formData?.tableName || component.tableName || 'default_table'; + const recordId = formData?.id || 'temp_record'; + const columnName = component.columnName || component.id; + const targetObjid = `${tableName}:${recordId}:${columnName}`; + const uploadData = { - tableName: component.tableName || 'default_table', - fieldName: component.columnName || component.id, - recordId: formData?.id || 'temp_record', + tableName: tableName, + fieldName: columnName, + recordId: recordId, docType: component.fileConfig?.docType || 'DOCUMENT', docTypeName: component.fileConfig?.docTypeName || '일반 문서', + targetObjid: targetObjid, // InteractiveDataTable 호환을 위한 targetObjid 추가 + columnName: columnName, // 가상 파일 컬럼 지원 + isVirtualFileColumn: true, // 가상 파일 컬럼으로 처리 }; console.log("📤 파일 업로드 시작:", { - files: files.map(f => ({ name: f.name, size: f.size })), + originalFiles: files.length, + filesToUpload: filesToUpload.length, + files: filesToUpload.map(f => ({ name: f.name, size: f.size })), uploadData }); - const response = await uploadFiles(files, uploadData); + const response = await uploadFiles({ + files: filesToUpload, + ...uploadData + }); console.log("📤 파일 업로드 API 응답:", response); @@ -239,6 +351,34 @@ export const FileUploadComponent: React.FC = ({ console.warn("localStorage 백업 실패:", e); } + // 전역 상태 업데이트 (모든 파일 컴포넌트 동기화) + if (typeof window !== 'undefined') { + // 전역 파일 상태 업데이트 + const globalFileState = (window as any).globalFileState || {}; + globalFileState[component.id] = updatedFiles; + (window as any).globalFileState = globalFileState; + + // 모든 파일 컴포넌트에 동기화 이벤트 발생 + const syncEvent = new CustomEvent('globalFileStateChanged', { + detail: { + componentId: component.id, + files: updatedFiles, + fileCount: updatedFiles.length, + timestamp: Date.now() + } + }); + window.dispatchEvent(syncEvent); + + console.log("🌐 전역 파일 상태 업데이트 및 동기화 이벤트 발생:", { + componentId: component.id, + fileCount: updatedFiles.length, + globalState: Object.keys(globalFileState).map(id => ({ + id, + fileCount: globalFileState[id]?.length || 0 + })) + }); + } + // 컴포넌트 업데이트 if (onUpdate) { const timestamp = Date.now(); @@ -255,6 +395,27 @@ export const FileUploadComponent: React.FC = ({ console.warn("⚠️ onUpdate 콜백이 없습니다!"); } + // 그리드 파일 상태 새로고침 이벤트 발생 + if (typeof window !== 'undefined') { + const refreshEvent = new CustomEvent('refreshFileStatus', { + detail: { + tableName: tableName, + recordId: recordId, + columnName: columnName, + targetObjid: targetObjid, + fileCount: updatedFiles.length + } + }); + window.dispatchEvent(refreshEvent); + console.log("🔄 그리드 파일 상태 새로고침 이벤트 발생:", { + tableName, + recordId, + columnName, + targetObjid, + fileCount: updatedFiles.length + }); + } + // 폼 데이터 업데이트 if (onFormDataChange && component.columnName) { const fileIds = updatedFiles.map(f => f.objid); @@ -310,6 +471,31 @@ export const FileUploadComponent: React.FC = ({ console.warn("localStorage 백업 업데이트 실패:", e); } + // 전역 상태 업데이트 (모든 파일 컴포넌트 동기화) + if (typeof window !== 'undefined') { + // 전역 파일 상태 업데이트 + const globalFileState = (window as any).globalFileState || {}; + globalFileState[component.id] = updatedFiles; + (window as any).globalFileState = globalFileState; + + // 모든 파일 컴포넌트에 동기화 이벤트 발생 + const syncEvent = new CustomEvent('globalFileStateChanged', { + detail: { + componentId: component.id, + files: updatedFiles, + fileCount: updatedFiles.length, + timestamp: Date.now() + } + }); + window.dispatchEvent(syncEvent); + + console.log("🗑️ 파일 삭제 후 전역 상태 동기화:", { + componentId: component.id, + deletedFile: fileName, + remainingFiles: updatedFiles.length + }); + } + // 컴포넌트 업데이트 if (onUpdate) { const timestamp = Date.now(); @@ -407,16 +593,16 @@ export const FileUploadComponent: React.FC = ({ ${safeComponentConfig.disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-gray-400'} ${uploadStatus === 'uploading' ? 'opacity-75' : ''} `} - onClick={handleClick} + onClick={handleClick} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} - onDragStart={onDragStart} - onDragEnd={onDragEnd} - > - + = ({ {uploadedFiles.length > 0 ? ( - uploadedFiles.map((file) => ( - - -
-
-
- {getFileIcon(file.fileExt)} -
-
-

- {file.realFileName} -

-
- {formatFileSize(file.fileSize)} - - {file.fileExt.toUpperCase()} - {file.uploadedAt && ( - <> - - {new Date(file.uploadedAt).toLocaleDateString()} - - )} -
-
-
- -
- - - -
+
+ {uploadedFiles.map((file) => ( +
+
+ {getFileIcon(file.fileExt)}
- - {/* 파일 상태 표시 */} - {file.status !== 'ACTIVE' && ( -
- - - 파일 상태: {file.status} - -
- )} - - - )) + + {file.realFileName} + + + {formatFileSize(file.fileSize)} + +
+ ))} +
+ 💡 파일 관리는 상세설정에서 가능합니다 +
+
) : (
@@ -541,7 +676,7 @@ export const FileUploadComponent: React.FC = ({

상세설정에서 파일을 업로드하세요

)} -
+ )}