Skip to content

Conversation

@yw6938
Copy link
Collaborator

@yw6938 yw6938 commented Nov 8, 2025

#️⃣연관된 이슈

ex) #779

📝작업 내용

기존 MultipartFile에서 Pre-Signed URL으로 이미지 업로드 기능을 리팩토링 하였습니다.

test Url의 작동 방식은 위의 사진처럼 클라이언트가 직접 이미지를 올리는 방식입니다. 그래서 이런 방식을 쓰면 기존보다 서버의 부하를 크게 줄일 수 있어서 채택하였습니다.

트러블슈팅

Url에 사진을 올릴때 계속 SignatureDoesNotMatch가 떴습니다. 이유는 클라이언트가 서버에 보낸 요청(쿼리·헤더·바디)이 presigned URL을 만들 때 사용된 정확한 문자열과 달라서 발생합니다. 따라서 Bearer토큰을 빼고 요청을 보내서 성공했습니다.

또한 jpg타입같은 경우에는 따로 Content-type에 image/jpg라고 명시를 해줘야 오류가 안나타났습니다.

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • 새로운 기능

    • 클럽 로고/피드/커버 업로드를 프리사인드 기반 2단계(업로드 URL 생성 → 업로드 완료 신고)로 전환
  • 개선

    • 업로드 요청/완료 DTO 도입 및 클라이언트 업로드 흐름 개선
    • 파일명·MIME·URL·크기 검증 및 업로드 제한(피드 개수 등) 강화
    • 피드 업데이트 요청에 입력 유효성 추가
  • 버그 수정

    • 상세 오류 코드와 사용자 메시지 대거 추가/정비
  • 테스트

    • 이미지 업로드 관련 일부 단위 테스트 제거
  • 기타(설정)

    • 업로드 관련 서버 구성 및 시그너(프리사인드) 설정 추가

@yw6938 yw6938 self-assigned this Nov 8, 2025
@yw6938 yw6938 added 🔨 Refactor 코드 리팩토링 💾 BE Backend labels Nov 8, 2025
@vercel
Copy link

vercel bot commented Nov 8, 2025

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

Project Deployment Preview Comments Updated (UTC)
moadong Ready Ready Preview Comment Nov 19, 2025 5:37am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 8, 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

멀티파트 직접 업로드 엔드포인트를 제거하고 AWS S3 Presigned URL 기반의 2단계 업로드(생성 → 클라이언트 업로드 → 완료 확인)를 도입했으며, 다수의 ErrorCode 상수를 추가하고 S3Presigner 빈/구성을 추가했습니다.

Changes

Cohort / File(s) 변경 요약
빌드 의존성
\backend/build.gradle``
software.amazon.awssdk:url-connection-client:2.26.0 의존성 추가
에러 코드 확장
\backend/src/main/java/moadong/global/exception/ErrorCode.java``
클럽/파일/사용자/토큰/비밀번호/지원서/암호화/FCM 등 다수(약 27개) ErrorCode 상수 추가 및 enum 종결 세미콜론 제거
컨트롤러: multipart → presigned URL
\backend/src/main/java/moadong/media/controller/ClubImageController.java``
Multipart 업로드 엔드포인트 제거, generate* / complete* 2단계 엔드포인트로 교체; DTO(UploadUrlRequest, UploadCompleteRequest, PresignedUploadResponse) 사용; 생성자 주입(@Qualifier("cloudflare")) 추가; putFeeds@Valid 적용
서비스 인터페이스 변경
\backend/src/main/java/moadong/media/service/ClubImageService.java``
Multipart 기반 메서드 제거; generateLogoUploadUrl, generateFeedUploadUrls, generateCoverUploadUrl(PresignedUploadResponse 반환) 및 completeLogoUpload, completeCoverUpload 추가; 기존 delete/update 메서드 유지
클라우드 이미지 구현 변경
\backend/src/main/java/moadong/media/service/CloudflareImageService.java``
S3Presigner 통합으로 presigned URL 생성/완료 로직 추가; 업로드 파일명·URL·크기 검증; headObject 검사; 트랜잭션 경계 추가; 로깅·예외 처리 강화; 내부 헬퍼 메서드 다수 추가
업로드 DTO 추가
\backend/src/main/java/moadong/media/dto/PresignedUploadResponse.java`, `backend/src/main/java/moadong/media/dto/UploadCompleteRequest.java`, `backend/src/main/java/moadong/media/dto/UploadUrlRequest.java``
Presigned URL 응답 및 생성/완료 요청을 위한 record 타입 추가(유효성 애노테이션 포함)
S3 설정 확장
\backend/src/main/java/moadong/media/util/S3Config.java``
S3Presigner 빈 추가(close destroyMethod), 자격증명 유효성 검증 로직 추가 및 s3Client/presigner 생성 시 검증 호출
테스트 제거
\backend/src/test/java/moadong/media/service/CloudflareClubImageServiceFeedTest.java`, `backend/src/test/java/moadong/media/service/CloudflareClubImageServiceLogoTest.java``
기존 multipart 기반 및 이전 흐름에 의존한 단위 테스트 클래스 삭제
DTO 유효성 강화
\backend/src/main/java/moadong/media/dto/FeedUpdateRequest.java``
feeds 필드에 @NotNull 추가

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as ClubImageController
    participant Service as ClubImageService
    participant Impl as CloudflareImageService
    participant Presigner as S3Presigner
    participant S3 as AWS S3

    rect rgb(220,235,255)
    Note over Client,Impl: 1) Presigned URL 생성
    Client->>Controller: POST /generateXUploadUrl(clubId, fileName, contentType)
    Controller->>Service: generateXUploadUrl(...)
    Service->>Impl: generateXUploadUrl(...)
    Impl->>Impl: validateFileName()/prepareFinalUrl()
    Impl->>Presigner: presignPutObject(...)
    Presigner-->>Impl: presignedUrl + requiredHeaders
    Impl-->>Service: PresignedUploadResponse
    Service-->>Controller: PresignedUploadResponse
    Controller-->>Client: {presignedUrl, finalUrl, requiredHeaders}
    end

    rect rgb(220,255,220)
    Note over Client,S3: 2) 클라이언트가 S3로 업로드
    Client->>S3: PUT presignedUrl + headers + body
    S3-->>Client: 200 OK
    end

    rect rgb(255,240,220)
    Note over Client,Impl: 3) 업로드 완료 확인
    Client->>Controller: POST /completeXUpload(clubId, fileUrl)
    Controller->>Service: completeXUpload(...)
    Service->>Impl: completeXUpload(...)
    Impl->>S3: headObject(key)
    S3-->>Impl: metadata(size)
    alt size 및 유효성 통과
        Impl->>Impl: update DB (transactional)
        Impl-->>Service: void
        Service-->>Controller: 200 OK
    else 크기 초과/유효하지 않음
        Impl->>S3: deleteObject(key)
        Impl-->>Service: throw RestApiException
        Service-->>Controller: error
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

  • 추가로 집중 검토 권장 파일/영역:
    • backend/src/main/java/moadong/media/service/CloudflareImageService.java : presigner 통합, headObject 검사, 검증 로직, 트랜잭션 경계
    • backend/src/main/java/moadong/media/util/S3Config.java : presigner 빈 구성 및 자격증명 검증
    • backend/src/main/java/moadong/global/exception/ErrorCode.java : 신규 코드 일관성 및 중복 여부
    • 컨트롤러/서비스 시그니처 변경에 따른 API 호환성, 클라이언트 영향 및 삭제된 테스트 보강 필요성

Possibly related issues

Possibly related PRs

Suggested labels

✨ Feature, 📬 API

Suggested reviewers

  • PororoAndFriends
  • Zepelown
  • lepitaaar

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 Pre-Signed URL 기반 다중 업로드 리팩토링의 핵심 내용을 명확하게 반영하고 있습니다.
Linked Issues check ✅ Passed 코드 변경사항이 MOA-284의 핵심 목표인 Pre-Signed URL 기반 다중 이미지 업로드 구현 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 Pre-Signed URL 업로드 흐름 리팩토링과 관련된 범위 내 변경입니다.
✨ 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/#779-club-photo-multi-upload-MOA-284

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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.

@github-actions
Copy link

github-actions bot commented Nov 8, 2025

Test Results

68 tests   65 ✅  15s ⏱️
11 suites   3 💤
11 files     0 ❌

Results for commit 8759bf2.

♻️ This comment has been updated with latest results.

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

📜 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 e2cb5cf and d36bd65.

📒 Files selected for processing (9)
  • backend/build.gradle (1 hunks)
  • backend/src/main/java/moadong/global/exception/ErrorCode.java (2 hunks)
  • backend/src/main/java/moadong/media/controller/ClubImageController.java (2 hunks)
  • backend/src/main/java/moadong/media/dto/PresignedUploadResponse.java (1 hunks)
  • backend/src/main/java/moadong/media/dto/UploadCompleteRequest.java (1 hunks)
  • backend/src/main/java/moadong/media/dto/UploadUrlRequest.java (1 hunks)
  • backend/src/main/java/moadong/media/service/CloudflareImageService.java (4 hunks)
  • backend/src/main/java/moadong/media/service/ClubImageService.java (1 hunks)
  • backend/src/main/java/moadong/media/util/S3Config.java (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.

Applied to files:

  • backend/src/main/java/moadong/media/service/ClubImageService.java
🧬 Code graph analysis (1)
backend/src/main/java/moadong/media/service/CloudflareImageService.java (3)
backend/src/main/java/moadong/club/entity/Club.java (1)
  • Slf4j (28-153)
backend/src/main/java/moadong/global/util/ObjectIdConverter.java (1)
  • ObjectIdConverter (7-17)
backend/src/main/java/moadong/global/util/RandomStringUtil.java (1)
  • RandomStringUtil (5-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (3)
backend/src/main/java/moadong/global/exception/ErrorCode.java (3)

10-10: LGTM! 에러 코드 분류 주석이 가독성을 크게 개선합니다.

각 에러 코드 범위별로 명확한 주석을 추가하여 코드의 유지보수성과 가독성이 향상되었습니다.

Also applies to: 22-22, 34-34, 41-41, 45-45, 49-49, 57-57, 61-61


30-30: Pre-Signed URL 워크플로우에 적합한 에러 코드 추가입니다.

새로 추가된 INVALID_FILE_URLFILE_TOO_LARGE는 Pre-Signed URL 업로드 방식에 필요한 검증 시나리오를 잘 다룹니다:

  • INVALID_FILE_URL: 업로드 완료 후 URL 검증 시 사용
  • FILE_TOO_LARGE: 클라이언트 측 파일 크기 제한 검증 시 사용

Also applies to: 32-32


26-26: IMAGE_DELETE_FAILED와 FILE_DELETE_FAILED의 구분 의도를 명확히 하세요.

두 에러 코드가 의미상 매우 유사합니다:

  • 601-4: "이미지 삭제에 실패하였습니다"
  • 601-9: "파일 삭제에 실패하였습니다."

다만 명명상 구분(IMAGE vs FILE)이 있으므로, 각각이 어떤 컨텍스트에서 사용될 예정인지 명확히 하거나 통합을 고려하세요. 현재 두 에러 코드 모두 코드베이스에서 실제 사용되지 않으므로, 구현 시점에 적절한 에러 코드를 사용하도록 주의하세요.

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

🧹 Nitpick comments (2)
backend/src/main/java/moadong/media/service/CloudflareImageService.java (2)

66-76: 트랜잭션 내부에서 S3 작업을 수행하는 설계를 재고하세요.

@Transactional 내에서 deleteFile (S3 네트워크 호출)을 수행하면 트랜잭션이 길어질 수 있습니다. S3 작업이 실패할 경우 DB 업데이트도 롤백되어 일관성은 유지되지만, 트랜잭션 타임아웃이나 성능 문제를 유발할 수 있습니다.

대안:

  1. S3 삭제를 트랜잭션 외부로 이동 (orphaned files 허용)
  2. 비동기 삭제 큐 사용
  3. 현재 방식 유지하되 설계 의도를 주석으로 명시

현재 방식도 데이터 일관성 측면에서 유효하므로 팀의 요구사항에 따라 선택하시면 됩니다.

Also applies to: 154-164


333-340: 파일명이 null/empty일 때의 에러 코드를 재고하세요.

fileName이 null이거나 비어있을 때 FILE_NOT_FOUND를 발생시키는 것은 의미상 약간 어색합니다. FILE_NOT_FOUND는 일반적으로 파일 경로에 파일이 존재하지 않을 때 사용하는데, 여기서는 파라미터가 누락되거나 유효하지 않은 상황입니다.

INVALID_FILE_NAME 또는 MISSING_FILE_NAME과 같은 에러 코드가 더 명확할 수 있습니다. 다만 기존 ErrorCode 정의에 제약이 있다면 현재 방식도 수용 가능합니다.

📜 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 c6c3bb1 and 66dcafd.

📒 Files selected for processing (1)
  • backend/src/main/java/moadong/media/service/CloudflareImageService.java (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/src/main/java/moadong/media/service/CloudflareImageService.java (3)
backend/src/main/java/moadong/club/entity/Club.java (1)
  • Slf4j (28-153)
backend/src/main/java/moadong/global/util/ObjectIdConverter.java (1)
  • ObjectIdConverter (7-17)
backend/src/main/java/moadong/global/util/RandomStringUtil.java (1)
  • RandomStringUtil (5-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (9)
backend/src/main/java/moadong/media/service/CloudflareImageService.java (9)

6-13: LGTM! 프리사인드 URL 흐름에 필요한 의존성이 적절히 추가되었습니다.

로깅 지원(@slf4j), S3Presigner, 그리고 파일 검증을 위한 필요한 임포트들이 올바르게 추가되었습니다.

Also applies to: 36-36, 45-45


80-109: LGTM! 보안 개선이 반영되었습니다.

기존 URL은 재검증하지 않고 새 URL만 검증하는 최적화가 적절하며, 이전 리뷰에서 지적된 클럽별 경로 검증(clubId, FileType 파라미터)이 추가되어 보안이 크게 개선되었습니다.


126-151: LGTM! 방어적 에러 처리가 적절합니다.

null 체크, 로깅, 그리고 삭제 실패 시에도 계속 진행하는 방식은 멱등성을 고려한 적절한 에러 처리입니다.


167-190: LGTM! URL 생성 시점의 사전 검증이 적절합니다.

generateFeedUploadUrl에서 MAX_FEED_COUNT 체크는 사전 검증으로 의미가 있습니다. URL 발급과 실제 업로드 완료 사이에 race condition이 있을 수 있지만, completeFeedUpload에서 재검증하므로 최종적으로 보호됩니다.


193-205: LGTM! 업로드 완료 로직이 올바르게 구현되었습니다.

기존 파일 삭제 → 새 파일 URL 설정 → 저장 순서가 적절하며, 보안 검증(validateFileConstraints)도 올바르게 수행됩니다.

Also applies to: 233-244


247-280: LGTM! 이전 리뷰의 보안 이슈가 해결되었습니다.

clubIdFileType를 파라미터로 받아 파일 경로가 요청한 클럽과 파일 타입에 해당하는지 검증하여, 다른 클럽의 파일을 참조하거나 임의 삭제하는 보안 취약점이 해결되었습니다.

추가로 HeadObject를 통한 파일 크기 검증과 초과 시 즉시 삭제하는 로직도 적절합니다.

Based on learnings


286-292: LGTM! URL에서 key 추출 로직이 명확합니다.

null 반환을 통해 호출자가 에러를 처리하도록 하는 방식이 적절합니다.


294-331: LGTM! Presigned URL 생성 로직이 올바르게 구현되었습니다.

파일명에 유효하지 않은 문자가 포함된 경우 랜덤 문자열로 대체하는 처리가 적절하며, 10분 만료 시간과 필수 헤더 정보 제공이 클라이언트 직접 업로드에 적합합니다.


342-347: LGTM! Null 체크와 에러 처리가 적절합니다.

ClubRecruitmentInformation이 null인 경우를 방어하여 NPE를 방지하며, 로깅과 명확한 예외 처리가 이루어집니다.

Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

커밋단위가 조금 큰거같네요. 커밋하실때 줄이는게 좋을꺼같습니다!

Copy link
Contributor

Choose a reason for hiding this comment

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

각에러코드 설명 주석 추가해주신거 좋습니다

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

♻️ Duplicate comments (1)
backend/src/main/java/moadong/media/service/CloudflareImageService.java (1)

58-62: viewEndpoint가 null일 때 명시적으로 실패 처리하세요.

현재 viewEndpoint가 null이면 normalizedViewEndpoint가 빈 문자열이 되고, extractKeyOrNull에서 prefix가 "/"가 되어 의도하지 않은 URL과 매칭될 수 있습니다.

다음과 같이 명시적 검증을 추가하세요:

 @PostConstruct
 private void init() {
+    if (viewEndpoint == null || viewEndpoint.isBlank()) {
+        throw new IllegalStateException("cloud.aws.s3.view-endpoint must be configured");
+    }
-    normalizedViewEndpoint = viewEndpoint != null ? viewEndpoint.replaceAll("/+$", "") : "";
+    normalizedViewEndpoint = viewEndpoint.replaceAll("/+$", "");
 }
🧹 Nitpick comments (3)
backend/src/main/java/moadong/media/service/ClubImageService.java (1)

17-26: 피드 업로드 흐름의 불일치를 명확히 문서화하세요.

로고/커버는 generateXxxUploadUrlcompleteXxxUpload 2단계 흐름을 따르지만, 피드는 generateFeedUploadUrl (여러 번 호출 가능) → updateFeeds (전체 URL 리스트 전달) 패턴을 사용합니다. 이 비대칭 설계는 피드의 다중 업로드를 지원하기 위한 것으로 보이지만, API 사용자에게 혼란을 줄 수 있습니다.

인터페이스에 Javadoc을 추가하여 각 메서드의 사용 시나리오와 호출 순서를 명확히 문서화하는 것을 권장합니다.

예시:

+    /**
+     * 로고 이미지 업로드를 위한 Presigned URL을 생성합니다.
+     * 생성된 URL로 클라이언트가 직접 업로드 완료 후 completeLogoUpload를 호출해야 합니다.
+     */
     PresignedUploadResponse generateLogoUploadUrl(String clubId, String fileName, String contentType);
+
+    /**
+     * 피드 이미지 업로드를 위한 Presigned URL을 생성합니다.
+     * 여러 이미지를 업로드하려면 이 메서드를 여러 번 호출한 후,
+     * 모든 업로드가 완료되면 updateFeeds에 최종 URL 리스트를 전달해야 합니다.
+     */
     PresignedUploadResponse generateFeedUploadUrl(String clubId, String fileName, String contentType);
backend/src/main/java/moadong/media/service/CloudflareImageService.java (2)

168-180: 피드 개수 검증의 동시성 이슈를 인지하세요.

generateFeedUploadUrl의 피드 개수 검증(lines 174-178)은 트랜잭션 밖에서 수행되므로, 동시 호출 시 두 요청이 모두 제한 이하로 판단하여 URL을 생성할 수 있습니다.

하지만 최종 검증은 updateFeeds(line 87-89)에서 트랜잭션 내에서 수행되고 Club 엔티티의 @Version 필드를 통한 낙관적 잠금으로 보호되므로, 실제 데이터 오염은 발생하지 않습니다. 현재 구현은 다층 방어(defense in depth) 패턴으로 볼 수 있습니다.

클라이언트가 OptimisticLockingFailureException 발생 시 업로드된 파일을 정리하거나 재시도하는 방법을 API 문서에 안내하는 것을 권장합니다.


219-252: 파일 크기 검증의 한계를 인지하고 있어야 합니다.

Presigned URL 방식의 특성상 파일 크기 검증은 클라이언트가 업로드를 완료한 후에만 가능합니다(line 228-246). 이는 초과 크기 파일이 업로드된 후 삭제되므로 대역폭이 낭비되는 한계가 있습니다.

현재 구현은 초과 파일을 탐지하고 삭제한 후 적절한 예외를 던지므로 올바르게 처리되었습니다. 하지만 클라이언트 측에서 업로드 전에 파일 크기를 미리 검증하도록 API 문서에 권장 사항을 추가하는 것이 좋습니다.

📜 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 66dcafd and 33cb954.

📒 Files selected for processing (3)
  • backend/src/main/java/moadong/media/controller/ClubImageController.java (2 hunks)
  • backend/src/main/java/moadong/media/service/CloudflareImageService.java (4 hunks)
  • backend/src/main/java/moadong/media/service/ClubImageService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/src/main/java/moadong/media/controller/ClubImageController.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.

Applied to files:

  • backend/src/main/java/moadong/media/service/CloudflareImageService.java
  • backend/src/main/java/moadong/media/service/ClubImageService.java
🧬 Code graph analysis (1)
backend/src/main/java/moadong/media/service/CloudflareImageService.java (3)
backend/src/main/java/moadong/club/entity/Club.java (1)
  • Slf4j (28-153)
backend/src/main/java/moadong/global/util/ObjectIdConverter.java (1)
  • ObjectIdConverter (7-17)
backend/src/main/java/moadong/global/util/RandomStringUtil.java (1)
  • RandomStringUtil (5-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (6)
backend/src/main/java/moadong/media/service/CloudflareImageService.java (6)

77-105: 검증 로직과 트랜잭션 처리가 올바릅니다.

새로운 URL 리스트에 대한 검증을 먼저 수행한 후 기존 파일을 삭제하는 순서가 적절합니다. 또한 validateFileConstraints에서 clubId와 fileType을 검증하여 다른 클럽의 파일 URL을 사용하는 것을 방지합니다.


122-147: 개선된 에러 핸들링이 적절합니다.

정리(cleanup) 작업에서 예외를 던지는 대신 경고 로그를 남기고 계속 진행하는 것은 올바른 접근입니다. null/empty 체크와 S3 예외 처리가 적절하게 구현되었습니다.


188-216: 업로드 완료 메서드가 올바르게 구현되었습니다.

validateFileConstraints에서 clubId와 fileType 경로 검증을 수행하여(lines 191, 206), 다른 클럽에서 발급받은 URL을 사용하는 것을 방지합니다. 트랜잭션과 기존 파일 삭제 로직도 적절합니다.


266-303: Presigned URL 생성 로직이 올바릅니다.

파일명의 잘못된 문자 처리, 10분 유효기간 설정, 필수 헤더 정보 제공이 적절하게 구현되었습니다. requiredHeaders에 Content-Type만 포함된 것은 Presigned URL 서명에 Content-Type이 포함되어 클라이언트가 반드시 동일한 Content-Type으로 업로드해야 하기 때문에 올바른 구현입니다.


305-312: LGTM!

파일명과 확장자 검증이 명확하고 올바르게 구현되었습니다.


314-319: LGTM!

ClubRecruitmentInformation null 체크와 에러 로깅이 적절합니다.

Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

dto 검증 부분만 필수적으로 추가해주시면 좋겠습니다!
또한 complete 요청을 보내지않으면 고아파일들이 생길꺼같아 그부분도 나중에 같이 고민해보면 좋겠습니다!

@PostConstruct
private void init() {
// viewEndpoint 정규화: 후행 슬래시 제거
normalizedViewEndpoint = viewEndpoint != null ? viewEndpoint.replaceAll("/+$", "") : "";
Copy link
Contributor

Choose a reason for hiding this comment

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

endpoint를 왜 정규화하시는걸까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

url의 //오류 또는 /의 누락 때문에 있는게 좋아보여서 썼습니당 굳이 필요없고 더 햇갈리기만 할까여?

Comment on lines +342 to 347
private void validateClubRecruitmentInformation(Club club) {
if (club.getClubRecruitmentInformation() == null) {
log.error("ClubRecruitmentInformation is null for club: {}", club.getId());
throw new RestApiException(ErrorCode.CLUB_INFORMATION_NOT_FOUND);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

검증하는 방어적 기법좋아보입니다! 근데 모집정보가 null일 경우가 존재하나요?

Copy link
Contributor

Choose a reason for hiding this comment

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

실제 club엔티티가 처음 생성할때 clubRecruitmentInformation을 초기화하는걸 볼 수 있습니다!

    public Club() {
        this.name = "";
        this.category = "";
        this.division = "";
        this.state = ClubState.UNAVAILABLE;
        this.clubRecruitmentInformation = ClubRecruitmentInformation.builder().build();
    }


// Presigned URL 생성 (10분 유효)
PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10))
Copy link
Contributor

Choose a reason for hiding this comment

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

Duration 상수화나 프로퍼티로 빼는것도 좋아보이네요

Comment on lines 297 to 303
if (containsInvalidChars(fileName)) {
fileName = RandomStringUtil.generateRandomString(10);
}
if (file.getSize() > MAX_SIZE) {
try {
file = resizeImage(file, MAX_SIZE);
} catch (IOException e) {
throw new RestApiException(ErrorCode.FILE_TRANSFER_ERROR);
String extension = "";
if (fileName.contains(".")) {
extension = fileName.substring(fileName.lastIndexOf("."));
}
processedFileName = RandomStringUtil.generateRandomString(10) + extension;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

왜 containsInvalidChars일때만 이름을 변경하는것일까요? 시큐어 코딩관점에선 파일을 저장할때 파일명을 난수로 저장하는것이 안전해서 무조건 파일이름을 변경하는게 좋을꺼같네요

// Presigned URL 방식 엔드포인트
@PostMapping("/{clubId}/logo/upload-url")
@Operation(summary = "로고 이미지 업로드 URL 생성", description = "로고 이미지 업로드를 위한 Presigned URL을 생성합니다.")
public ResponseEntity<?> generateLogoUploadUrl(@PathVariable String clubId,
Copy link
Contributor

Choose a reason for hiding this comment

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

현재 이미지 업로드 URL 생성이 단일 파일이름만 받아 업로드를 구현하는 것 같은데, 다중업로드를 위해 한개의 file이름을 받는것보다 리스트형태로 받을 수 있으면 좋지않을까요?

Copy link
Collaborator

@alsdddk alsdddk left a comment

Choose a reason for hiding this comment

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

고생하셨어요!! 다중업로드가 필요한 피드에 presigned url을 적용해서 서버 부담을 줄이는 구조가 넘넘 좋은 거 같아요. 많이 배웠어요👍
다른 분이 언급하신 부분 반영해서 수정하면 될 거 같아요~!

Copy link
Collaborator

Choose a reason for hiding this comment

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

추후에 다중업로드가 지원되는 피드만큼은 정상/예외 케이스를 검증하는 단위테스트 작성해보는 것도 좋을 거 같네용

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵 알겠습니당

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

Caution

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

⚠️ Outside diff range comments (1)
backend/src/main/java/moadong/media/controller/ClubImageController.java (1)

27-28: 클럽 이미지 작업에 대한 소유권/관리자 권한 검증이 완전히 누락되어 있습니다.

검증 결과, 서비스 레이어(CloudflareImageService)의 모든 이미지 작업 메서드들이 clubId 파라미터만 받고 현재 사용자 정보를 포함하지 않습니다. 결과적으로:

  • deleteLogo(), deleteCover(), updateFeeds(), completeLogoUpload(), completeCoverUpload() 등 모든 메서드에서 소유권 검증이 없습니다
  • 인증된 모든 사용자가 다른 클럽의 이미지를 삭제/수정할 수 있습니다 (예: 사용자 A가 /api/club/{clubB_id}/logo DELETE 호출 시 실행됨)

서비스 메서드는 현재 사용자 컨텍스트에 접근할 수 없으므로, 컨트롤러 레벨에서 명시적으로 현재 사용자를 받아 소유권을 검증한 후 서비스를 호출하거나, @PreAuthorize에 커스텀 SpEL 표현식으로 역할/소유권 검증을 추가해야 합니다.

🧹 Nitpick comments (4)
backend/src/main/java/moadong/media/controller/ClubImageController.java (1)

83-83: 더 이상 사용하지 않는 API에 대한 주석을 정리하세요.

주석에 "feed complete API는 더 이상 사용하지 않습니다"라고 명시되어 있습니다. 대신 updateFeeds에서 검증한다고 하는데, 이는 워크플로우를 이해하는 데 도움이 됩니다.

더 명확하게 하려면:

  1. 왜 별도의 complete API가 필요 없는지 (피드는 여러 개를 한 번에 업데이트하므로)
  2. updateFeeds 엔드포인트와의 관계

를 주석에 추가하는 것을 권장합니다.

backend/src/main/java/moadong/media/service/CloudflareImageService.java (3)

173-203: 배치 URL 생성 로직의 에러 처리 전략을 명확히 하세요.

현재 구현에서 일부 요청이 실패해도 성공한 요청의 URL은 반환하고, 초과된 요청에 대해서는 에러 응답을 추가합니다. 이는 부분 성공(partial success) 패턴입니다.

고려 사항:

  1. 클라이언트가 success=false인 응답을 어떻게 처리해야 하는지 API 문서에 명시 필요
  2. 일부 URL 생성 실패 시 클라이언트의 재시도 전략 고려
  3. limit 계산 후 나머지 요청에 대해 단일 에러 응답을 추가하는 방식이 적절한지 검토 (각 요청별로 개별 에러를 반환하는 것이 더 명확할 수 있음)

82-110: 동시성 제어에 대한 문서화를 개선하세요.

과거 리뷰에서 "트랜잭션 내에서 최신 데이터를 다시 조회하여 Race Condition 방지" 주석에 대한 지적이 있었습니다. 실제 동시성 보호는 Club 엔티티의 @Version 필드를 통한 낙관적 잠금으로 이루어집니다.

현재 코드에는 관련 주석이 없는데, 다음을 고려하세요:

  1. @Version 낙관적 잠금이 동시 수정을 방어한다는 점을 메서드 주석에 명시
  2. OptimisticLockingFailureException 발생 시 클라이언트 처리 방법 문서화
  3. 클라이언트가 동시 수정 실패 시 업로드된 파일 정리 필요성 안내

Based on past review comments


336-341: Null 체크는 방어적 코딩이지만 실제로는 불필요합니다.

과거 리뷰 코멘트에서 지적된 바와 같이, Club 엔티티의 생성자가 항상 clubRecruitmentInformation을 초기화하므로 이 필드가 null일 수 없습니다.

이 검증은 방어적 코딩으로 남겨두어도 무방하지만, 실제로 도달 불가능한 코드임을 인지하고 있어야 합니다. 로그만 남기고 예외를 던지지 않거나, 주석으로 "방어적 검증"임을 명시하는 것을 고려하세요.

Based on learnings

📜 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 33cb954 and 8759bf2.

📒 Files selected for processing (7)
  • backend/src/main/java/moadong/media/controller/ClubImageController.java (2 hunks)
  • backend/src/main/java/moadong/media/dto/FeedUpdateRequest.java (1 hunks)
  • backend/src/main/java/moadong/media/dto/PresignedUploadResponse.java (1 hunks)
  • backend/src/main/java/moadong/media/dto/UploadCompleteRequest.java (1 hunks)
  • backend/src/main/java/moadong/media/dto/UploadUrlRequest.java (1 hunks)
  • backend/src/main/java/moadong/media/service/CloudflareImageService.java (4 hunks)
  • backend/src/main/java/moadong/media/service/ClubImageService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/src/main/java/moadong/media/service/ClubImageService.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
Repo: Moadong/moadong PR: 406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.

Applied to files:

  • backend/src/main/java/moadong/media/service/CloudflareImageService.java
📚 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:

  • backend/src/main/java/moadong/media/service/CloudflareImageService.java
🧬 Code graph analysis (2)
backend/src/main/java/moadong/media/controller/ClubImageController.java (1)
backend/src/main/java/moadong/user/controller/UserController.java (1)
  • RestController (34-120)
backend/src/main/java/moadong/media/service/CloudflareImageService.java (4)
backend/src/main/java/moadong/club/entity/Club.java (1)
  • Slf4j (28-153)
backend/src/main/java/moadong/global/util/ObjectIdConverter.java (1)
  • ObjectIdConverter (7-17)
backend/src/main/java/moadong/global/util/RandomStringUtil.java (1)
  • RandomStringUtil (5-22)
frontend/src/apis/updateFeedImages.ts (1)
  • updateFeedImages (4-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (12)
backend/src/main/java/moadong/media/dto/FeedUpdateRequest.java (1)

3-9: 유효성 검증 추가가 적절합니다.

@NotNull 제약 조건 추가로 feeds 필드의 null 안전성이 보장되며, 컨트롤러의 @Valid 어노테이션과 잘 연동됩니다.

backend/src/main/java/moadong/media/dto/UploadCompleteRequest.java (1)

1-9: 업로드 완료 DTO 구현이 적절합니다.

@NotBlank 제약 조건으로 fileUrl의 유효성을 보장하며, presigned URL 워크플로우의 완료 단계에 적합한 간결한 구조입니다.

backend/src/main/java/moadong/media/dto/PresignedUploadResponse.java (1)

5-11: 응답 구조가 성공/실패 케이스를 모두 처리합니다.

성공 시 presignedUrl, finalUrl, requiredHeaders를, 실패 시 success=falsefailureReason을 반환하는 구조가 명확합니다. 클라이언트가 success 플래그로 분기 처리할 수 있어 사용성이 좋습니다.

backend/src/main/java/moadong/media/controller/ClubImageController.java (1)

75-81: 다중 피드 업로드를 위한 배치 엔드포인트가 잘 구현되었습니다.

List<UploadUrlRequest>를 받아 여러 파일의 업로드 URL을 한 번에 생성하는 방식이 효율적입니다. 과거 리뷰 코멘트에서 제안된 다중 업로드 지원이 적절히 반영되었습니다.

backend/src/main/java/moadong/media/service/CloudflareImageService.java (7)

60-67: viewEndpoint 검증이 적절히 구현되었습니다.

과거 리뷰 코멘트에서 제기된 viewEndpoint null 처리 문제가 해결되었습니다. @PostConstruct에서 필수 설정값을 명시적으로 검증하고, 누락 시 IllegalStateException을 발생시켜 애플리케이션 시작 시점에 설정 오류를 조기 발견할 수 있습니다.

Based on past review comments


289-294: 파일명 무작위화로 보안이 강화되었습니다.

RandomStringUtil.generateRandomString(10)을 사용해 파일명을 무작위로 생성하는 것은 시큐어 코딩 관점에서 적절합니다. 원본 파일명에 포함될 수 있는 특수문자나 경로 탐색 공격을 방지합니다.

Based on past review comments


211-224: 트랜잭션 처리와 파일 정리가 적절합니다.

업로드 완료 메서드가 다음을 올바르게 수행합니다:

  1. validateFileConstraints로 파일 유효성 검증 (clubId 및 파일 타입 경로 포함)
  2. 기존 파일 삭제로 스토리지 누수 방지
  3. @Transactional 보장으로 DB 일관성 유지

과거 리뷰 코멘트에서 제기된 클럽별 파일 경로 검증 문제가 validateFileConstraints에서 해결되었습니다.

Based on past review comments


242-275: 파일 제약 검증이 포괄적으로 구현되었습니다.

검증 로직이 다음을 모두 확인합니다:

  1. URL 길이 제한
  2. clubId와 fileType 경로 검증 (무단 접근 방지)
  3. R2에서 실제 파일 존재 및 크기 확인
  4. 크기 초과 시 자동 삭제

과거 리뷰 코멘트에서 제기된 클럽별 경로 검증 이슈가 해결되었습니다 (Line 247).

성능 고려사항: HeadObjectRequest는 업로드 완료 단계에서 추가 네트워크 호출을 발생시킵니다. 현재는 보안과 무결성을 우선하는 트레이드오프로 보이나, 대량 업로드 시 지연이 발생할 수 있습니다. 필요 시 비동기 처리나 배치 검증을 고려하세요.

Based on past review comments


126-152: 파일 삭제 메서드의 에러 처리가 견고합니다.

null/빈 경로, 잘못된 형식, S3 예외를 모두 graceful하게 처리하고 로그를 남깁니다. 삭제 실패가 전체 트랜잭션을 롤백하지 않도록 예외를 잡아서 처리하는 것이 적절합니다.

이는 파일이 이미 삭제되었거나 권한 문제가 있어도 DB 상태 업데이트는 계속 진행되도록 하여 시스템의 복원력을 높입니다.


53-58: 설정값이 적절히 외부화되었습니다.

과거 리뷰 코멘트에서 제안된 Duration 상수화/프로퍼티 외부화가 @Value 어노테이션을 통해 구현되었습니다. 기본값도 제공되어 있어 설정 누락 시 안전합니다.

Based on past review comments


317-319: Review comment is incorrect — R2 presigned URLs only require headers that were signed.

R2 presigned URLs require only the exact headers that were included when the URL was signed; if only Content-Type was specified during signing, no additional headers like x-amz-* are required.

The code shows PutObjectRequest.builder().contentType(contentType) — meaning only Content-Type is signed. The current requiredHeaders implementation correctly returns only Content-Type, which matches what was signed. No additional headers are needed.

The review's assumption that x-amz-* headers are required appears to be based on AWS S3 conventions, which differ from Cloudflare R2's presigned URL behavior.

Likely an incorrect or invalid review comment.

backend/src/main/java/moadong/media/dto/UploadUrlRequest.java (1)

10-11: 정규식 패턴 수정이 필요합니다. image/jpg는 표준이 아닙니다.

표준 MIME 타입은 image/jpeg이며 image/jpg는 존재하지 않습니다. 코드에서 검증된 contentType이 그대로 S3/Cloudflare API로 전달되므로, 일부 검증 프로세스는 image/jpeg만 명시적으로 확인하여 image/jpg를 거부할 수 있습니다.

정규식에서 image/jpg를 제거하고 image/jpeg만 허용하거나, 클라이언트가 image/jpg를 보내는 경우 백엔드에서 image/jpeg으로 정규화하는 방식을 고려하세요.

Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~~ 반영잘해주셨네요

@yw6938 yw6938 merged commit d1bc8fe into develop/be Nov 19, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💾 BE Backend 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants