1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
/**
* 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<UploadResult> {
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);
});
}
|