[refactor] 이미지 업로드를 Presigned URL 직접 업로드 방식으로 개선#906
Conversation
- 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를 명시적으로 전달하도록 변경
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| 코호트 / 파일(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
- [feature] 동아리 활동사진 이미지 다중 업로드 지원하도록 수정한다 #808 — 백엔드에서 multipart → presigned URL로 변경한 PR로, 프론트 presigned URL 흐름과 직접 연관
- [feature] 관리자 페이지 클럽 로고 수정 및 초기화 UI/UX 수정 #342 — ClubLogoEditor 및 로고 업로드/삭제 흐름을 수정한 PR로 UI·훅 변화에 연관
- [release] v1.0.2 #388 — ImageUpload 컴포넌트 관련 변경/삭제로 파일 업로드 컴포넌트 충돌 가능성 높음
Suggested reviewers
- Zepelown
- PororoAndFriends
- lepitaaar
- seongwon030
Pre-merge checks and finishing touches
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | 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.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
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.ts와logo.ts에도 동일하게 정의되어 있습니다.frontend/src/apis/image/feed.ts의 리뷰 코멘트를 참고하여 공통 타입 파일로 추출하세요.frontend/src/apis/image/logo.ts (1)
4-7: PresignedData 인터페이스 중복 정의.이 인터페이스는
feed.ts와cover.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 코멘트 해결을 고려하세요.세분화된 에러 메시지가 필요하다고 표시되어 있습니다.
uploadToStorage와feedApi.updateFeeds의 에러를 구분하여 사용자에게 더 명확한 피드백을 제공하세요.frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx (1)
41-54: Presigned URL 기반 mutation 사용 방식 적절
- 업로드 시: 파일 선택 후 사이즈 검증 →
uploadMutation.mutate({ clubId, file })로LogoUploadParams시그니처에 맞게clubId와file을 함께 전달하고 있고,- 초기화 시: 사용자 확인 후
deleteMutation.mutate(clubId)로 단일 인자(clubId)를 넘겨useDeleteLogo의mutationFn과도 일치합니다.에러 처리는 훅 내부
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.
📒 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 withif/elseor 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.tsfrontend/src/apis/image/cover.tsfrontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsxfrontend/src/apis/image/uploadToStorage.tsfrontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsxfrontend/src/apis/image/logo.tsfrontend/src/apis/image/feed.tsfrontend/src/hooks/queries/club/images/useFeedMutation.tsfrontend/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.tsfrontend/src/apis/image/cover.tsfrontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsxfrontend/src/apis/image/uploadToStorage.tsfrontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsxfrontend/src/apis/image/logo.tsfrontend/src/apis/image/feed.tsfrontend/src/hooks/queries/club/images/useFeedMutation.tsfrontend/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.tsxfrontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsxfrontend/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)와도 일관적이라 문제 없어 보입니다.
| 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); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find frontend -name "useFeedMutation.ts" -type fRepository: Moadong/moadong
Length of output: 116
🏁 Script executed:
cat -n frontend/src/hooks/queries/club/images/useFeedMutation.tsRepository: 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.tsRepository: 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 3Repository: Moadong/moadong
Length of output: 86
🏁 Script executed:
rg -i "cleanup|rollback|delete.*storage|orphan" frontend/src -A 3Repository: Moadong/moadong
Length of output: 41
🏁 Script executed:
find . -name "*.py" -o -name "*.java" -o -name "*.go" -o -name "*.kt" | head -20Repository: 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 -20Repository: Moadong/moadong
Length of output: 567
🏁 Script executed:
cat -n backend/src/main/java/moadong/gcs/service/ClubImageService.javaRepository: Moadong/moadong
Length of output: 7210
🏁 Script executed:
cat -n backend/src/main/java/moadong/gcs/controller/ClubImageController.javaRepository: Moadong/moadong
Length of output: 3290
🏁 Script executed:
rg -i "presigned|ttl|expir" backend --type java -B 2 -A 2Repository: 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:
- Files 1-3 upload successfully, file 4-5 fail → Promise.all rejects, files 1-3 orphaned in storage
- All files upload successfully, but
feedApi.updateFeedsfails → 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.
frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx
Outdated
Show resolved
Hide resolved
frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx
Outdated
Show resolved
Hide resolved
문제:
- input에 multiple={true}로 설정되어 여러 파일 선택이 가능했지만,
파일 크기 검증은 첫 번째 파일만 수행하여 두 번째 이후 파일이
10MB를 초과해도 업로드가 가능했던 보안 취약점
해결:
- Array.from(files).find()를 사용하여 모든 파일의 크기를 검증
- 용량 초과 시 어떤 파일이 문제인지 파일명을 포함한 명확한 에러 메시지 제공
- 검증 실패 시 input을 초기화하여 의도치 않은 업로드 방지
문제: - 다중 파일 업로드 시 Promise.all 사용으로 인해 1개 파일만 실패해도 전체 실패 처리 - 성공한 파일들은 R2 스토리지에 업로드되었지만 DB에 등록되지 않아 고아 파일 발생 해결: - Promise.allSettled로 변경하여 개별 파일의 성공/실패를 독립적으로 추적 - 성공한 파일만 successfulUrls에 저장하여 DB에 등록 - 실패한 파일명을 사용자에게 명확히 안내 - 모든 파일이 실패한 경우에만 에러 처리 개선 효과: - 10개 중 2개 실패 시 → 8개는 정상 등록, 실패한 2개만 재업로드 가능 - 고아 파일 문제 해결 및 사용자 경험 개선
#️⃣연관된 이슈
🎯 작업 배경 및 목적
📝작업 내용
1. Presigned URL 기반 이미지 업로드 시스템 도입
백엔드 API 구현
프론트엔드 리팩토링
createFeedImage.ts,updateClubLogo.ts,deleteClubLogo.ts등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: 문제점: 이중 전송, 서버 부하 높음변경: 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: 개선: 서버 우회, 직접 업로드비교
중점적으로 리뷰받고 싶은 부분
🫡 참고사항
Summary by CodeRabbit
새로운 기능
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.