Skip to content

Conversation

@sweatbuckets
Copy link
Contributor

@sweatbuckets sweatbuckets commented Dec 13, 2025

  1. #⃣ 연관된 이슈
    • 관련 이슈를 명시해주세요.
    • 예: #이슈번호#이슈번호
  2. 📝 작업 내용
    • casting, notice 이미지는 null 허용, poster는 무조건 하나 필수
    • poster, notice, casting 이미지 수정 기능 추가
    • presignedUrl 발급시 notice, casting 이미지는 다른 폴더로 올리게끔 수정
    • filePath, contentId로 이미지를 유일하게 식별가능하도록 notice, casting 분리
  3. 📸 스크린샷 (선택)
    • 작업 내용을 시각적으로 표현할 스크린샷을 포함하세요.
  4. 💬 리뷰 요구사항 (선택)
    • 리뷰어가 특히 검토해주었으면 하는 부분이 있다면 작성해주세요.
    • 예: "메서드 XXX의 이름을 더 명확히 하고 싶은데, 좋은 아이디어가 있으신가요?"

Summary by CodeRabbit

  • Bug Fixes

    • Prevented errors from missing or blank image keys by skipping invalid image operations and adding null-checks.
    • Ensure associated images (posters, notices, castings) are removed when deleting a show.
  • Improvements

    • More robust image save/update flows that accept optional image URLs and properly propagate updates to posters, notices, and castings.
    • Added support for notice and casting image types across the enroll, update, and delete processes.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 13, 2025

Walkthrough

Centralizes amateur-show image handling into URL-aware ImageService methods, adds null/blank guards and S3-key validation, updates poster/notice/casting save/update/delete flows, extends FilePath with notice and casting, and adds minor S3 controller/service guard and doc tweaks.

Changes

Cohort / File(s) Summary
Amateur show service updates
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java
Validate poster keyName (throw INVALID_S3_KEY if missing); skip image ops when keyNames null/blank; replace direct image save/delete with imageService.saveImageWithImageUrl and imageService.updateShowImage; save casting/notice images using URL-aware methods; publish NewShowEvent only when likers exist; delete poster, notice, and casting images on show deletion.
Image service additions & changes
src/main/java/cc/backend/image/service/ImageService.java
Added saveImageWithImageUrl(Long, FullImageRequestDTO, Optional<String>) and updateShowImage(Long, String, Optional<String>, Long, FilePath); saveImage delegates to new URL-aware method; new methods validate member and S3 key existence, handle find/delete/replace of existing Image records, and persist new Image entries with optional imageUrl.
FilePath enum extension
src/main/java/cc/backend/image/FilePath.java
Added notice and casting enum constants used by new ImageService flows.
S3 controller docs & guard
src/main/java/cc/backend/config/s3/S3Controller.java
Updated presigned URL parameter descriptions and added null-check in doesObjectExist() to return false when keyName is null.
S3 service MIME handling tweak
src/main/java/cc/backend/config/s3/S3Service.java
Commented-out MIME type resolution block for common image extensions (jpg/jpeg/png/gif); PutObjectRequest construction otherwise unchanged.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant AmateurService as AmateurServiceImpl
    participant ImageService
    participant S3Service
    participant DB as Database
    participant EventPub as EventPublisher

    Client->>AmateurService: enroll/update/delete show (includes image keyNames & optional imageUrls)
    AmateurService->>ImageService: saveImageWithImageUrl(...) / updateShowImage(...) / delete image calls
    ImageService->>S3Service: doesObjectExist(keyName)
    alt S3 object exists
        ImageService->>DB: find existing Image by (filePath, contentId)
        alt keyName changed or new
            ImageService->>DB: delete existing Image (if any)
            ImageService->>DB: save new Image (with optional imageUrl)
        else same keyName and imageUrl present
            ImageService->>DB: update Image URL
        end
    else S3 object missing
        ImageService-->>AmateurService: throw INVALID_S3_KEY / error
    end
    AmateurService->>DB: persist AmateurShow and related entities (castings, notices)
    AmateurService->>EventPub: publish NewShowEvent (only when likers exist)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review transactional boundaries and Optional handling in saveImageWithImageUrl / updateShowImage.
  • Verify S3 existence checks and error mapping to INVALID_S3_KEY.
  • Confirm update/delete logic for poster/notice/casting avoids orphaned DB records and uses correct FilePath values.
  • Check commented MIME logic in S3Service is intentional and documented.

Possibly related PRs

Poem

🐰
I hopped on keys both new and old,
Wrapped URLs in threads of gold,
I guarded S3 with careful paw,
Replaced and updated every draw,
Hooray — the show’s bright images unfold! 🎨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.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
Title check ✅ Passed The title accurately describes the main changes: adding image update functionality for poster, notice, and casting images, and fixing the logic to save imageUrl to the database.
Description check ✅ Passed The PR description includes the required working content section with detailed implementation details, but the related issue section is incomplete (placeholder text only) and screenshots/review requests are optional sections left unfilled.
✨ 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/updateShowImage

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

🧹 Nitpick comments (1)
src/main/java/cc/backend/image/service/ImageService.java (1)

63-85: Avoid persisting imageUrl=""; use null to represent “no URL”
Right now saveImageWithImageUrl stores empty-string when absent (Line 72), while the existing saveImage(...) path doesn’t set imageUrl at all (likely NULL). This inconsistency will bite querying/serialization.

-        String imageUrl = imageUrlOpt.orElse("");
+        String imageUrl = imageUrlOpt.orElse(null);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ccb2fd9 and 8b11aee.

📒 Files selected for processing (3)
  • src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (7 hunks)
  • src/main/java/cc/backend/config/s3/S3Controller.java (1 hunks)
  • src/main/java/cc/backend/image/service/ImageService.java (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/cc/backend/config/s3/S3Controller.java (1)
src/main/java/cc/backend/config/s3/S3Service.java (1)
  • doesObjectExist (188-211)
src/main/java/cc/backend/image/service/ImageService.java (2)
src/main/java/cc/backend/image/controller/ImageController.java (1)
  • Operation (25-31)
src/main/java/cc/backend/image/entity/Image.java (1)
  • Entity (16-61)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (1)
src/main/java/cc/backend/image/DTO/ImageRequestDTO.java (1)
  • ImageRequestDTO (12-66)

Comment on lines +86 to 89
if(keyName == null){
return ResponseEntity.ok(false);
}
return ResponseEntity.ok(s3Service.doesObjectExist(keyName, member.getId()));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

/s3/exist should likely treat blank keyName the same as null
Returning false for null but throwing for "" / " " is surprising for an “exist” endpoint.

-        if(keyName == null){
+        if (keyName == null || keyName.isBlank()) {
             return ResponseEntity.ok(false);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(keyName == null){
return ResponseEntity.ok(false);
}
return ResponseEntity.ok(s3Service.doesObjectExist(keyName, member.getId()));
if (keyName == null || keyName.isBlank()) {
return ResponseEntity.ok(false);
}
return ResponseEntity.ok(s3Service.doesObjectExist(keyName, member.getId()));
🤖 Prompt for AI Agents
In src/main/java/cc/backend/config/s3/S3Controller.java around lines 86 to 89,
the endpoint returns false when keyName is null but will throw for empty or
whitespace-only strings; normalize the input by treating blank values the same
as null (e.g., check keyName == null || keyName.trim().isEmpty() or use
StringUtils.isBlank(keyName)) and return ResponseEntity.ok(false) for those
cases so empty/whitespace keys do not cause errors and behavior is consistent.

Comment on lines 189 to 226
@Transactional
public void updateImage(
Long memberId,
String keyName,
Optional<String> imageUrlOpt,
Long contentId,
FilePath filePath
) {
if (keyName == null || keyName.isBlank()) {
return; // keyName 없으면 처리하지 않음
}

String imageUrl = imageUrlOpt.orElse("");

// 기존 이미지 조회 (filePath + contentId 기준)
Image existingImage = imageRepository
.findAllByFilePathAndContentId(filePath, contentId)
.stream()
.findFirst()
.orElse(null);

// 기존 이미지가 있고, keyName이 다르면 삭제 후 새로 저장
if (existingImage != null && !existingImage.getKeyName().equals(keyName)) {
deleteImage(existingImage.getId(), memberId);
}

// 새 이미지 저장
Image image = Image.builder()
.keyName(keyName)
.imageUrl(imageUrl)
.filePath(filePath)
.contentId(contentId)
.uploadedAt(LocalDateTime.now())
.memberId(memberId)
.build();

imageRepository.save(image);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

updateImage creates duplicate DB rows and can corrupt “one image per (filePath, contentId)” semantics

  • If existingImage != null and keyName is unchanged, you don’t delete, but you still save(image) (Line 225) ⇒ duplicates.
  • You also findFirst() among potentially many rows (Line 206-208), so behavior becomes non-deterministic as duplicates accumulate.
  • No S3 existence check here, so you can persist a key that was never uploaded.

A minimally invasive fix (prevents duplicates, adds S3 validation, and avoids S3 delete on same-key rewrites):

     public void updateImage(
             Long memberId,
             String keyName,
             Optional<String> imageUrlOpt,
             Long contentId,
             FilePath filePath
     ) {
         if (keyName == null || keyName.isBlank()) {
             return; // keyName 없으면 처리하지 않음
         }
 
-        String imageUrl = imageUrlOpt.orElse("");
+        if (!s3Service.doesObjectExist(keyName, memberId)) {
+            throw new GeneralException(ErrorStatus.NOT_FOUND_IN_S3);
+        }
+
+        String imageUrl = imageUrlOpt.orElse(null);
 
         // 기존 이미지 조회 (filePath + contentId 기준)
         Image existingImage = imageRepository
                 .findAllByFilePathAndContentId(filePath, contentId)
                 .stream()
                 .findFirst()
                 .orElse(null);
 
-        // 기존 이미지가 있고, keyName이 다르면 삭제 후 새로 저장
-        if (existingImage != null && !existingImage.getKeyName().equals(keyName)) {
-            deleteImage(existingImage.getId(), memberId);
-        }
+        if (existingImage != null) {
+            // keyName 변경이면 S3/DB 모두 삭제
+            if (!existingImage.getKeyName().equals(keyName)) {
+                deleteImage(existingImage.getId(), memberId);
+            } else {
+                // 같은 keyName이면 DB row만 교체(중복 방지), S3는 유지
+                imageRepository.delete(existingImage);
+            }
+        }
 
         // 새 이미지 저장
         Image image = Image.builder()
                 .keyName(keyName)
                 .imageUrl(imageUrl)
                 .filePath(filePath)
                 .contentId(contentId)
                 .uploadedAt(LocalDateTime.now())
                 .memberId(memberId)
                 .build();
 
         imageRepository.save(image);
     }

If you truly need multiple images (e.g., multiple casting images) under the same filePath+contentId, then this API/lookup key needs redesign (e.g., contentId = castingId, or add an ImageType column and query by (filePath, contentId, type)).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/cc/backend/image/service/ImageService.java around lines
189-226, the updateImage method currently can create duplicate DB rows, uses a
non-deterministic findFirst() over many rows, and does not validate S3
existence; change the flow so: 1) query for all images by filePath+contentId and
deterministically pick the canonical existingImage (preferably the one with
matching keyName or the newest), 2) if an existingImage exists and its keyName
equals the incoming keyName, do NOT insert a new row — update the existingImage
fields (imageUrl, uploadedAt, memberId) and save the entity instead, 3) if
existingImage exists and keyName differs, first validate that the new keyName
exists in S3 (or throw/return) then delete any other DB rows and their S3
objects for the old key(s) before inserting the new Image row, 4) if no
existingImage, validate the new keyName exists in S3 before creating and saving
a single Image row; ensure the repository enforces or is migrated toward a
uniqueness constraint on (filePath, contentId) or introduce an ImageType if
multiple images per key are required.

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

