Skip to content

[REFACTOR] Presigned URL API 공용으로 분리#42

Merged
Jong0128 merged 6 commits into
devfrom
refactor/#36-presigned-url-seperation
Jun 21, 2026
Merged

[REFACTOR] Presigned URL API 공용으로 분리#42
Jong0128 merged 6 commits into
devfrom
refactor/#36-presigned-url-seperation

Conversation

@Jong0128

@Jong0128 Jong0128 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

🚀 Related issue

Closes #36

#️⃣ Summary

  • 외주·포트폴리오에 중복돼 있던 파일 업로드(presigned URL 발급 + 키 검증) 로직을 공용 S3 모듈로 통합

🔧 Changes

  • presigned URL 발급을 공용 엔드포인트 POST /api/v1/files/presigned-url로 이전
  • 업로드 종류별 정책(디렉토리/버킷/허용 타입)을 UploadTarget enum으로 생성
  • 파일 키 검증(중복, 형식, 존재, 크기)을 S3FileService.validateUploadedKeys로 공용화
  • commission, portfolio가 공용 로직에 위임하도록 변경 + 중복/미사용 에러코드 정리

📸 Test Evidence

💬 Reviewer Notes

  • 외주 presign 경로 및 바디 변경 (/instructors/commissions/files/presigned-url/files/presigned-url, fileKindtarget)
  • 포트폴리오 presign 엔드포인트는 이메일 인증 때문에 designer auth에 그대로 두고 로직만 공용화

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced unified file upload API endpoint for S3 presigned URL issuance supporting multiple upload targets.
  • Refactor

    • Centralized file upload validation logic and reorganized S3 module structure.
    • Migrated presigned URL generation from domain-specific controllers to shared service layer.

@Jong0128 Jong0128 requested a review from fervovita as a code owner June 19, 2026 15:37
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Jong0128, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 35 minutes and 45 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 145fea2d-b77f-40a3-b382-d91218ab5a08

📥 Commits

Reviewing files that changed from the base of the PR and between 3242fac and 0c028cd.

📒 Files selected for processing (2)
  • src/main/java/ditda/backend/domain/commission/core/controller/InstructorCommissionController.java
  • src/main/java/ditda/backend/domain/commission/core/facade/InstructorCommissionFacade.java
📝 Walkthrough

Walkthrough

도메인별(CommissionController, PortfolioService 등)로 분산되어 있던 S3 presigned URL 발급·파일 검증 로직을 전역 S3FileServiceS3FileController로 통합합니다. UploadTarget enum으로 업로드 대상을 중앙화하고, S3 관련 클래스들을 manager/config/dto 하위 패키지로 재구성합니다.

Changes

S3 Presigned URL 공용화 리팩토링

