Skip to content

feat: presigned URL API 연동 및 이미지 업로드 방식 수정#237

Merged
ljh130334 merged 1 commit intodevelopfrom
hotfix/image
Sep 9, 2025
Merged

feat: presigned URL API 연동 및 이미지 업로드 방식 수정#237
ljh130334 merged 1 commit intodevelopfrom
hotfix/image

Conversation

@ljh130334
Copy link
Member

@ljh130334 ljh130334 commented Sep 9, 2025

#️⃣ 연관된 이슈

#229

📝 작업 내용

기존 multipart/form-data 방식에서 S3 Presigned URL을 사용한 직접 업로드 방식으로 변경하여 서버 부하를 줄이고 보안을 강화했습니다.

1. 새로운 API 함수 추가

  • src/api/feeds/getPresignedUrl.ts: 피드 이미지 업로드용 presigned URL 발급 API
  • src/api/feeds/updateToS3.ts: S3 직접 업로드 함수

2. 기존 피드 생성 API 함수 수정

  • CreateFeedBody 인터페이스에 imageUrls 필드 추가
  • multipart 방식에서 JSON 방식으로 변경
  • 이미지 파일 대신 업로드된 CloudFront URL 전송

3. 피드 생성 플로우 개선

  • 이미지 파일 검증 (확장자, 크기, 개수) 유지
  • Presigned URL 발급 → S3 직접 업로드 → 피드 생성 순서로 변경

새로운 업로드 플로우:
1️⃣ 이미지 파일 선택 및 클라이언트 검증
2️⃣ POST /feeds/images/presigned-url → presigned URL 발급
3️⃣ PUT to S3 → 각 이미지를 S3에 직접 업로드
4️⃣ POST /feeds → 업로드된 이미지 URL과 함께 피드 생성

Summary by CodeRabbit

  • 신규 기능

    • 이미지가 사전 발급 URL로 업로드된 뒤 게시물에 자동 첨부되어 업로드 안정성과 속도가 향상되었습니다.
    • 다중 이미지를 병렬로 업로드해 전체 대기 시간이 단축되었습니다.
    • 업로드 실패 시 명확한 안내 메시지를 표시하고 작업을 안전하게 중단합니다.
  • 문서

    • 공개 API 문서에 이미지 업로드 절차 변경 사항과 이미지 URL 기반 첨부 방식이 반영되었습니다.

@ljh130334 ljh130334 self-assigned this Sep 9, 2025
@ljh130334 ljh130334 added the ✨ Feature 기능 개발 label Sep 9, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 9, 2025

Walkthrough

피드 생성 흐름이 멀티파트 업로드에서 사전 업로드(프리사인드 URL) 후 이미지 URL을 JSON에 포함하는 방식으로 전환됨. 이를 위해 프리사인드 URL API와 S3 업로드 유틸이 추가되고, 훅(useCreateFeed)에서 업로드·검증·에러 처리 흐름이 갱신됨. 페이지 파일은 주석만 수정됨.

Changes

Cohort / File(s) Summary of changes
피드 생성 API 전환
src/api/feeds/createFeed.ts
createFeed(body)로 시그니처 단순화(이미지 인자 제거), 본문 JSON 전송으로 변경, CreateFeedBodyimageUrls?: string[] 추가
이미지 사전업로드 지원(API/유틸)
src/api/feeds/getPresignedUrl.ts, src/api/feeds/uploadToS3.ts
프리사인드 URL 요청/응답 타입 및 getPresignedUrl() 추가, S3 업로드 유틸 uploadFileToS3() 추가
클라이언트 업로드 흐름 갱신
src/hooks/useCreateFeed.ts
프리사인드 URL 요청 → S3 업로드(병렬) → 업로드 성공 시 imageUrlscreateFeed 호출; 실패 시 스낵바 알림 및 중단; 기존 이미지·태그 검증 유지
페이지 주석 정리
src/pages/post/CreatePost.tsx
주석 문구만 수정(기능 변화 없음)

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: 새 글 작성 API 연동 #102 — 이전에 createFeed에 멀티파트/이미지 첨부 흐름을 도입한 PR로, 이번 PR이 해당 방식을 JSON+프리사인드 URL로 대체하므로 직접 연관됨.

Suggested labels

📬 API

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목은 presigned URL API 연동과 이미지 업로드 방식 수정이라는 핵심 변경 사항을 간결하게 요약하고 있어 내용과 완전히 일치하며 동료가 변경 이력을 빠르게 이해할 수 있습니다.
Description Check ✅ Passed PR 설명은 관련 이슈(#229)부터 새로운 API 함수 추가, 기존 함수 수정, 업로드 플로우 단계에 이르기까지 변경 사항을 구체적으로 다루하고 있어 변경 내용과 긴밀히 연관된 정보를 제공합니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

새벽 버그 숲을 폴짝 건너
토끼는 URL을 주워 물고 오네 🥕
먼저 하늘(S3)에 사진을 걸고
링크를 묶어 피드에 전해요
멀티파트 굴레 벗고 가벼운 발걸음,
탁— 클릭에 피드가 피어납니다.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch hotfix/image

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel
Copy link

vercel bot commented Sep 9, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Sep 9, 2025 6:08am

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 03812ff and 77af4b7.

📒 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 구성 방식 LGTM

imageUrls를 조건부로 합치는 패턴이 간결합니다.

Comment on lines +84 to +92
if (!presignedResponse.isSuccess || !presignedResponse.data) {
openSnackbar({
message: presignedResponse.message || 'Presigned URL 발급에 실패했습니다.',
variant: 'top',
onClose: closePopup,
});
return { success: false as const };
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

@ljh130334 ljh130334 merged commit 3300b4e into develop Sep 9, 2025
3 checks passed
@ljh130334 ljh130334 deleted the hotfix/image branch November 17, 2025 00:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant