import { useCallback, useRef, useState } from 'react'; import { completeChunkUpload, uploadFileChunk } from '../api/imagery'; export type FileUploadStatus = 'idle' | 'uploading' | 'completed' | 'error' export type UploadingFile = { id: string; file: File; fileName: string; progress: number; status: FileUploadStatus; totalSize: number; uploadedSize: number; uuid?: string; } const CHUNK_SIZE = 10 * 1024 * 1024 // 10MB const MAX_CONCURRENT_UPLOADS = 3 export const useAerialChunkUpload = () => { const [files, setFiles] = useState([]) const abortControllersRef = useRef>(new Map()) const updateFileState = useCallback((id: string, updates: Partial) => { setFiles((prev) => prev.map((f) => f.id === id ? { ...f, ...updates } : f)) }, []) const uploadSingleFile = useCallback(async (uploadFile: UploadingFile) => { const { id, file } = uploadFile const abortController = new AbortController() abortControllersRef.current.set(id, abortController) const { signal } = abortController const fileSize = file.size const fileName = file.name const totalChunks = Math.ceil(fileSize / CHUNK_SIZE) updateFileState(id, { status: 'uploading', progress: 0 }) let uuid = '' try { for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { if (signal.aborted) { return } const start = chunkIndex * CHUNK_SIZE const end = Math.min(start + CHUNK_SIZE, fileSize) const chunkFile = file.slice(start, end) const response = await uploadFileChunk({ fileName, fileSize, chunkIndex, chunkTotalIndex: totalChunks - 1, chunkFile, }) if (signal.aborted) { return } // 첫 번째 청크 응답에서 uuid 저장 if (chunkIndex === 0 && response.uuid) { uuid = response.uuid updateFileState(id, { uuid }) } const progress = Math.round(((chunkIndex + 1) / totalChunks) * 100) const uploadedSize = end updateFileState(id, { progress, uploadedSize, }) } // 청크 업로드 완료 후 completeChunkUpload 호출 if (uuid) { await completeChunkUpload(uuid) } updateFileState(id, { status: 'completed', progress: 100 }) } catch (error) { if (!signal.aborted) { console.error('Upload error:', error) updateFileState(id, { status: 'error' }) } } finally { abortControllersRef.current.delete(id) } }, [updateFileState]) const addFiles = useCallback((newFiles: File[]) => { const uploadFiles: UploadingFile[] = newFiles.map((file) => ({ id: crypto.randomUUID(), file, fileName: file.name, progress: 0, status: 'idle' as FileUploadStatus, totalSize: file.size, uploadedSize: 0, })) setFiles((prev) => [...prev, ...uploadFiles]) return uploadFiles }, []) const startUpload = useCallback(async (filesToUpload?: UploadingFile[]) => { const targetFiles = filesToUpload || files.filter((f) => f.status === 'idle') // 병렬 업로드 (최대 MAX_CONCURRENT_UPLOADS개씩) for (let i = 0; i < targetFiles.length; i += MAX_CONCURRENT_UPLOADS) { const batch = targetFiles.slice(i, i + MAX_CONCURRENT_UPLOADS) await Promise.all(batch.map((file) => uploadSingleFile(file))) } }, [files, uploadSingleFile]) const removeFile = useCallback((id: string) => { // 업로드 중이면 취소 const controller = abortControllersRef.current.get(id) if (controller) { controller.abort() abortControllersRef.current.delete(id) } setFiles((prev) => prev.filter((f) => f.id !== id)) }, []) const cancelAllUploads = useCallback(() => { abortControllersRef.current.forEach((controller) => controller.abort()) abortControllersRef.current.clear() setFiles([]) }, []) const reset = useCallback(() => { cancelAllUploads() }, [cancelAllUploads]) const isUploading = files.some((f) => f.status === 'uploading') const isAllCompleted = files.length > 0 && files.every((f) => f.status === 'completed') const hasFiles = files.length > 0 const totalSize = files.reduce((acc, f) => acc + f.totalSize, 0) const uploadedSize = files.reduce((acc, f) => acc + f.uploadedSize, 0) const overallProgress = totalSize > 0 ? Math.round((uploadedSize / totalSize) * 100) : 0 return { files, addFiles, startUpload, removeFile, cancelAllUploads, reset, isUploading, isAllCompleted, hasFiles, totalSize, uploadedSize, overallProgress, } }