Skip to content

feat: 이미지 변경 API 구현#232

Merged
yooooonshine merged 1 commit intodevelopfrom
feature/230-implement-update-user-profile
Oct 26, 2025
Merged

feat: 이미지 변경 API 구현#232
yooooonshine merged 1 commit intodevelopfrom
feature/230-implement-update-user-profile

Conversation

@yooooonshine
Copy link
Contributor

@yooooonshine yooooonshine commented Oct 26, 2025

개요

작업사항

  • 유저 프로필 변경 API 구현

Summary by CodeRabbit

릴리스 노트

  • 새 기능

    • 사용자 이미지 업데이트 기능 추가 - 사용자가 프로필 이미지를 변경할 수 있는 새로운 API 엔드포인트 제공
  • 개선

    • 이미지 관련 오류 처리 메커니즘을 더욱 체계적으로 개선하여 일관된 오류 응답 제공

@coderabbitai
Copy link

coderabbitai bot commented Oct 26, 2025

Walkthrough

사용자 프로필 이미지 업데이트 기능을 구현합니다. 도메인 모델에 updateImage() 메서드를 추가하고, 서비스 계층에서 검증 및 기존 이미지 삭제를 처리한 후, 컨트롤러에 PATCH 엔드포인트를 추가합니다. 에러 핸들링을 ApiErrorMapping 어노테이션으로 중앙화합니다.

Changes

Cohort / File(s) 변경 사항
도메인 모델 업데이트
src/main/java/hanium/modic/backend/domain/image/domain/Image.java
이미지 경로, 파일명, 확장자, 용도를 일괄 업데이트하는 updateImage() 메서드 추가
서비스 계층 로직
src/main/java/hanium/modic/backend/domain/user/service/UserImageService.java
이미지 소유권 검증, 중복 확인, 파일명 파싱, 기존 이미지 삭제를 포함하는 트랜잭션 updateImage() 메서드 추가
컨트롤러 엔드포인트 및 에러 처리
src/main/java/hanium/modic/backend/web/user/controller/UserImageController.java
PATCH 엔드포인트 updateUserImage() 추가 및 기존 엔드포인트의 에러 핸들링을 ApiErrorMapping 어노테이션으로 중앙화

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Controller as UserImageController
    participant Service as UserImageService
    participant Validation as ValidationService
    participant Repository as UserImageRepository
    participant Util as ImageUtil
    participant Domain as Image Domain

    User->>Controller: PATCH /user/image<br/>(imagePath, imagePrefix)
    Controller->>Service: updateImage(userId, imagePrefix, filename, path)
    
    rect rgb(240, 248, 255)
        Note over Service: 검증 단계
        Service->>Repository: findByUserId(userId)
        Service->>Validation: validateDuplicatedImagePath(path)
        Service->>Validation: validateUserImageOwnership(userId, image)
        Service->>Service: parseImageName(filename)
    end
    
    rect rgb(240, 255, 240)
        Note over Service: 업데이트 단계
        Service->>Domain: updateImage(path, filename, name, extension, prefix)
        Service->>Repository: save(userImageEntity)
    end
    
    rect rgb(255, 250, 240)
        Note over Service: 정리 단계
        Service->>Util: deleteFile(oldImagePath)
    end
    
    Service->>Controller: return UserImageEntity
    Controller->>User: 200 OK<br/>(CallbackImageSaveUrlResponse)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • UserImageService.updateImage(): 순차적 검증 로직, 파일 핸들링, 트랜잭션 관리 흐름 검토 필요
  • 컨트롤러 에러 핸들링: 기존 인라인 ApiResponse에서 ApiErrorMapping으로의 마이그레이션 일관성 확인
  • 도메인 모델: 상태 변경 메서드의 정합성과 불변성 고려사항 검토

Poem

