Skip to content

[refactor] 이미지 업로드를 Presigned URL 직접 업로드 방식으로 개선#906

Merged
oesnuj merged 12 commits intodevelop-fefrom
refactor/#883-frontend-image-upload-MOA-374
Dec 20, 2025
Merged

[refactor] 이미지 업로드를 Presigned URL 직접 업로드 방식으로 개선#906
oesnuj merged 12 commits intodevelop-fefrom
refactor/#883-frontend-image-upload-MOA-374

Conversation

@oesnuj
Copy link
Member

@oesnuj oesnuj commented Nov 30, 2025

#️⃣연관된 이슈

#883


🎯 작업 배경 및 목적

  • 기존 멀티파트 업로드 방식으로 인한 서버 메모리 부하 증가
  • 동시 업로드 처리 시 서버 리소스 병목 현상

📝작업 내용

1. Presigned URL 기반 이미지 업로드 시스템 도입

기존 멀티파트 업로드 방식에서 Presigned URL을 활용한 직접 업로드 방식으로 전환하여 서버 부하 감소 및 업로드 성능 개선

백엔드 API 구현

  • 동아리 로고 Presigned URL 발급 API 구현
  • 동아리 피드 다중 이미지 Presigned URL 발급 API 구현
  • 동아리 커버 이미지 Presigned URL 발급 API 구현
  • R2 스토리지 직접 업로드를 위한 공통 유틸 fetch 함수 구현

프론트엔드 리팩토링

  • TanStack Query mutation hook 기반 이미지 업로드 로직 재구성
  • 레거시 이미지 관리 API 및 hooks 제거
    • createFeedImage.ts, updateClubLogo.ts, deleteClubLogo.ts
  • 로고 에디터 및 피드 이미지 업로드 컴포넌트를 새로운 mutation hook으로 전환



2. 이미지 업로드 아키텍처 개선

클라이언트 → 서버 → 스토리지 경로에서 클라이언트 → 스토리지 직접 업로드로 변경

기존: Client --multipart--> Server --upload--> R2 Storage

sequenceDiagram
    participant Client
    participant Server
    participant R2

    Client->>Server: 1. POST /upload (파일 전송)
    Note right of Server: 서버 메모리에 파일 로드
    Server->>R2: 2. 파일 업로드
    R2-->>Server: 업로드 완료
    Server-->>Client: 3. 이미지 URL 반환
    
    Note over Client,R2: 문제점: 이중 전송, 서버 부하 높음
Loading

변경: Client --presigned URL--> R2 Storage (Direct Upload)

sequenceDiagram
    participant Client
    participant Server
    participant R2

    Client->>Server: 1. POST /upload-url {fileName, contentType}
    Server->>R2: Presigned URL 생성 요청
    R2-->>Server: presignedUrl 반환
    Server-->>Client: 2. {presignedUrl, finalUrl}
    
    Client->>R2: 3. PUT presignedUrl (파일 직접 업로드)
    R2-->>Client: 업로드 완료
    
    Client->>Server: 4. POST /complete {fileUrl}
    Server-->>Client: 저장 완료
    
    Note over Client,R2: 개선: 서버 우회, 직접 업로드
Loading

비교

image

중점적으로 리뷰받고 싶은 부분

  • Presigned URL 발급 후 직접 업로드하는 플로우가 안전하게 구현되었는지 확인 부탁드립니다
  • 기존 이미지 업로드 기능이 정상 동작하는지 QA 부탁드립니다 (로고, 피드 이미지)



🫡 참고사항

  • 레거시 이미지 업로드 관련 코드 완전히 제거됨

Summary by CodeRabbit

  • 새로운 기능

    • 사전 서명 업로드(발급·완료·삭제) 및 스토리지 업로드 유틸 추가로 서버 기반 이미지 업로드 흐름 도입.
    • 피드·로고 이미지용 신규 업로드/삭제 훅 및 관련 업로드 도구 추가.
  • 개선 사항

    • 사진 편집 탭을 서버 연동 흐름으로 전환하고 업로드 버튼·숨김 입력·로딩 상태 통합.
    • 이미지 미리보기의 삭제 버튼 비활성화 표시로 UX 개선.
    • 파일 개수 제한 상향: 최대 15개(MAX_FILE_COUNT).

