Skip to content

Conversation

@zyovn
Copy link
Member

@zyovn zyovn commented Nov 10, 2025

🔗 연관된 이슈

🚀 변경 유형

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

📝 작업 내용

  • 사진 다운로드 API (다운로드 presigned url 발급)
  • CDN 설정

📸 스크린샷

사진 다운로드 API (다운로드 presigned url 발급)
스크린샷 2025-11-10 171216
스크린샷 2025-11-10 165407

1시간 이내 다운로드 이력이 존재할 경우
스크린샷 2025-11-10 165419

CDN 설정
스크린샷 2025-11-09 045853

💬 리뷰 요구사항

📜 리뷰 규칙

Reviewer는 아래 P5 Rule을 참고하여 리뷰를 진행합니다.
P5 Rule을 통해 Reviewer는 Reviewee에게 리뷰의 의도를 보다 정확히 전달할 수 있습니다.

  • P1: 꼭 반영해주세요 (Comment)
  • P2: 적극적으로 고려해주세요 (Comment)
  • P3: 웬만하면 반영해 주세요 (Comment)
  • P4: 반영해도 좋고 넘어가도 좋습니다 (Approve)
  • P5: 그냥 사소한 의견입니다 (Approve)
  • 리팩토링 막막하다
  • 해야 할 일: 조회 남은 거 하기, 조회 로직 수정, 사용자 앨범 카운트

Summary by CodeRabbit

  • 새로운 기능
    • 사진 다운로드: 시간 제한(10분) presigned URL 발급 API 추가
    • 1시간 내 다운로드 이력 기반 중복 다운로드 제한 및 이력 기록
    • 앨범 기반 다운로드 권한 검증 강화(앨범 내 사진만 다운로드 허용)
    • CDN 적용으로 원본/썸네일 URL 최적화 및 응답 개선
  • 기타
    • 다운로드용 파일명/메타데이터 포함 응답 추가

@zyovn zyovn self-assigned this Nov 10, 2025
@zyovn zyovn added the ✨feature New feature or request label Nov 10, 2025
@zyovn zyovn linked an issue Nov 10, 2025 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link

coderabbitai bot commented Nov 10, 2025

Walkthrough

사진 다운로드 워크플로우를 추가: 앨범 참여자 검증(공통), 사진-앨범 일치 검증, 다운로드용 GET 프리사인 URL 생성, 다운로드 이력 조회/갱신, CDN URL 해석 및 S3 유틸리티와 관련 DTO/매퍼/레포지토리 추가가 포함됩니다.

Changes

