Skip to content

[REFACTOR] 원본 이미지 대신 압축된 썸네일을 반환하도록 수정#140

Merged
jaemin0413 merged 5 commits intodevfrom
refactor/#128_image-thumbnail
Feb 14, 2026
Merged

[REFACTOR] 원본 이미지 대신 압축된 썸네일을 반환하도록 수정#140
jaemin0413 merged 5 commits intodevfrom
refactor/#128_image-thumbnail

Conversation

@jaemin0413
Copy link
Member

@jaemin0413 jaemin0413 commented Feb 14, 2026

🎋 이슈 및 작업중인 브랜치

🔑 주요 내용

  • aws lambda를 도입해 이미지 업로드시 악성파일 검증, 썸네일 생성
  • 이미지 상세조회 api를 제외한 다른 모든 이미지 조회 api에서 이미지 썸네일을 반환하도록 수정
  • 이미지 엔티티에 thumbnail_url 컬럼 추가

Check List

  • Reviewers 등록을 하였나요?
  • Assignees 등록을 하였나요?
  • 라벨(Label) 등록을 하였나요?
  • PR 머지하기 전 반드시 CI가 정상적으로 작동하는지 확인해주세요!

Summary by CodeRabbit

  • Bug Fixes

    • 앱 전반에 썸네일 이미지 우선 적용(없을 땐 원본 자동 대체)로 로딩·표시 안정성 개선
    • 이미지 이벤트 처리 흐름 정비로 업로드/처리 실패 로그 및 재시도 가시성 향상
  • Refactor

    • 이미지 관리 및 저장소 처리 방식 개선으로 전송·삭제 동작 신뢰성 향상

@jaemin0413 jaemin0413 self-assigned this Feb 14, 2026
@jaemin0413 jaemin0413 added the ♻️ REFACTOR 리팩토링 관련 라벨 label Feb 14, 2026
@jaemin0413 jaemin0413 linked an issue Feb 14, 2026 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

이 PR은 이미지 URL 참조를 썸네일 URL로 전환하고 Image 엔티티에 thumbnailUrl을 추가합니다. S3 이벤트/경로 처리(S3EventDto 단순화, presign 경로를 raw/로 변경, 삭제 시 raw/ 적용)와 관련된 이미지 업로드·처리 로직도 정리됩니다.

Changes

Cohort / File(s) Summary
DTO 응답 매핑
src/main/java/com/umc/nuvibe/domain/archive/dto/response/BoardDetailResponse.java, src/main/java/com/umc/nuvibe/domain/archive/dto/response/BoardImageResponse.java, src/main/java/com/umc/nuvibe/domain/archive/dto/response/RecapDataResponse.java, src/main/java/com/umc/nuvibe/domain/tribe/dto/response/chat/ChatDetailRes.java, src/main/java/com/umc/nuvibe/domain/tribe/dto/response/chat/ChatGridItemRes.java, src/main/java/com/umc/nuvibe/domain/tribe/dto/response/chat/ChatTimelineItemRes.java
응답의 imageUrl 매핑을 기존 getImageUrl()에서 getThumbnailUrl()로 변경(일부에 주석 추가).
서비스 계층 이미지 URL 처리
src/main/java/com/umc/nuvibe/domain/archive/service/ArchiveBoardServiceImpl.java, src/main/java/com/umc/nuvibe/domain/archive/service/RecapServiceImpl.java, src/main/java/com/umc/nuvibe/domain/home/service/HomeServiceImpl.java, src/main/java/com/umc/nuvibe/domain/tribe/service/chat/ChatServiceImpl.java
서비스에서 반환하던 이미지 URL들을 썸네일 기반으로 변경(데이터 소스 교체, 로직 흐름은 동일).
이미지 엔티티 및 S3 DTO/서비스 변경
src/main/java/com/umc/nuvibe/domain/image/entity/Image.java, src/main/java/com/umc/nuvibe/domain/image/dto/response/S3EventDto.java, src/main/java/com/umc/nuvibe/domain/image/service/ImageServiceImpl.java
Image에 thumbnailUrl 필드·접근자·업데이트 메서드 추가, S3EventDto를 중첩 레코드에서 단일 평면 레코드로 단순화, presign 경로 접두사를 imagesraw로 변경하고 반환 파일명에서 raw/ 제거.
저장소·스케줄러·이벤트 리스너
src/main/java/com/umc/nuvibe/domain/tribe/repository/ScrapedImageRepository.java, src/main/java/com/umc/nuvibe/global/scheduler/ImageCleanupScheduler.java, src/main/java/com/umc/nuvibe/global/service/ImageEventListener.java
JPQL에서 COALESCE(img.thumbnailUrl, img.imageUrl)로 썸네일 폴백 추가, S3 삭제 시 raw/<filename> 경로 사용, Lambda S3 이벤트 처리 로직 단순화 및 DB 갱신 방식 변경.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ⚠️ Unable to check for merge conflicts: Invalid branch name format
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경사항의 핵심을 정확하게 반영합니다: 원본 이미지 대신 압축된 썸네일 반환. 광범위한 리팩토링에서 주요 변경 사항을 명확하고 간결하게 설명합니다.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#128_image-thumbnail
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch refactor/#128_image-thumbnail
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉


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

Caution

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

⚠️ Outside diff range comments (2)
src/main/java/com/umc/nuvibe/domain/home/service/HomeServiceImpl.java (1)

78-83: ⚠️ Potential issue | 🟡 Minor

Collectors.toMap에서 getThumbnailUrl()null을 반환하면 NPE 발생

getThumbnailUrl()thumbnailUrlimageUrl이 모두 null인 경우 null을 반환합니다. Collectors.toMapnull 값을 허용하지 않아 NullPointerException이 발생합니다.

imageUrl이 항상 설정되어 있다면 실질적 문제는 아니지만, 방어적으로 처리하는 것이 안전합니다.

방어적 처리 예시
         Map<Long, String> thumbnailMap = boardImageRepository.findLatestByBoardIds(boardIds)
                         .stream()
                         .collect(Collectors.toMap(
                                         bi -> bi.getBoard().getId(),
-                                        bi -> bi.getImage().getThumbnailUrl(),
+                                        bi -> Optional.ofNullable(bi.getImage().getThumbnailUrl()).orElse(""),
                                         (existing, replacement) -> existing));
src/main/java/com/umc/nuvibe/domain/tribe/dto/response/chat/ChatGridItemRes.java (1)

19-20: 🧹 Nitpick | 🔵 Trivial

Swagger @Schema 설명이 실제 반환 값과 불일치

imageUrl 필드가 이제 썸네일 URL을 반환하지만, @Schema(description = "이미지 URL (S3 경로)")는 여전히 원본 이미지를 암시합니다. API 문서의 정확성을 위해 설명을 업데이트하는 것을 권장합니다. 동일한 패턴을 사용하는 다른 DTO(ChatDetailRes, ChatTimelineItemRes 등)도 확인해 보세요.

🤖 Fix all issues with AI agents
In `@src/main/java/com/umc/nuvibe/domain/image/entity/Image.java`:
- Around line 49-54: getThumbnailUrl() falls back to imageUrl when thumbnailUrl
is null or blank but the JPQL uses COALESCE(img.thumbnailUrl, img.imageUrl)
which treats empty string as a valid value; make behavior consistent by either
updating the JPQL in ScrapedImageRepository to treat empty string as null (e.g.,
use COALESCE(NULLIF(img.thumbnailUrl, ''), img.imageUrl) or a CASE/NULLIF
variant) or normalize blank thumbnailUrl to null on persist/update (e.g., in the
Image entity setter for thumbnailUrl or in the repository before saving) so
getThumbnailUrl(), the DB query, and stored data all behave the same.

In `@src/main/java/com/umc/nuvibe/domain/image/service/ImageServiceImpl.java`:
- Around line 43-44: In ImageServiceImpl where you compute pureFileName from
preSignedUrl.fileName(), don't use String.replace which removes all "raw/"
occurrences; instead call replaceFirst with a prefix-only regex (e.g.,
replaceFirst("^raw/", "")) or explicitly check startsWith and strip the leading
segment so only the leading "raw/" is removed (update the assignment to
pureFileName accordingly).

In
`@src/main/java/com/umc/nuvibe/domain/tribe/dto/response/chat/ChatDetailRes.java`:
- Line 36: ChatDetailRes currently returns image.getThumbnailUrl() but per PR
requirements the 채팅 이미지 상세 조회 응답 must return the original image URL; update the
response construction in ChatDetailRes to use image.getImageUrl() (replace any
reference to getThumbnailUrl() for the detailed response) so the original image
URL is returned while keeping thumbnail usage unchanged elsewhere.

In `@src/main/java/com/umc/nuvibe/global/scheduler/ImageCleanupScheduler.java`:
- Around line 56-64: The PENDING-image cleanup only deletes the raw/ object and
can leave an orphaned thumbnail; update ImageCleanupScheduler to also delete the
thumbnail when image.getThumbnailUrl() is non-null/non-blank: extract the S3 key
from image.getThumbnailUrl() (or use it directly if your s3Service expects a
key), call s3Service.deleteFile(thumbnailKey) the same way you call
s3Service.deleteFile(rawPath), and log success/failure similarly; place this
logic alongside the existing raw/ deletion and use image.getThumbnailUrl(),
image.getFileName(), and s3Service.deleteFile(...) to locate and remove the
thumbnail.

In `@src/main/java/com/umc/nuvibe/global/service/ImageEventListener.java`:
- Around line 51-54: The current ImageEventListener lambda logs orphaned fileIds
but doesn't remove the corresponding S3 objects; update the lambda in
ImageEventListener where log.warn("DB에 존재하지 않는 fileId: {}", event.fileId()) is
called to remove S3 originals/thumbnails for that event.fileId(): build the S3
object keys (original and thumbnail naming used elsewhere), call the S3 client
delete (or batch delete) for those keys (or enqueue the keys to an async cleanup
queue/job if you prefer eventual deletion), handle and log any exceptions from
the S3 operation, and ensure deletions are idempotent so repeated events won't
fail.
- Around line 22-26: The required-field check should also validate the event's
status to make intent explicit: add event.status() == null to the existing
null-check condition that currently tests event.fileId(), event.originalUrl(),
and event.thumbnailUrl(); update the if condition in ImageEventListener to
return after logging the same warning if any of those including event.status()
are null so the later `"SUCCESS".equals(event.status())` scenario is never
evaluated against null.

Comment on lines +49 to +54
// 썸네일 URL이 없으면 원본 URL을 반환
public String getThumbnailUrl() {
return (thumbnailUrl != null && !thumbnailUrl.isBlank())
? thumbnailUrl
: imageUrl;
}
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

getThumbnailUrl()의 폴백 로직과 JPQL COALESCE의 동작 불일치

이 메서드는 thumbnailUrlnull이거나 빈 문자열일 때 imageUrl로 폴백하지만, ScrapedImageRepository의 JPQL에서는 COALESCE(img.thumbnailUrl, img.imageUrl)을 사용하여 NULL만 체크합니다. thumbnailUrl이 빈 문자열("")인 경우 Java 측에서는 imageUrl을 반환하지만 JPQL 측에서는 빈 문자열을 그대로 반환하게 됩니다.

일관성을 위해 JPQL 쿼리에서도 빈 문자열을 처리하거나, 저장 시점에서 빈 문자열이 들어오지 않도록 보장해야 합니다.

JPQL 쪽 수정 예시 (ScrapedImageRepository)
- COALESCE(img.thumbnailUrl, img.imageUrl)
+ CASE WHEN img.thumbnailUrl IS NOT NULL AND img.thumbnailUrl <> '' THEN img.thumbnailUrl ELSE img.imageUrl END
🤖 Prompt for AI Agents
In `@src/main/java/com/umc/nuvibe/domain/image/entity/Image.java` around lines 49
- 54, getThumbnailUrl() falls back to imageUrl when thumbnailUrl is null or
blank but the JPQL uses COALESCE(img.thumbnailUrl, img.imageUrl) which treats
empty string as a valid value; make behavior consistent by either updating the
JPQL in ScrapedImageRepository to treat empty string as null (e.g., use
COALESCE(NULLIF(img.thumbnailUrl, ''), img.imageUrl) or a CASE/NULLIF variant)
or normalize blank thumbnailUrl to null on persist/update (e.g., in the Image
entity setter for thumbnailUrl or in the repository before saving) so
getThumbnailUrl(), the DB query, and stored data all behave the same.

Comment on lines +43 to +44
// fileName에서 prefix 제거
String pureFileName = preSignedUrl.fileName().replace("raw/", "");
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

replace 대신 replaceFirst 사용 권장

String.replace("raw/", "")는 문자열 내 모든 "raw/" 발생을 제거합니다. 접두사만 제거하려는 의도라면 replaceFirst가 더 안전합니다.

수정 제안
-        String pureFileName = preSignedUrl.fileName().replace("raw/", "");
+        String pureFileName = preSignedUrl.fileName().replaceFirst("^raw/", "");
📝 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
// fileName에서 prefix 제거
String pureFileName = preSignedUrl.fileName().replace("raw/", "");
// fileName에서 prefix 제거
String pureFileName = preSignedUrl.fileName().replaceFirst("^raw/", "");
🤖 Prompt for AI Agents
In `@src/main/java/com/umc/nuvibe/domain/image/service/ImageServiceImpl.java`
around lines 43 - 44, In ImageServiceImpl where you compute pureFileName from
preSignedUrl.fileName(), don't use String.replace which removes all "raw/"
occurrences; instead call replaceFirst with a prefix-only regex (e.g.,
replaceFirst("^raw/", "")) or explicitly check startsWith and strip the leading
segment so only the leading "raw/" is removed (update the assignment to
pureFileName accordingly).

Comment on lines +56 to 64
// Lambda 시스템에서는 raw/ prefix를 추가해야 함
if (image.getFileName() != null && !image.getFileName().isBlank()) {
s3Deleted = s3Service.deleteFile(image.getFileName());
String rawPath = "raw/" + image.getFileName();
s3Deleted = s3Service.deleteFile(rawPath);
if (s3Deleted) {
log.info("S3 파일 삭제 성공 - ID: {}, FileName: {}", image.getId(), image.getFileName());
log.info("S3 파일 삭제 성공 - ID: {}, Path: {}", image.getId(), rawPath);
} else {
log.warn("S3 파일 삭제 실패 - ID: {}, FileName: {}", image.getId(), image.getFileName());
log.warn("S3 파일 삭제 실패 - ID: {}, Path: {}", image.getId(), rawPath);
}
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

PENDING 이미지 정리 시 썸네일 파일도 삭제가 필요할 수 있음

Lambda가 raw/ 파일을 처리하여 썸네일을 이미 생성했지만, 이미지 상태가 아직 ACTIVE로 전환되지 않은 경우(경합 조건), raw/ 파일만 삭제하면 S3에 고아 썸네일 파일이 남을 수 있습니다.

발생 확률이 낮은 엣지 케이스이지만, thumbnailUrl이 설정된 PENDING 이미지가 있다면 해당 썸네일 경로도 함께 삭제하는 것을 고려해 보세요.

🤖 Prompt for AI Agents
In `@src/main/java/com/umc/nuvibe/global/scheduler/ImageCleanupScheduler.java`
around lines 56 - 64, The PENDING-image cleanup only deletes the raw/ object and
can leave an orphaned thumbnail; update ImageCleanupScheduler to also delete the
thumbnail when image.getThumbnailUrl() is non-null/non-blank: extract the S3 key
from image.getThumbnailUrl() (or use it directly if your s3Service expects a
key), call s3Service.deleteFile(thumbnailKey) the same way you call
s3Service.deleteFile(rawPath), and log success/failure similarly; place this
logic alongside the existing raw/ deletion and use image.getThumbnailUrl(),
image.getFileName(), and s3Service.deleteFile(...) to locate and remove the
thumbnail.

Comment on lines +22 to 26
// 필수 필드 검증
if (event.fileId() == null || event.originalUrl() == null || event.thumbnailUrl() == null) {
log.warn("잘못된 Lambda 메시지 수신: {}", event);
return;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

status 필드도 null 검증에 포함하는 것이 안전합니다.

현재 status가 null인 경우 line 29의 "SUCCESS".equals(null)이 false를 반환하여 에러로 처리되지만, 이는 의도가 불명확합니다. 필수 필드 검증에 status를 포함하면 의미가 명확해집니다.

♻️ 수정 제안
-        if (event.fileId() == null || event.originalUrl() == null || event.thumbnailUrl() == null) {
+        if (event.fileId() == null || event.originalUrl() == null || event.thumbnailUrl() == null || event.status() == null) {
🤖 Prompt for AI Agents
In `@src/main/java/com/umc/nuvibe/global/service/ImageEventListener.java` around
lines 22 - 26, The required-field check should also validate the event's status
to make intent explicit: add event.status() == null to the existing null-check
condition that currently tests event.fileId(), event.originalUrl(), and
event.thumbnailUrl(); update the if condition in ImageEventListener to return
after logging the same warning if any of those including event.status() are null
so the later `"SUCCESS".equals(event.status())` scenario is never evaluated
against null.

Comment on lines +51 to +54
() -> {
log.warn("DB에 존재하지 않는 fileId: {}", event.fileId());
// Lambda가 생성한 파일이지만 DB에 없는 경우 (고아 파일)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

고아 파일 정리 로직이 누락되어 있습니다.

DB에 존재하지 않는 fileId에 대해 Lambda가 생성한 original/thumbnail 파일이 S3에 남게 됩니다. 시간이 지남에 따라 불필요한 스토리지 비용이 누적될 수 있으므로, S3 파일 삭제 처리 또는 별도 배치 정리 작업을 추가하는 것을 권장합니다.

🤖 Prompt for AI Agents
In `@src/main/java/com/umc/nuvibe/global/service/ImageEventListener.java` around
lines 51 - 54, The current ImageEventListener lambda logs orphaned fileIds but
doesn't remove the corresponding S3 objects; update the lambda in
ImageEventListener where log.warn("DB에 존재하지 않는 fileId: {}", event.fileId()) is
called to remove S3 originals/thumbnails for that event.fileId(): build the S3
object keys (original and thumbnail naming used elsewhere), call the S3 client
delete (or batch delete) for those keys (or enqueue the keys to an async cleanup
queue/job if you prefer eventual deletion), handle and log any exceptions from
the S3 operation, and ensure deletions are idempotent so repeated events won't
fail.

@jaemin0413 jaemin0413 merged commit f671587 into dev Feb 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

♻️ REFACTOR 리팩토링 관련 라벨

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] 람다를 통한 이미지 썸네일 적용

1 participant