✏️ Tip: You can customize this high-level summary in your review settings.

- POST /api/club/:clubId/logo/upload-url - S3 Presigned URL 발급
- POST /api/club/:clubId/logo/complete - 업로드 완료 후 DB 업데이트
- DELETE /api/club/:clubId/logo - 로고 삭제
- POST /api/club/:clubId/feed/upload-urls - 여러 파일에 대한 Presigned URL 배치 발급
- PUT /api/club/:clubId/feed - 피드 이미지 URL 배열 업데이트 (순서 변경/삭제 포함)
- POST /api/club/:clubId/cover/upload-url - S3 Presigned URL 발급
- POST /api/club/:clubId/cover/complete - 업로드 완료 후 최종 URL 저장
- DELETE /api/club/:clubId/cover - 커버 이미지 삭제
- Presigned URL로 파일을 직접 r2에 업로드하는 헬퍼 함수
- Content-Type 헤더 설정으로 파일 형식 보장
- 로고/커버/피드 API에서 공통으로 사용
WHY:
- 서버 상태 변경 작업의 일관된 처리
- 로딩/에러 상태의 선언적 관리
- 쿼리 캐시와의 자동 동기화

WHAT:
- useUploadLogo: r2 업로드 플로우 관리
- useDeleteLogo: 로고 삭제 처리
- isPending으로 진행 상태 노출
- onSuccess에서 clubDetail 캐시 무효화
- onError로 에러 핸들링
WHY:
- 서버 상태 변경 작업의 일관된 처리
- 로딩/에러 상태의 선언적 관리
- 쿼리 캐시와의 자동 동기화

WHAT:
- useUploadFeed: presigned URL 발급 → r2 병렬 업로드 → 서버 동기화
- useUpdateFeed: 기존 피드 URL 배열 수정 처리
- isPending으로 진행 상태 노출
- onSuccess에서 clubDetail 캐시 무효화
- onError로 에러 핸들링
- TanStack Query mutation hook으로 대체 완료
- 로고/피드 이미지 관리 구조 정리
1. 기존의 useUpdateFeedImages, useCreateFeedImage 대신
useUploadFeed, useUpdateFeed 훅을 사용하도록 변경
2. 더 이상 사용하지 않는 ImageUpload 컴포넌트를 제거
- useUploadClubLogo/useDeleteClubLogo를 useLogoMutation의 useUploadLogo/useDeleteLogo로 대체
- mutation 호출 시 clubId를 명시적으로 전달하도록 변경
@oesnuj oesnuj self-assigned this Nov 30, 2025
@oesnuj oesnuj added ✨ Feature 기능 개발 🔨 Refactor 코드 리팩토링 💻 FE Frontend labels Nov 30, 2025
@vercel
Copy link

vercel bot commented Nov 30, 2025

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

Project Deployment Review Updated (UTC)
moadong Ready Ready Preview, Comment Dec 20, 2025 8:18am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 30, 2025

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

클라이언트 이미지 업로드를 FormData 직접 업로드에서 사전 서명된 URL 기반 3단계(서명 URL 요청 → 스토리지 업로드 → 완료 통보)로 전환했습니다. 기존 직접 업로드 API 및 관련 훅/컴포넌트를 제거하고, 이미지별 API 모듈·스토리지 업로드 유틸·새 React Query 뮤테이션 훅을 추가했습니다.

Changes

코호트 / 파일(s) 요약
삭제된 기존 API
frontend/src/apis/createFeedImage.ts, frontend/src/apis/updateClubLogo.ts, frontend/src/apis/deleteClubLogo.ts, frontend/src/apis/updateFeedImages.ts
FormData 기반 직접 업로드 및 관련 DELETE/POST 유틸 제거
새 이미지 API 모듈
frontend/src/apis/image/cover.ts, frontend/src/apis/image/feed.ts, frontend/src/apis/image/logo.ts, frontend/src/apis/image/uploadToStorage.ts
presigned URL 워크플로우 지원 API 추가(서명 URL 요청, 업로드 완료 통보, 삭제) 및 S3 PUT 업로드 유틸
삭제된 기존 훅
frontend/src/hooks/queries/club/useClubLogo.ts, frontend/src/hooks/queries/club/useCreateFeedImage.ts, frontend/src/hooks/queries/club/useUpdateFeedImages.ts
이전 아키텍처용 React Query 훅 제거
새 이미지 뮤테이션 훅
frontend/src/hooks/queries/club/images/useLogoMutation.ts, frontend/src/hooks/queries/club/images/useFeedMutation.ts
presigned URL 흐름 통합 훅 추가: useUploadLogo, useDeleteLogo, useUploadFeed, useUpdateFeed
컴포넌트 변경: ClubLogoEditor
frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx
훅 교체 및 mutate 시그니처 변경(이제 mutate에 { clubId, file } 또는 clubId 전달)
PhotoEditTab 리팩토링
frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx
ImageUpload 컴포넌트 제거, 파일 입력 직접 처리로 대체, useUploadFeed/useUpdateFeed 사용 및 업로드 카운트/사이즈 검증·로딩 통합
삭제된 ImageUpload 컴포넌트 및 스타일
frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.tsx, .../ImageUpload.styles.ts
기존 이미지 업로드 컴포넌트 및 스타일 전면 삭제
ImagePreview 변경
frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx
props에 optional disabled?: boolean 추가, 삭제 버튼 비활성화 UI 반영
상수 추가
frontend/src/constants/uploadLimit.ts
export const MAX_FILE_COUNT = 15 추가

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as Admin UI (PhotoEditTab / ClubLogoEditor)
participant Hook as React-Query Mutation (useUploadFeed / useUploadLogo)
participant API as Frontend API Module (feedApi / logoApi / coverApi)
participant Storage as S3 (Presigned URL)
participant Backend as Backend API