응집도 / 파일(들) 변경 요약
검증 계층
src/main/java/com/cheeeese/album/application/validator/AlbumValidator.java
다운로드 권한 검증 메서드 validateDownloadPermission(Album, User, List<Photo>) 추가 및 업로드/다운로드 공통 검증을 위한 validateAlbumParticipant 보조 메서드 도입; 사진이 앨범에 속하는지 검증하고 불일치 시 PhotoException(PHOTO_NOT_FOUND_IN_ALBUM) 발생
도메인 엔티티
src/main/java/com/cheeeese/global/domain/BaseEntity.java, src/main/java/com/cheeeese/photo/domain/PhotoHistory.java
BaseEntity.markUpdated() 추가로 수동 업데이트 타임스탬프 지원; PhotoHistory.touch() 추가 (markUpdated 위임)
S3 유틸/ CDN 리졸버
src/main/java/com/cheeeese/global/util/S3Util.java, src/main/java/com/cheeeese/global/util/resolver/CdnUrlResolver.java
S3 객체 키/파일명 추출 유틸 S3Util 추가; 도메인별 경로 결합을 수행하는 Spring 컴포넌트 CdnUrlResolver 추가 (원본/썸네일/4컷)
프리사인 URL 서비스
src/main/java/com/cheeeese/photo/application/PresignedUrlService.java
GET용 프리사인된 URL 생성 메서드 generatePresignedGetUrl(String) 추가 (10분 유효기간)
사진 서비스 / 쿼리
src/main/java/com/cheeeese/photo/application/PhotoService.java, src/main/java/com/cheeeese/photo/application/PhotoQueryService.java
PhotoService.getDownloadPresignedUrls(User, PhotoDownloadRequest) 추가: 사진 조회, 앨범 참여/사진 소속 검증, 최근 다운로드(1시간) 체크, 프리사인 URL 생성 및 PhotoHistory 생성/업데이트; PhotoQueryServiceCdnUrlResolver 주입 및 응답 빌드에 URL 해석 적용
저장소 및 매퍼
src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoRepository.java, src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoHistoryRepository.java, src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoHistoryMapper.java, src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java
PhotoRepository.findAllByIdIn(List<Long>) 추가; PhotoHistoryRepository.findByUserIdAndPhotoId(Long, Long) 추가; PhotoHistoryMapper.toEntity(User, Photo) 추가; PhotoMapper에 CDN-aware 매핑 및 다운로드용 응답 빌더 추가
DTO 및 예외
src/main/java/com/cheeeese/photo/dto/request/PhotoDownloadRequest.java, src/main/java/com/cheeeese/photo/dto/response/PhotoDownloadResponse.java, src/main/java/com/cheeeese/photo/exception/code/PhotoErrorCode.java
PhotoDownloadRequest 레코드(앨범 코드, 사진 ID 리스트) 추가; PhotoDownloadResponse 레코드(DownloadFileInfo 리스트) 추가; 에러 코드 PHOTO_NOT_FOUND_IN_ALBUM 추가
프레젠테이션 / 문서화
src/main/java/com/cheeeese/photo/presentation/PhotoController.java, src/main/java/com/cheeeese/photo/presentation/swagger/PhotoSwagger.java
POST /download-url 엔드포인트 및 Swagger 문서화 추가 (PhotoDownloadRequest → PhotoDownloadResponse)

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PhotoController
    participant PhotoService
    participant AlbumValidator
    participant PhotoRepository
    participant PhotoHistoryRepository
    participant PresignedUrlService
    participant CdnUrlResolver

    Client->>PhotoController: POST /download-url (PhotoDownloadRequest)
    PhotoController->>PhotoService: getDownloadPresignedUrls(user, request)

    rect rgb(230,240,255)
    Note over PhotoService,AlbumValidator: 사진 조회 및 참여자/소속 검증
    PhotoService->>PhotoRepository: findAllByIdIn(photoIds)
    PhotoService->>AlbumValidator: validateDownloadPermission(album, user, photos)
    AlbumValidator-->>PhotoService: 검증 결과
    end

    rect rgb(230,255,230)
    Note over PhotoService,PhotoHistoryRepository: 최근 다운로드(1시간) 확인
    PhotoService->>PhotoHistoryRepository: findByUserIdAndPhotoId(...)
    PhotoHistoryRepository-->>PhotoService: 이력 목록
    end

    rect rgb(255,240,220)
    Note over PhotoService,PresignedUrlService: 프리사인 GET URL 생성 (필요 시)
    PhotoService->>PresignedUrlService: generatePresignedGetUrl(objectKey)
    PresignedUrlService-->>PhotoService: presignedUrl
    end

    rect rgb(245,230,255)
    Note over PhotoService,PhotoHistoryRepository: 이력 생성/갱신
    PhotoService->>PhotoHistoryRepository: save/update PhotoHistory
    PhotoHistoryRepository-->>PhotoService: 저장 완료
    end

    PhotoService->>CdnUrlResolver: resolveThumbnail/resolveOriginal(...)
    CdnUrlResolver-->>PhotoService: resolved URLs

    PhotoService-->>PhotoController: PhotoDownloadResponse
    PhotoController-->>Client: CommonResponse<PhotoDownloadResponse>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • 주의할 파일/영역:
    • PhotoService.getDownloadPresignedUrls: 다운로드 이력(쿼리/시간 윈도우), 동시성/중복 처리, null/예외 경로
    • AlbumValidator.validateDownloadPermission: 참여자/블랙리스트/만료 검증과 사진-앨범 일치 검사
    • PresignedUrlService.generatePresignedGetUrl: S3 presign 파라미터(버킷/키/유효기간) 및 예외 처리
    • PhotoMapper / PhotoQueryService 통합: CDN URL 전달에 따른 응답 형식 변화 영향 범위
    • PhotoHistoryRepository/Mapper: toEntity 및 저장 로직(merge vs save) 검증

