Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/main/java/hanium/modic/backend/domain/image/domain/Image.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,18 @@ public class Image extends BaseEntity {
@Enumerated(STRING)
@Column(nullable = false)
private ImagePrefix imagePurpose;

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,41 @@ public UserImageEntity saveImage(
);
}

// 이미지 변경
@Transactional
public UserImageEntity updateImage(
final long userId,
final ImagePrefix imagePrefix,
final String fullFileName,
final String imagePath
) {
// 기존 이미지 조회
UserImageEntity userImage = userImageRepository.findByUserId(userId)
.orElseThrow(() -> new AppException(IMAGE_NOT_FOUND_EXCEPTION));

// 검증(새 이미지 저장되었는지, 중복 경로, 소유권)
imageValidationService.validateImageSaved(imagePath);
validateDuplicatedImagePath(imagePath);
validateUserImageOwnership(userId, userImage.getId());


// 이미지 정보 업데이트
final ParsedImageName parsedImageName = imageUtil.parseFullImageName(fullFileName);
userImage.updateImage(
imagePath,
fullFileName,
parsedImageName.imageName(),
ImageExtension.from(parsedImageName.fileExtension()),
imagePrefix
);
userImage = userImageRepository.save(userImage);

// 기존 이미지 삭제
imageUtil.deleteImage(userImage.getImagePath());

return userImage;
Comment on lines +136 to +139
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.

}

// 중복된 이미지 경로 검증
private void validateDuplicatedImagePath(final String imagePath) {
if (userImageRepository.existsByImagePath(imagePath)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package hanium.modic.backend.web.user.controller;

import static hanium.modic.backend.common.error.ErrorCode.*;
import static org.springframework.http.HttpStatus.*;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import hanium.modic.backend.common.annotation.user.CurrentUser;
import hanium.modic.backend.common.error.ErrorCode;
import hanium.modic.backend.common.response.AppResponse;
import hanium.modic.backend.common.swagger.ApiErrorMapping;
import hanium.modic.backend.domain.image.dto.CreateImageSaveUrlDto;
import hanium.modic.backend.domain.user.entity.UserEntity;
import hanium.modic.backend.domain.user.service.UserImageService;
Expand All @@ -36,12 +40,9 @@ public class UserImageController {
@PostMapping("/save-url")
@Operation(
summary = "사용자 이미지 저장 URL 생성 API",
description = "사용자 이미지 저장을 위한 URL을 생성합니다.",
responses = {
@ApiResponse(responseCode = "400", description = "사용자 입력 오류[C-001]"),
@ApiResponse(responseCode = "400", description = "잘못된 이미지 파일 이름입니다.[I-003]")
}
description = "사용자 이미지 저장을 위한 URL을 생성합니다."
)
@ApiErrorMapping({USER_INPUT_EXCEPTION, INVALID_IMAGE_FILE_NAME_EXCEPTION})
public ResponseEntity<AppResponse<CreateImageSaveUrlResponse>> createImageSaveUrl(
@RequestBody @Valid CreateImageSaveUrlRequest request
) {
Expand All @@ -56,14 +57,9 @@ public ResponseEntity<AppResponse<CreateImageSaveUrlResponse>> createImageSaveUr
@PostMapping("/save-url/callback")
@Operation(
summary = "사용자 이미지 저장 콜백 API",
description = "사용자 이미지 저장 완료 후 호출되는 콜백 API입니다.",
responses = {
@ApiResponse(responseCode = "400", description = "사용자 입력 오류[C-001]"),
@ApiResponse(responseCode = "400", description = "이미지가 저장되지 않았습니다.[I-001]"),
@ApiResponse(responseCode = "400", description = "잘못된 이미지 파일 이름입니다.[I-003]"),
@ApiResponse(responseCode = "409", description = "이미지 경로가 중복되었습니다.[I-005]")
}
description = "사용자 이미지 저장 완료 후 호출되는 콜백 API입니다."
)
@ApiErrorMapping({USER_INPUT_EXCEPTION, IMAGE_NOT_STORE_EXCEPTION, INVALID_IMAGE_FILE_NAME_EXCEPTION, IMAGE_PATH_DUPLICATED_EXCEPTION})
public ResponseEntity<AppResponse<CallbackImageSaveUrlResponse>> callbackImageSaveUrl(
@RequestBody @Valid CallbackImageSaveUrlRequest request,
@CurrentUser UserEntity user
Expand All @@ -82,11 +78,9 @@ public ResponseEntity<AppResponse<CallbackImageSaveUrlResponse>> callbackImageSa
@GetMapping("/get-url")
@Operation(
summary = "사용자 이미지 조회 URL 생성 API",
description = "사용자 이미지는 public이므로 URL을 직접 반환합니다.",
responses = {
@ApiResponse(responseCode = "404", description = "해당 이미지를 찾을 수 없습니다.[I-002]")
}
description = "사용자 이미지는 public이므로 URL을 직접 반환합니다."
)
@ApiErrorMapping({IMAGE_NOT_FOUND_EXCEPTION})
public ResponseEntity<AppResponse<CreateImageGetUrlResponse>> createImageGetUrl(
@CurrentUser UserEntity user
) {
Expand All @@ -98,17 +92,40 @@ public ResponseEntity<AppResponse<CreateImageGetUrlResponse>> createImageGetUrl(
@DeleteMapping("/{imageId}")
@Operation(
summary = "사용자 이미지 삭제 API",
description = "사용자 이미지를 삭제합니다. 변경 필요 시 삭제 요청 후 생성",
responses = {
@ApiResponse(responseCode = "404", description = "해당 이미지를 찾을 수 없습니다.[I-002]"),
@ApiResponse(responseCode = "403", description = "유저 권한 오류[C-002]")
}
description = "사용자 이미지를 삭제합니다. 변경 필요 시 삭제 요청 후 생성"
)
@ApiErrorMapping({IMAGE_NOT_FOUND_EXCEPTION, USER_ROLE_EXCEPTION})
public ResponseEntity<AppResponse<Void>> deleteImage(
@CurrentUser UserEntity user,
@PathVariable Long imageId
) {
userImageService.deleteImage(user.getId(), imageId);
return ResponseEntity.status(NO_CONTENT).body(AppResponse.noContent());
}

@PatchMapping
@Operation(
summary = "사용자 이미지 변경 콜백 API",
description = "사용자 이미지를 변경합니다. 기존 이미지는 삭제됩니다."
)
@ApiErrorMapping({
USER_INPUT_EXCEPTION,
IMAGE_NOT_STORE_EXCEPTION,
INVALID_IMAGE_FILE_NAME_EXCEPTION,
IMAGE_PATH_DUPLICATED_EXCEPTION,
USER_ROLE_EXCEPTION
})
public ResponseEntity<AppResponse<CallbackImageSaveUrlResponse>> updateUserImage(
@RequestBody @Valid CallbackImageSaveUrlRequest request,
@CurrentUser UserEntity user
) {
Long id = userImageService.updateImage(
user.getId(),
request.imageUsagePurpose(),
request.fileName(),
request.imagePath()
).getId();

return ResponseEntity.ok(AppResponse.ok(new CallbackImageSaveUrlResponse(id)));
}
}