Layer / File(s) Summary
전역 S3 계약 및 에러 코드 정의
global/s3/enums/UploadTarget.java, global/s3/exception/S3ErrorCode.java, global/s3/dto/request/PresignRequest.java, global/s3/dto/PresignedUpload.java, global/s3/dto/response/PresignResponse.java, global/apipayload/code/GeneralErrorCode.java
UploadTarget enum으로 업로드 대상별 S3 경로·버킷·허용 타입을 정의하고, S3ErrorCode 신규 추가, 공용 PresignRequest/PresignResponse DTO 도입, PresignedUpload 패키지 이동, GeneralErrorCode에서 FILE_URL_GENERATION_FAILED 제거 및 상수 순서 조정.
S3 manager 패키지 재구성 및 에러 코드 전환
global/s3/config/S3Properties.java, global/s3/manager/S3FileManager.java, global/s3/manager/S3PresignedUrlGenerator.java, global/s3/manager/S3UploadManager.java, global/s3/manager/S3UrlResolver.java
S3Properties, S3FileManager, S3PresignedUrlGenerator, S3UploadManager, S3UrlResolverditda.backend.global.s3.manager/config 하위 패키지로 이동하고, S3PresignedUrlGenerator의 예외 코드를 GeneralErrorCode에서 S3ErrorCode로 교체.
S3FileService 공용 서비스 구현
global/s3/service/S3FileService.java
S3FileService 신규 도입. issuePresignedUpload에서 contentType 검증 후 S3UploadManager로 presigned URL 발급 위임, validateUploadedKeys에서 중복·temp key 형식·객체 크기 검증 수행.
전역 S3FileController 엔드포인트 추가
global/s3/controller/S3FileController.java
/api/v1/files/presigned-url POST 엔드포인트 신규 추가. PresignRequest 검증 후 S3FileService.issuePresignedUpload 결과를 PresignResponse로 변환해 ApiResponse로 반환.
도메인별 presigned URL 코드 제거 및 위임
commission/core/controller/CommissionController.java, commission/core/facade/CommissionFacade.java, commission/core/service/CommissionCreateFileService.java, commission/core/exception/CommissionErrorCode.java, designer/service/PortfolioService.java, designer/exception/DesignerErrorCode.java
CommissionController/CommissionFacade에서 issueFilePresignedUrls 엔드포인트 삭제. CommissionCreateFileService·PortfolioService의 직접 검증을 S3FileService 위임으로 교체. CommissionErrorCode 파일 관련 상수 제거 및 400번대 코드 재매핑. DesignerErrorCode 파일 관련 에러 상수 3종 삭제.
소비자 import 경로 업데이트
auth/facade/DesignerAuthFacade.java, auth/mapper/AuthResponseMapper.java, auth/notification/DesignerSignupNotifier.java, commission/draft/mapper/DraftResponseMapper.java
S3 클래스 패키지 이동에 따라 DesignerAuthFacade·AuthResponseMapper·DesignerSignupNotifier·DraftResponseMapper의 import 경로를 새 패키지(manager, dto)로 업데이트.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant S3FileController
  participant S3FileService
  participant S3UploadManager

  rect rgba(70, 130, 180, 0.5)
    Note over Client,S3UploadManager: Presigned URL 발급 흐름
    Client->>S3FileController: POST /api/v1/files/presigned-url (PresignRequest)
    S3FileController->>S3FileService: issuePresignedUpload(target, contentType)
    S3FileService->>S3FileService: S3ContentType 변환 및 UploadTarget.allowed 검증
    alt 허용되지 않는 contentType
      S3FileService-->>S3FileController: GeneralException(UNSUPPORTED_CONTENT_TYPE)
      S3FileController-->>Client: 400 에러 응답
    else 허용됨
      S3FileService->>S3UploadManager: issueTempUpload(dir, contentType)
      S3UploadManager-->>S3FileService: PresignedUpload(key, presignedUrl)
      S3FileService-->>S3FileController: PresignedUpload
      S3FileController-->>Client: ApiResponse(PresignResponse)
    end
  end

  rect rgba(60, 179, 113, 0.5)
    Note over Client,S3UploadManager: 업로드 키 검증 흐름 (PortfolioService / CommissionCreateFileService)
    Client->>S3FileService: validateUploadedKeys(target, keys)
    S3FileService->>S3FileService: null / 중복 키 검사
    loop 각 key
      S3FileService->>S3UploadManager: isTempKey(key)
      S3FileService->>S3UploadManager: getObjectSize(key)
      alt 객체 없음 또는 크기 초과
        S3FileService-->>Client: GeneralException(INVALID_FILE / FILE_SIZE_EXCEEDED)
      end
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Ditda-Official/Ditda-Backend#31: 포트폴리오 업로드용 PresignedUpload/S3UploadManager presigned PUT 업로드 흐름을 도입했으며, 본 PR에서 S3FileService로 해당 로직을 중앙화함.
  • Ditda-Official/Ditda-Backend#33: PortfolioService.generatePresignedUpload/validateKeys API 표면을 수정했으며, 본 PR에서 동일 메서드를 S3FileService 위임 방식으로 교체함.
  • Ditda-Official/Ditda-Backend#34: 커미션 전용 presigned URL 엔드포인트(CommissionController, CommissionFacade, CommissionFilePresignRequest/Response)를 도입했으며, 본 PR에서 해당 구현을 전역 S3FileController/S3FileService로 대체하며 삭제함.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경사항(Presigned URL API 공용 분리)을 명확하고 간결하게 요약하고 있습니다.