🐰 이미지를 갈아 입을 때,
검증은 든든히, 옛것은 깔끔히,
한 줄의 updateImage()
세 겹의 계층을 누비며,
사용자의 새 얼굴 완성! ✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning raw_summary에 따르면 UserImageController의 기존 엔드포인트들(createImageSaveUrl, callbackImageSaveUrl, createImageGetUrl, deleteImage)에서 인라인 ApiResponse 에러 정의를 제거하고 새로운 ApiErrorMapping 어노테이션으로 교체하는 리팩토링이 수행되었습니다. 이 변경사항은 PR 제목과 설명에 명시된 "사용자 프로필 변경 API 구현"의 핵심 요구사항과 직접적인 관련이 없으며, 링크된 이슈 #230에도 포함되어 있지 않습니다. 이는 기존 코드의 에러 처리 메커니즘을 리팩토링하는 추가적인 범위 외 변경으로 보입니다. 기존 엔드포인트들의 ApiErrorMapping 리팩토링 작업을 별도 PR로 분리하거나, 이 변경사항이 새로운 updateUserImage 엔드포인트 구현에 필수적인 이유를 PR 설명에 명확히 기술해야 합니다. 현재 상태에서는 사용자 프로필 이미지 변경 API 구현이라는 PR의 주요 목표와 무관한 범위 외 변경으로 간주됩니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "feat: 이미지 변경 API 구현"은 변경사항의 핵심 내용과 정확히 일치합니다. raw_summary에서 Image 클래스의 updateImage 메서드, UserImageService의 updateImage 메서드, 그리고 UserImageController의 updateUserImage 엔드포인트가 추가된 것이 확인되며, 이는 모두 사용자 프로필 이미지 변경 API를 구현하는 것으로 수렴합니다. 제목은 간결하고 명확하며, 팀원들이 변경 이력을 스캔할 때 주요 변경사항을 쉽게 파악할 수 있습니다.
Linked Issues Check ✅ Passed 링크된 이슈 #230의 요구사항은 "유저 프로필 변경 API 구현"이며, 이 PR의 코드 변경사항이 이를 완벽히 충족합니다. Image 도메인 클래스에 updateImage 메서드가 추가되었고, UserImageService에 검증 및 파일 처리 로직이 포함된 updateImage 메서드가 구현되었으며, UserImageController에 새로운 PATCH 엔드포인트인 updateUserImage가 추가되었습니다. 이 모든 변경사항은 사용자 프로필 이미지 변경 API를 구현하기 위한 필수 요소들입니다.
Description Check ✅ Passed PR 설명이 저장소의 필수 템플릿인 "개요"와 "작업사항" 두 섹션을 모두 포함하고 있습니다. 개요 섹션에서는 관련 이슈 #230을 참조하고 있으며, 작업사항 섹션에서는 "유저 프로필 변경 API 구현"이라는 구체적인 작업 내용이 기술되어 있습니다. 두 섹션 모두 채워져 있으므로 기본 템플릿 요구사항을 충족합니다.
✨ 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 feature/230-implement-update-user-profile

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.

@yooooonshine yooooonshine merged commit 4c0379e into develop Oct 26, 2025
1 of 2 checks passed
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: 1

🧹 Nitpick comments (5)
src/main/java/hanium/modic/backend/domain/image/domain/Image.java (1)

40-52: null 값 조기 검증으로 무결성 보장

updateImage가 @column(nullable = false) 필드들에 그대로 대입합니다. null이 유입되면 커밋 시점에야 폭발하므로 메서드 진입 시 즉시 검증하는 편이 안전합니다.

적용 예시:

 public void updateImage(
   String imagePath,
   String fullImageName,
   String imageName,
   ImageExtension extension,
   ImagePrefix imagePurpose
 ) {
-  this.imagePath = imagePath;
-  this.fullImageName = fullImageName;
-  this.imageName = imageName;
-  this.extension = extension;
-  this.imagePurpose = imagePurpose;
+  this.imagePath = Objects.requireNonNull(imagePath, "imagePath");
+  this.fullImageName = Objects.requireNonNull(fullImageName, "fullImageName");
+  this.imageName = Objects.requireNonNull(imageName, "imageName");
+  this.extension = Objects.requireNonNull(extension, "extension");
+  this.imagePurpose = Objects.requireNonNull(imagePurpose, "imagePurpose");
 }

추가 import:

import java.util.Objects;
src/main/java/hanium/modic/backend/domain/user/service/UserImageService.java (2)

119-123: 경로 중복 검증이 자기 자신까지 포함됨(업데이트 불가 케이스) + 소유권 검증 중복 가능성

  • 기존 경로를 그대로 두는 업데이트에서도 existsByImagePath(imagePath)가 true가 되어 불필요하게 실패할 수 있습니다. 자기 자신은 제외하거나, 경로가 불변이면 스킵하세요.
  • findByUserId로 이미 본인 레코드를 조회했으므로 추가 소유권 검증은 중복일 수 있습니다(정책에 따라 유지 가능).

최소 변경 예:

-    imageValidationService.validateImageSaved(imagePath);
-    validateDuplicatedImagePath(imagePath);
-    validateUserImageOwnership(userId, userImage.getId());
+    imageValidationService.validateImageSaved(imagePath);
+    if (!imagePath.equals(oldImagePath)) {
+        validateDuplicatedImagePath(imagePath);
+    }
+    // 필요 시 정책에 따라 유지: validateUserImageOwnership(userId, userImage.getId());

검증 강화(선택): 저장소에 existsByImagePathAndIdNot(String path, Long id)를 추가해 현재 레코드 제외 검증을 권장합니다.


134-135: save 호출 불필요 — 더티 체킹으로 반영됨

userImage는 영속 상태이므로 save 재호출 없이 변경사항이 플러시/커밋됩니다. 불필요한 DB I/O 제거를 권장합니다.

-    userImage = userImageRepository.save(userImage);
src/main/java/hanium/modic/backend/web/user/controller/UserImageController.java (2)

111-117: PATCH 엔드포인트의 오류 매핑에 IMAGE_NOT_FOUND_EXCEPTION 누락

updateImage는 기존 이미지를 찾지 못하면 IMAGE_NOT_FOUND_EXCEPTION을 던집니다. 문서/핸들링 일관성을 위해 매핑에 포함하는 것을 권장합니다.

 @ApiErrorMapping({
     USER_INPUT_EXCEPTION,
     IMAGE_NOT_STORE_EXCEPTION,
     INVALID_IMAGE_FILE_NAME_EXCEPTION,
     IMAGE_PATH_DUPLICATED_EXCEPTION,
-    USER_ROLE_EXCEPTION
+    USER_ROLE_EXCEPTION,
+    IMAGE_NOT_FOUND_EXCEPTION
 })

118-130: 요청 DTO 명/의도 명확화 제안

PATCH에서도 CallbackImageSaveUrlRequest를 재사용하면 “저장 콜백”과 “변경”의 의미가 모호해질 수 있습니다. UpdateUserImageRequest 등 의미가 드러나는 DTO로 분리하면 API 사용성이 좋아집니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 797cb97 and 01fb1f8.

📒 Files selected for processing (3)
  • src/main/java/hanium/modic/backend/domain/image/domain/Image.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/user/service/UserImageService.java (1 hunks)
  • src/main/java/hanium/modic/backend/web/user/controller/UserImageController.java (5 hunks)

Comment on lines +136 to +139
// 기존 이미지 삭제
imageUtil.deleteImage(userImage.getImagePath());

return userImage;
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

새 이미지가 삭제되는 치명적 버그 + 커밋 이후 삭제 필요

userImage.updateImage 이후 userImage.getImagePath()는 “새 경로”를 가리킵니다. 현재 코드는 새 이미지를 지우게 됩니다. 또한 파일 삭제는 트랜잭션 커밋 후 실행해야 롤백 시 데이터 손실을 막을 수 있습니다.

권장 수정:

-    // 기존 이미지 삭제
-    imageUtil.deleteImage(userImage.getImagePath());
+    // 기존 이미지 삭제는 트랜잭션 커밋 후 실행
+    final String oldImagePathFinal = oldImagePath; // 아래 diff에서 oldImagePath를 추가합니다.
+    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+        @Override
+        public void afterCommit() {
+            imageUtil.deleteImage(oldImagePathFinal);
+        }
+    });

위 수정이 동작하려면, 기존 이미지 경로를 업데이트 전에 보관하세요:

-    UserImageEntity userImage = userImageRepository.findByUserId(userId)
-        .orElseThrow(() -> new AppException(IMAGE_NOT_FOUND_EXCEPTION));
+    UserImageEntity userImage = userImageRepository.findByUserId(userId)
+        .orElseThrow(() -> new AppException(IMAGE_NOT_FOUND_EXCEPTION));
+    final String oldImagePath = userImage.getImagePath();

필요 import:

import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
🤖 Prompt for AI Agents
In src/main/java/hanium/modic/backend/domain/user/service/UserImageService.java
around lines 136 to 139, the code deletes userImage.getImagePath() after calling
userImage.updateImage(...) which now points to the new image — causing the new
image to be removed — and the file deletion is executed inside the transaction
(risking data loss on rollback). Fix by capturing the existing image path into a
local variable before calling updateImage(), then register a
TransactionSynchronization via
TransactionSynchronizationManager.registerSynchronization so that
imageUtil.deleteImage(oldPath) runs in afterCommit(); also add null/empty checks
and skip deletion if oldPath equals the new path. Ensure you import
org.springframework.transaction.support.TransactionSynchronization and
org.springframework.transaction.support.TransactionSynchronizationManager.

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.

유저 프로필 변경 API 구현

1 participant