♻️ Duplicate comments (1)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (1)

79-84: Poster DTO null check still missing—potential NPE.

Line 82 calls dto.getKeyName() but dto itself (from requestDTO.getPosterImageRequestDTO()) can be null, causing an NPE.

 ImageRequestDTO.PosterImageRequestDTO dto = requestDTO.getPosterImageRequestDTO();

 //poster 이미지는 없으면 에러
-if(dto.getKeyName() == null || dto.getKeyName().isBlank()){
+if(dto == null || dto.getKeyName() == null || dto.getKeyName().isBlank()){
     throw new GeneralException(ErrorStatus.INVALID_S3_KEY);
 }
🧹 Nitpick comments (2)
src/main/java/cc/backend/image/service/ImageService.java (2)

63-88: Consider refactoring to reduce code duplication with saveImage.

saveImageWithImageUrl duplicates most of the logic from saveImage (lines 42-61). Consider extracting common logic or having saveImage delegate to saveImageWithImageUrl with Optional.empty().

 @Transactional
 public ImageResponseDTO.ImageResultWithPresignedUrlDTO saveImage(Long memberId, ImageRequestDTO.FullImageRequestDTO requestDTO) {
-    memberRepository.findById(memberId).orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND));
-    //S3에 실제 존재하는 이미지인지 검증
-    if(!s3Service.doesObjectExist(requestDTO.getKeyName(), memberId)) {
-        throw new GeneralException(ErrorStatus.NOT_FOUND_IN_S3);
-    }
-
-    Image image = Image.builder()
-            .keyName(requestDTO.getKeyName())
-            .filePath(requestDTO.getFilePath())
-            .contentId(requestDTO.getContentId())
-            .uploadedAt(LocalDateTime.now())
-            .memberId(memberId)
-            .build();
-
-    Image newImage = imageRepository.save(image);
-
-    return getImage(newImage.getKeyName(), memberId);
+    return saveImageWithImageUrl(memberId, requestDTO, Optional.empty());
 }