Linked Issues check ✅ Passed 모든 코딩 요구사항이 충족되었습니다. Presigned URL 엔드포인트가 /instructors에서 /api/v1/files/presigned-url로 이동되었고, 중복 로직이 S3FileService로 통합되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 PR 목표와 관련이 있습니다. 패키지 재조직, 불필요한 에러 코드 제거 등은 모두 리팩토링 목표를 지원합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/#36-presigned-url-seperation

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/ditda/backend/global/s3/enums/UploadTarget.java`:
- Around line 13-19: The `allowed` field is exposing a mutable EnumSet directly,
which allows external code to modify the upload policy at runtime. Locate the
getter method (likely `getAllowed()`) that returns the `allowed` field and
modify it to return an unmodifiable/immutable view of the set instead of the raw
collection. Use a method like `Collections.unmodifiableSet()` to wrap the
EnumSet before returning it, ensuring the upload policy cannot be tampered with
at runtime.

In `@src/main/java/ditda/backend/global/s3/S3FileService.java`:
- Around line 38-53: The validateUploadedKeys method does not handle null or
blank input values, which can cause NullPointerException instead of a proper
client error response. Add null checks at the beginning of the
validateUploadedKeys method to validate that the keys parameter itself is not
null, and add validation to ensure no individual elements in the keys list are
null or blank strings. For any of these cases, throw a GeneralException with
GeneralErrorCode.INVALID_FILE before proceeding with the existing validation
logic. This ensures null inputs are explicitly rejected as invalid file requests
rather than causing internal errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: f3517650-dea3-4b71-80fa-faa8b74d96e1

📥 Commits

Reviewing files that changed from the base of the PR and between 382f6d0 and e8539a0.

📒 Files selected for processing (14)
  • src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java
  • src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionFilePresignRequest.java
  • src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java
  • src/main/java/ditda/backend/domain/commission/core/exception/CommissionErrorCode.java
  • src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java
  • src/main/java/ditda/backend/domain/commission/core/service/CommissionCreateFileService.java
  • src/main/java/ditda/backend/domain/designer/exception/DesignerErrorCode.java
  • src/main/java/ditda/backend/domain/designer/service/PortfolioService.java
  • src/main/java/ditda/backend/global/apipayload/code/GeneralErrorCode.java
  • src/main/java/ditda/backend/global/s3/S3FileService.java
  • src/main/java/ditda/backend/global/s3/controller/S3FileController.java
  • src/main/java/ditda/backend/global/s3/dto/request/PresignRequest.java
  • src/main/java/ditda/backend/global/s3/dto/response/PresignResponse.java
  • src/main/java/ditda/backend/global/s3/enums/UploadTarget.java
💤 Files with no reviewable changes (4)
  • src/main/java/ditda/backend/domain/commission/core/dto/response/CommissionFilePresignResponse.java
  • src/main/java/ditda/backend/domain/commission/core/dto/request/CommissionFilePresignRequest.java
  • src/main/java/ditda/backend/domain/commission/core/controller/CommissionController.java
  • src/main/java/ditda/backend/domain/commission/core/facade/CommissionFacade.java

Comment thread src/main/java/ditda/backend/global/s3/enums/UploadTarget.java
Comment thread src/main/java/ditda/backend/global/s3/service/S3FileService.java
@Jong0128 Jong0128 self-assigned this Jun 19, 2026

@fervovita fervovita left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

s3 패키지에 폴더가 많아져서 너무 flat한 느낌이 들어서, 아래와 같이 변경하는 거는 어떨까요....??

global/s3/
├── controller/        S3FileController
├── enums/             BucketType, S3ContentType, UploadTarget
├── dto/
│   ├── request/        PresignRequest
│   └── response/    PresignResponse, PresignedUpload  
├── service/             S3FileService
├── exception/         S3ErrorCode
├── manager/           S3UploadManager
│                                     S3FileManager
│                                     S3PresignedUrlGenerator
│                                     S3UrlResolver
└──  config/             S3Properties

(대충 예시입니다!)

확인해보시고 merge 하셔도 될 것 같아요😄

Comment on lines +22 to +25
// 파일 형식 검증
S3ContentType type = S3ContentType.from(contentType);
if (type == null || !target.getAllowed().contains(type)) {
throw new GeneralException(GeneralErrorCode.UNSUPPORTED_CONTENT_TYPE);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

UNSUPPORTED_CONTENT_TYPE에러는 API 요청 헤더의 Content-Type을 잘못 보냈을 때 터지는 에러인데, 여기서 같이 사용하게 되면 추후 에러 트래킹시 정확히 에러가 터지는 지점을 파악하기 어려울 것 같습니다.

제 생각에는 s3에도 S3ErrorCode와 같이 정의해서 따로 에러 코드를 관리하는 것이 좋을 것 같아요!

@Jong0128 Jong0128 merged commit e3ea69a into dev Jun 21, 2026
2 checks passed
@Jong0128 Jong0128 deleted the refactor/#36-presigned-url-seperation branch June 21, 2026 12:27
@fervovita fervovita mentioned this pull request Jun 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] Presigned URL API 공용으로 분리

2 participants