UI->>Hook: mutate({ clubId, files }) / mutate({ clubId, file })
Hook->>API: POST /api/club/{id}/.../upload-url (single or batch)
API-->>Hook: [{ presignedUrl, finalUrl }]
Hook->>Storage: PUT presignedUrl (parallel for multiple files)
Storage-->>Hook: 200 OK
Hook->>API: POST /api/club/{id}/.../complete (with finalUrl(s))
API->>Backend: persist finalUrl(s)
API-->>Hook: 200 OK
Hook-->>UI: onSuccess -> invalidate ['clubDetail', clubId]

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25분

  • 주의할 파일/영역:
    • frontend/src/apis/image/* (presigned URL 요청/응답 포맷, 에러 메시지)
    • frontend/src/hooks/queries/club/images/useFeedMutation.ts (Promise.all 병렬 업로드, 실패/부분 실패 처리, finalUrl 수집)
    • frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx (파일 입력 검증, MAX_FILE_COUNT/사이즈 제한, 로딩 동기화)
    • frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx (mutate 페이로드와 훅 시그니처 일치 여부)

Possibly related PRs

Suggested reviewers

  • Zepelown
  • PororoAndFriends
  • lepitaaar
  • seongwon030

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항을 명확히 요약하고 있습니다. Presigned URL 직접 업로드 방식으로의 이미지 업로드 개선이라는 핵심 목표를 잘 반영합니다.
Linked Issues check ✅ Passed PR의 코드 변경사항이 MOA-374의 목표를 충족합니다. 레거시 이미지 업로드 API 제거, Presigned URL 기반 새 API 모듈 추가, mutation hook 재구성 등이 구현되어 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 이미지 업로드 구조 개선과 관련된 범위 내에 있습니다. 로고, 피드, 커버 이미지 업로드 재설계 및 레거시 코드 제거만 포함되어 있습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#883-frontend-image-upload-MOA-374

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.

Copy link
Contributor

@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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx (1)

21-32: clubId 조기 반환 때문에 React Hooks 규칙을 위반합니다

지금 구조에서는:

  • useMixpanelTrack, useAdminClubContext까지만 호출한 뒤
  • if (!clubId) return null;에서 조기 반환하고
  • 이후에 선언된 useState, useRef, useUploadLogo, useDeleteLogo, useEffect 훅들이 호출되지 않습니다.

이 상태에서 나중에 clubId가 truthy로 바뀌면, 해당 렌더에서는 위 훅들이 모두 호출되기 때문에, 렌더마다 훅 호출 개수/순서가 달라져 React 내부 훅 상태 배열이 꼬일 수 있습니다(Static analysis 에러와 동일 이슈).

조치 제안:

  • 모든 훅 호출을 컴포넌트 최상단에서 항상 실행한 뒤,
  • clubId 기반 조기 반환은 마지막 훅(useEffect) 이후, JSX 반환 직전에 두는 것을 권장합니다.

예시:

-  const { clubId } = useAdminClubContext();
-  if (!clubId) return null;
-
-  const [isMenuOpen, setIsMenuOpen] = useState(false);
-  const menuRef = useRef<HTMLDivElement>(null);
-  const fileInputRef = useRef<HTMLInputElement>(null);
-
-  const uploadMutation = useUploadLogo();
-  const deleteMutation = useDeleteLogo();
+  const { clubId } = useAdminClubContext();
+
+  const [isMenuOpen, setIsMenuOpen] = useState(false);
+  const menuRef = useRef<HTMLDivElement>(null);
+  const fileInputRef = useRef<HTMLInputElement>(null);
+
+  const uploadMutation = useUploadLogo();
+  const deleteMutation = useDeleteLogo();
@@
-  useEffect(() => {
+  useEffect(() => {
@@
-  }, [isMenuOpen]);
+  }, [isMenuOpen]);
+
+  if (!clubId) return null;

이렇게 하면 모든 렌더에서 훅이 동일한 순서로 호출되어 React 19 환경에서도 안전하게 동작합니다.

Also applies to: 72-86

♻️ Duplicate comments (2)
frontend/src/apis/image/cover.ts (1)

4-7: PresignedData 인터페이스 중복 정의.

이 인터페이스는 feed.tslogo.ts에도 동일하게 정의되어 있습니다. frontend/src/apis/image/feed.ts의 리뷰 코멘트를 참고하여 공통 타입 파일로 추출하세요.

frontend/src/apis/image/logo.ts (1)

4-7: PresignedData 인터페이스 중복 정의.

이 인터페이스는 feed.tscover.ts에도 동일하게 정의되어 있습니다. frontend/src/apis/image/feed.ts의 리뷰 코멘트를 참고하여 공통 타입 파일로 추출하세요.

🧹 Nitpick comments (6)
frontend/src/apis/image/feed.ts (1)

4-12: PresignedData 인터페이스 중복 정의를 공통 타입 파일로 추출하세요.

동일한 PresignedData 인터페이스가 cover.ts, logo.ts, feed.ts 세 파일에 중복 정의되어 있습니다. 타입 일관성 유지와 유지보수성을 위해 공통 타입 파일로 추출하는 것을 권장합니다.

예시:

// frontend/src/types/image.ts
export interface PresignedData {
  presignedUrl: string;
  finalUrl: string;
}

export interface FeedUploadRequest {
  fileName: string;
  contentType: string;
}

그 후 각 API 파일에서 import하여 사용:

+import { PresignedData, FeedUploadRequest } from '@/types/image';
-interface FeedUploadRequest {
-  fileName: string;
-  contentType: string;
-}
-
-interface PresignedData {
-  presignedUrl: string;
-  finalUrl: string;
-}
frontend/src/apis/image/uploadToStorage.ts (1)

1-14: 에러 메시지의 스토리지 명칭을 확인하세요.

PR 설명에 따르면 R2 스토리지를 사용하는데, 에러 메시지에는 "S3 업로드 실패"로 표기되어 있습니다. 용어 일관성을 위해 "스토리지 업로드 실패" 또는 "R2 업로드 실패"로 변경하는 것을 권장합니다.

-    throw new Error(`S3 업로드 실패 : ${response.status}`);
+    throw new Error(`스토리지 업로드 실패 : ${response.status}`);
frontend/src/hooks/queries/club/images/useLogoMutation.ts (1)

34-34: TODO 코멘트 해결을 고려하세요.

세분화된 에러 메시지 처리가 필요하다고 표시되어 있습니다. 사용자 경험 개선을 위해 API 에러 응답에 따라 다른 메시지를 제공하는 것이 좋습니다.

백엔드 API의 에러 응답 스펙이 정의되어 있다면, 에러 핸들링 로직 구현을 도와드릴 수 있습니다.

frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx (1)

18-25: 스타일링 일관성을 고려하세요.

이 프로젝트는 styled-components를 사용하고 있는데, inline style로 disabled 상태를 처리하고 있습니다. 일관성을 위해 ImagePreview.styles.ts에서 disabled prop을 받아 처리하는 것을 권장합니다.

예시:

// ImagePreview.styles.ts
export const DeleteButton = styled.button<{ disabled?: boolean }>`
  /* 기존 스타일 */
  ${({ disabled }) =>
    disabled &&
    `
    opacity: 0.5;
    cursor: not-allowed;
  `}