192-233: Repository method correctly returns single Image; verify uniqueness is enforced elsewhere.

The method findByFilePathAndContentId exists on line 15 of ImageRepository.java and correctly returns a single Image instead of a list. The S3 existence check and duplicate-prevention logic in updateShowImage are sound. However, note that the database index on file_path and content_id (line 23 of Image.java) is for query performance only—it does not enforce uniqueness. If uniqueness is required, add @UniqueConstraint(columnNames = {"file_path", "content_id"}) to the @Table annotation on the Image entity to prevent duplicate entries at the database level.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b11aee and 52fe013.

📒 Files selected for processing (5)
  • src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (8 hunks)
  • src/main/java/cc/backend/config/s3/S3Controller.java (3 hunks)
  • src/main/java/cc/backend/config/s3/S3Service.java (1 hunks)
  • src/main/java/cc/backend/image/FilePath.java (1 hunks)
  • src/main/java/cc/backend/image/service/ImageService.java (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/cc/backend/config/s3/S3Service.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/cc/backend/config/s3/S3Controller.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (1)
src/main/java/cc/backend/image/DTO/ImageRequestDTO.java (1)
  • ImageRequestDTO (12-66)
🔇 Additional comments (4)
src/main/java/cc/backend/image/FilePath.java (1)

4-4: LGTM!

The new notice and casting enum constants follow the existing naming convention and support the new image handling flows.

src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (3)

119-138: LGTM!

The null/blank guard for keyName (Lines 122-124) and usage of casting.getId() as contentId with FilePath.casting are correct. Each casting image is properly associated with its casting entity.


200-211: LGTM!

Proper null and blank validation before calling updateShowImage, and the show's posterImageUrl is updated accordingly.


96-105: LGTM!

Good optimization to only publish NewShowEvent when there are actual likers to notify.

Comment on lines +405 to +407
//포스터 삭제
Image posterImg = imageRepository.findByFilePathAndContentId(FilePath.amateurShow, amateurShowId);
imageService.deleteImage(posterImg.getId(), memberId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing null check for poster image before deletion.

findByFilePathAndContentId may return null if no poster image exists, causing an NPE on Line 407. Add a null check for consistency with the notice image handling below.

 //포스터 삭제
 Image posterImg = imageRepository.findByFilePathAndContentId(FilePath.amateurShow, amateurShowId);
-imageService.deleteImage(posterImg.getId(), memberId);
+if (posterImg != null) {
+    imageService.deleteImage(posterImg.getId(), memberId);
+}
🤖 Prompt for AI Agents
In
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java
around lines 405 to 407, the code calls
imageRepository.findByFilePathAndContentId(...) and immediately uses
posterImg.getId(), which can NPE if no poster exists; add a null check like the
one used for the notice image: only call imageService.deleteImage(...) when
posterImg is not null (and handle/log accordingly) to avoid
NullPointerException.

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

Caution

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

⚠️ Outside diff range comments (1)
src/main/java/cc/backend/image/service/ImageService.java (1)

42-72: imageUrlOpt.orElse("") should likely be null (avoid empty-string “nullable” drift).
Right now saveImageWithImageUrl writes "" when absent (Line 59), while updateShowImage writes null (Line 191). This can break “nullable” semantics and any downstream “is null” queries.

-        String imageUrl = imageUrlOpt.orElse("");
+        String imageUrl = imageUrlOpt.orElse(null);
♻️ Duplicate comments (3)
src/main/java/cc/backend/image/service/ImageService.java (1)

176-217: Consider “update-in-place” + “delete-on-blank” semantics for show images.

  • If keyName is blank you return (Line 184-186), so callers cannot remove an existing notice/casting image (even though those are “nullable” per PR objectives). Consider: blank/null ⇒ delete existing (filePath, contentId) row (and S3 object) instead of no-op.
  • When existingImage has the same keyName, you delete the row and re-insert (Line 202-204, 206-216). This avoids duplicates, but churns IDs and is less safe than updating fields on the existing entity (this is closely related to the earlier duplicate-row concern).

Proposed direction (illustrative):

     public void updateShowImage(...){
-        if (keyName == null || keyName.isBlank()) {
-            return; // keyName 없으면 처리하지 않음
-        }
+        Image existingImage = imageRepository.findByFilePathAndContentId(filePath, contentId);
+        if (keyName == null || keyName.isBlank()) {
+            if (existingImage != null) deleteImage(existingImage.getId(), memberId);
+            return;
+        }

         if (!s3Service.doesObjectExist(keyName, memberId)) {
             throw new GeneralException(ErrorStatus.NOT_FOUND_IN_S3);
         }
         String imageUrl = imageUrlOpt.orElse(null);

-        Image existingImage = imageRepository
-                .findByFilePathAndContentId(filePath, contentId);
-
-        if (existingImage != null) {
-            if (!existingImage.getKeyName().equals(keyName)) {
-                deleteImage(existingImage.getId(), memberId);}
-            else {
-                imageRepository.delete(existingImage);
-            }
-        }
-        Image image = Image.builder()...
-        imageRepository.save(image);
+        if (existingImage == null) {
+            imageRepository.save(Image.builder()...build());
+            return;
+        }
+        if (!existingImage.getKeyName().equals(keyName)) {
+            deleteImage(existingImage.getId(), memberId);
+            imageRepository.save(Image.builder()...build());
+            return;
+        }
+        // same keyName: update in place
+        existingImage.updateImageUrl(imageUrl);
+        existingImage.updateUploadedAt(LocalDateTime.now());
+        existingImage.updateMemberId(memberId);
+        imageRepository.save(existingImage);
     }

(You’ll need setters / update methods on Image to avoid direct field mutation.)

src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (2)

79-84: Poster DTO can be null → NPE on dto.getKeyName() (poster “required” validation still incomplete).
This matches the earlier review feedback: requestDTO.getPosterImageRequestDTO() can be null, so Line 82 can throw.

-        ImageRequestDTO.PosterImageRequestDTO dto = requestDTO.getPosterImageRequestDTO();
-        //poster 이미지는 없으면 에러
-        if(dto.getKeyName() == null || dto.getKeyName().isBlank()){
+        ImageRequestDTO.PosterImageRequestDTO dto = requestDTO.getPosterImageRequestDTO();
+        // poster 이미지는 없으면 에러
+        if (dto == null || dto.getKeyName() == null || dto.getKeyName().isBlank()) {
             throw new GeneralException(ErrorStatus.INVALID_S3_KEY);
         }

413-435: Poster deletion can NPE when no poster image exists.
findByFilePathAndContentId may return null; Line 415 then crashes. This matches the earlier review feedback.

         //포스터 삭제
         Image posterImg = imageRepository.findByFilePathAndContentId(FilePath.amateurShow, amateurShowId);
-        imageService.deleteImage(posterImg.getId(), memberId);
+        if (posterImg != null) {
+            imageService.deleteImage(posterImg.getId(), memberId);
+        }
🧹 Nitpick comments (2)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (2)

146-161: Notice image DTO null-guard looks good; confirm requestDTO.getNotice() can’t be null here.
You guard noticeImageDTO, but still dereference requestDTO.getNotice() (Line 146). If requestDTO.getNotice() is nullable at API level, consider caching it earlier and null-checking before use.


275-305: New casting save: add savedCasting to updatedList (clarity/consistency).
You save(newCasting) but add newCasting to updatedList (Line 288-290). Usually the saved instance is clearer/safer.

-                AmateurCasting savedCasting = amateurCastingRepository.save(newCasting);
-                updatedList.add(newCasting);
+                AmateurCasting savedCasting = amateurCastingRepository.save(newCasting);
+                updatedList.add(savedCasting);
                 contentId = savedCasting.getId();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52fe013 and 8f3190c.

📒 Files selected for processing (2)
  • src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (8 hunks)
  • src/main/java/cc/backend/image/service/ImageService.java (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (2)
src/main/java/cc/backend/image/DTO/ImageRequestDTO.java (1)
  • ImageRequestDTO (12-66)
src/main/java/cc/backend/amateurShow/converter/AmateurConverter.java (1)
  • AmateurConverter (17-281)
🔇 Additional comments (3)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (3)

93-105: New show event publish guarded by liker existence looks good.
Avoids firing events with empty target lists (Line 96-105).


120-137: Casting image save: good (FilePath.casting, contentId=castingId) + null/blank guard.
This aligns “one image per casting” and prevents accidental overwrites.


249-263: Notice update: good null-guards + using persisted notice ID.
Fixes the earlier noticeDTO NPE risk and avoids using request DTO IDs (Line 251-262).

Comment on lines 201 to 211
if (dto != null && dto.getKeyName() != null && !dto.getKeyName().isBlank()) {
// 현재 포스터 이미지 조회 (show당 1개)
Image existingImage = imageRepository
.findAllByFilePathAndContentId(FilePath.amateurShow, amateurShow.getId())
.stream()
.findFirst()
.orElse(null);

// 기존 keyName과 다르면 기존 이미지 삭제 후 교체
if (existingImage != null && !existingImage.getKeyName().equals(dto.getKeyName())) {
imageService.deleteImage(existingImage.getId(), memberId);
}
imageService.updateShowImage(
memberId,
dto.getKeyName(),
Optional.ofNullable(dto.getImageUrl()),
amateurShow.getId(),
FilePath.amateurShow
);

ImageRequestDTO.FullImageRequestDTO fullImageRequestDTO = ImageRequestDTO.FullImageRequestDTO.builder()
.keyName(dto.getKeyName())
.filePath(FilePath.amateurShow)
.contentId(amateurShow.getId())
.memberId(memberId)
.build();

imageService.saveImage(memberId, fullImageRequestDTO);
//amateurShow 엔티티 내 posterImageUrl 필드 수정
amateurShow.updatePosterImageUrl(dto.getImageUrl());

}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Poster update via updateShowImage is cleaner than delete+insert; watch for null imageUrl.
If FE ever sends keyName but omits imageUrl, you’ll set amateurShow.posterImageUrl to null (Line 210-211). If poster URL is required for reads, consider requiring imageUrl when keyName is provided (or derive URL server-side).

🤖 Prompt for AI Agents
In
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java
around lines 201-211, the code updates amateurShow.posterImageUrl with
dto.getImageUrl() even when dto.getKeyName() is present but imageUrl is null,
which can wipe out the stored URL; either validate and require dto.getImageUrl()
when dto.getKeyName() is provided (return a 4xx error), or else derive/resolve
the image URL server-side before updating and only call
amateurShow.updatePosterImageUrl(...) when a non-null imageUrl is available;
implement one of these fixes and add a unit-test or input validation to prevent
null poster URL updates.

Comment on lines +193 to +205
// 기존 이미지 조회 (filePath + contentId 기준)
Image existingImage = imageRepository
.findByFilePathAndContentId(filePath, contentId);

// 기존 이미지가 있고, keyName이 다르면 삭제 후 새로 저장
if (existingImage != null) {
// keyName 변경이면 S3/DB 모두 삭제
if (!existingImage.getKeyName().equals(keyName)) {
deleteImage(existingImage.getId(), memberId);}
else {// 같은 keyName이면 DB row만 교체(중복 방지), S3는 유지
imageRepository.delete(existingImage);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find ImageRepository interface definition
echo "=== Searching for ImageRepository interface ==="
rg -n "interface ImageRepository|findByFilePathAndContentId" --type=java -A 5

# 2) Find Image entity class
echo "=== Searching for Image entity class ==="
rg -n "class Image\b" --type=java -B 3 -A 30 | head -100

# 3) Search for unique constraints or indexes in entity or annotations
echo "=== Searching for unique constraints/indexes ==="
rg -n "@UniqueConstraint|@Index|uniqueConstraints|@Column.*unique" --type=java -B 2 -A 2

Repository: SeeATheater/CC_Backend

Length of output: 8773


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for migration files or SQL schema definitions
echo "=== Searching for migrations or schema files ==="
fd -e 'sql' -e 'yml' -e 'yaml' | head -20

echo ""
echo "=== Searching for Flyway/Liquibase migrations ==="
find . -type f \( -name "V*.sql" -o -name "*.sql" \) 2>/dev/null | head -20

echo ""
echo "=== Checking for unique constraint in schema files ==="
rg -n "UNIQUE|unique|constraint" -g '*.sql' -C 2 | grep -i "file_path\|content_id\|image" -C 2 || echo "No matches found"

Repository: SeeATheater/CC_Backend

Length of output: 565


Add unique constraint on (filePath, contentId) to prevent duplicate image rows.

findByFilePathAndContentId() at line 195 assumes a single row exists, but the database does not enforce uniqueness on this combination. The @Index(name = "idx_file_content", columnList = "file_path, content_id") on the Image entity is for query performance only and allows duplicates. Additionally, the repository defines both findByFilePathAndContentId() (singular) and findAllByFilePathAndContentId() (plural), indicating duplicates are possible. If duplicates occur, only the first result is returned with undefined ordering, risking data inconsistency.

Add uniqueConstraints = @UniqueConstraint(columnNames = {"file_path", "content_id"}) to the @Table annotation on the Image entity to enforce this constraint at the database level.

🤖 Prompt for AI Agents
In src/main/java/cc/backend/image/service/ImageService.java around lines 193 to
205, the code assumes (filePath, contentId) uniqueness but the DB lacks
enforcement; update the Image entity's @Table annotation (where Image is
defined) to include uniqueConstraints = @UniqueConstraint(columnNames =
{"file_path","content_id"}) so the database enforces uniqueness, create and run
a migration to add that unique constraint (cleaning or deduping existing rows
first as part of the migration), and ensure repository/logic continues to use
singular findByFilePathAndContentId() once the constraint is added.

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

Caution

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

⚠️ Outside diff range comments (1)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (1)

237-264: updateNotice: image update may be skipped for newly created notice
When creating a new notice (Lines 243–246), AmateurNotice notice = amateurShow.getAmateurNotice() (Line 253) can still be null depending on how the association is managed, so the image update can silently skip. Prefer carrying the saved/new notice reference through and using that for contentId.

 private void updateNotice(AmateurShow amateurShow, AmateurUpdateRequestDTO.UpdateNotice noticeDTO) {
+    AmateurNotice notice = null;
     if (noticeDTO != null) {
         AmateurNotice existing = amateurShow.getAmateurNotice();
         if (existing != null) {
             existing.update(noticeDTO);
+            notice = existing;
         } else {
             AmateurNotice newNotice = AmateurConverter.toAmateurNoticeEntity(noticeDTO, amateurShow);
             if (newNotice != null) {
-                amateurNoticeRepository.save(newNotice);
+                notice = amateurNoticeRepository.save(newNotice);
             }
         }
     }

     //NoticeImage 수정
     if (noticeDTO == null) return;
-
-    AmateurNotice notice = amateurShow.getAmateurNotice();
     ImageRequestDTO.NoticeImageRequestDTO dto = noticeDTO.getNoticeImageRequestDTO();
     if (notice != null && dto != null && dto.getKeyName() != null && !dto.getKeyName().isBlank()){
         imageService.updateShowImage(
             amateurShow.getMember().getId(),
             dto.getKeyName(),
             Optional.ofNullable(dto.getImageUrl()),
             notice.getId(),
             FilePath.notice
         );
     }
 }
♻️ Duplicate comments (3)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (3)

79-85: Fix potential NPE: poster DTO can be null; also consider AFTER_COMMIT for NewShowEvent
requestDTO.getPosterImageRequestDTO() can be null, causing an NPE at Line 82. Also, publishing NewShowEvent inside the transaction can be observed before commit if any listener is synchronous/reads DB.

 ImageRequestDTO.PosterImageRequestDTO dto = requestDTO.getPosterImageRequestDTO();

 //poster 이미지는 없으면 에러
-if(dto.getKeyName() == null || dto.getKeyName().isBlank()){
+if (dto == null || dto.getKeyName() == null || dto.getKeyName().isBlank()) {
     throw new GeneralException(ErrorStatus.INVALID_S3_KEY);
 }

Recommended: ensure the event listener uses @TransactionalEventListener(phase = AFTER_COMMIT) (or equivalent) if it depends on committed data.

Also applies to: 93-105


200-211: Poster update can wipe stored URL when imageUrl is null
If FE sends keyName without imageUrl, Line 210 sets posterImageUrl to null. Either require imageUrl when keyName is present, or only update the field when imageUrl is non-null.

 if (dto != null && dto.getKeyName() != null && !dto.getKeyName().isBlank()) {
     imageService.updateShowImage(
         memberId,
         dto.getKeyName(),
         Optional.ofNullable(dto.getImageUrl()),
         amateurShow.getId(),
         FilePath.amateurShow
     );
-
-    amateurShow.updatePosterImageUrl(dto.getImageUrl());
+    if (dto.getImageUrl() != null && !dto.getImageUrl().isBlank()) {
+        amateurShow.updatePosterImageUrl(dto.getImageUrl());
+    }
 }

413-416: Fix critical NPE in deleteShow when poster image row is missing
findByFilePathAndContentId(...) can return null; Line 415 will NPE.

 //포스터 삭제
 Image posterImg = imageRepository.findByFilePathAndContentId(FilePath.amateurShow, amateurShowId);
-imageService.deleteImage(posterImg.getId(), memberId);
+if (posterImg != null) {
+    imageService.deleteImage(posterImg.getId(), memberId);
+}
🧹 Nitpick comments (1)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (1)

119-138: Casting image save: contentId/filePath split looks correct; avoid double-fetching keyName
Logic is fine (skip null/blank key; FilePath.casting, contentId = amateurCasting.getId()). Minor cleanup: reuse keyName local when building DTO to avoid divergence.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f3190c and dd59382.

📒 Files selected for processing (2)
  • src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (8 hunks)
  • src/main/java/cc/backend/image/service/ImageService.java (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/cc/backend/image/service/ImageService.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (4)
src/main/java/cc/backend/image/DTO/ImageRequestDTO.java (1)
  • ImageRequestDTO (12-66)
src/main/java/cc/backend/amateurShow/converter/AmateurConverter.java (1)
  • AmateurConverter (17-281)
src/main/java/cc/backend/amateurShow/entity/AmateurNotice.java (1)
  • Entity (11-41)
src/main/java/cc/backend/amateurShow/entity/AmateurShow.java (1)
  • updatePosterImageUrl (148-150)
🔇 Additional comments (2)
src/main/java/cc/backend/amateurShow/service/amateurShowService/AmateurServiceImpl.java (2)

146-161: Notice image save: null-guard is good; verify noticeImageKeyName is persisted if needed
Good: noticeImageDTO null-checked before dereference and FilePath.notice, contentId = amateurNotice.getId(). If the domain needs AmateurNotice.noticeImageKeyName persisted too, confirm it’s set somewhere (entity update sets it, but enroll converter currently doesn’t).


266-316: updateCasting: good fix to persist new casting before image update; consider cleanup on removals
Nice: new castings are saved first so contentId isn’t null, and images are keyed by FilePath.casting + castingId.
However, when castings are removed (Lines 308–311), their Image rows/S3 objects aren’t deleted here—verify you won’t leak orphan images on updates (depending on your Image cleanup strategy).

@sweatbuckets sweatbuckets merged commit f55b13a into develop Dec 13, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants