diff --git a/frontend/components/screen/panels/ComponentsPanel.tsx b/frontend/components/screen/panels/ComponentsPanel.tsx index a076b867..aa7b894d 100644 --- a/frontend/components/screen/panels/ComponentsPanel.tsx +++ b/frontend/components/screen/panels/ComponentsPanel.tsx @@ -114,8 +114,7 @@ export function ComponentsPanel({ "image-display", // → v2-media (image) // 공통코드관리로 통합 예정 "category-manager", // → 공통코드관리 기능으로 통합 예정 - // 분할 패널 정리 (split-panel-layout v1 유지) - "split-panel-layout2", // → split-panel-layout로 통합 + // 분할 패널 정리 "screen-split-panel", // 화면 임베딩 방식은 사용하지 않음 // 미완성/미사용 컴포넌트 (기존 화면 호환성 유지, 새 추가만 막음) "accordion-basic", // 아코디언 컴포넌트 diff --git a/frontend/components/v2/V2Media.tsx b/frontend/components/v2/V2Media.tsx index 733d6657..0a4faaae 100644 --- a/frontend/components/v2/V2Media.tsx +++ b/frontend/components/v2/V2Media.tsx @@ -2,13 +2,13 @@ /** * V2Media - * + * * 통합 미디어 컴포넌트 (레거시 FileUploadComponent 기능 통합) * - file: 파일 업로드 * - image: 이미지 업로드/표시 * - video: 비디오 * - audio: 오디오 - * + * * 핵심 기능: * - FileViewerModal / FileManagerModal (자세히보기) * - 대표 이미지 설정 @@ -24,9 +24,23 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { V2MediaProps } from "@/types/v2-components"; -import { - Upload, X, File, Image as ImageIcon, Video, Music, Eye, Download, Trash2, Plus, - FileText, Archive, Presentation, FileImage, FileVideo, FileAudio +import { + Upload, + X, + File, + Image as ImageIcon, + Video, + Music, + Eye, + Download, + Trash2, + Plus, + FileText, + Archive, + Presentation, + FileImage, + FileVideo, + FileAudio, } from "lucide-react"; import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; @@ -77,115 +91,276 @@ const getFileIcon = (extension: string) => { /** * V2 미디어 컴포넌트 (레거시 기능 통합) */ -export const V2Media = forwardRef( - (props, ref) => { - const { - id, - label, - required, - readonly, - disabled, - style, - size, - config: configProp, - value, - onChange, - formData, - columnName, - tableName, - onFormDataChange, - isDesignMode = false, - isInteractive = true, - onUpdate, - ...restProps - } = props; +export const V2Media = forwardRef((props, ref) => { + const { + id, + label, + required, + readonly, + disabled, + style, + size, + config: configProp, + value, + onChange, + formData, + columnName, + tableName, + onFormDataChange, + isDesignMode = false, + isInteractive = true, + onUpdate, + ...restProps + } = props; - // 인증 정보 - const { user } = useAuth(); + // 인증 정보 + const { user } = useAuth(); - // config 기본값 - const config = configProp || { type: "file" as const }; - const mediaType = config.type || "file"; + // config 기본값 + const config = configProp || { type: "file" as const }; + const mediaType = config.type || "file"; - // 파일 상태 - const [uploadedFiles, setUploadedFiles] = useState([]); - const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle"); - const [dragOver, setDragOver] = useState(false); - const [representativeImageUrl, setRepresentativeImageUrl] = useState(null); - - // 모달 상태 - const [viewerFile, setViewerFile] = useState(null); - const [isViewerOpen, setIsViewerOpen] = useState(false); - const [isFileManagerOpen, setIsFileManagerOpen] = useState(false); - - const fileInputRef = useRef(null); + // 파일 상태 + const [uploadedFiles, setUploadedFiles] = useState([]); + const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle"); + const [dragOver, setDragOver] = useState(false); + const [representativeImageUrl, setRepresentativeImageUrl] = useState(null); - // 레코드 모드 판단 - const isRecordMode = !!(formData?.id && !String(formData.id).startsWith('temp_')); - const recordTableName = formData?.tableName || tableName; - const recordId = formData?.id; - // 🔑 columnName 우선 사용 (실제 DB 컬럼명), 없으면 id, 최후에 attachments - const effectiveColumnName = columnName || id || 'attachments'; + // 모달 상태 + const [viewerFile, setViewerFile] = useState(null); + const [isViewerOpen, setIsViewerOpen] = useState(false); + const [isFileManagerOpen, setIsFileManagerOpen] = useState(false); - // 레코드용 targetObjid 생성 - const getRecordTargetObjid = useCallback(() => { - if (isRecordMode && recordTableName && recordId) { - return `${recordTableName}:${recordId}:${effectiveColumnName}`; + const fileInputRef = useRef(null); + + // 레코드 모드 판단 + const isRecordMode = !!(formData?.id && !String(formData.id).startsWith("temp_")); + const recordTableName = formData?.tableName || tableName; + const recordId = formData?.id; + // 🔑 columnName 우선 사용 (실제 DB 컬럼명), 없으면 id, 최후에 attachments + const effectiveColumnName = columnName || id || "attachments"; + + // 레코드용 targetObjid 생성 + const getRecordTargetObjid = useCallback(() => { + if (isRecordMode && recordTableName && recordId) { + return `${recordTableName}:${recordId}:${effectiveColumnName}`; + } + return null; + }, [isRecordMode, recordTableName, recordId, effectiveColumnName]); + + // 레코드별 고유 키 생성 + const getUniqueKey = useCallback(() => { + if (isRecordMode && recordTableName && recordId) { + return `v2media_${recordTableName}_${recordId}_${id}`; + } + return `v2media_${id}`; + }, [isRecordMode, recordTableName, recordId, id]); + + // 레코드 ID 변경 시 파일 목록 초기화 + const prevRecordIdRef = useRef(null); + useEffect(() => { + if (prevRecordIdRef.current !== recordId) { + prevRecordIdRef.current = recordId; + if (isRecordMode) { + setUploadedFiles([]); } - return null; - }, [isRecordMode, recordTableName, recordId, effectiveColumnName]); + } + }, [recordId, isRecordMode]); - // 레코드별 고유 키 생성 - const getUniqueKey = useCallback(() => { - if (isRecordMode && recordTableName && recordId) { - return `v2media_${recordTableName}_${recordId}_${id}`; - } - return `v2media_${id}`; - }, [isRecordMode, recordTableName, recordId, id]); + // 컴포넌트 마운트 시 localStorage에서 파일 복원 + useEffect(() => { + if (!id) return; - // 레코드 ID 변경 시 파일 목록 초기화 - const prevRecordIdRef = useRef(null); - useEffect(() => { - if (prevRecordIdRef.current !== recordId) { - prevRecordIdRef.current = recordId; - if (isRecordMode) { - setUploadedFiles([]); - } - } - }, [recordId, isRecordMode]); + try { + const backupKey = getUniqueKey(); + const backupFiles = localStorage.getItem(backupKey); + if (backupFiles) { + const parsedFiles = JSON.parse(backupFiles); + if (parsedFiles.length > 0) { + setUploadedFiles(parsedFiles); - // 컴포넌트 마운트 시 localStorage에서 파일 복원 - useEffect(() => { - if (!id) return; - - try { - const backupKey = getUniqueKey(); - const backupFiles = localStorage.getItem(backupKey); - if (backupFiles) { - const parsedFiles = JSON.parse(backupFiles); - if (parsedFiles.length > 0) { - setUploadedFiles(parsedFiles); - - if (typeof window !== "undefined") { - (window as any).globalFileState = { - ...(window as any).globalFileState, - [backupKey]: parsedFiles, - }; - } + if (typeof window !== "undefined") { + (window as any).globalFileState = { + ...(window as any).globalFileState, + [backupKey]: parsedFiles, + }; } } - } catch (e) { - console.warn("파일 복원 실패:", e); } - }, [id, getUniqueKey, recordId]); + } catch (e) { + console.warn("파일 복원 실패:", e); + } + }, [id, getUniqueKey, recordId]); - // DB에서 파일 목록 로드 - const loadComponentFiles = useCallback(async () => { - if (!id) return false; + // DB에서 파일 목록 로드 + const loadComponentFiles = useCallback(async () => { + if (!id) return false; + + try { + let screenId = formData?.screenId; + + if (!screenId && typeof window !== "undefined") { + const pathname = window.location.pathname; + const screenMatch = pathname.match(/\/screens\/(\d+)/); + if (screenMatch) { + screenId = parseInt(screenMatch[1]); + } + } + + if (!screenId && isDesignMode) { + screenId = 999999; + } + + if (!screenId) { + screenId = 0; + } + + const params = { + screenId, + componentId: id, + tableName: recordTableName || formData?.tableName || tableName, + recordId: recordId || formData?.id, + columnName: effectiveColumnName, + }; + + const response = await getComponentFiles(params); + + if (response.success) { + const formattedFiles = response.totalFiles.map((file: any) => ({ + objid: file.objid || file.id, + savedFileName: file.savedFileName || file.saved_file_name, + realFileName: file.realFileName || file.real_file_name, + fileSize: file.fileSize || file.file_size, + fileExt: file.fileExt || file.file_ext, + regdate: file.regdate, + status: file.status || "ACTIVE", + uploadedAt: file.uploadedAt || new Date().toISOString(), + targetObjid: file.targetObjid || file.target_objid, + filePath: file.filePath || file.file_path, + ...file, + })); + + // localStorage와 병합 + let finalFiles = formattedFiles; + const uniqueKey = getUniqueKey(); + try { + const backupFiles = localStorage.getItem(uniqueKey); + if (backupFiles) { + const parsedBackupFiles = JSON.parse(backupFiles); + const serverObjIds = new Set(formattedFiles.map((f: any) => f.objid)); + const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid)); + finalFiles = [...formattedFiles, ...additionalFiles]; + } + } catch (e) { + console.warn("파일 병합 오류:", e); + } + + setUploadedFiles(finalFiles); + + if (typeof window !== "undefined") { + (window as any).globalFileState = { + ...(window as any).globalFileState, + [uniqueKey]: finalFiles, + }; + + GlobalFileManager.registerFiles(finalFiles, { + uploadPage: window.location.pathname, + componentId: id, + screenId: formData?.screenId, + recordId: recordId, + }); + + try { + localStorage.setItem(uniqueKey, JSON.stringify(finalFiles)); + } catch (e) { + console.warn("localStorage 백업 실패:", e); + } + } + return true; + } + } catch (error) { + console.error("파일 조회 오류:", error); + } + return false; + }, [ + id, + tableName, + columnName, + formData?.screenId, + formData?.tableName, + formData?.id, + getUniqueKey, + recordId, + isRecordMode, + recordTableName, + effectiveColumnName, + isDesignMode, + ]); + + // 파일 동기화 + useEffect(() => { + loadComponentFiles(); + }, [loadComponentFiles]); + + // 전역 상태 변경 감지 + useEffect(() => { + const handleGlobalFileStateChange = (event: CustomEvent) => { + const { componentId, files, isRestore } = event.detail; + + if (componentId === id) { + setUploadedFiles(files); + + try { + const backupKey = getUniqueKey(); + 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); + }; + } + }, [id, getUniqueKey]); + + // 파일 업로드 처리 + const handleFileUpload = useCallback( + async (files: File[]) => { + if (!files.length) return; + + // 중복 체크 + const existingFileNames = uploadedFiles.map((f) => f.realFileName.toLowerCase()); + const duplicates: string[] = []; + const uniqueFiles: File[] = []; + + files.forEach((file) => { + const fileName = file.name.toLowerCase(); + if (existingFileNames.includes(fileName)) { + duplicates.push(file.name); + } else { + uniqueFiles.push(file); + } + }); + + if (duplicates.length > 0) { + toast.error(`중복된 파일: ${duplicates.join(", ")}`); + if (uniqueFiles.length === 0) return; + toast.info(`${uniqueFiles.length}개의 새로운 파일만 업로드합니다.`); + } + + const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files; + setUploadStatus("uploading"); + toast.loading("파일 업로드 중...", { id: "file-upload" }); try { + const effectiveTableName = recordTableName || formData?.tableName || tableName || "default_table"; + const effectiveRecordId = recordId || formData?.id; + let screenId = formData?.screenId; - if (!screenId && typeof window !== "undefined") { const pathname = window.location.pathname; const screenMatch = pathname.match(/\/screens\/(\d+)/); @@ -194,368 +369,73 @@ export const V2Media = forwardRef( } } - if (!screenId && isDesignMode) { - screenId = 999999; + let targetObjid; + const effectiveIsRecordMode = + isRecordMode || (effectiveRecordId && !String(effectiveRecordId).startsWith("temp_")); + + if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) { + targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`; + } else if (screenId) { + targetObjid = `screen_files:${screenId}:${id}:${effectiveColumnName}`; + } else { + targetObjid = `temp_${id}`; } - if (!screenId) { - screenId = 0; - } + const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode; - const params = { - screenId, - componentId: id, - tableName: recordTableName || formData?.tableName || tableName, - recordId: recordId || formData?.id, + const finalLinkedTable = effectiveIsRecordMode + ? effectiveTableName + : formData?.linkedTable || effectiveTableName; + + const uploadData = { + autoLink: formData?.autoLink || true, + linkedTable: finalLinkedTable, + recordId: effectiveRecordId || `temp_${id}`, columnName: effectiveColumnName, + isVirtualFileColumn: formData?.isVirtualFileColumn || true, + docType: config?.docType || "DOCUMENT", + docTypeName: config?.docTypeName || "일반 문서", + companyCode: userCompanyCode, + tableName: effectiveTableName, + fieldName: effectiveColumnName, + targetObjid: targetObjid, + isRecordMode: effectiveIsRecordMode, }; - const response = await getComponentFiles(params); + const response = await uploadFiles({ + files: filesToUpload, + ...uploadData, + }); if (response.success) { - const formattedFiles = response.totalFiles.map((file: any) => ({ + const fileData = response.files || (response as any).data || []; + + if (fileData.length === 0) { + throw new Error("업로드된 파일 데이터를 받지 못했습니다."); + } + + const newFiles = fileData.map((file: any) => ({ objid: file.objid || file.id, - savedFileName: file.savedFileName || file.saved_file_name, - realFileName: file.realFileName || file.real_file_name, - fileSize: file.fileSize || file.file_size, - fileExt: file.fileExt || file.file_ext, + savedFileName: file.saved_file_name || file.savedFileName, + realFileName: file.real_file_name || file.realFileName || file.name, + fileSize: file.file_size || file.fileSize || file.size, + fileExt: file.file_ext || file.fileExt || file.extension, + filePath: file.file_path || file.filePath || file.path, + docType: file.doc_type || file.docType, + docTypeName: file.doc_type_name || file.docTypeName, + targetObjid: file.target_objid || file.targetObjid, + parentTargetObjid: file.parent_target_objid || file.parentTargetObjid, + companyCode: file.company_code || file.companyCode, + writer: file.writer, regdate: file.regdate, status: file.status || "ACTIVE", - uploadedAt: file.uploadedAt || new Date().toISOString(), - targetObjid: file.targetObjid || file.target_objid, - filePath: file.filePath || file.file_path, + uploadedAt: new Date().toISOString(), ...file, })); - // localStorage와 병합 - let finalFiles = formattedFiles; - const uniqueKey = getUniqueKey(); - try { - const backupFiles = localStorage.getItem(uniqueKey); - if (backupFiles) { - const parsedBackupFiles = JSON.parse(backupFiles); - const serverObjIds = new Set(formattedFiles.map((f: any) => f.objid)); - const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid)); - finalFiles = [...formattedFiles, ...additionalFiles]; - } - } catch (e) { - console.warn("파일 병합 오류:", e); - } - - setUploadedFiles(finalFiles); - - if (typeof window !== "undefined") { - (window as any).globalFileState = { - ...(window as any).globalFileState, - [uniqueKey]: finalFiles, - }; - - GlobalFileManager.registerFiles(finalFiles, { - uploadPage: window.location.pathname, - componentId: id, - screenId: formData?.screenId, - recordId: recordId, - }); - - try { - localStorage.setItem(uniqueKey, JSON.stringify(finalFiles)); - } catch (e) { - console.warn("localStorage 백업 실패:", e); - } - } - return true; - } - } catch (error) { - console.error("파일 조회 오류:", error); - } - return false; - }, [id, tableName, columnName, formData?.screenId, formData?.tableName, formData?.id, getUniqueKey, recordId, isRecordMode, recordTableName, effectiveColumnName, isDesignMode]); - - // 파일 동기화 - useEffect(() => { - loadComponentFiles(); - }, [loadComponentFiles]); - - // 전역 상태 변경 감지 - useEffect(() => { - const handleGlobalFileStateChange = (event: CustomEvent) => { - const { componentId, files, isRestore } = event.detail; - - if (componentId === id) { - setUploadedFiles(files); - - try { - const backupKey = getUniqueKey(); - 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); - }; - } - }, [id, getUniqueKey]); - - // 파일 업로드 처리 - const handleFileUpload = useCallback( - async (files: File[]) => { - if (!files.length) return; - - // 중복 체크 - const existingFileNames = uploadedFiles.map((f) => f.realFileName.toLowerCase()); - const duplicates: string[] = []; - const uniqueFiles: File[] = []; - - files.forEach((file) => { - const fileName = file.name.toLowerCase(); - if (existingFileNames.includes(fileName)) { - duplicates.push(file.name); - } else { - uniqueFiles.push(file); - } - }); - - if (duplicates.length > 0) { - toast.error(`중복된 파일: ${duplicates.join(", ")}`); - if (uniqueFiles.length === 0) return; - toast.info(`${uniqueFiles.length}개의 새로운 파일만 업로드합니다.`); - } - - const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files; - setUploadStatus("uploading"); - toast.loading("파일 업로드 중...", { id: "file-upload" }); - - try { - const effectiveTableName = recordTableName || formData?.tableName || tableName || "default_table"; - const effectiveRecordId = recordId || formData?.id; - - let screenId = formData?.screenId; - if (!screenId && typeof window !== "undefined") { - const pathname = window.location.pathname; - const screenMatch = pathname.match(/\/screens\/(\d+)/); - if (screenMatch) { - screenId = parseInt(screenMatch[1]); - } - } - - let targetObjid; - const effectiveIsRecordMode = isRecordMode || (effectiveRecordId && !String(effectiveRecordId).startsWith('temp_')); - - if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) { - targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`; - } else if (screenId) { - targetObjid = `screen_files:${screenId}:${id}:${effectiveColumnName}`; - } else { - targetObjid = `temp_${id}`; - } - - const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode; - - const finalLinkedTable = effectiveIsRecordMode - ? effectiveTableName - : (formData?.linkedTable || effectiveTableName); - - const uploadData = { - autoLink: formData?.autoLink || true, - linkedTable: finalLinkedTable, - recordId: effectiveRecordId || `temp_${id}`, - columnName: effectiveColumnName, - isVirtualFileColumn: formData?.isVirtualFileColumn || true, - docType: config?.docType || "DOCUMENT", - docTypeName: config?.docTypeName || "일반 문서", - companyCode: userCompanyCode, - tableName: effectiveTableName, - fieldName: effectiveColumnName, - targetObjid: targetObjid, - isRecordMode: effectiveIsRecordMode, - }; - - const response = await uploadFiles({ - files: filesToUpload, - ...uploadData, - }); - - if (response.success) { - const fileData = response.files || (response as any).data || []; - - if (fileData.length === 0) { - throw new Error("업로드된 파일 데이터를 받지 못했습니다."); - } - - const newFiles = fileData.map((file: any) => ({ - objid: file.objid || file.id, - savedFileName: file.saved_file_name || file.savedFileName, - realFileName: file.real_file_name || file.realFileName || file.name, - fileSize: file.file_size || file.fileSize || file.size, - fileExt: file.file_ext || file.fileExt || file.extension, - filePath: file.file_path || file.filePath || file.path, - docType: file.doc_type || file.docType, - docTypeName: file.doc_type_name || file.docTypeName, - targetObjid: file.target_objid || file.targetObjid, - parentTargetObjid: file.parent_target_objid || file.parentTargetObjid, - companyCode: file.company_code || file.companyCode, - writer: file.writer, - regdate: file.regdate, - status: file.status || "ACTIVE", - uploadedAt: new Date().toISOString(), - ...file, - })); - - const updatedFiles = [...uploadedFiles, ...newFiles]; - setUploadedFiles(updatedFiles); - setUploadStatus("success"); - - // localStorage 백업 - try { - const backupKey = getUniqueKey(); - localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); - } catch (e) { - console.warn("localStorage 백업 실패:", e); - } - - // 전역 상태 업데이트 - if (typeof window !== "undefined") { - const globalFileState = (window as any).globalFileState || {}; - const uniqueKey = getUniqueKey(); - globalFileState[uniqueKey] = updatedFiles; - (window as any).globalFileState = globalFileState; - - GlobalFileManager.registerFiles(newFiles, { - uploadPage: window.location.pathname, - componentId: id, - screenId: formData?.screenId, - recordId: recordId, - }); - - const syncEvent = new CustomEvent("globalFileStateChanged", { - detail: { - componentId: id, - uniqueKey: uniqueKey, - recordId: recordId, - files: updatedFiles, - fileCount: updatedFiles.length, - timestamp: Date.now(), - }, - }); - window.dispatchEvent(syncEvent); - } - - // 부모 컴포넌트 업데이트 - if (onUpdate) { - onUpdate({ - uploadedFiles: updatedFiles, - lastFileUpdate: Date.now(), - }); - } - - // onChange 콜백 (objid 배열 또는 단일 값) - const fileIds = updatedFiles.map((f) => f.objid); - const finalValue = config.multiple ? fileIds : fileIds[0] || ""; - const targetColumn = columnName || effectiveColumnName; - - console.log("📤 [V2Media] 파일 업로드 완료 - 값 전달:", { - columnName: targetColumn, - fileIds, - finalValue, - hasOnChange: !!onChange, - hasOnFormDataChange: !!onFormDataChange, - }); - - if (onChange) { - onChange(finalValue); - } - - // 폼 데이터 업데이트 - 부모 컴포넌트 시그니처에 맞게 (fieldName, value) 형식 - if (onFormDataChange && targetColumn) { - // 🔑 단일 파일: 첫 번째 objid만 전달 (DB 컬럼에 저장될 값) - // 복수 파일: 콤마 구분 문자열로 전달 - const formValue = config.multiple - ? fileIds.join(',') - : (fileIds[0] || ''); - - console.log("📝 [V2Media] formData 업데이트:", { - columnName: targetColumn, - fileIds, - formValue, - isMultiple: config.multiple, - isRecordMode: effectiveIsRecordMode, - }); - // (fieldName: string, value: any) 형식으로 호출 - onFormDataChange(targetColumn, formValue); - } - - // 그리드 파일 상태 새로고침 이벤트 발생 - if (typeof window !== "undefined") { - const refreshEvent = new CustomEvent("refreshFileStatus", { - detail: { - tableName: effectiveTableName, - recordId: effectiveRecordId, - columnName: targetColumn, - targetObjid: targetObjid, - fileCount: updatedFiles.length, - }, - }); - window.dispatchEvent(refreshEvent); - } - - toast.dismiss("file-upload"); - toast.success(`${newFiles.length}개 파일 업로드 완료`); - } else { - throw new Error(response.message || (response as any).error || "파일 업로드 실패"); - } - } catch (error) { - console.error("파일 업로드 오류:", error); - setUploadStatus("error"); - toast.dismiss("file-upload"); - toast.error(`업로드 오류: ${error instanceof Error ? error.message : "알 수 없는 오류"}`); - } - }, - [config, uploadedFiles, onChange, id, getUniqueKey, recordId, isRecordMode, recordTableName, effectiveColumnName, tableName, onUpdate, onFormDataChange, user, columnName], - ); - - // 파일 뷰어 열기/닫기 - const handleFileView = useCallback((file: FileInfo) => { - setViewerFile(file); - setIsViewerOpen(true); - }, []); - - const handleViewerClose = useCallback(() => { - setIsViewerOpen(false); - setViewerFile(null); - }, []); - - // 파일 다운로드 - const handleFileDownload = useCallback(async (file: FileInfo) => { - try { - await downloadFile({ - fileId: file.objid, - serverFilename: file.savedFileName, - originalName: file.realFileName, - }); - toast.success(`${file.realFileName} 다운로드 완료`); - } catch (error) { - console.error("파일 다운로드 오류:", error); - toast.error("파일 다운로드 실패"); - } - }, []); - - // 파일 삭제 - const handleFileDelete = useCallback( - async (file: FileInfo | string) => { - try { - const fileId = typeof file === "string" ? file : file.objid; - const fileName = typeof file === "string" ? "파일" : file.realFileName; - const serverFilename = typeof file === "string" ? "temp_file" : file.savedFileName; - - await deleteFile(fileId, serverFilename); - - const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId); + const updatedFiles = [...uploadedFiles, ...newFiles]; setUploadedFiles(updatedFiles); + setUploadStatus("success"); // localStorage 백업 try { @@ -572,6 +452,13 @@ export const V2Media = forwardRef( globalFileState[uniqueKey] = updatedFiles; (window as any).globalFileState = globalFileState; + GlobalFileManager.registerFiles(newFiles, { + uploadPage: window.location.pathname, + componentId: id, + screenId: formData?.screenId, + recordId: recordId, + }); + const syncEvent = new CustomEvent("globalFileStateChanged", { detail: { componentId: id, @@ -580,12 +467,12 @@ export const V2Media = forwardRef( files: updatedFiles, fileCount: updatedFiles.length, timestamp: Date.now(), - action: "delete", }, }); window.dispatchEvent(syncEvent); } + // 부모 컴포넌트 업데이트 if (onUpdate) { onUpdate({ uploadedFiles: updatedFiles, @@ -593,15 +480,17 @@ export const V2Media = forwardRef( }); } - // onChange 콜백 + // onChange 콜백 (objid 배열 또는 단일 값) const fileIds = updatedFiles.map((f) => f.objid); const finalValue = config.multiple ? fileIds : fileIds[0] || ""; const targetColumn = columnName || effectiveColumnName; - console.log("🗑️ [V2Media] 파일 삭제 완료 - 값 전달:", { + console.log("📤 [V2Media] 파일 업로드 완료 - 값 전달:", { columnName: targetColumn, fileIds, finalValue, + hasOnChange: !!onChange, + hasOnFormDataChange: !!onFormDataChange, }); if (onChange) { @@ -612,120 +501,286 @@ export const V2Media = forwardRef( if (onFormDataChange && targetColumn) { // 🔑 단일 파일: 첫 번째 objid만 전달 (DB 컬럼에 저장될 값) // 복수 파일: 콤마 구분 문자열로 전달 - const formValue = config.multiple - ? fileIds.join(',') - : (fileIds[0] || ''); - - console.log("🗑️ [V2Media] 삭제 후 formData 업데이트:", { + const formValue = config.multiple ? fileIds.join(",") : fileIds[0] || ""; + + console.log("📝 [V2Media] formData 업데이트:", { columnName: targetColumn, fileIds, formValue, + isMultiple: config.multiple, + isRecordMode: effectiveIsRecordMode, }); // (fieldName: string, value: any) 형식으로 호출 onFormDataChange(targetColumn, formValue); } - toast.success(`${fileName} 삭제 완료`); - } catch (error) { - console.error("파일 삭제 오류:", error); - toast.error("파일 삭제 실패"); - } - }, - [uploadedFiles, onUpdate, id, isRecordMode, onFormDataChange, recordTableName, recordId, effectiveColumnName, getUniqueKey, onChange, config.multiple, columnName], - ); - - // 대표 이미지 로드 - const loadRepresentativeImage = useCallback( - async (file: FileInfo) => { - try { - const isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes( - file.fileExt.toLowerCase().replace(".", "") - ); - - if (!isImage) { - setRepresentativeImageUrl(null); - return; + // 그리드 파일 상태 새로고침 이벤트 발생 + if (typeof window !== "undefined") { + const refreshEvent = new CustomEvent("refreshFileStatus", { + detail: { + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: targetColumn, + targetObjid: targetObjid, + fileCount: updatedFiles.length, + }, + }); + window.dispatchEvent(refreshEvent); } - if (!file.objid || file.objid === "0" || file.objid === "") { - setRepresentativeImageUrl(null); - return; - } - - const response = await apiClient.get(`/files/download/${file.objid}`, { - params: { serverFilename: file.savedFileName }, - responseType: "blob", - }); - - const blob = new Blob([response.data]); - const url = window.URL.createObjectURL(blob); - - if (representativeImageUrl) { - window.URL.revokeObjectURL(representativeImageUrl); - } - - setRepresentativeImageUrl(url); - } catch (error) { - console.error("대표 이미지 로드 실패:", error); - setRepresentativeImageUrl(null); + toast.dismiss("file-upload"); + toast.success(`${newFiles.length}개 파일 업로드 완료`); + } else { + throw new Error(response.message || (response as any).error || "파일 업로드 실패"); } - }, - [representativeImageUrl], - ); - - // 대표 이미지 설정 - const handleSetRepresentative = useCallback( - async (file: FileInfo) => { - try { - const { setRepresentativeFile } = await import("@/lib/api/file"); - await setRepresentativeFile(file.objid); - - const updatedFiles = uploadedFiles.map((f) => ({ - ...f, - isRepresentative: f.objid === file.objid, - })); - - setUploadedFiles(updatedFiles); - loadRepresentativeImage(file); - } catch (e) { - console.error("대표 파일 설정 실패:", e); - } - }, - [uploadedFiles, loadRepresentativeImage] - ); - - // uploadedFiles 변경 시 대표 이미지 로드 - useEffect(() => { - const representativeFile = uploadedFiles.find(f => f.isRepresentative) || uploadedFiles[0]; - if (representativeFile) { - loadRepresentativeImage(representativeFile); - } else { - setRepresentativeImageUrl(null); + } catch (error) { + console.error("파일 업로드 오류:", error); + setUploadStatus("error"); + toast.dismiss("file-upload"); + toast.error(`업로드 오류: ${error instanceof Error ? error.message : "알 수 없는 오류"}`); } + }, + [ + config, + uploadedFiles, + onChange, + id, + getUniqueKey, + recordId, + isRecordMode, + recordTableName, + effectiveColumnName, + tableName, + onUpdate, + onFormDataChange, + user, + columnName, + ], + ); + + // 파일 뷰어 열기/닫기 + const handleFileView = useCallback((file: FileInfo) => { + setViewerFile(file); + setIsViewerOpen(true); + }, []); + + const handleViewerClose = useCallback(() => { + setIsViewerOpen(false); + setViewerFile(null); + }, []); + + // 파일 다운로드 + const handleFileDownload = useCallback(async (file: FileInfo) => { + try { + await downloadFile({ + fileId: file.objid, + serverFilename: file.savedFileName, + originalName: file.realFileName, + }); + toast.success(`${file.realFileName} 다운로드 완료`); + } catch (error) { + console.error("파일 다운로드 오류:", error); + toast.error("파일 다운로드 실패"); + } + }, []); + + // 파일 삭제 + const handleFileDelete = useCallback( + async (file: FileInfo | string) => { + try { + const fileId = typeof file === "string" ? file : file.objid; + const fileName = typeof file === "string" ? "파일" : file.realFileName; + const serverFilename = typeof file === "string" ? "temp_file" : file.savedFileName; + + await deleteFile(fileId, serverFilename); + + const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId); + setUploadedFiles(updatedFiles); + + // localStorage 백업 + try { + const backupKey = getUniqueKey(); + localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); + } catch (e) { + console.warn("localStorage 백업 실패:", e); + } + + // 전역 상태 업데이트 + if (typeof window !== "undefined") { + const globalFileState = (window as any).globalFileState || {}; + const uniqueKey = getUniqueKey(); + globalFileState[uniqueKey] = updatedFiles; + (window as any).globalFileState = globalFileState; + + const syncEvent = new CustomEvent("globalFileStateChanged", { + detail: { + componentId: id, + uniqueKey: uniqueKey, + recordId: recordId, + files: updatedFiles, + fileCount: updatedFiles.length, + timestamp: Date.now(), + action: "delete", + }, + }); + window.dispatchEvent(syncEvent); + } + + if (onUpdate) { + onUpdate({ + uploadedFiles: updatedFiles, + lastFileUpdate: Date.now(), + }); + } + + // onChange 콜백 + const fileIds = updatedFiles.map((f) => f.objid); + const finalValue = config.multiple ? fileIds : fileIds[0] || ""; + const targetColumn = columnName || effectiveColumnName; + + console.log("🗑️ [V2Media] 파일 삭제 완료 - 값 전달:", { + columnName: targetColumn, + fileIds, + finalValue, + }); + + if (onChange) { + onChange(finalValue); + } + + // 폼 데이터 업데이트 - 부모 컴포넌트 시그니처에 맞게 (fieldName, value) 형식 + if (onFormDataChange && targetColumn) { + // 🔑 단일 파일: 첫 번째 objid만 전달 (DB 컬럼에 저장될 값) + // 복수 파일: 콤마 구분 문자열로 전달 + const formValue = config.multiple ? fileIds.join(",") : fileIds[0] || ""; + + console.log("🗑️ [V2Media] 삭제 후 formData 업데이트:", { + columnName: targetColumn, + fileIds, + formValue, + }); + // (fieldName: string, value: any) 형식으로 호출 + onFormDataChange(targetColumn, formValue); + } + + toast.success(`${fileName} 삭제 완료`); + } catch (error) { + console.error("파일 삭제 오류:", error); + toast.error("파일 삭제 실패"); + } + }, + [ + uploadedFiles, + onUpdate, + id, + isRecordMode, + onFormDataChange, + recordTableName, + recordId, + effectiveColumnName, + getUniqueKey, + onChange, + config.multiple, + columnName, + ], + ); + + // 대표 이미지 로드 + const loadRepresentativeImage = useCallback( + async (file: FileInfo) => { + try { + const isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes( + file.fileExt.toLowerCase().replace(".", ""), + ); + + if (!isImage) { + setRepresentativeImageUrl(null); + return; + } + + if (!file.objid || file.objid === "0" || file.objid === "") { + setRepresentativeImageUrl(null); + return; + } + + const response = await apiClient.get(`/files/download/${file.objid}`, { + params: { serverFilename: file.savedFileName }, + responseType: "blob", + }); + + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); - return () => { if (representativeImageUrl) { window.URL.revokeObjectURL(representativeImageUrl); } - }; - }, [uploadedFiles]); - // 드래그 앤 드롭 핸들러 - const handleDragOver = useCallback((e: React.DragEvent) => { + setRepresentativeImageUrl(url); + } catch (error) { + console.error("대표 이미지 로드 실패:", error); + setRepresentativeImageUrl(null); + } + }, + [representativeImageUrl], + ); + + // 대표 이미지 설정 + const handleSetRepresentative = useCallback( + async (file: FileInfo) => { + try { + const { setRepresentativeFile } = await import("@/lib/api/file"); + await setRepresentativeFile(file.objid); + + const updatedFiles = uploadedFiles.map((f) => ({ + ...f, + isRepresentative: f.objid === file.objid, + })); + + setUploadedFiles(updatedFiles); + loadRepresentativeImage(file); + } catch (e) { + console.error("대표 파일 설정 실패:", e); + } + }, + [uploadedFiles, loadRepresentativeImage], + ); + + // uploadedFiles 변경 시 대표 이미지 로드 + useEffect(() => { + const representativeFile = uploadedFiles.find((f) => f.isRepresentative) || uploadedFiles[0]; + if (representativeFile) { + loadRepresentativeImage(representativeFile); + } else { + setRepresentativeImageUrl(null); + } + + return () => { + if (representativeImageUrl) { + window.URL.revokeObjectURL(representativeImageUrl); + } + }; + }, [uploadedFiles]); + + // 드래그 앤 드롭 핸들러 + const handleDragOver = useCallback( + (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!readonly && !disabled) { setDragOver(true); } - }, [readonly, disabled]); + }, + [readonly, disabled], + ); - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setDragOver(false); - }, []); + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragOver(false); + }, []); - const handleDrop = useCallback((e: React.DragEvent) => { + const handleDrop = useCallback( + (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragOver(false); @@ -736,93 +791,93 @@ export const V2Media = forwardRef( handleFileUpload(files); } } - }, [readonly, disabled, handleFileUpload]); + }, + [readonly, disabled, handleFileUpload], + ); - // 파일 선택 - const handleFileSelect = useCallback(() => { - if (fileInputRef.current) { - fileInputRef.current.click(); - } - }, []); + // 파일 선택 + const handleFileSelect = useCallback(() => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }, []); - const handleInputChange = useCallback((e: React.ChangeEvent) => { + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length > 0) { handleFileUpload(files); } - e.target.value = ''; - }, [handleFileUpload]); + e.target.value = ""; + }, + [handleFileUpload], + ); - // 파일 설정 - const fileConfig: FileUploadConfig = { - accept: config.accept || "*/*", - multiple: config.multiple || false, - maxSize: config.maxSize || 10 * 1024 * 1024, - disabled: disabled, - readonly: readonly, - }; + // 파일 설정 + const fileConfig: FileUploadConfig = { + accept: config.accept || "*/*", + multiple: config.multiple || false, + maxSize: config.maxSize || 10 * 1024 * 1024, + disabled: disabled, + readonly: readonly, + }; - const showLabel = label && style?.labelDisplay !== false; - const componentWidth = size?.width || style?.width; - const componentHeight = size?.height || style?.height; + const showLabel = label && style?.labelDisplay !== false; + const componentWidth = size?.width || style?.width; + const componentHeight = size?.height || style?.height; - return ( -
- {/* 라벨 */} - {showLabel && ( - - )} - - {/* 메인 컨테이너 */} -
+ {/* 라벨 */} + {showLabel && ( +