feat: presigned URL API 연동 및 이미지 업로드 방식 수정#237
Conversation
Walkthrough피드 생성 흐름이 멀티파트 업로드에서 사전 업로드(프리사인드 URL) 후 이미지 URL을 JSON에 포함하는 방식으로 전환됨. 이를 위해 프리사인드 URL API와 S3 업로드 유틸이 추가되고, 훅(useCreateFeed)에서 업로드·검증·에러 처리 흐름이 갱신됨. 페이지 파일은 주석만 수정됨. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant H as useCreateFeed Hook
participant A as API: getPresignedUrl
participant S as S3 (Presigned URL)
participant F as API: createFeed
U->>H: 제출(본문 + 선택 이미지)
H->>H: 로컬 검증(개수/확장자/용량)
alt 이미지 존재
H->>A: POST /feeds/images/presigned-url (확장자/사이즈[])
A-->>H: presignedUrl, fileUrl[]
par 각 이미지 병렬 업로드
H->>S: PUT presignedUrl (Content-Type: file.type)
S-->>H: 200 OK / 실패
end
alt 업로드 실패
H-->>U: 스낵바 오류 표시
H-->>U: 종료
else 업로드 성공
H->>F: POST /feeds (JSON: imageUrls[])
F-->>H: 응답
H-->>U: 결과 반환
end
else 이미지 없음
H->>F: POST /feeds (JSON)
F-->>H: 응답
H-->>U: 결과 반환
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Pre-merge checks (3 passed)✅ Passed checks (3 passed)
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/hooks/useCreateFeed.ts (1)
45-74: 이미지 파일 크기 제한 및 WebP 확장자 지원 추가 필요
- src/hooks/useCreateFeed.ts 내 이미지 업로드 로직에 최대 크기 검증이 없어 10MB 등 실제 백엔드 정책에 맞춘 클라이언트 선검증 로직을 추가하세요.
- WebP 지원 여부를 백엔드와 확정했다면
extOk정규식과 안내 메시지에webp를 포함하도록 업데이트하세요.- const extOk = (name: string) => /\.(jpe?g|png|gif)$/i.test(name); + const extOk = (name: string) => /\.(jpe?g|png|gif|webp)$/i.test(name); + // 크기 제한 (예: 10MB) — 백엔드 정책에 맞춰 값 조정 + const MAX_IMAGE_SIZE = 10 * 1024 * 1024; + if (images.some(f => f.size > MAX_IMAGE_SIZE)) { + openSnackbar({ + message: '이미지 용량이 너무 커요. 10MB 이하로 업로드해 주세요.', + variant: 'top', + onClose: closePopup, + }); + return { success: false as const }; + }
🧹 Nitpick comments (6)
src/pages/post/CreatePost.tsx (1)
189-193: 핀 기반 작성 시 본문 편집 비활성화 반영 필요위쪽 주석(라인 77-79)과 달리 PostContentSection은 항상 편집 가능으로 전달됩니다. 핀에서 진입 시 본문 편집을 막아야 한다면 아래처럼 isFromPin을 반영하는 편이 일관됩니다.
- <PostContentSection - content={postContent} - onContentChange={setPostContent} - readOnly={false} - /> + <PostContentSection + content={postContent} + onContentChange={setPostContent} + readOnly={isFromPin} + />src/api/feeds/uploadToS3.ts (1)
1-19: 기본 구현은 충분히 간결합니다. Abort/타임아웃·요구 헤더 대응을 옵션으로 열어두면 더 견고해집니다일부 presigned URL은 특정 헤더를 요구하거나(예: Content-Type 고정) 네트워크 대기 중 취소가 필요할 수 있습니다. 옵션 파라미터로 AbortSignal과 추가 헤더를 받을 수 있게 확장하는 것을 권장합니다.
-export const uploadFileToS3 = async ( - presignedUrl: string, - file: File -): Promise<boolean> => { +export const uploadFileToS3 = async ( + presignedUrl: string, + file: File, + options?: { + signal?: AbortSignal; + extraHeaders?: Record<string, string | undefined>; + } +): Promise<boolean> => { try { const response = await fetch(presignedUrl, { method: 'PUT', body: file, - headers: { - 'Content-Type': file.type, - }, + signal: options?.signal, + headers: { + 'Content-Type': file.type, + ...(options?.extraHeaders ?? {}), + }, }); return response.ok; } catch (error) { console.error('S3 업로드 실패:', error); return false; } };
- 백엔드가 서명 시 특정 헤더(예: Content-Type, x-amz-acl)를 강제하는지 확인 부탁드립니다. 강제된다면 getPresignedUrl 응답에 “필수 헤더”를 포함시켜 여기서 그대로 적용하는 구조가 안전합니다.
src/api/feeds/getPresignedUrl.ts (2)
3-6: 요청 스키마에 contentType 포함 권장클라이언트가 PUT 시 Content-Type을 전송하는 만큼, 서버 서명이 Content-Type을 포함하는 경우를 대비해 요청에 contentType을 함께 전달하는 편이 안전합니다.
export interface PresignedUrlRequest { extension: string; size: number; + /** 예: image/jpeg, image/png */ + contentType?: string; }
8-18: 응답 타입을 성공/실패 유니온으로 강화하면 사용성이 좋아집니다createFeed 응답과 동일한 패턴을 사용하면 분기 처리가 더 안전해집니다.
-export interface PresignedUrlResponse { - isSuccess: boolean; - code: number; - message: string; - data?: { - presignedUrls: Array<{ - presignedUrl: string; - fileUrl: string; - }>; - }; -} +export type PresignedUrlResponse = + | { + isSuccess: true; + code: number; + message: string; + data: { + presignedUrls: Array<{ + presignedUrl: string; + fileUrl: string; + }>; + }; + } + | { + isSuccess: false; + code: number; + message: string; + };src/hooks/useCreateFeed.ts (2)
75-80: presigned 요청에 contentType 포함 권장서명이 Content-Type을 요구하는 경우 업로드 실패를 방지할 수 있습니다.
- const presignedRequests: PresignedUrlRequest[] = images.map(file => ({ - extension: file.name.split('.').pop()?.toLowerCase() || 'jpg', - size: file.size, - })); + const presignedRequests: PresignedUrlRequest[] = images.map(file => ({ + extension: file.name.split('.').pop()?.toLowerCase() || 'jpg', + size: file.size, + contentType: file.type, + }));
19-41: 태그는 검증만 하고 정규화된 값으로 덮어쓰지 않습니다trim/중복 제거 후 body.tagList를 정규화된 값으로 치환해 API로 전달하는 편이 일관됩니다.
if (body.tagList) { // 최대 5개 if (body.tagList.length > 5) { openSnackbar({ message: '태그는 최대 5개까지 입력할 수 있어요.', variant: 'top', onClose: closePopup, }); return { success: false as const }; } // 중복 제거 체크 - const trimmed = body.tagList.map(t => t.trim()).filter(Boolean); - const uniq = new Set(trimmed); - if (uniq.size !== trimmed.length) { + const trimmed = body.tagList.map(t => t.trim()).filter(Boolean); + const uniq = Array.from(new Set(trimmed)); + if (uniq.length !== trimmed.length) { openSnackbar({ message: '태그는 중복될 수 없어요.', variant: 'top', onClose: closePopup, }); return { success: false as const }; } + // 정규화된 태그로 덮어쓰기 + body = { ...body, tagList: uniq }; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
src/api/feeds/createFeed.ts(2 hunks)src/api/feeds/getPresignedUrl.ts(1 hunks)src/api/feeds/uploadToS3.ts(1 hunks)src/hooks/useCreateFeed.ts(3 hunks)src/pages/post/CreatePost.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/api/feeds/getPresignedUrl.ts (1)
src/api/index.ts (1)
apiClient(7-14)
src/hooks/useCreateFeed.ts (3)
src/api/feeds/getPresignedUrl.ts (2)
PresignedUrlRequest(3-6)getPresignedUrl(20-29)src/api/feeds/uploadToS3.ts (1)
uploadFileToS3(1-19)src/api/feeds/createFeed.ts (3)
CreateFeedBody(4-10)CreateFeedResponse(29-29)createFeed(36-41)
src/api/feeds/createFeed.ts (1)
src/api/index.ts (1)
apiClient(7-14)
🔇 Additional comments (6)
src/pages/post/CreatePost.tsx (1)
104-104: 주석 업데이트 적절합니다FormData 경로가 제거된 현재 플로우를 반영한 주석 변경이 맞습니다.
src/api/feeds/createFeed.ts (1)
9-9: JSON 전환 및 imageUrls 필드 추가 적절presigned 업로드 후 URL만 전달하는 계약이 명확히 드러났습니다. 주석도 일관됩니다.
Also applies to: 33-35, 39-39
src/api/feeds/getPresignedUrl.ts (1)
20-29: 서버 계약 확인: 요청 바디 형태·응답 순서 보장 여부
- 서버가 배열 원본 순서를 유지해 presignedUrls를 동일 순서로 돌려주는지 확인 필요합니다. 보장이 없다면 각 요청에 clientKey(예: 파일 인덱스, uuid)를 포함하고 응답에도 echo-back 받아 매칭하세요.
src/hooks/useCreateFeed.ts (3)
3-4: 의존성 주입 적절새 presigned/업로드 유틸 의존성이 간결히 정리되었습니다.
93-101: 인덱스 매칭 가정 확인현재 업로드는 응답 배열과 images의 인덱스가 동일하다는 가정을 전제로 합니다. 백엔드가 순서를 보장하는지 확인하거나 clientKey를 왕복시켜 매칭하는 방식을 고려하세요.
126-133: feedBody 구성 방식 LGTMimageUrls를 조건부로 합치는 패턴이 간결합니다.
| if (!presignedResponse.isSuccess || !presignedResponse.data) { | ||
| openSnackbar({ | ||
| message: presignedResponse.message || 'Presigned URL 발급에 실패했습니다.', | ||
| variant: 'top', | ||
| onClose: closePopup, | ||
| }); | ||
| return { success: false as const }; | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
발급 개수/순서 검증 추가
서버가 순서를 보장하지 않거나 일부 발급에 실패해 개수가 어긋나면 인덱스 매칭이 틀어질 수 있습니다. 개수 검증 후 진행하세요.
if (!presignedResponse.isSuccess || !presignedResponse.data) {
openSnackbar({
message: presignedResponse.message || 'Presigned URL 발급에 실패했습니다.',
variant: 'top',
onClose: closePopup,
});
return { success: false as const };
}
+ const urls = presignedResponse.data.presignedUrls;
+ if (urls.length !== images.length) {
+ openSnackbar({
+ message: '이미지 개수와 Presigned URL 개수가 일치하지 않아요.',
+ variant: 'top',
+ onClose: closePopup,
+ });
+ return { success: false as const };
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!presignedResponse.isSuccess || !presignedResponse.data) { | |
| openSnackbar({ | |
| message: presignedResponse.message || 'Presigned URL 발급에 실패했습니다.', | |
| variant: 'top', | |
| onClose: closePopup, | |
| }); | |
| return { success: false as const }; | |
| } | |
| if (!presignedResponse.isSuccess || !presignedResponse.data) { | |
| openSnackbar({ | |
| message: presignedResponse.message || 'Presigned URL 발급에 실패했습니다.', | |
| variant: 'top', | |
| onClose: closePopup, | |
| }); | |
| return { success: false as const }; | |
| } | |
| const urls = presignedResponse.data.presignedUrls; | |
| if (urls.length !== images.length) { | |
| openSnackbar({ | |
| message: '이미지 개수와 Presigned URL 개수가 일치하지 않아요.', | |
| variant: 'top', | |
| onClose: closePopup, | |
| }); | |
| return { success: false as const }; | |
| } |
🤖 Prompt for AI Agents
In src/hooks/useCreateFeed.ts around lines 84 to 92, the code currently only
checks presignedResponse.isSuccess and presignedResponse.data presence; add a
verification that the number of presigned entries matches the number of files to
upload (and optionally validate any index/order metadata) before proceeding. If
counts differ (or indices are missing/mismatched), call openSnackbar with an
appropriate message, closePopup, and return { success: false }. If the server
may return out-of-order items, reorder presignedResponse.data deterministically
to match the original file order (e.g., by an index/key provided) before using
it.
#️⃣ 연관된 이슈
#229
📝 작업 내용
기존 multipart/form-data 방식에서 S3 Presigned URL을 사용한 직접 업로드 방식으로 변경하여 서버 부하를 줄이고 보안을 강화했습니다.
1. 새로운 API 함수 추가
src/api/feeds/getPresignedUrl.ts: 피드 이미지 업로드용 presigned URL 발급 APIsrc/api/feeds/updateToS3.ts: S3 직접 업로드 함수2. 기존 피드 생성 API 함수 수정
CreateFeedBody인터페이스에imageUrls필드 추가3. 피드 생성 플로우 개선
새로운 업로드 플로우:
1️⃣ 이미지 파일 선택 및 클라이언트 검증
2️⃣ POST /feeds/images/presigned-url → presigned URL 발급
3️⃣ PUT to S3 → 각 이미지를 S3에 직접 업로드
4️⃣ POST /feeds → 업로드된 이미지 URL과 함께 피드 생성
Summary by CodeRabbit
신규 기능
문서