Possibly related issues

Possibly related PRs

Suggested reviewers

  • dahyun24

Poem

🐰 달빛 아래 프리사인 URL 반짝여,
앨범 문을 확인하고 사진 길을 찾아,
CDN 길목을 지나 파일 이름을 부르고,
이력은 살짝 건드려 기억을 새기네.
작은 당근 축하 케이크로 배포 완료! 🥕📸

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 풀 리퀘스트 제목은 주요 변경 사항인 '사진 다운로드 로직 구현'과 'CDN 설정'을 명확하게 요약하고 있으며, raw_summary의 여러 파일 변경 사항들과 일치합니다.
✨ 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/#45-photo-download

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

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

87-87: 변수명 개선 제안

변수명 existsPhotoInAlbum은 "어떤 사진이 존재하는지"를 암시하지만, allMatch는 모든 사진이 조건을 만족하는지 검증합니다. allPhotosInAlbum 또는 allPhotosMatchAlbum 같은 이름이 더 명확할 것 같습니다.

-        boolean existsPhotoInAlbum = photos.stream().allMatch(photo -> photo.getAlbum().getId().equals(album.getId()));
+        boolean allPhotosInAlbum = photos.stream().allMatch(photo -> photo.getAlbum().getId().equals(album.getId()));
 
-        if (!existsPhotoInAlbum) {
+        if (!allPhotosInAlbum) {
             throw new PhotoException(PhotoErrorCode.PHOTO_NOT_FOUND_IN_ALBUM);
         }
src/main/java/com/cheeeese/photo/presentation/swagger/PhotoSwagger.java (1)

158-176: API 문서화 적절함 - 에러 응답 추가 고려

다운로드 presigned URL 발급 API에 대한 기본 문서화가 적절하게 작성되었습니다.

다만, 다른 API들과의 일관성을 위해 다음과 같은 에러 응답 케이스 추가를 고려해보세요:

  • 400: 존재하지 않는 photoId 포함 시
  • 403: 앨범 참가자가 아닌 경우
  • 404: 앨범이 존재하지 않는 경우
📜 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 876e116 and 5864974.

📒 Files selected for processing (17)
  • src/main/java/com/cheeeese/album/application/validator/AlbumValidator.java (2 hunks)
  • src/main/java/com/cheeeese/global/domain/BaseEntity.java (1 hunks)
  • src/main/java/com/cheeeese/global/util/S3Util.java (1 hunks)
  • src/main/java/com/cheeeese/global/util/resolver/CdnUrlResolver.java (1 hunks)
  • src/main/java/com/cheeeese/photo/application/PhotoQueryService.java (4 hunks)
  • src/main/java/com/cheeeese/photo/application/PhotoService.java (6 hunks)
  • src/main/java/com/cheeeese/photo/application/PresignedUrlService.java (2 hunks)
  • src/main/java/com/cheeeese/photo/domain/PhotoHistory.java (2 hunks)
  • src/main/java/com/cheeeese/photo/dto/request/PhotoDownloadRequest.java (1 hunks)
  • src/main/java/com/cheeeese/photo/dto/response/PhotoDownloadResponse.java (1 hunks)
  • src/main/java/com/cheeeese/photo/exception/code/PhotoErrorCode.java (1 hunks)
  • src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoHistoryMapper.java (1 hunks)
  • src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java (3 hunks)
  • src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoHistoryRepository.java (2 hunks)
  • src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoRepository.java (1 hunks)
  • src/main/java/com/cheeeese/photo/presentation/PhotoController.java (2 hunks)
  • src/main/java/com/cheeeese/photo/presentation/swagger/PhotoSwagger.java (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-05T03:06:41.847Z
Learnt from: zyovn
Repo: Say-Cheeeese/BE PR: 44
File: src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoHistoryRepository.java:36-36
Timestamp: 2025-11-05T03:06:41.847Z
Learning: PhotoHistoryRepository의 existsByUserIdAndPhotoIdAndCreatedAt 메서드는 정확한 타임스탬프 일치를 확인하는 시그니처로, "1시간 이내 다운로드" 체크에는 부적합합니다. Spring Data JPA에서는 existsByUserIdAndPhotoIdAndCreatedAtAfter를 사용하여 >= 비교를 수행해야 합니다.

Applied to files:

  • src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoHistoryRepository.java
  • src/main/java/com/cheeeese/photo/application/PhotoService.java
📚 Learning: 2025-10-31T13:17:52.523Z
Learnt from: dahyun24
Repo: Say-Cheeeese/BE PR: 35
File: src/main/java/com/cheeeese/photo/application/PhotoService.java:46-52
Timestamp: 2025-10-31T13:17:52.523Z
Learning: In src/main/java/com/cheeeese/photo/application/PhotoService.java, the getRecentThumbnailUrls method intentionally returns only the first thumbnail URL when photos.size() < 5, rather than returning all available thumbnails. This is according to product requirements: 0 photos → empty list, 1-4 photos → single thumbnail (most recent), 5 photos → all 5 thumbnails.

Applied to files:

  • src/main/java/com/cheeeese/photo/presentation/PhotoController.java
  • src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java
  • src/main/java/com/cheeeese/photo/application/PhotoQueryService.java
  • src/main/java/com/cheeeese/photo/application/PhotoService.java
🧬 Code graph analysis (3)
src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoHistoryMapper.java (1)
src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoLikesMapper.java (2)
  • PhotoLikesMapper (7-15)
  • toEntity (9-14)
src/main/java/com/cheeeese/photo/application/PhotoQueryService.java (1)
src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java (1)
  • PhotoMapper (14-150)
src/main/java/com/cheeeese/photo/application/PhotoService.java (3)
src/main/java/com/cheeeese/global/util/S3Util.java (1)
  • S3Util (6-46)
src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoHistoryMapper.java (1)
  • PhotoHistoryMapper (7-15)
src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java (1)
  • PhotoMapper (14-150)
🔇 Additional comments (12)
src/main/java/com/cheeeese/photo/exception/code/PhotoErrorCode.java (1)

32-32: 새로운 에러 코드 추가 확인 완료

앨범 소속 검증을 위한 에러 코드가 적절하게 추가되었습니다. HttpStatus와 메시지가 기존 패턴과 일관성 있게 정의되었습니다.

src/main/java/com/cheeeese/album/application/validator/AlbumValidator.java (1)

84-92: <!-- [scratchpad_start]
[task verification_1/2 npe_risk]
Verify NPE risk from photo.getAlbum() null return
[observations]

  • Line 30 in Photo.java: @joincolumn(name = "album_id", nullable = false)
  • Album field marked with nullable = false constraint
  • Database-level constraint ensures album is never null
    [analysis]
  • NPE risk is mitigated by JPA nullable constraint
  • photo.getAlbum() will never return null for persisted Photo entities
    [done]
    NPE concern is RESOLVED - album field is guaranteed non-null
    [/scratchpad_start]

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

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

82-82: 변수명을 로직에 맞게 수정하세요.

allMatch를 사용하고 있으므로 변수명을 existsPhotoInAlbum에서 allPhotosInAlbum 또는 allPhotosMatchAlbum으로 변경하는 것이 더 명확합니다.

-        boolean existsPhotoInAlbum = photos.stream().allMatch(photo -> photo.getAlbum().getId().equals(album.getId()));
+        boolean allPhotosInAlbum = photos.stream().allMatch(photo -> photo.getAlbum().getId().equals(album.getId()));

-        if (!existsPhotoInAlbum) {
+        if (!allPhotosInAlbum) {
             throw new PhotoException(PhotoErrorCode.PHOTO_NOT_FOUND_IN_ALBUM);
         }
📜 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 5864974 and 88096be.

📒 Files selected for processing (3)
  • src/main/java/com/cheeeese/album/application/validator/AlbumValidator.java (2 hunks)
  • src/main/java/com/cheeeese/photo/application/PhotoQueryService.java (4 hunks)
  • src/main/java/com/cheeeese/photo/domain/PhotoHistory.java (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-31T13:17:52.523Z
Learnt from: dahyun24
Repo: Say-Cheeeese/BE PR: 35
File: src/main/java/com/cheeeese/photo/application/PhotoService.java:46-52
Timestamp: 2025-10-31T13:17:52.523Z
Learning: In src/main/java/com/cheeeese/photo/application/PhotoService.java, the getRecentThumbnailUrls method intentionally returns only the first thumbnail URL when photos.size() < 5, rather than returning all available thumbnails. This is according to product requirements: 0 photos → empty list, 1-4 photos → single thumbnail (most recent), 5 photos → all 5 thumbnails.

Applied to files:

  • src/main/java/com/cheeeese/photo/application/PhotoQueryService.java
🧬 Code graph analysis (2)
src/main/java/com/cheeeese/photo/domain/PhotoHistory.java (1)
src/main/java/com/cheeeese/photo/infrastructure/persistence/PhotoHistoryRepository.java (2)
  • Query (25-36)
  • PhotoHistoryRepository (12-37)
src/main/java/com/cheeeese/photo/application/PhotoQueryService.java (1)
src/main/java/com/cheeeese/photo/infrastructure/mapper/PhotoMapper.java (1)
  • PhotoMapper (14-150)
🔇 Additional comments (5)
src/main/java/com/cheeeese/photo/domain/PhotoHistory.java (1)

41-43: 구현이 정확합니다.

touch() 메서드가 BaseEntitymarkUpdated()를 올바르게 호출하여 다운로드 이력 갱신 시 updatedAt 타임스탬프를 업데이트합니다.

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

5-5: CDN URL 해석 로직이 일관되게 잘 구현되었습니다.

CdnUrlResolver가 모든 메서드(getPhotoDetail, getPhotoPageFromDB, buildPhotoLikedResponses)에서 일관되게 사용되고 있으며, 이전 리뷰에서 지적된 썸네일 URL 처리 버그도 수정되었습니다.

Also applies to: 36-36, 99-100, 108-108, 114-114, 170-170, 173-173

src/main/java/com/cheeeese/album/application/validator/AlbumValidator.java (3)

79-87: 빈 리스트 엣지 케이스를 처리해주세요.

photos 리스트가 비어있을 경우 allMatchtrue를 반환하여 검증을 통과합니다. 빈 리스트에 대한 명시적인 검증이 필요한지 확인해주세요.

만약 빈 리스트를 허용하지 않는다면 다음과 같이 수정하세요:

     public void validateDownloadPermission(Album album, User user, List<Photo> photos) {
         validateAlbumParticipant(album, user);
+
+        if (photos == null || photos.isEmpty()) {
+            throw new PhotoException(PhotoErrorCode.PHOTO_NOT_FOUND);
+        }

         boolean allPhotosInAlbum = photos.stream().allMatch(photo -> photo.getAlbum().getId().equals(album.getId()));

         if (!allPhotosInAlbum) {
             throw new PhotoException(PhotoErrorCode.PHOTO_NOT_FOUND_IN_ALBUM);
         }
     }

75-77: 공통 검증 로직 추출이 잘 되었습니다.

validateAlbumParticipant 메서드로 공통 검증 로직을 추출하여 코드 중복을 제거하고 유지보수성을 향상시켰습니다.

Also applies to: 89-96


82-82: 이 리뷰 코멘트는 근거가 없습니다.

Photo 엔티티의 album 관계는 @ManyToOne(fetch = FetchType.LAZY)로 설정되어 있으나, validateDownloadPermission()@Transactional 메서드 내에서 호출되므로 트랜잭션 범위 내에서 lazy loading이 정상 작동합니다. 또한 @JoinColumn(name = "album_id", nullable = false)로 인해 데이터베이스 제약 수준에서 album_id가 NULL이 될 수 없으므로, photo.getAlbum()은 절대 null을 반환하지 않습니다. NPE 위험은 실제로 존재하지 않습니다.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@dahyun24 dahyun24 left a comment

Choose a reason for hiding this comment

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

수고하셨습니당

LGTM~~~

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.

feat: 사진 다운로드 로직 구현 및 CDN 설정

3 participants