-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 새 글 작성 API 연동 #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 새 글 작성 API 연동 #102
Changes from all commits
9650cc7
528fb8f
7674110
c7e3e9c
9fc7737
28642dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,55 @@ | ||||||||||||||
| import { apiClient } from '../index'; | ||||||||||||||
|
|
||||||||||||||
| /** 서버에 보낼 request JSON 페이로드 */ | ||||||||||||||
| export interface CreateFeedBody { | ||||||||||||||
| isbn: string; | ||||||||||||||
| contentBody: string; | ||||||||||||||
| isPublic: boolean; | ||||||||||||||
| tagList?: string[]; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** 성공 응답 */ | ||||||||||||||
| export interface CreateFeedSuccess { | ||||||||||||||
| isSuccess: true; | ||||||||||||||
| code: number; | ||||||||||||||
| message: string; | ||||||||||||||
| data: { | ||||||||||||||
| feedId: number; | ||||||||||||||
| }; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** 실패 응답 */ | ||||||||||||||
| export interface CreateFeedFail { | ||||||||||||||
| isSuccess: false; | ||||||||||||||
| code: number; | ||||||||||||||
| message: string; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| export type CreateFeedResponse = CreateFeedSuccess | CreateFeedFail; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 피드 작성 API | ||||||||||||||
| * - multipart/form-data | ||||||||||||||
| * - request: application/json (Blob로 감싸 전송) | ||||||||||||||
| * - images: File[] (선택값, 없으면 미첨부) | ||||||||||||||
| */ | ||||||||||||||
| export const createFeed = async ( | ||||||||||||||
| body: CreateFeedBody, | ||||||||||||||
| images?: File[], | ||||||||||||||
| ): Promise<CreateFeedResponse> => { | ||||||||||||||
| const form = new FormData(); | ||||||||||||||
|
|
||||||||||||||
| // request 파트(JSON) - 필수 | ||||||||||||||
| form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' })); | ||||||||||||||
|
|
||||||||||||||
| // images 파트들 - 선택 | ||||||||||||||
| if (images && images.length > 0) { | ||||||||||||||
| images.forEach(file => form.append('images', file)); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, { | ||||||||||||||
| headers: { 'Content-Type': 'multipart/form-data' }, | ||||||||||||||
| }); | ||||||||||||||
|
Comment on lines
+50
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainContent-Type 헤더 설정 확인 필요 axios는 FormData를 전송할 때 자동으로 적절한 Content-Type과 boundary를 설정합니다. 명시적으로 'multipart/form-data'를 설정하면 boundary가 누락될 수 있습니다. 헤더를 제거하거나 axios가 자동으로 설정하도록 하는 것을 고려해보세요: -const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, {
- headers: { 'Content-Type': 'multipart/form-data' },
-});
+const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form);🏁 Script executed: #!/bin/bash
# apiClient의 기본 헤더 설정 확인
rg -A 5 "apiClient.*create|axios.create" src/api/Length of output: 294 FormData 전송 시 Content-Type 자동 설정 검토 필요 현재
위 수정 후, FormData 전송 시 브라우저(또는 axios)가 자동으로 설정하는 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| return data; | ||||||||||||||
| }; | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ export interface FeedDetailData { | |
| bookTitle: string; | ||
| bookAuthor: string; | ||
| contentBody: string; | ||
| contentsUrl: string[]; | ||
| contentUrls: string[]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainAPI 응답 키 rename(contentUrls, isSuccess): 방향성 및 일관성 LGTM. 잔존 사용처 점검 권장 서버-클라이언트 계약 불일치로 발생하던 이미지 미노출 문제를 해결하는 바람직한 변경입니다. 동일 패턴(Other/My/Total Feed 등)으로 전역 적용도 잘 맞습니다. 남아있는 legacy 키 사용을 한번 더 스캔해 주세요. 다음 스크립트로 레거시 키 사용 여부를 전역 점검할 수 있습니다. 기대 결과는 빈 출력(= 더 이상 사용 없음)입니다. Also applies to: 26-26 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "1) 레거시 'contentsUrl' 사용처 검색 (없어야 정상)"
rg -n -S '\bcontentsUrl\b' --glob '!node_modules'
echo
echo "2) 레거시 'success' 키 사용처 검색 (isSuccess 제외)"
rg -n -S -P '(?<!is)success\s*[:=]' --glob '!node_modules' | rg -v 'isSuccess' || true
echo
echo "3) 혹시 모를 snake_case 'contents_url' 검색"
rg -n -S '\bcontents_url\b' --glob '!node_modules' || true
echo
echo "4) 프로퍼티 접근 형태 '.success' 검색 ('.isSuccess'는 제외)"
rg -n -S -P '\.success\b' --glob '!node_modules' || trueLength of output: 160 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
echo "1) 레거시 'contentsUrl' 사용처 검색 (없어야 정상)"
rg -n -S '\bcontentsUrl\b' --glob '!node_modules' || true
echo
echo "2) 레거시 'success' 키 사용처 검색 (isSuccess 제외)"
rg -n -S -P '(?<!is)success\s*[:=]' --glob '!node_modules' | rg -v 'isSuccess' || true
echo
echo "3) 혹시 모를 snake_case 'contents_url' 검색"
rg -n -S '\bcontents_url\b' --glob '!node_modules' || true
echo
echo "4) 프로퍼티 접근 형태 '.success' 검색 ('.isSuccess'는 제외)"
rg -n -S -P '\.success\b' --glob '!node_modules' | rg -v '\.isSuccess\b' || trueLength of output: 1740 남아있는
위 항목들을 반영하여 잔여 legacy 키( |
||
| likeCount: number; | ||
| commentCount: number; | ||
| isSaved: boolean; | ||
|
|
@@ -23,7 +23,7 @@ export interface FeedDetailData { | |
|
|
||
| // API 응답 타입 | ||
| export interface FeedDetailResponse { | ||
| success: boolean; | ||
| isSuccess: boolean; | ||
| code: number; | ||
| message: string; | ||
| data: FeedDetailData; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,7 @@ export interface TotalFeedData { | |
|
|
||
| // API 응답 타입 | ||
| export interface TotalFeedResponse { | ||
| success: boolean; | ||
| isSuccess: boolean; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain응답 필드 success → isSuccess로의 리네임: 전역 일관성 및 서버 스펙 확인 필요
다음 스크립트로 레포 내 남아있는 사용처를 점검할 수 있습니다. 추가로, 응답 스키마 중복을 줄이기 위해 공용 제네릭 타입 도입을 권장드립니다. 예: // src/types/api.ts
export interface ApiResponse<T> {
isSuccess: boolean;
code: number;
message: string;
data: T;
}이후 본 파일에서는: export type TotalFeedResponse = ApiResponse<TotalFeedData>;🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "[검색] contentsUrl, contentUrl, contentsUrls 잔존 레거시 키 탐지"
rg -n --hidden -S '\bcontentsUrl\b|\bcontentUrl\b|\bcontentsUrls\b' -g '!node_modules' -g '!dist' -g '!build' -g '!.next' || true
echo
echo "[검색] .success 프로퍼티 접근 탐지"
rg -n --hidden -S '\.success\b' -g '!node_modules' -g '!dist' -g '!build' -g '!.next' || true
echo
echo "[검색] 구조분해 { success } 탐지"
rg -n --hidden -S '\{\s*success\s*(,|\})' -g '!node_modules' -g '!dist' -g '!build' -g '!.next' || true
echo
echo "[검색] 타입/인터페이스에서 success: 키 정의 탐지"
rg -n --hidden -S '\bsuccess\s*:' --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!.next' || true
echo
echo "[점검] PostData가 contentUrls를 포함하는지 확인"
rg -n --hidden -A 20 'interface\s+PostData' src || true
echo
echo "[점검] apiClient 인터셉터에서 success 참조 여부 확인"
fd index.ts src/api | xargs -I{} rg -n --hidden -A 3 -B 3 '\bsuccess\b' {} || trueLength of output: 2887 success → isSuccess 리네임: 남은 참조/타입 일괄 수정 필요 아래 위치들에서 여전히
추가로:
🤖 Prompt for AI Agents |
||
| code: number; | ||
| message: string; | ||
| data: TotalFeedData; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,128 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import { apiClient } from '../index'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** 단일 이미지 업로드 성공 시 데이터 */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export interface UploadImageData { | ||||||||||||||||||||||||||||||||||||||||||||||
| imageUrl: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** 서버 공통 응답 타입 */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export interface UploadImageResponse { | ||||||||||||||||||||||||||||||||||||||||||||||
| isSuccess: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||
| code: number; | ||||||||||||||||||||||||||||||||||||||||||||||
| message: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| data?: UploadImageData; // 성공 시에만 존재 | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** 내부 유틸: 허용 확장자 */ | ||||||||||||||||||||||||||||||||||||||||||||||
| const IMAGE_EXT_REGEX = /\.(jpe?g|png|gif)$/i; | ||||||||||||||||||||||||||||||||||||||||||||||
| /** 가이드 최대 업로드 개수 (서버는 FEED 생성 시 최대 3장 제약) */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export const MAX_IMAGES = 3; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** 파일 사전 검증: 빈 파일 / 확장자 */ | ||||||||||||||||||||||||||||||||||||||||||||||
| function validateFile(file: File) { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!file || file.size === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||
| // 서버 코드 170001과 의미 일치 | ||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('업로드하려는 이미지가 비어있습니다.'); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!IMAGE_EXT_REGEX.test(file.name)) { | ||||||||||||||||||||||||||||||||||||||||||||||
| // 서버 코드 170003과 의미 일치 | ||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('파일 형식은 jpg, jpeg, png, gif만 가능합니다.'); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 확장자만으로는 불충분 — MIME 타입 기반 검사 추가 캡처 이미지처럼 확장자가 없는 파일명 케이스가 존재합니다. file.type('image/*')로 1차 검증하고, 불명확할 때만 확장자 regex를 보조로 쓰는 게 안전합니다. function validateFile(file: File) {
if (!file || file.size === 0) {
// 서버 코드 170001과 의미 일치
throw new Error('업로드하려는 이미지가 비어있습니다.');
}
- if (!IMAGE_EXT_REGEX.test(file.name)) {
+ const isImageByMime = !!file.type && file.type.startsWith('image/');
+ const isImageByName = IMAGE_EXT_REGEX.test(file.name || '');
+ if (!isImageByMime && !isImageByName) {
// 서버 코드 170003과 의미 일치
- throw new Error('파일 형식은 jpg, jpeg, png, gif만 가능합니다.');
+ throw new Error('이미지 파일만 업로드할 수 있습니다. (jpg, jpeg, png, gif)');
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** 단일 이미지 업로드 */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export const uploadImage = async ( | ||||||||||||||||||||||||||||||||||||||||||||||
| file: File, | ||||||||||||||||||||||||||||||||||||||||||||||
| options?: { signal?: AbortSignal }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<UploadImageResponse> => { | ||||||||||||||||||||||||||||||||||||||||||||||
| // 사전 검증 | ||||||||||||||||||||||||||||||||||||||||||||||
| validateFile(file); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const formData = new FormData(); | ||||||||||||||||||||||||||||||||||||||||||||||
| // 서버가 단일 업로드에서 기대하는 필드명이 image라면 유지 | ||||||||||||||||||||||||||||||||||||||||||||||
| formData.append('image', file); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, { | ||||||||||||||||||||||||||||||||||||||||||||||
| headers: { 'Content-Type': 'multipart/form-data' }, | ||||||||||||||||||||||||||||||||||||||||||||||
| signal: options?.signal, | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+45
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FormData 전송 시 Content-Type 수동 지정 금지(브라우저가 boundary 자동 설정) Axios 브라우저 환경에서 FormData를 보낼 때 'Content-Type'을 직접 설정하면 boundary가 빠져 서버에서 파싱 실패할 수 있습니다. 명시 헤더를 제거하세요. - const { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, {
- headers: { 'Content-Type': 'multipart/form-data' },
- signal: options?.signal,
- });
+ const { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, {
+ signal: options?.signal,
+ });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return data; | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * 다중 이미지 업로드 | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 전부 성공하면 URL 배열 반환 | ||||||||||||||||||||||||||||||||||||||||||||||
| * - 하나라도 실패하면 실패 내역을 포함해 throw | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export const uploadMultipleImages = async ( | ||||||||||||||||||||||||||||||||||||||||||||||
| files: File[], | ||||||||||||||||||||||||||||||||||||||||||||||
| options?: { signal?: AbortSignal; enforceMax?: boolean }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<string[]> => { | ||||||||||||||||||||||||||||||||||||||||||||||
| // 개수 제한(선택) – FEED 생성 정책에 맞춰 사전 차단하고 싶을 때 사용 | ||||||||||||||||||||||||||||||||||||||||||||||
| if (options?.enforceMax && files.length > MAX_IMAGES) { | ||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`이미지는 최대 ${MAX_IMAGES}장까지 업로드할 수 있습니다.`); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // 파일별 사전 검증 | ||||||||||||||||||||||||||||||||||||||||||||||
| files.forEach(validateFile); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // 병렬 업로드 (각 요청 독립) | ||||||||||||||||||||||||||||||||||||||||||||||
| const results = await Promise.allSettled( | ||||||||||||||||||||||||||||||||||||||||||||||
| files.map(file => uploadImage(file, { signal: options?.signal })), | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const successUrls: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||
| const failures: { index: number; reason: string }[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| results.forEach((res, idx) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (res.status === 'fulfilled') { | ||||||||||||||||||||||||||||||||||||||||||||||
| const value = res.value; | ||||||||||||||||||||||||||||||||||||||||||||||
| if (value.isSuccess && value.data?.imageUrl) { | ||||||||||||||||||||||||||||||||||||||||||||||
| successUrls.push(value.data.imageUrl); | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| failures.push({ | ||||||||||||||||||||||||||||||||||||||||||||||
| index: idx, | ||||||||||||||||||||||||||||||||||||||||||||||
| reason: value.message || '파일 업로드에 실패하였습니다.', | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| failures.push({ | ||||||||||||||||||||||||||||||||||||||||||||||
| index: idx, | ||||||||||||||||||||||||||||||||||||||||||||||
| reason: (res.reason as Error)?.message || '네트워크 오류로 파일 업로드에 실패하였습니다.', | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (failures.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||
| // 어떤 항목이 왜 실패했는지 상세 메시지 | ||||||||||||||||||||||||||||||||||||||||||||||
| const detail = failures.map(f => `#${f.index + 1}: ${f.reason}`).join(' / '); | ||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`일부 이미지 업로드에 실패했습니다. (${detail})`); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return successUrls; | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||||||||||||||
| 사용 예시: | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // 단일 | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const res = await uploadImage(file); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (res.isSuccess) { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.log('업로드된 URL:', res.data?.imageUrl); | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.error('실패:', res.message); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.error('오류:', e); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // 다중 | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const urls = await uploadMultipleImages(files, { enforceMax: true }); | ||||||||||||||||||||||||||||||||||||||||||||||
| console.log('업로드된 URL들:', urls); | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.error('다중 업로드 실패:', e); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Content-Type 헤더 설정 제거 필요
multipart/form-data를 사용할 때는 브라우저가 자동으로 boundary를 포함한 Content-Type을 설정합니다. 명시적으로 설정하면 boundary가 누락되어 서버에서 파싱 오류가 발생할 수 있습니다.📝 Committable suggestion
🤖 Prompt for AI Agents