/** * XMLHttpRequest를 사용하여 파일 업로드 진행도 추적 */ export interface UploadProgressCallback { onProgress: (fileIndex: number, progress: number) => void; onFileComplete: (fileIndex: number) => void; onFileError: (fileIndex: number, error: string) => void; } export interface UploadFilesWithProgressOptions { uploadId: string; userId: string; files: File[]; callbacks: UploadProgressCallback; } export interface UploadResult { success: boolean; uploadedCount?: number; error?: string; } /** * 진행도 추적을 지원하는 파일 업로드 함수 */ export async function uploadFilesWithProgress({ uploadId, userId, files, callbacks, }: UploadFilesWithProgressOptions): Promise { return new Promise((resolve) => { const formData = new FormData(); formData.append("uploadId", uploadId); formData.append("userId", userId); formData.append("fileCount", String(files.length)); files.forEach((file, index) => { formData.append(`file_${index}`, file); }); const xhr = new XMLHttpRequest(); // 타임아웃 설정 (1시간) xhr.timeout = 3600000; // 1시간 (밀리초) // 전체 업로드 진행도 // 주의: xhr.upload.progress는 클라이언트→서버 전송만 추적 // 서버에서 DOLCE API로 재업로드하는 과정은 별도 (Node.js fetch는 업로드 진행도 추적 미지원) // → UI에서 90% 이상일 때 "서버에서 DOLCE API로 전송 중..." 메시지 표시 xhr.upload.addEventListener("progress", (event) => { if (event.lengthComputable) { // 전송 완료 = 서버에 도착 (실제 DOLCE API 업로드 시작) // 서버 처리를 위해 최대 95%까지만 표시 (나머지 5%는 서버→DOLCE 업로드) const totalProgress = Math.min((event.loaded / event.total) * 95, 95); // 현재 업로드 중인 파일 인덱스 추정 const filesCompleted = Math.floor((totalProgress / 95) * files.length); const currentFileIndex = Math.min(filesCompleted, files.length - 1); // 각 파일별 진행도 계산 files.forEach((_, index) => { if (index < filesCompleted) { callbacks.onProgress(index, 95); } else if (index === currentFileIndex) { const fileProgress = ((totalProgress / 95) * files.length - filesCompleted) * 95; callbacks.onProgress(index, Math.min(fileProgress, 94)); } else { callbacks.onProgress(index, 0); } }); } }); xhr.addEventListener("load", () => { if (xhr.status >= 200 && xhr.status < 300) { try { const response = JSON.parse(xhr.responseText); // 서버 응답 검증 if (response.success) { console.log(`[업로드 클라이언트] 서버 처리 완료: ${response.uploadedCount}개 파일`); // 서버에서 실제 처리 완료 시에만 100% files.forEach((_, index) => { callbacks.onProgress(index, 100); callbacks.onFileComplete(index); }); resolve(response); } else { // 서버에서 에러 응답 const errorMsg = response.error || "서버에서 업로드 실패"; console.error(`[업로드 클라이언트] 서버 에러:`, errorMsg); files.forEach((_, index) => { callbacks.onFileError(index, errorMsg); }); resolve({ success: false, error: errorMsg, }); } } catch (error) { const errorMsg = `응답 파싱 실패: ${xhr.responseText?.substring(0, 100)}`; console.error(`[업로드 클라이언트] 파싱 에러:`, error, xhr.responseText); files.forEach((_, index) => { callbacks.onFileError(index, errorMsg); }); resolve({ success: false, error: errorMsg, }); } } else { const errorMsg = `업로드 실패: ${xhr.status} ${xhr.statusText}`; console.error(`[업로드 클라이언트] HTTP 에러:`, errorMsg, xhr.responseText); files.forEach((_, index) => { callbacks.onFileError(index, errorMsg); }); resolve({ success: false, error: errorMsg, }); } }); xhr.addEventListener("error", () => { const errorMsg = "네트워크 오류"; console.error(`[업로드 클라이언트] 네트워크 에러`); files.forEach((_, index) => { callbacks.onFileError(index, errorMsg); }); resolve({ success: false, error: errorMsg, }); }); xhr.addEventListener("abort", () => { const errorMsg = "업로드가 취소되었습니다"; console.warn(`[업로드 클라이언트] 업로드 취소됨`); files.forEach((_, index) => { callbacks.onFileError(index, errorMsg); }); resolve({ success: false, error: errorMsg, }); }); xhr.addEventListener("timeout", () => { const errorMsg = "업로드 타임아웃 (1시간 초과)"; console.error(`[업로드 클라이언트] 타임아웃 발생 (1시간 초과)`); files.forEach((_, index) => { callbacks.onFileError(index, errorMsg); }); resolve({ success: false, error: errorMsg, }); }); console.log(`[업로드 클라이언트] 시작: ${files.length}개 파일`); xhr.open("POST", "/api/dolce/upload-files"); xhr.send(formData); }); }