Skip to content

Conversation

@dahyun24
Copy link
Contributor

@dahyun24 dahyun24 commented Oct 24, 2025

🔗 연관된 이슈

🚀 변경 유형

  • ✨ 기능 추가 (feature)
  • 🐛 버그 수정 (fix)
  • 📝 문서 변경 (docs)
  • ♻️ 리팩토링 (refactor)
  • 🧪 테스트 추가 / 수정 (test)
  • ⚙️ 설정 변경 (chore)

📝 작업 내용

  • presigned-url 발급 로직 구현
    • 모두 다 성공을 예상하고 count 증가하는 로직으로 구현했습니다.
  • 업로드 후 결과를 보고 받는 api 구현
    • 실패한 업로드에 대하여 count 롤백을 진행합니다.
    • 일단, FAILED로 상태 변경 후에 DB에서 삭제는 진행하지 않았습니다.(추후 변경 가능)

📸 스크린샷

  • presigned-url 발급 성공
    스크린샷 2025-10-24 오후 5 26 28

  • 최대 사진 개수 초과
    스크린샷 2025-10-24 오후 1 42 38

  • 6MB 파일 크기 초과
    스크린샷 2025-10-24 오후 1 43 14

  • 파일명 누락
    스크린샷 2025-10-24 오후 1 44 21

  • 이미지 형식 불일치
    스크린샷 2025-10-24 오후 1 44 40

  • 빈 업로드 파일 목록
    스크린샷 2025-10-24 오후 1 46 11

  • report 성공
    스크린샷 2025-10-24 오후 5 30 16

  • 존재하지 않는 ID
    스크린샷 2025-10-24 오후 4 50 39

  • 소유자 불일치
    스크린샷 2025-10-24 오후 4 51 15

  • 동일 앨범 아님
    스크린샷 2025-10-24 오후 4 51 58

💬 리뷰 요구사항

  • FAILED 처리를 어떻게 해야할지 고민이 더 필요할 것 같습니다.
  • s3 접근을 위한 yml 파일 노션에 수정했슴다

Summary by CodeRabbit

  • New Features

    • Presigned URL 기반 사진 업로드 워크플로 도입(업로드 URL 발급 및 업로드 결과 보고)
    • 사진 상태 추적 도입(UPLOADING → PROCESSING → COMPLETED/FAILED)
    • 업로드 파일 유효성 검사 및 앨범 참가자 권한 확인 강화(부분 성공/실패 보고 및 충돌 처리)
    • 앨범 사진 수의 원자적 증감 지원 및 업로드 관련 응답/오류 코드 추가
    • 관련 REST API 및 OpenAPI 문서 추가
  • Chores

    • S3 연동 지원 추가(클라이언트·서명자 구성 및 AWS SDK 의존성 추가)

@coderabbitai
Copy link

coderabbitai bot commented Oct 24, 2025

Walkthrough

S3 SDK 의존성 추가, S3Client·S3Presigner 빈 등록, 프리사인드 URL 발급 서비스 및 사진 업로드 워크플로우(요청 검증·엔티티 생성·상태 전이·앨범 카운트 증감·업로드 결과 보고)와 관련 검증·예외·DTO·매퍼·저장소 쿼리·컨트롤러가 추가되었습니다.

Changes

Cohort / File(s) Summary
빌드 설정
\build.gradle``
AWS SDK 의존성 software.amazon.awssdk:s3:2.25.46, software.amazon.awssdk:auth:2.25.46, software.amazon.awssdk:regions:2.25.46 추가
S3 설정 및 프리사이너
src/main/java/com/cheeeese/global/config/S3Config.java
S3Client·S3Presigner 빈 등록(리전·엔드포인트·정적 크리덴셜) 추가
Presigned URL 서비스
src/main/java/com/cheeeese/photo/application/PresignedUrlService.java
S3Presigner로 PUT용 presigned URL 생성 메서드(generatePresignedPutUrl) 추가
Photo 서비스
src/main/java/com/cheeeese/photo/application/PhotoService.java
createPresignedUrlsreportUploadResult 엔드투엔드 로직(검증·Photo 엔티티 저장·상태 전이·앨범 카운트 조정·프리사인드 생성·결과 처리) 추가
검증 계층
src/main/java/com/cheeeese/photo/application/validator/PhotoValidator.java, src/main/java/com/cheeeese/album/application/validator/AlbumValidator.java
파일 메타·업로드 수 제한 검증, 사진 소유·단일 앨범 검증, 업로드 권한(앨범 참가자 여부) 검증 추가
도메인 변경
src/main/java/com/cheeeese/photo/domain/Photo.java, src/main/java/com/cheeeese/photo/domain/PhotoStatus.java
Photo.status(UPLOADING, PROCESSING, COMPLETED, FAILED) 필드 추가, imageUrl nullable 전환 및 updateImageUrl 메서드 추가
저장소 쿼리
src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoRepository.java, src/main/java/com/cheeeese/album/infrastructure/persistence/AlbumRepository.java
사진 상태 일괄 업데이트 JPQL 쿼리 추가(updateStatusByIdsAndUserIdAndExpectedStatus), 앨범 currentPhotoCount 증감 원자 쿼리 추가(incrementPhotoCount, decrementPhotoCount)
DTO / 매퍼
src/main/java/com/cheeeese/photo/dto/request/..., src/main/java/com/cheeeese/photo/dto/response/..., src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java
presigned URL 요청·응답 및 업로드 결과 DTO 추가, Photo 엔티티 변환 및 presigned 응답 매퍼 구현
예외·코드·응답
src/main/java/com/cheeeese/photo/exception/..., src/main/java/com/cheeeese/album/exception/code/AlbumErrorCode.java, src/main/java/com/cheeeese/global/common/code/SuccessCode.java
PhotoErrorCode·PhotoException 추가, AlbumErrorCodeUSER_NOT_PARTICIPANT 추가, 성공 코드 PRESIGNED_URL_ISSUE_SUCCESS, PHOTO_UPLOAD_REPORT_SUCCESS 추가
프레젠테이션·스웨거
src/main/java/com/cheeeese/photo/presentation/PhotoController.java, src/main/java/com/cheeeese/photo/presentation/swagger/PhotoSwagger.java
POST /v1/photo/presigned-url, POST /v1/photo/report 컨트롤러 및 OpenAPI 정의 추가

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant PhotoService
    participant AlbumValidator
    participant PhotoValidator
    participant PhotoRepo
    participant AlbumRepo
    participant PresignedUrlService
    participant S3

    rect rgb(242,248,255)
    Note over Client,Controller: 프리사인드 URL 생성 (요청→검증→엔티티 저장→카운트 증가→프리사인드 발급)
    Client->>Controller: POST /v1/photo/presigned-url
    Controller->>PhotoService: createPresignedUrls(user, req)
    PhotoService->>AlbumValidator: validateUploadPermission(album, user)
    PhotoService->>PhotoValidator: validateFileInfos(...)
    PhotoService->>PhotoRepo: save(Photo status=UPLOADING) [N]
    PhotoService->>AlbumRepo: incrementPhotoCount(albumId, N)
    loop per file
      PhotoService->>PresignedUrlService: generatePresignedPutUrl(key, contentType)
      PresignedUrlService->>S3: presign PUT (10m)
      S3-->>PresignedUrlService: presigned URL
    end
    PhotoService-->>Controller: PhotoPresignedUrlResponse
    Controller-->>Client: 200 OK
    end

    rect rgb(242,255,242)
    Note over Client,Controller: 업로드 결과 보고 (성공/실패 업데이트·롤백)
    Client->>Controller: POST /v1/photo/report
    Controller->>PhotoService: reportUploadResult(user, req)
    PhotoService->>PhotoValidator: validatePhotos(userId, ids)
    PhotoService->>PhotoRepo: updateStatusByIds(successIds, UPLOADING→PROCESSING)
    PhotoService->>PhotoRepo: updateStatusByIds(failureIds, UPLOADING→FAILED)
    PhotoService->>AlbumRepo: decrementPhotoCount(albumId, failureCount)
    PhotoService-->>Controller: void
    Controller-->>Client: 200 OK
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • 집중 검토가 권장되는 파일/영역:
    • PhotoService (트랜잭션 경계, 상태 전이, 동시성·원자성 처리)
    • AlbumRepository/PhotoRepository의 JPQL 수정 쿼리와 @Modifying 설정
    • S3ConfigPresignedUrlService (자격증명·엔드포인트·리전 설정)
    • PhotoValidator의 소유권·동일 앨범 검증 로직 및 예외 처리

Possibly related issues

Possibly related PRs

Suggested reviewers

  • zyovn

Poem

🐰 구름 위에 프리사인드 한 줌 들고,
키를 찍어 달려가면 업로드가 춤추네.
UPLOADING은 PROCESSING으로, 실패는 FAILED로,
카운트는 조심히 늘고 줄어들며 반짝이네.
치즈 냄새 나는 배포를 껴안고 기뻐하노라.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목 "feat: 사진 presignedUrl 발급 및 업로드 보고 로직 구현"은 변경사항의 핵심을 정확하게 반영합니다. 변경사항 전체가 두 가지 주요 기능 중심으로 구성되어 있습니다: (1) presigned URL 생성 기능(S3Config, PresignedUrlService, PhotoService.createPresignedUrls 등)과 (2) 업로드 결과 보고 기능(PhotoService.reportUploadResult, 상태 관리, 검증 로직 등). 제목은 간결하고 구체적이며, 팀원이 이력을 스캔할 때 변경사항의 의도를 명확하게 이해할 수 있습니다.
✨ 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 feat/#16-photo-upload

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c8f33d and c07a39f.

📒 Files selected for processing (1)
  • src/main/java/com/cheeeese/global/common/code/SuccessCode.java (1 hunks)
🔇 Additional comments (1)
src/main/java/com/cheeeese/global/common/code/SuccessCode.java (1)

27-30: LGTM!

사진 업로드 기능을 위한 두 개의 성공 코드가 적절하게 추가되었습니다. 네이밍 컨벤션, 메시지 형식, 그리고 코드 구조가 기존 패턴과 일관성 있게 작성되었습니다.


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

@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

🧹 Nitpick comments (17)
src/main/java/com/cheeeese/album/application/validator/AlbumValidator.java (1)

71-78: 코드 중복을 줄이기 위한 리팩토링을 권장합니다.

validateUploadPermission 메서드는 validateAlbumEntry 메서드(lines 63-69)와 동일한 검증 로직(만료 확인, 블랙리스트 확인)을 중복 구현하고 있습니다.

다음 중 하나의 방식으로 리팩토링을 고려해보세요:

방법 1: validateAlbumEntry를 재사용

 public void validateUploadPermission(Album album, User user) {
-    validateAlbumExpiration(album);
-
-    validateUserBlacklisted(album, user);
+    validateAlbumEntry(album, user);

     albumParticipantRepository.findByUserIdAndAlbumId(user.getId(), album.getId())
             .orElseThrow(() -> new AlbumException(AlbumErrorCode.USER_NOT_PARTICIPANT));
 }

방법 2: 공통 검증 메서드 추출

private void validateBasicPermission(Album album, User user) {
    validateAlbumExpiration(album);
    validateUserBlacklisted(album, user);
}

public void validateAlbumEntry(Album album, User user) {
    validateBasicPermission(album, user);
}

public void validateUploadPermission(Album album, User user) {
    validateBasicPermission(album, user);
    albumParticipantRepository.findByUserIdAndAlbumId(user.getId(), album.getId())
            .orElseThrow(() -> new AlbumException(AlbumErrorCode.USER_NOT_PARTICIPANT));
}
src/main/java/com/cheeeese/global/config/S3Config.java (1)

17-27: 주입된 프로퍼티의 유효성 검증 추가를 권장합니다.

현재 @Value로 주입되는 설정값들에 대한 검증이 없어, 잘못된 값이 주입될 경우 런타임에 실패할 수 있습니다. 특히 region 값이 유효하지 않으면 Region.of(region)에서 예외가 발생합니다.

@PostConstruct를 사용해 빈 초기화 시점에 필수 값들을 검증하는 로직을 추가하는 것을 고려해보세요:

@PostConstruct
public void validateConfiguration() {
    if (accessKey == null || accessKey.isBlank()) {
        throw new IllegalStateException("NCP access key must be configured");
    }
    if (secretKey == null || secretKey.isBlank()) {
        throw new IllegalStateException("NCP secret key must be configured");
    }
    if (endpoint == null || endpoint.isBlank()) {
        throw new IllegalStateException("NCP endpoint must be configured");
    }
    if (region == null || region.isBlank()) {
        throw new IllegalStateException("NCP region must be configured");
    }
}
src/main/java/com/cheeeese/photo/exception/PhotoException.java (1)

7-12: @Getter 어노테이션이 불필요할 수 있습니다.

이 클래스에는 선언된 필드가 없으므로 @Getter 어노테이션이 현재로서는 효과가 없습니다. 다만 BusinessException의 상속된 필드를 위한 것이거나 향후 확장을 위한 것이라면 유지해도 무방합니다.

build.gradle (1)

58-60: AWS SDK 버전 업데이트를 권장합니다.

현재 사용 중인 AWS SDK 2.25.46 버전은 최신 버전 2.34.0에서 9개 버전 뒤쳐져 있습니다. 다행히 이 버전에서는 보고된 보안 취약점이 없으나, 버그 수정, 성능 개선, 유지보수 목적으로 최신 버전으로 업데이트할 것을 권장합니다:

implementation 'software.amazon.awssdk:s3:2.34.0'
implementation 'software.amazon.awssdk:auth:2.34.0'
implementation 'software.amazon.awssdk:regions:2.34.0'
src/main/java/com/cheeeese/photo/exception/code/PhotoErrorCode.java (1)

12-21: 업로드 결과 보고 중 충돌 케이스용 에러 코드 추가 제안

successPhotoIdsfailurePhotoIds가 겹치는 경우를 식별할 별도 에러 코드가 필요합니다(현재 서비스 레벨에서 검증·차단 권장). 아래 상수 추가를 제안드립니다.

     PHOTO_ID_NOT_FOUND(HttpStatus.NOT_FOUND, "보고된 사진 ID 중 존재하지 않는 ID가 포함되어 있습니다."),
+    PHOTO_REPORT_CONFLICTING_IDS(HttpStatus.BAD_REQUEST, "successPhotoIds와 failurePhotoIds에 중복된 ID가 포함되어 있습니다."),
     ;
src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java (1)

18-20: 시간 소스 주입으로 테스트 가능성과 일관성 개선

LocalDateTime.now() 대신 Clock 주입 후 LocalDateTime.now(clock) 사용을 권장합니다. 시간대/테스트 안정성 향상에 유리합니다.

src/main/java/com/cheeeese/photo/application/PresignedUrlService.java (1)

29-31: Presigned URL 유효기간을 설정값으로 외부화

현재 10분 하드코딩입니다. 운영/테스트 환경별 조정 위해 프로퍼티화 권장.

 @RequiredArgsConstructor
 public class PresignedUrlService {

     private final S3Presigner s3Presigner;

     @Value("${ncp.object-storage.bucket}")
     private String bucket;
+    @Value("${photo.presign.ttl-minutes:10}")
+    private long ttlMinutes;

     public String generatePresignedPutUrl(String uniqueKey, String contentType) {
         PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                 .bucket(bucket)
                 .key(uniqueKey)
                 .contentType(contentType)
                 .build();

         PresignedPutObjectRequest presignedRequest =
                 s3Presigner.presignPutObject(p -> p
-                        .signatureDuration(Duration.ofMinutes(10))
+                        .signatureDuration(Duration.ofMinutes(ttlMinutes))
                         .putObjectRequest(putObjectRequest)
                 );
src/main/java/com/cheeeese/photo/dto/request/PhotoUploadReportRequest.java (1)

10-19: 계약 명시: 두 목록의 ‘겹침 금지’와 중복 처리 방침

문서에 “success/failure는 서로 겹치면 안 됨”을 명시하고(서비스에서 검증), 중복 ID 허용 여부도 정의하세요. 현 구현은 중복 시 카운트 왜곡 위험이 큽니다.

-@Schema(description = "사진 업로드 결과 보고 요청 (부분 성공/실패 처리)")
+@Schema(description = "사진 업로드 결과 보고 요청 (부분 성공/실패 처리). " +
+        "successPhotoIds와 failurePhotoIds는 서로 겹치면 안 됩니다(겹치면 400).")
 public record PhotoUploadReportRequest(
@@
-        @Schema(description = "업로드가 성공적으로 완료된 사진 ID 목록 (UPLOADING -> PROCESSING)", example = "[100, 102]")
+        @Schema(description = "업로드가 성공적으로 완료된 사진 ID 목록 (UPLOADING -> PROCESSING). 중복은 무시 또는 400으로 명시.", example = "[100, 102]")
         List<Long> successPhotoIds,
@@
-        @Schema(description = "업로드 중 실패하거나 취소된 사진 ID 목록 (UPLOADING -> FAILED & 롤백)", example = "[101, 103]")
+        @Schema(description = "업로드 중 실패하거나 취소된 사진 ID 목록 (UPLOADING -> FAILED & 롤백). success 목록과 겹치면 안 됨.", example = "[101, 103]")
         List<Long> failurePhotoIds
src/main/java/com/cheeeese/photo/presentation/swagger/PhotoSwagger.java (2)

23-35: 문서 강화: URL 유효기간과 업로드 시 헤더 요구사항 명시

  • Presigned URL 유효기간(기본 10분, 설정화 시 해당 값)을 명시.
  • 업로드 시 서명에 포함된 Content-Type 헤더를 반드시 동일하게 전송해야 함을 명시.
-            description = """ 
+            description = """ 
                     ### RequestBody
                     ---
                     `albumCode`: 사진을 업로드할 앨범의 코드 \n
                     `fileInfos`: 업로드할 파일 정보 목록 (파일명, 크기, Content-Type) \n
                     
                     ### 로직 상세
                     ---
                     1. 앨범의 존재 및 만료 여부 확인
                     2. 앨범의 최대 사진 개수 (`maxPhotoCount`) 초과 여부 확인
                     3. 파일별 크기(6MB), Content-Type(image/*) 유효성 검증
                     4. 검증 통과 시, DB에 `Photo` 레코드를 `UPLOADING` 상태로 생성
-                    5. 클라우드 스토리지 Presigned URL을 발급하여 반환
+                    5. 클라우드 스토리지 Presigned URL을 발급하여 반환 (URL 유효기간: 기본 10분) \n
+                    6. 업로드 시 요청의 `Content-Type` 헤더는 발급 시 지정된 값과 동일해야 함
                     """

79-88: 보고 API 문서에 재실행(idempotency) 및 겹침 금지 명시

동일 ID를 여러 번 보고해도 안전해야 함(특히 실패 재보고). success/failure 겹침 금지와 중복 처리 방침을 여기에 명시해 주세요.

src/main/java/com/cheeeese/photo/application/PhotoService.java (1)

123-134: 성공 전이 결과 확인(선택): 영향 행 수 점검

상태 전이 업데이트의 영향 행 수를 확인해 불일치 시 로깅/모니터링을 권장합니다(검증 우회나 중간 상태 변경 감지).

-        photoRepository.updateStatusByIdsAndUserIdAndExpectedStatus(
+        int updated = photoRepository.updateStatusByIdsAndUserIdAndExpectedStatus(
                 successPhotoIds,
                 userId,
                 PhotoStatus.PROCESSING,
                 PhotoStatus.UPLOADING
         );
+        // TODO: updated != successPhotoIds.size() 시 경고 로그/메트릭 적재 권장
src/main/java/com/cheeeese/photo/presentation/PhotoController.java (2)

26-32: 요청/응답 콘텐츠 타입을 명시하고 검증 활성화 권장

API 계약을 안정화하려면 consumes/produces를 명시하세요. 클래스에 @validated 추가도 고려.

 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;

-@RestController
-@RequiredArgsConstructor
+@RestController
+@RequiredArgsConstructor
+@Validated
 @RequestMapping("/v1/photo")
 public class PhotoController implements PhotoSwagger {

-    @PostMapping("/presigned-url")
+    @PostMapping(
+        value = "/presigned-url",
+        consumes = MediaType.APPLICATION_JSON_VALUE,
+        produces = MediaType.APPLICATION_JSON_VALUE
+    )
     public CommonResponse<PhotoPresignedUrlResponse> createPresignedUrls(

35-42: /report 엔드포인트는 멱등성 보장 필요

클라이언트 재시도(네트워크 타임아웃/브라우저 재전송) 시 중복 반영/중복 롤백 위험. 업로드 배치/리포트 식별자(예: issuanceId, uploadId, or ETag)로 멱등 키를 강제하고, DB에 유니크 제약을 두거나 “이미 처리됨”을 안전하게 반환하도록 설계하세요. 서비스 레벨에서 트랜잭션 경계도 점검 바랍니다.

src/main/java/com/cheeeese/photo/dto/request/PhotoPresignedUrlRequest.java (2)

13-20: DTO 유효성 강화: albumCode 공백 금지, 파일 목록 최소 1개, 요소 검증 전파

현재 @NotNull 만으로는 ""(빈 문자열) 및 빈 리스트가 통과합니다. 리스트 요소에 대한 @Valid 전파도 필요합니다.

-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import jakarta.validation.Valid

 @Builder
 @Schema(description = "Presigned URL 발급 요청")
 public record PhotoPresignedUrlRequest(
-        @NotNull
+        @NotBlank
         @Schema(description = "앨범 코드", example = "786ccd09-5f22-4aa9-a32b-f62dd2e94cc8")
         String albumCode,

-        @NotNull
-        @Schema(description = "업로드할 파일 정보 목록")
-        List<FileInfo> fileInfos
+        @NotNull
+        @Size(min = 1)
+        @Schema(description = "업로드할 파일 정보 목록(최소 1개)")
+        List<@Valid FileInfo> fileInfos
 ) {

21-34: fileSize 하한 검증 추가 및 예시 명확화

0 또는 음수 크기를 조기에 차단하세요. Validator에서도 상한만 체크하므로 하한을 DTO에서 보완하면 UX가 좋아집니다.

-import lombok.Builder;
+import lombok.Builder;
+import jakarta.validation.constraints.Positive;

 @Builder
 @Schema(description = "개별 파일 정보")
 public record FileInfo(
         @NotBlank
         @Schema(description = "원본 파일명", example = "my_holiday_pic.jpg")
         String fileName,

-        @Schema(description = "파일 크기 (Byte)", example = "3000000")
-        long fileSize,
+        @Positive
+        @Schema(description = "파일 크기 (Byte, >0)", example = "3000000")
+        long fileSize,

         @NotBlank
         @Schema(description = "파일 Content-Type", example = "image/jpeg")
         String contentType
 ) {}
src/main/java/com/cheeeese/photo/application/validator/PhotoValidator.java (2)

23-25: Content-Type 검사: 대소문자/공백을 정규화하고 Set으로 검사

클라이언트별로 대소문자/공백 차이가 있습니다. 소문자 트림 후 검사하고, contains 성능/의도를 위해 Set 사용을 권장합니다.

-import java.util.List;
+import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import java.util.stream.Collectors;

-    private static final long MAX_FILE_SIZE = 6 * 1024 * 1024; // 6MB
-    private static final List<String> ALLOWED_TYPES = List.of("image/jpeg", "image/png", "image/jpg");
+    private static final long MAX_FILE_SIZE = 6 * 1024 * 1024; // 6MB
+    private static final Set<String> ALLOWED_TYPES = Set.of("image/jpeg", "image/png", "image/jpg");
@@
-            if (!ALLOWED_TYPES.contains(file.contentType())) {
+            String ct = file.contentType() == null ? null : file.contentType().trim().toLowerCase(Locale.ROOT);
+            if (ct == null || !ALLOWED_TYPES.contains(ct)) {
                 throw new PhotoException(PhotoErrorCode.PHOTO_INVALID_CONTENT_TYPE);
             }

Also applies to: 43-45


29-47: 파일 목록 요소 null, 크기 하한 미검증

요소가 null이면 NPE 발생 위험. 파일 크기 0/음수 허용도 비정상 흐름을 유발할 수 있습니다. 조기 검증을 추가하세요.

     public void validateFileInfos(List<PhotoPresignedUrlRequest.FileInfo> fileInfos) {
         if (fileInfos == null || fileInfos.isEmpty()) {
             throw new PhotoException(PhotoErrorCode.PHOTO_FILE_LIST_EMPTY);
         }

-        for (PhotoPresignedUrlRequest.FileInfo file : fileInfos) {
+        for (PhotoPresignedUrlRequest.FileInfo file : fileInfos) {
+            if (file == null) {
+                // 전용 에러코드가 없으면 PHOTO_FILE_NAME_REQUIRED 대신 별도 코드 추가를 권장
+                throw new PhotoException(PhotoErrorCode.PHOTO_FILE_NAME_REQUIRED);
+            }
             if (file.fileName() == null || file.fileName().isBlank()) {
                 throw new PhotoException(PhotoErrorCode.PHOTO_FILE_NAME_REQUIRED);
             }
 
-            if (file.fileSize() > MAX_FILE_SIZE) {
+            if (file.fileSize() <= 0 || file.fileSize() > MAX_FILE_SIZE) {
                 throw new PhotoException(PhotoErrorCode.PHOTO_FILE_SIZE_EXCEEDED);
             }
 
-            if (!ALLOWED_TYPES.contains(file.contentType())) {
+            // 대소문자/공백 정규화는 상단 코멘트의 diff 참고
+            if (!ALLOWED_TYPES.contains(file.contentType())) {
                 throw new PhotoException(PhotoErrorCode.PHOTO_INVALID_CONTENT_TYPE);
             }
         }
     }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc8b680 and a820741.

📒 Files selected for processing (20)
  • build.gradle (1 hunks)
  • src/main/java/com/cheeeese/album/application/validator/AlbumValidator.java (1 hunks)
  • src/main/java/com/cheeeese/album/exception/code/AlbumErrorCode.java (1 hunks)
  • src/main/java/com/cheeeese/album/infrastructure/persistence/AlbumRepository.java (1 hunks)
  • src/main/java/com/cheeeese/global/common/code/SuccessCode.java (1 hunks)
  • src/main/java/com/cheeeese/global/config/S3Config.java (1 hunks)
  • src/main/java/com/cheeeese/photo/application/PhotoService.java (2 hunks)
  • src/main/java/com/cheeeese/photo/application/PresignedUrlService.java (1 hunks)
  • src/main/java/com/cheeeese/photo/application/validator/PhotoValidator.java (1 hunks)
  • src/main/java/com/cheeeese/photo/domain/Photo.java (2 hunks)
  • src/main/java/com/cheeeese/photo/domain/PhotoStatus.java (1 hunks)
  • src/main/java/com/cheeeese/photo/dto/request/PhotoPresignedUrlRequest.java (1 hunks)
  • src/main/java/com/cheeeese/photo/dto/request/PhotoUploadReportRequest.java (1 hunks)
  • src/main/java/com/cheeeese/photo/dto/response/PhotoPresignedUrlResponse.java (1 hunks)
  • src/main/java/com/cheeeese/photo/exception/PhotoException.java (1 hunks)
  • src/main/java/com/cheeeese/photo/exception/code/PhotoErrorCode.java (1 hunks)
  • src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java (1 hunks)
  • src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoRepository.java (2 hunks)
  • src/main/java/com/cheeeese/photo/presentation/PhotoController.java (1 hunks)
  • src/main/java/com/cheeeese/photo/presentation/swagger/PhotoSwagger.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/cheeeese/photo/application/PhotoService.java (1)
src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java (1)
  • PhotoMapper (11-40)
src/main/java/com/cheeeese/photo/exception/code/PhotoErrorCode.java (1)
src/main/java/com/cheeeese/photo/exception/PhotoException.java (1)
  • Getter (7-12)
🔇 Additional comments (9)
src/main/java/com/cheeeese/album/exception/code/AlbumErrorCode.java (1)

24-24: LGTM!

새로운 에러 코드 추가가 깔끔합니다. FORBIDDEN 상태 코드와 명확한 에러 메시지가 적절하게 설정되었습니다.

src/main/java/com/cheeeese/global/common/code/SuccessCode.java (1)

30-31: LGTM!

새로운 성공 코드가 명확하게 정의되어 있습니다. 메시지도 직관적이고 일관성 있습니다.

src/main/java/com/cheeeese/global/config/S3Config.java (1)

30-40: 자격 증명 로테이션 시 재시작이 필요함을 인지하세요.

StaticCredentialsProvider를 사용하면 애플리케이션 시작 시점에 자격 증명이 한 번만 로드되므로, NCP Object Storage의 액세스 키를 로테이션할 경우 애플리케이션을 재시작해야 합니다. 현재 구현은 단순하고 명확하지만, 향후 무중단 자격 증명 로테이션이 필요하다면 동적 자격 증명 프로바이더로 전환을 고려해야 합니다.

src/main/java/com/cheeeese/photo/dto/response/PhotoPresignedUrlResponse.java (1)

8-23: LGTM!

Record 기반 DTO 구조가 깔끔하고, Swagger 문서화도 잘 되어 있습니다. 중첩된 PresignedUrlInfo 레코드도 적절하게 구성되었습니다.

src/main/java/com/cheeeese/photo/domain/PhotoStatus.java (1)

3-8: LGTM!

사진의 생명주기를 명확하게 표현한 상태 열거형입니다. PR에서 언급한 낙관적 증가 방식(optimistic increment)과 실패 시 롤백 로직에 잘 부합합니다.

src/main/java/com/cheeeese/photo/domain/Photo.java (3)

31-31: LGTM!

imageUrl을 nullable로 변경한 것은 PR에서 언급한 낙관적 증가 방식과 잘 부합합니다. 업로드 전에 Photo 엔티티를 생성하고, 업로드 완료 후 URL을 업데이트하는 전략이 명확히 드러납니다.


43-45: LGTM!

PhotoStatusEnumType.STRING으로 저장하는 것은 좋은 선택입니다. 향후 enum 순서가 변경되거나 새로운 상태가 추가되어도 데이터베이스의 기존 데이터가 영향을 받지 않습니다.


64-66: LGTM!

업로드 완료 후 이미지 URL을 업데이트하기 위한 메서드가 명확하게 구현되어 있습니다. 도메인 주도 설계 원칙에 따라 엔티티의 상태를 캡슐화하여 변경하는 좋은 접근입니다.

src/main/java/com/cheeeese/photo/presentation/swagger/PhotoSwagger.java (1)

3-3: CommonResponse 임포트 경로 확인 완료 - 이슈 없음

코드베이스 검증 결과, com.cheeeese.global.common.CommonResponse 임포트 경로가 정확합니다. CommonResponse 클래스는 src/main/java/com/cheeeese/global/common/CommonResponse.java에 위치하며, 동일한 임포트 패턴이 UserSwagger.java, AuthSwagger.java, UserController.java 등 다른 파일에서도 일관되게 사용되고 있습니다. 대체 경로(common.dto.CommonResponse)는 존재하지 않습니다.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@zyovn zyovn added the ✨feature New feature or request label Oct 24, 2025
Copy link
Member

@zyovn zyovn left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~~ 🧀

photoService 내에 메서드가 굉장히 많고 추후에도 계속 늘어날 것 같아서 나중에 분리하면 조을 것 같아요! 나중에 같이 얘기 해봅시당

@dahyun24 dahyun24 merged commit 02bc6e6 into develop Oct 25, 2025
1 check passed
@zyovn zyovn deleted the feat/#16-photo-upload branch October 31, 2025 06:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants