"use client"; import React, { useState, useEffect, useCallback, useRef } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { FileComponent, TableInfo } from "@/types/screen"; import { Plus, X, Upload, File, Image, FileText, Download, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { FileInfo, FileUploadResponse } from "@/lib/registry/components/file-upload/types"; import { uploadFiles, downloadFile, deleteFile } from "@/lib/api/file"; import { formatFileSize } from "@/lib/utils"; import { toast } from "sonner"; interface FileComponentConfigPanelProps { component: FileComponent; onUpdateProperty: (componentId: string, path: string, value: any) => void; currentTable?: TableInfo; currentTableName?: string; } export const FileComponentConfigPanel: React.FC = ({ component, onUpdateProperty, currentTable, currentTableName, }) => { // console.log("🎨🎨🎨 FileComponentConfigPanel λ Œλ”λ§:", { // componentId: component?.id, // componentType: component?.type, // hasOnUpdateProperty: !!onUpdateProperty, // currentTable, // currentTableName // }); // fileConfigκ°€ μ—†λŠ” 경우 μ΄ˆκΈ°ν™” React.useEffect(() => { if (!component.fileConfig) { const defaultFileConfig = { docType: "DOCUMENT", docTypeName: "일반 λ¬Έμ„œ", dragDropText: "νŒŒμΌμ„ λ“œλž˜κ·Έν•˜κ±°λ‚˜ ν΄λ¦­ν•˜μ—¬ μ—…λ‘œλ“œν•˜μ„Έμš”", maxSize: 10, maxFiles: 5, multiple: true, showPreview: true, showProgress: true, autoLink: false, accept: [], linkedTable: "", linkedField: "", }; onUpdateProperty(component.id, "fileConfig", defaultFileConfig); } }, [component.fileConfig, component.id, onUpdateProperty]); // 둜컬 μƒνƒœ const [localInputs, setLocalInputs] = useState({ docType: component.fileConfig?.docType || "DOCUMENT", docTypeName: component.fileConfig?.docTypeName || "일반 λ¬Έμ„œ", dragDropText: component.fileConfig?.dragDropText || "νŒŒμΌμ„ λ“œλž˜κ·Έν•˜κ±°λ‚˜ ν΄λ¦­ν•˜μ—¬ μ—…λ‘œλ“œν•˜μ„Έμš”", maxSize: component.fileConfig?.maxSize || 10, maxFiles: component.fileConfig?.maxFiles || 5, newAcceptType: "", linkedTable: component.fileConfig?.linkedTable || "", linkedField: component.fileConfig?.linkedField || "", }); const [localValues, setLocalValues] = useState({ multiple: component.fileConfig?.multiple ?? true, showPreview: component.fileConfig?.showPreview ?? true, showProgress: component.fileConfig?.showProgress ?? true, autoLink: component.fileConfig?.autoLink ?? false, }); const [acceptTypes, setAcceptTypes] = useState(component.fileConfig?.accept || []); // μ „μ—­ 파일 μƒνƒœ 관리λ₯Ό window 객체에 μ €μž₯ (μ»΄ν¬λ„ŒνŠΈ μ–Έλ§ˆμš΄νŠΈ μ‹œμ—λ„ μœ μ§€) const getGlobalFileState = (): {[key: string]: FileInfo[]} => { if (typeof window !== 'undefined') { return (window as any).globalFileState || {}; } return {}; }; const setGlobalFileState = (updater: (prev: {[key: string]: FileInfo[]}) => {[key: string]: FileInfo[]}) => { if (typeof window !== 'undefined') { const currentState = getGlobalFileState(); const newState = updater(currentState); (window as any).globalFileState = newState; // console.log("🌐 μ „μ—­ 파일 μƒνƒœ μ—…λ°μ΄νŠΈ:", { // componentId: component.id, // newFileCount: newState[component.id]?.length || 0, // totalComponents: Object.keys(newState).length // }); // κ°•μ œ λ¦¬λ Œλ”λ§μ„ μœ„ν•œ 이벀트 λ°œμƒ window.dispatchEvent(new CustomEvent('globalFileStateChanged', { detail: { componentId: component.id, fileCount: newState[component.id]?.length || 0 } })); // λ””λ²„κΉ…μš© μ „μ—­ ν•¨μˆ˜ 등둝 (window as any).debugFileState = () => { // console.log("πŸ” μ „μ—­ 파일 μƒνƒœ 디버깅:", { // globalState: (window as any).globalFileState, // localStorage: Object.keys(localStorage).filter(key => key.startsWith('fileComponent_')).map(key => ({ // key, // data: JSON.parse(localStorage.getItem(key) || '[]') // })) // }); }; } }; // 파일 μ—…λ‘œλ“œ κ΄€λ ¨ μƒνƒœ - μ΄ˆκΈ°ν™” μ‹œ μ „μ—­ μƒνƒœμ—μ„œ 볡원 const initializeUploadedFiles = (): FileInfo[] => { const componentFiles = component.uploadedFiles || []; const globalFiles = getGlobalFileState()[component.id] || []; // 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 { parsedBackupFiles = JSON.parse(backupFiles); } catch (error) { // console.error("λ°±μ—… 파일 νŒŒμ‹± μ‹€νŒ¨:", error); } } if (tempBackupFiles) { try { parsedTempFiles = JSON.parse(tempBackupFiles); } catch (error) { // console.error("μž„μ‹œ 파일 νŒŒμ‹± μ‹€νŒ¨:", error); } } // 🎯 μ‹€μ œ ν™”λ©΄ 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; // console.log("πŸš€ FileComponentConfigPanel μ΄ˆκΈ°ν™”:", { // componentId: component.id, // componentFiles: componentFiles.length, // globalFiles: globalFiles.length, // backupFiles: parsedBackupFiles.length, // tempFiles: parsedTempFiles.length, // fileUploadFiles: parsedFileUploadFiles.length, // 🎯 μ‹€μ œ ν™”λ©΄ 파일 수 // finalFiles: finalFiles.length, // source: globalFiles.length > 0 ? 'global' : // parsedFileUploadFiles.length > 0 ? 'fileUploadComponent' : // 🎯 μ‹€μ œ ν™”λ©΄ μ†ŒμŠ€ // parsedBackupFiles.length > 0 ? 'localStorage' : // parsedTempFiles.length > 0 ? 'temp' : 'component' // }); return finalFiles; }; const [uploadedFiles, setUploadedFiles] = useState(() => { const initialFiles = initializeUploadedFiles(); // μ΄ˆκΈ°ν™”λœ 파일이 있고 μ»΄ν¬λ„ŒνŠΈ 속성과 λ‹€λ₯΄λ©΄ μ¦‰μ‹œ 동기화 if (initialFiles.length > 0 && JSON.stringify(initialFiles) !== JSON.stringify(component.uploadedFiles || [])) { setTimeout(() => { onUpdateProperty(component.id, "uploadedFiles", initialFiles); onUpdateProperty(component.id, "lastFileUpdate", Date.now()); // console.log("πŸ”„ μ΄ˆκΈ°ν™” μ‹œ μ»΄ν¬λ„ŒνŠΈ 속성 동기화:", { // componentId: component.id, // fileCount: initialFiles.length // }); }, 0); } return initialFiles; }); const [dragOver, setDragOver] = useState(false); const [uploading, setUploading] = useState(false); // 이전 μ»΄ν¬λ„ŒνŠΈ ID μΆ”μ μš© ref const prevComponentIdRef = useRef(component.id); // 파일 νƒ€μž…λ³„ μ•„μ΄μ½˜ λ°˜ν™˜ const getFileIcon = (fileExt: string) => { const ext = fileExt.toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) { return ; } if (['pdf', 'doc', 'docx', 'txt', 'rtf'].includes(ext)) { return ; } return ; }; // 파일 μ—…λ‘œλ“œ 처리 const handleFileUpload = useCallback(async (files: FileList | File[]) => { // 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[] = []; // 파일 검증 for (const file of fileArray) { if (file.size > localInputs.maxSize * 1024 * 1024) { toast.error(`${file.name}: 파일 크기가 ${localInputs.maxSize}MBλ₯Ό μ΄ˆκ³Όν•©λ‹ˆλ‹€.`); continue; } // 파일 νƒ€μž… 검증 (acceptTypesκ°€ μ„€μ •λœ κ²½μš°μ—λ§Œ) if (acceptTypes.length > 0) { const fileExt = '.' + file.name.split('.').pop()?.toLowerCase(); const isAllowed = acceptTypes.some(type => type === '*/*' || type === file.type || type === fileExt || (type.startsWith('.') && fileExt === type) || (type.includes('/*') && file.type.startsWith(type.split('/')[0])) ); if (!isAllowed) { toast.error(`${file.name}: ν—ˆμš©λ˜μ§€ μ•ŠλŠ” 파일 ν˜•μ‹μž…λ‹ˆλ‹€. (ν—ˆμš©: ${acceptTypes.join(', ')})`); // console.log(`파일 검증 μ‹€νŒ¨:`, { // fileName: file.name, // fileType: file.type, // fileExt, // acceptTypes, // isAllowed // }); continue; } } // console.log(`파일 검증 성곡:`, { // fileName: file.name, // fileType: file.type, // fileSize: file.size, // acceptTypesCount: acceptTypes.length // }); validFiles.push(file); } 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("πŸ”„ 파일 μ—…λ‘œλ“œ μ‹œμž‘:", { // originalFiles: validFiles.length, // filesToUpload: filesToUpload.length, // uploading // }); setUploading(true); toast.loading(`${filesToUpload.length}개 파일 μ—…λ‘œλ“œ 쀑...`); // 🎯 μ—¬λŸ¬ λ°©λ²•μœΌλ‘œ 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'; // 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, // 🎯 λ°±μ—”λ“œ APIκ°€ κΈ°λŒ€ν•˜λŠ” μ •ν™•ν•œ ν˜•μ‹μœΌλ‘œ μ„€μ • autoLink: true, // μžλ™ μ—°κ²° ν™œμ„±ν™” linkedTable: 'screen_files', // μ—°κ²° ν…Œμ΄λΈ” recordId: screenId, // λ ˆμ½”λ“œ ID columnName: fieldName, // 컬럼λͺ… isVirtualFileColumn: true, // 가상 파일 컬럼 docType: localInputs.docType, docTypeName: localInputs.docTypeName, }); // console.log("πŸ“€ 파일 μ—…λ‘œλ“œ 응닡:", response); if (response.success && (response.data || response.files)) { const filesData = response.data || response.files; // console.log("πŸ“ μ—…λ‘œλ“œλœ 파일 데이터:", filesData); const newFiles: FileInfo[] = filesData.map((file: any) => ({ objid: file.objid || `temp_${Date.now()}_${Math.random()}`, savedFileName: file.saved_file_name || file.savedFileName, realFileName: file.real_file_name || file.realFileName, fileSize: file.file_size || file.fileSize, fileExt: file.file_ext || file.fileExt, filePath: file.file_path || file.filePath, docType: localInputs.docType, docTypeName: localInputs.docTypeName, targetObjid: file.target_objid || file.targetObjid || component.id, parentTargetObjid: file.parent_target_objid || file.parentTargetObjid, companyCode: file.company_code || file.companyCode || 'DEFAULT', writer: file.writer || 'user', regdate: file.regdate || new Date().toISOString(), status: file.status || 'ACTIVE', path: file.file_path || file.filePath, name: file.real_file_name || file.realFileName, id: file.objid, size: file.file_size || file.fileSize, type: localInputs.docType, uploadedAt: file.regdate || new Date().toISOString(), })); const updatedFiles = localValues.multiple ? [...uploadedFiles, ...newFiles] : newFiles; setUploadedFiles(updatedFiles); // μžλ™μœΌλ‘œ 영ꡬ μ €μž₯ (μ €μž₯ λ²„νŠΌ 없이 λ°”λ‘œ μ €μž₯) const timestamp = Date.now(); // μ „μ—­ μƒνƒœμ— μ €μž₯ setGlobalFileState(prev => ({ ...prev, [component.id]: updatedFiles })); // μ»΄ν¬λ„ŒνŠΈ 속성에 μ €μž₯ onUpdateProperty(component.id, "uploadedFiles", updatedFiles); onUpdateProperty(component.id, "lastFileUpdate", timestamp); // localStorage에 영ꡬ μ €μž₯ const backupKey = `fileComponent_${component.id}_files`; localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); // μ „μ—­ 파일 μƒνƒœ λ³€κ²½ 이벀트 λ°œμƒ (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: 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 μžλ™ μ €μž₯:", { // componentId: component.id, // uploadedFiles: updatedFiles.length, // status: "μžλ™ 영ꡬ μ €μž₯됨", // onUpdatePropertyExists: typeof onUpdateProperty === 'function', // globalFileStateUpdated: getGlobalFileState()[component.id]?.length || 0, // localStorageBackup: localStorage.getItem(`fileComponent_${component.id}_files`) ? 'saved' : 'not saved' // }); // κ·Έλ¦¬λ“œ 파일 μƒνƒœ μƒˆλ‘œκ³ μΉ¨ 이벀트 λ°œμƒ if (typeof window !== 'undefined') { const tableName = component.tableName || currentTableName || 'unknown'; const columnName = component.columnName || component.id; const recordId = component.id; // μž„μ‹œλ‘œ μ»΄ν¬λ„ŒνŠΈ ID μ‚¬μš© const targetObjid = component.id; 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("βœ… 파일 μ—…λ‘œλ“œ 성곡:", { // newFilesCount: newFiles.length, // totalFiles: updatedFiles.length, // componentId: component.id, // updatedFiles: updatedFiles.map(f => ({ objid: f.objid, name: f.realFileName })) // }); } else { throw new Error(response.message || '파일 μ—…λ‘œλ“œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); } } 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(`파일 μ—…λ‘œλ“œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: ${error?.message || 'μ•Œ 수 μ—†λŠ” 였λ₯˜'}`); } finally { // console.log("🏁 파일 μ—…λ‘œλ“œ μ™„λ£Œ, λ‘œλ”© μƒνƒœ ν•΄μ œ"); setUploading(false); } }, [localInputs, localValues, uploadedFiles, onUpdateProperty, currentTableName, component, acceptTypes]); // 파일 λ‹€μš΄λ‘œλ“œ 처리 const handleFileDownload = useCallback(async (file: FileInfo) => { try { await downloadFile({ fileId: file.objid || file.id || '', serverFilename: file.savedFileName, originalName: file.realFileName || file.name || 'download', }); toast.success(`${file.realFileName || file.name} λ‹€μš΄λ‘œλ“œκ°€ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.`); } catch (error) { // console.error('파일 λ‹€μš΄λ‘œλ“œ 였λ₯˜:', error); toast.error('파일 λ‹€μš΄λ‘œλ“œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); } }, []); // 파일 μ‚­μ œ 처리 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); // μ „μ—­ μƒνƒœμ—λ„ μ—…λ°μ΄νŠΈ setGlobalFileState(prev => ({ ...prev, [component.id]: updatedFiles })); // μ»΄ν¬λ„ŒνŠΈ 속성 μ—…λ°μ΄νŠΈ (RealtimePreview κ°•μ œ λ¦¬λ Œλ”λ§μš©) const timestamp = Date.now(); onUpdateProperty(component.id, "uploadedFiles", updatedFiles); onUpdateProperty(component.id, "lastFileUpdate", timestamp); // localStorage 백업도 μ—…λ°μ΄νŠΈ (영ꡬ μ €μž₯μ†Œμ™€ μž„μ‹œ μ €μž₯μ†Œ λͺ¨λ‘) const backupKey = `fileComponent_${component.id}_files`; const tempBackupKey = `fileComponent_${component.id}_files_temp`; localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); localStorage.setItem(tempBackupKey, JSON.stringify(updatedFiles)); // console.log("πŸ—‘οΈ FileComponentConfigPanel 파일 μ‚­μ œ:", { // componentId: component.id, // deletedFileId: fileId, // remainingFiles: updatedFiles.length, // timestamp: timestamp // }); // 🎯 RealtimePreview 동기화λ₯Ό μœ„ν•œ μ „μ—­ 이벀트 λ°œμƒ if (typeof window !== 'undefined') { 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); } // κ·Έλ¦¬λ“œ 파일 μƒνƒœ μƒˆλ‘œκ³ μΉ¨ μ΄λ²€νŠΈλ„ μœ μ§€ 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, // columnName, // targetObjid, // fileCount: updatedFiles.length // }); } toast.success('파일이 μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); } catch (error) { // console.error('파일 μ‚­μ œ 였λ₯˜:', error); toast.error('파일 μ‚­μ œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); } }, [uploadedFiles, onUpdateProperty, component.id]); // 파일 μ €μž₯ 처리 (μž„μ‹œ β†’ 영ꡬ μ €μž₯) const handleSaveFiles = useCallback(() => { try { // μ»΄ν¬λ„ŒνŠΈ 속성에 영ꡬ μ €μž₯ const timestamp = Date.now(); onUpdateProperty(component.id, "uploadedFiles", uploadedFiles); onUpdateProperty(component.id, "lastFileUpdate", timestamp); // μ „μ—­ μƒνƒœμ—λ„ μ €μž₯ setGlobalFileState(prev => ({ ...prev, [component.id]: uploadedFiles })); // localStorage에도 λ°±μ—… const backupKey = `fileComponent_${component.id}_files`; localStorage.setItem(backupKey, JSON.stringify(uploadedFiles)); // μž„μ‹œ 파일 μ‚­μ œ const tempBackupKey = `fileComponent_${component.id}_files_temp`; localStorage.removeItem(tempBackupKey); // console.log("πŸ’Ύ 파일 μ €μž₯ μ™„λ£Œ:", { // componentId: component.id, // fileCount: uploadedFiles.length, // timestamp: timestamp, // files: uploadedFiles.map(f => ({ objid: f.objid, name: f.realFileName })) // }); toast.success(`${uploadedFiles.length}개 파일이 영ꡬ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`); } catch (error) { // console.error('파일 μ €μž₯ 였λ₯˜:', error); toast.error('파일 μ €μž₯에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); } }, [uploadedFiles, onUpdateProperty, component.id, setGlobalFileState]); // λ“œλž˜κ·Έμ•€λ“œλ‘­ 이벀트 ν•Έλ“€λŸ¬ const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setDragOver(false); }, []); const handleDrop = useCallback((e: React.DragEvent) => { 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, 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); } e.target.value = ''; }, [handleFileUpload]); // μ»΄ν¬λ„ŒνŠΈ λ³€κ²½ μ‹œ 둜컬 μƒνƒœ 동기화 useEffect(() => { setLocalInputs({ docType: component.fileConfig?.docType || "DOCUMENT", docTypeName: component.fileConfig?.docTypeName || "일반 λ¬Έμ„œ", dragDropText: component.fileConfig?.dragDropText || "νŒŒμΌμ„ λ“œλž˜κ·Έν•˜κ±°λ‚˜ ν΄λ¦­ν•˜μ—¬ μ—…λ‘œλ“œν•˜μ„Έμš”", maxSize: component.fileConfig?.maxSize || 10, maxFiles: component.fileConfig?.maxFiles || 5, newAcceptType: "", linkedTable: component.fileConfig?.linkedTable || "", linkedField: component.fileConfig?.linkedField || "", }); setLocalValues({ multiple: component.fileConfig?.multiple ?? true, showPreview: component.fileConfig?.showPreview ?? true, showProgress: component.fileConfig?.showProgress ?? true, autoLink: component.fileConfig?.autoLink ?? false, }); setAcceptTypes(component.fileConfig?.accept || []); // 파일 λͺ©λ‘ 동기화 - μ»΄ν¬λ„ŒνŠΈ IDκ°€ λ³€κ²½λ˜μ—ˆμ„ λ•Œλ§Œ μ΄ˆκΈ°ν™” const componentFiles = component.uploadedFiles || []; if (prevComponentIdRef.current !== component.id) { // μƒˆλ‘œμš΄ μ»΄ν¬λ„ŒνŠΈλ‘œ λ³€κ²½λœ 경우 // console.log("πŸ”„ FileComponentConfigPanel μƒˆ μ»΄ν¬λ„ŒνŠΈ 선택:", { // prevComponentId: prevComponentIdRef.current, // newComponentId: component.id, // componentFiles: componentFiles.length, // action: "μƒˆ μ»΄ν¬λ„ŒνŠΈ β†’ μƒνƒœ μ΄ˆκΈ°ν™”", // globalFileStateExists: !!getGlobalFileState()[component.id], // globalFileStateLength: getGlobalFileState()[component.id]?.length || 0, // localStorageExists: !!localStorage.getItem(`fileComponent_${component.id}_files`), // onUpdatePropertyExists: typeof onUpdateProperty === 'function' // }); // 1μˆœμœ„: μ „μ—­ μƒνƒœμ—μ„œ 파일 볡원 const globalFileState = getGlobalFileState(); const globalFiles = globalFileState[component.id]; if (globalFiles && globalFiles.length > 0) { // console.log("🌐 μ „μ—­ μƒνƒœμ—μ„œ 파일 볡원:", { // componentId: component.id, // globalFiles: globalFiles.length, // action: "μ „μ—­ μƒνƒœ β†’ μƒνƒœ 볡원" // }); setUploadedFiles(globalFiles); onUpdateProperty(component.id, "uploadedFiles", globalFiles); } // 2μˆœμœ„: localStorageμ—μ„œ λ°±μ—… 파일 볡원 else { const backupKey = `fileComponent_${component.id}_files`; const backupFiles = localStorage.getItem(backupKey); if (backupFiles && componentFiles.length === 0) { try { const parsedBackupFiles = JSON.parse(backupFiles); // console.log("πŸ“‚ localStorageμ—μ„œ 파일 볡원:", { // componentId: component.id, // backupFiles: parsedBackupFiles.length, // action: "λ°±μ—… β†’ μƒνƒœ 볡원" // }); setUploadedFiles(parsedBackupFiles); // μ „μ—­ μƒνƒœμ—λ„ μ €μž₯ setGlobalFileState(prev => ({ ...prev, [component.id]: parsedBackupFiles })); // μ»΄ν¬λ„ŒνŠΈ 속성에도 볡원 onUpdateProperty(component.id, "uploadedFiles", parsedBackupFiles); } catch (error) { // console.error("λ°±μ—… 파일 볡원 μ‹€νŒ¨:", error); setUploadedFiles(componentFiles); } } else { setUploadedFiles(componentFiles); } } prevComponentIdRef.current = component.id; } else if (componentFiles.length > 0 && JSON.stringify(componentFiles) !== JSON.stringify(uploadedFiles)) { // 같은 μ»΄ν¬λ„ŒνŠΈμ—μ„œ 파일이 μ—…λ°μ΄νŠΈλœ 경우 // console.log("πŸ”„ FileComponentConfigPanel 파일 동기화:", { // componentId: component.id, // componentFiles: componentFiles.length, // currentFiles: uploadedFiles.length, // action: "μ»΄ν¬λ„ŒνŠΈ β†’ μƒνƒœ 동기화" // }); setUploadedFiles(componentFiles); } }, [component.id]); // μ»΄ν¬λ„ŒνŠΈ IDκ°€ 변경될 λ•Œλ§Œ μ΄ˆκΈ°ν™” // μ „μ—­ 파일 μƒνƒœ λ³€κ²½ 감지 (ν™”λ©΄ 볡원 포함) useEffect(() => { const handleGlobalFileStateChange = (event: CustomEvent) => { 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 || 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 // }); } } } }; if (typeof window !== 'undefined') { window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); return () => { window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); }; } }, [component.id, onUpdateProperty]); // 미리 μ •μ˜λœ λ¬Έμ„œ νƒ€μž…λ“€ const docTypeOptions = [ { value: "CONTRACT", label: "κ³„μ•½μ„œ" }, { value: "DRAWING", label: "도면" }, { value: "PHOTO", label: "사진" }, { value: "DOCUMENT", label: "일반 λ¬Έμ„œ" }, { value: "REPORT", label: "λ³΄κ³ μ„œ" }, { value: "SPECIFICATION", label: "μ‚¬μ–‘μ„œ" }, { value: "MANUAL", label: "맀뉴얼" }, { value: "CERTIFICATE", label: "μΈμ¦μ„œ" }, { value: "OTHER", label: "기타" }, ]; // 미리 μ •μ˜λœ 파일 νƒ€μž…λ“€ const commonFileTypes = [ { value: "image/*", label: "이미지" }, { value: ".pdf", label: "PDF" }, { value: ".doc,.docx", label: "Word" }, { value: ".xls,.xlsx", label: "Excel" }, { value: ".ppt,.pptx", label: "PowerPoint" }, { value: ".hwp,.hwpx,.hwpml", label: "ν•œκΈ€" }, { value: ".hcdt", label: "ν•œμ…€" }, { value: ".hpt", label: "ν•œμ‡Ό" }, { value: ".pages", label: "Pages" }, { value: ".numbers", label: "Numbers" }, { value: ".keynote", label: "Keynote" }, { value: ".txt,.md,.rtf", label: "ν…μŠ€νŠΈ" }, { value: "video/*", label: "λΉ„λ””μ˜€" }, { value: "audio/*", label: "μ˜€λ””μ˜€" }, { value: ".zip,.rar,.7z", label: "μ••μΆ•νŒŒμΌ" }, ]; // 파일 νƒ€μž… μΆ”κ°€ const addCommonFileType = useCallback((fileType: string) => { const types = fileType.split(','); const newTypes = [...acceptTypes]; types.forEach(type => { if (!newTypes.includes(type.trim())) { newTypes.push(type.trim()); } }); setAcceptTypes(newTypes); onUpdateProperty(component.id, "fileConfig.accept", newTypes); }, [acceptTypes, component.id, onUpdateProperty]); // 파일 νƒ€μž… 제거 const removeAcceptType = useCallback((typeToRemove: string) => { const newTypes = acceptTypes.filter(type => type !== typeToRemove); setAcceptTypes(newTypes); onUpdateProperty(component.id, "fileConfig.accept", newTypes); }, [acceptTypes, component.id, onUpdateProperty]); return (
{/* κΈ°λ³Έ 정보 */}

κΈ°λ³Έ μ„€μ •

{ const newValue = e.target.value; setLocalInputs((prev) => ({ ...prev, docTypeName: newValue })); onUpdateProperty(component.id, "fileConfig.docTypeName", newValue); }} />
{/* 파일 μ—…λ‘œλ“œ μ œν•œ μ„€μ • */}

μ—…λ‘œλ“œ μ œν•œ

{ const newValue = parseInt(e.target.value) || 10; setLocalInputs((prev) => ({ ...prev, maxSize: newValue })); onUpdateProperty(component.id, "fileConfig.maxSize", newValue); }} />
{ const newValue = parseInt(e.target.value) || 5; setLocalInputs((prev) => ({ ...prev, maxFiles: newValue })); onUpdateProperty(component.id, "fileConfig.maxFiles", newValue); }} />
{ setLocalValues((prev) => ({ ...prev, multiple: checked as boolean })); onUpdateProperty(component.id, "fileConfig.multiple", checked); }} />
{/* ν—ˆμš© 파일 νƒ€μž… μ„€μ • */}