`;

그 후 컴포넌트에서:

-      <Styled.DeleteButton
-        onClick={disabled ? undefined : onDelete}
-        disabled={disabled}
-        style={{
-          opacity: disabled ? 0.5 : 1,
-          cursor: disabled ? 'not-allowed' : 'pointer',
-        }}
-      >
+      <Styled.DeleteButton
+        onClick={disabled ? undefined : onDelete}
+        disabled={disabled}
+      >
frontend/src/hooks/queries/club/images/useFeedMutation.ts (1)

51-52: TODO 코멘트 해결을 고려하세요.

세분화된 에러 메시지가 필요하다고 표시되어 있습니다. uploadToStoragefeedApi.updateFeeds의 에러를 구분하여 사용자에게 더 명확한 피드백을 제공하세요.

frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx (1)

41-54: Presigned URL 기반 mutation 사용 방식 적절

  • 업로드 시: 파일 선택 후 사이즈 검증 → uploadMutation.mutate({ clubId, file })LogoUploadParams 시그니처에 맞게 clubIdfile을 함께 전달하고 있고,
  • 초기화 시: 사용자 확인 후 deleteMutation.mutate(clubId)로 단일 인자(clubId)를 넘겨 useDeleteLogomutationFn과도 일치합니다.

에러 처리는 훅 내부 onError에서 alert를 띄우도록 위임되어 있어, 컴포넌트 쪽 로직도 단순해진 상태라 현재 구현은 그대로 가져가도 괜찮아 보입니다. 다만, 추후 UX 개선 시에는 uploadMutation.isPending / deleteMutation.isPending을 이용해 버튼 비활성화나 로딩 표시를 추가하는 것도 고려할 만합니다.

Also applies to: 61-70

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 43754a6 and d7fb500.

📒 Files selected for processing (18)
  • frontend/src/apis/createFeedImage.ts (0 hunks)
  • frontend/src/apis/deleteClubLogo.ts (0 hunks)
  • frontend/src/apis/image/cover.ts (1 hunks)
  • frontend/src/apis/image/feed.ts (1 hunks)
  • frontend/src/apis/image/logo.ts (1 hunks)
  • frontend/src/apis/image/uploadToStorage.ts (1 hunks)
  • frontend/src/apis/updateClubLogo.ts (0 hunks)
  • frontend/src/apis/updateFeedImages.ts (0 hunks)
  • frontend/src/hooks/queries/club/images/useFeedMutation.ts (1 hunks)
  • frontend/src/hooks/queries/club/images/useLogoMutation.ts (1 hunks)
  • frontend/src/hooks/queries/club/useClubLogo.ts (0 hunks)
  • frontend/src/hooks/queries/club/useCreateFeedImage.ts (0 hunks)
  • frontend/src/hooks/queries/club/useUpdateFeedImages.ts (0 hunks)
  • frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx (4 hunks)
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx (2 hunks)
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx (1 hunks)
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.styles.ts (0 hunks)
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.tsx (0 hunks)
💤 Files with no reviewable changes (9)
  • frontend/src/apis/createFeedImage.ts
  • frontend/src/hooks/queries/club/useUpdateFeedImages.ts
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.tsx
  • frontend/src/apis/updateClubLogo.ts
  • frontend/src/apis/deleteClubLogo.ts
  • frontend/src/hooks/queries/club/useClubLogo.ts
  • frontend/src/apis/updateFeedImages.ts
  • frontend/src/hooks/queries/club/useCreateFeedImage.ts
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.styles.ts
🧰 Additional context used
📓 Path-based instructions (3)
frontend/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{ts,tsx,js,jsx}: Replace magic numbers with named constants for clarity
Replace complex/nested ternaries with if/else or IIFEs for readability
Assign complex boolean conditions to named variables for explicit meaning
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle)
Use unique and descriptive names for custom wrappers/functions to avoid ambiguity
Define constants near related logic or ensure names link them clearly to avoid silent failures
Break down broad state management into smaller, focused hooks/contexts to reduce coupling

Files:

  • frontend/src/hooks/queries/club/images/useLogoMutation.ts
  • frontend/src/apis/image/cover.ts
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx
  • frontend/src/apis/image/uploadToStorage.ts
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx
  • frontend/src/apis/image/logo.ts
  • frontend/src/apis/image/feed.ts
  • frontend/src/hooks/queries/club/images/useFeedMutation.ts
  • frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

Use consistent return types for similar functions/hooks

Files:

  • frontend/src/hooks/queries/club/images/useLogoMutation.ts
  • frontend/src/apis/image/cover.ts
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx
  • frontend/src/apis/image/uploadToStorage.ts
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx
  • frontend/src/apis/image/logo.ts
  • frontend/src/apis/image/feed.ts
  • frontend/src/hooks/queries/club/images/useFeedMutation.ts
  • frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx
frontend/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (frontend/.cursorrules)

frontend/**/*.{tsx,jsx}: Abstract complex logic/interactions into dedicated components/HOCs
Separate significantly different conditional UI/logic into distinct components
Colocate simple, localized logic or use inline definitions to reduce context switching
Choose field-level or form-level cohesion based on form requirements when using form libraries like react-hook-form
Use Component Composition instead of Props Drilling to reduce coupling

Files:

  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx
  • frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx
  • frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx
🧠 Learnings (1)
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.

Applied to files:

  • frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx
🧬 Code graph analysis (6)
frontend/src/hooks/queries/club/images/useLogoMutation.ts (2)
frontend/src/apis/image/logo.ts (1)
  • logoApi (9-58)
frontend/src/apis/image/uploadToStorage.ts (1)
  • uploadToStorage (1-14)
frontend/src/apis/image/cover.ts (2)
frontend/public/mockServiceWorker.js (1)
  • response (125-125)
frontend/src/apis/auth/secureFetch.ts (1)
  • secureFetch (3-41)
frontend/src/apis/image/logo.ts (1)
frontend/src/apis/auth/secureFetch.ts (1)
  • secureFetch (3-41)
frontend/src/apis/image/feed.ts (1)
frontend/src/apis/auth/secureFetch.ts (1)
  • secureFetch (3-41)
frontend/src/hooks/queries/club/images/useFeedMutation.ts (2)
frontend/src/apis/image/feed.ts (1)
  • feedApi (14-50)
frontend/src/apis/image/uploadToStorage.ts (1)
  • uploadToStorage (1-14)
frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx (1)
frontend/src/hooks/queries/club/images/useLogoMutation.ts (2)
  • useUploadLogo (11-39)
  • useDeleteLogo (42-58)
🪛 Biome (2.1.2)
frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx

[error] 31-31: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 32-32: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (9)
frontend/src/apis/image/feed.ts (1)

36-49: LGTM!

피드 업데이트 로직이 명확하고 에러 처리가 적절합니다.

frontend/src/apis/image/cover.ts (1)

32-58: LGTM!

커버 이미지 완료 처리 및 삭제 로직이 명확하고 에러 처리가 적절합니다.

frontend/src/hooks/queries/club/images/useLogoMutation.ts (2)

11-39: LGTM! presigned URL 업로드 플로우가 정확합니다.

3단계 업로드 플로우(presigned URL 발급 → R2 업로드 → 완료 처리)가 명확하게 구현되어 있고, 성공 시 캐시 무효화 처리도 적절합니다.


42-58: 로고 삭제 로직이 명확합니다.

삭제 후 캐시 무효화 처리가 적절하게 구현되어 있습니다.

frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx (1)

7-14: LGTM! disabled 프롭 추가가 적절합니다.

disabled 상태를 지원하도록 컴포넌트가 확장되었습니다.

frontend/src/hooks/queries/club/images/useFeedMutation.ts (1)

60-77: LGTM! 피드 업데이트 로직이 명확합니다.

URL 배열 갱신 로직이 간단하고 명확하며, 캐시 무효화 처리도 적절합니다.

frontend/src/apis/image/logo.ts (1)

9-57: LGTM! 로고 API 구조가 명확합니다.

presigned URL 발급, 완료 처리, 삭제 플로우가 명확하게 구현되어 있고 에러 처리도 적절합니다.

frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx (1)

20-26: LGTM! 새로운 훅 기반 플로우로 잘 전환되었습니다.

useUploadFeed와 useUpdateFeed 훅을 사용하여 이미지 업로드 로직이 명확하게 분리되었고, 로딩 상태도 적절히 통합되었습니다.

frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx (1)

8-11: 새 로고 훅 import 구조 적절

기존 훅을 useUploadLogo, useDeleteLogo로 교체한 부분은 네이밍과 역할이 명확하고, 관련 파일 경로(useLogoMutation.ts)와도 일관적이라 문제 없어 보입니다.

Comment on lines 20 to 44
return useMutation({
mutationFn: async ({ clubId, files, existingUrls }: FeedUploadParams) => {
// 1. presigned URL 요청
const uploadRequests = files.map((file) => ({
fileName: file.name,
contentType: file.type,
}));
const feedResArr = await feedApi.getUploadUrls(clubId, uploadRequests);

// 2. r2에 병렬 업로드
await Promise.all(
files.map((file, i) =>
uploadToStorage(feedResArr[i].presignedUrl, file),
),
);

// 3. 새로 업로드된 URL 추출
const newUrls = feedResArr.map((res) => res.finalUrl);

// 4. 기존 URL과 합쳐서 전체 배열 생성
const allUrls = [...existingUrls, ...newUrls];

// 5. 서버에 전체 배열 PUT으로 갱신 (업로드 + 기존 피드 동기화)
await feedApi.updateFeeds(clubId, allUrls);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find frontend -name "useFeedMutation.ts" -type f

Repository: Moadong/moadong

Length of output: 116


🏁 Script executed:

cat -n frontend/src/hooks/queries/club/images/useFeedMutation.ts

Repository: Moadong/moadong

Length of output: 2706


🏁 Script executed:

find frontend -name "feed.ts" -o -name "feed.js" | grep -E "apis.*feed"

Repository: Moadong/moadong

Length of output: 90


🏁 Script executed:

cat -n frontend/src/apis/image/feed.ts

Repository: Moadong/moadong

Length of output: 1643


🏁 Script executed:

cat -n frontend/src/apis/image/uploadToStorage.ts 2>/dev/null || find frontend -name "uploadToStorage*"

Repository: Moadong/moadong

Length of output: 468


🏁 Script executed:

rg -i "cleanup|rollback|delete.*storage|orphan" frontend/src --type ts --type tsx -A 3

Repository: Moadong/moadong

Length of output: 86


🏁 Script executed:

rg -i "cleanup|rollback|delete.*storage|orphan" frontend/src -A 3

Repository: Moadong/moadong

Length of output: 41


🏁 Script executed:

find . -name "*.py" -o -name "*.java" -o -name "*.go" -o -name "*.kt" | head -20

Repository: Moadong/moadong

Length of output: 1487


🏁 Script executed:

find backend -name "*Feed*" -o -name "*feed*" | grep -v ".class"

Repository: Moadong/moadong

Length of output: 119


🏁 Script executed:

rg -i "feed" backend/src/main/java --type java -l | head -20

Repository: Moadong/moadong

Length of output: 567


🏁 Script executed:

cat -n backend/src/main/java/moadong/gcs/service/ClubImageService.java

Repository: Moadong/moadong

Length of output: 7210


🏁 Script executed:

cat -n backend/src/main/java/moadong/gcs/controller/ClubImageController.java

Repository: Moadong/moadong

Length of output: 3290


🏁 Script executed:

rg -i "presigned|ttl|expir" backend --type java -B 2 -A 2

Repository: Moadong/moadong

Length of output: 6003


Handle partial upload failures with Promise.allSettled and add cleanup logic for orphaned files.

The current implementation using Promise.all means any single file upload failure fails the entire mutation, leaving already-uploaded files orphaned in GCS. This also doesn't account for failures in feedApi.updateFeeds after files are successfully uploaded.

Specific scenarios to address:

  1. Files 1-3 upload successfully, file 4-5 fail → Promise.all rejects, files 1-3 orphaned in storage
  2. All files upload successfully, but feedApi.updateFeeds fails → database doesn't update, all files orphaned

Recommended changes:

Replace Promise.all with Promise.allSettled to track individual upload results:

const uploadResults = await Promise.allSettled(
  files.map((file, i) =>
    uploadToStorage(feedResArr[i].presignedUrl, file),
  ),
);

const successfulIndices = uploadResults
  .map((result, i) => (result.status === 'fulfilled' ? i : -1))
  .filter((i) => i !== -1);

const newUrls = successfulIndices.map((i) => feedResArr[i].finalUrl);

Additionally, coordinate with the backend to add cleanup logic for orphaned files that weren't persisted to the database. The backend updateFeeds method should either implement transactional cleanup on failure or provide a separate endpoint to delete orphaned URLs from GCS when the database update fails.

🤖 Prompt for AI Agents
In frontend/src/hooks/queries/club/images/useFeedMutation.ts around lines 20 to
44, the current upload flow uses Promise.all which aborts on the first upload
failure and can leave already-uploaded files orphaned, and it also doesn't
handle failures of feedApi.updateFeeds after uploads succeed; replace
Promise.all with Promise.allSettled to capture per-file upload results, build
newUrls only from successfully uploaded files, and then call feedApi.updateFeeds
with those successful URLs; additionally, add error handling so that if
updateFeeds fails you (a) call a backend cleanup endpoint to delete the
successfully uploaded but now-orphaned files (or request the backend to perform
transactional cleanup), and (b) surface a clear error to the user; ensure upload
and cleanup use the presigned URL metadata (feedResArr indices) to identify
which files to delete.

Copy link
Member

@seongwon030 seongwon030 left a comment

Choose a reason for hiding this comment

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

쿼리 훅에서 성공실패 처리를 한꺼번에 하니까 가져오는 파일 코드가 확 줄어드네요 👍

추가로 요청한 부분 한 번 봐 주시면 좋을 것 같아요

문제:
- input에 multiple={true}로 설정되어 여러 파일 선택이 가능했지만,
  파일 크기 검증은 첫 번째 파일만 수행하여 두 번째 이후 파일이
  10MB를 초과해도 업로드가 가능했던 보안 취약점

해결:
- Array.from(files).find()를 사용하여 모든 파일의 크기를 검증
- 용량 초과 시 어떤 파일이 문제인지 파일명을 포함한 명확한 에러 메시지 제공
- 검증 실패 시 input을 초기화하여 의도치 않은 업로드 방지
문제:
- 다중 파일 업로드 시 Promise.all 사용으로 인해 1개 파일만 실패해도 전체 실패 처리
- 성공한 파일들은 R2 스토리지에 업로드되었지만 DB에 등록되지 않아 고아 파일 발생

해결:
- Promise.allSettled로 변경하여 개별 파일의 성공/실패를 독립적으로 추적
- 성공한 파일만 successfulUrls에 저장하여 DB에 등록
- 실패한 파일명을 사용자에게 명확히 안내
- 모든 파일이 실패한 경우에만 에러 처리

개선 효과:
- 10개 중 2개 실패 시 → 8개는 정상 등록, 실패한 2개만 재업로드 가능
- 고아 파일 문제 해결 및 사용자 경험 개선
Copy link
Member

@seongwon030 seongwon030 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다~

@oesnuj oesnuj merged commit 4778611 into develop-fe Dec 20, 2025
3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 26, 2025
@lepitaaar lepitaaar deleted the refactor/#883-frontend-image-upload-MOA-374 branch January 18, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend ✨ Feature 기능 개발 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[refactor] MOA-374 이미지 (다중) 업로드 구조 변경에 맞춰 프론트엔드 구조를 변경한다

2 participants

Comments