165 lines
4.7 KiB
TypeScript
165 lines
4.7 KiB
TypeScript
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<UploadingFile[]>([])
|
|
const abortControllersRef = useRef<Map<string, AbortController>>(new Map())
|
|
|
|
const updateFileState = useCallback((id: string, updates: Partial<UploadingFile>) => {
|
|
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,
|
|
}
|
|
}
|