ν—ˆμš© 파일 νƒ€μž…

{acceptTypes.map((type, index) => ( {type} ))} {acceptTypes.length === 0 && λͺ¨λ“  파일 νƒ€μž… ν—ˆμš©}
{commonFileTypes.map((fileType) => ( ))}
{/* 파일 μ—…λ‘œλ“œ μ˜μ—­ */}

파일 μ—…λ‘œλ“œ

{ // 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 μš”μ†Œλ₯Ό 찾을 수 μ—†μŒ"); } } }} >
{uploading ? ( <>

μ—…λ‘œλ“œ 쀑...

) : ( <>

파일 μ—…λ‘œλ“œ

)}
{/* μ—…λ‘œλ“œλœ 파일 λͺ©λ‘ */} {uploadedFiles.length > 0 && (
총 {formatFileSize(uploadedFiles.reduce((sum, file) => sum + file.fileSize, 0))}
{uploadedFiles.map((file) => (
{getFileIcon(file.fileExt)}

{file.realFileName}

{formatFileSize(file.fileSize)} β€’ {file.fileExt.toUpperCase()}
))}
)}
{/* μ €μž₯ λ²„νŠΌ - μ£Όμ„μ²˜λ¦¬ (파일이 μžλ™μœΌλ‘œ μœ μ§€λ¨) */} {/* {uploadedFiles.length > 0 && (
{uploadedFiles.length}개 파일이 μž„μ‹œ μ €μž₯됨

λ‹€λ₯Έ μ»΄ν¬λ„ŒνŠΈλ‘œ μ΄λ™ν•˜κΈ° 전에 νŒŒμΌμ„ μ €μž₯ν•΄μ£Όμ„Έμš”.

)} */}
); };