feat: 항공영상 업로드 모달

This commit is contained in:
2026-04-10 12:24:10 +09:00
parent 35f6023f5a
commit 8b33161284
8 changed files with 807 additions and 173 deletions

View File

@@ -0,0 +1,164 @@
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,
}
}