From 5109f358802853eec62ccef79af268bb8e1ab64a Mon Sep 17 00:00:00 2001 From: Baek HyeonBin <81628455+WhiteBin-bin@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:28:17 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A8=20Refactor:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=8C=8C=EC=9D=BC=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 펫 생성 및 수정 시 imageFileUrl 저장/수정 로직을 ImageFileService로 위임 - ImageFileException 및 ImageFileErrorCode 추가로 이미지 파일 전용 예외 처리 분리 - ImageFileMapper를 추가해 프로필 이미지 URL 동적 업데이트 처리 - 이미지 파일 및 펫 서비스 테스트 보강 Closes #153 --- .../exception/GlobalExceptionHandler.java | 12 +- .../exception/ImageFileErrorCode.java | 48 +++++ .../exception/ImageFileException.java | 22 +++ .../imagefile/mapper/ImageFileMapper.java | 22 +++ .../imagefile/service/ImageFileService.java | 18 ++ .../service/ImageFileServiceImpl.java | 180 ++++++++++++++---- .../backend/pet/dto/request/PetRequest.java | 6 + .../backend/pet/service/PetServiceImpl.java | 28 ++- .../imagefile/mapper/ImageFileMapper.xml | 21 ++ .../service/ImageFileServiceTest.java | 38 +++- .../backend/pet/service/PetServiceTest.java | 4 + 11 files changed, 357 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/dodo/backend/imagefile/exception/ImageFileErrorCode.java create mode 100644 src/main/java/com/dodo/backend/imagefile/exception/ImageFileException.java create mode 100644 src/main/java/com/dodo/backend/imagefile/mapper/ImageFileMapper.java create mode 100644 src/main/resources/com/dodo/backend/imagefile/mapper/ImageFileMapper.xml diff --git a/src/main/java/com/dodo/backend/common/exception/GlobalExceptionHandler.java b/src/main/java/com/dodo/backend/common/exception/GlobalExceptionHandler.java index 5123ca0..f7323bd 100644 --- a/src/main/java/com/dodo/backend/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/dodo/backend/common/exception/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import com.dodo.backend.board.exception.BoardException; import com.dodo.backend.fence.exception.FenceException; import com.dodo.backend.healthanalysis.exception.HealthAnalysisException; +import com.dodo.backend.imagefile.exception.ImageFileException; import com.dodo.backend.pet.exception.PetException; import com.dodo.backend.petweight.exception.PetWeightException; import com.dodo.backend.reaction.exception.ReactionException; @@ -125,6 +126,15 @@ protected ResponseEntity handleReactionException(ReactionExceptio return toResponseEntity(e.getErrorCode()); } + /** + * 이미지 파일(ImageFile) 도메인 비즈니스 로직에서 발생하는 {@link ImageFileException}을 처리합니다. + */ + @ExceptionHandler(ImageFileException.class) + protected ResponseEntity handleImageFileException(ImageFileException e) { + log.error("ImageFileException occurred: {}", e.getErrorCode()); + return toResponseEntity(e.getErrorCode()); + } + /** * {@code @Valid} 어노테이션을 통한 요청 데이터 유효성 검증 실패 시 발생하는 예외를 처리합니다. */ @@ -164,4 +174,4 @@ protected ResponseEntity handleException(Exception e) { log.error("Internal Server Error : {}", e.getMessage(), e); return toResponseEntity(INTERNAL_SERVER_ERROR); } -} \ No newline at end of file +} diff --git a/src/main/java/com/dodo/backend/imagefile/exception/ImageFileErrorCode.java b/src/main/java/com/dodo/backend/imagefile/exception/ImageFileErrorCode.java new file mode 100644 index 0000000..cc5ad2a --- /dev/null +++ b/src/main/java/com/dodo/backend/imagefile/exception/ImageFileErrorCode.java @@ -0,0 +1,48 @@ +package com.dodo.backend.imagefile.exception; + +import com.dodo.backend.common.exception.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +/** + * 이미지 파일(ImageFile) 도메인에서 발생하는 예외 상황을 관리하는 에러 코드 정의 클래스입니다. + *

+ * 이미지 업로드, 프로필 이미지 저장 및 수정 과정에서 발생할 수 있는 예외를 포함하며, + * HTTP 상태 코드와 클라이언트에게 전달할 메시지를 {@code Key-Value} 형태로 관리합니다. + */ +@AllArgsConstructor +@Getter +public enum ImageFileErrorCode implements BaseErrorCode { + + /** + * 클라이언트의 요청 형식이 잘못되었거나 이미지 파일 검증을 통과하지 못했을 때 사용합니다. + *

+ * HTTP {@code 400 Bad Request}를 반환합니다. + */ + INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + + /** + * 이미지 업로드 결과에서 유효한 이미지 URL을 확인할 수 없을 때 사용합니다. + *

+ * HTTP {@code 500 Internal Server Error}를 반환합니다. + */ + IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드 중 오류가 발생했습니다."), + + /** + * 서버 내부에서 예상치 못한 오류가 발생했을 때 사용합니다. + *

+ * HTTP {@code 500 Internal Server Error}를 반환합니다. + */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다."); + + /** + * 에러 상황에 해당하는 HTTP 상태 코드입니다. + */ + private final HttpStatus httpStatus; + + /** + * 클라이언트에게 전달할 상세 에러 메시지입니다. + */ + private final String message; +} diff --git a/src/main/java/com/dodo/backend/imagefile/exception/ImageFileException.java b/src/main/java/com/dodo/backend/imagefile/exception/ImageFileException.java new file mode 100644 index 0000000..3d0485b --- /dev/null +++ b/src/main/java/com/dodo/backend/imagefile/exception/ImageFileException.java @@ -0,0 +1,22 @@ +package com.dodo.backend.imagefile.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 이미지 파일(ImageFile) 도메인 비즈니스 로직 수행 중 발생하는 전용 예외 클래스입니다. + *

+ * 이미지 업로드, 프로필 이미지 저장 및 수정 등 이미지 파일 관련 서비스 로직에서 + * 예외 상황 발생 시 이 클래스를 throw 합니다. + * {@code GlobalExceptionHandler}에서 이 예외를 가로채어 + * {@link ImageFileErrorCode}에 정의된 표준 응답으로 변환합니다. + */ +@AllArgsConstructor +@Getter +public class ImageFileException extends RuntimeException { + + /** + * 발생한 예외의 구체적인 종류(상태 코드, 메시지)를 담고 있는 Enum입니다. + */ + private final ImageFileErrorCode errorCode; +} diff --git a/src/main/java/com/dodo/backend/imagefile/mapper/ImageFileMapper.java b/src/main/java/com/dodo/backend/imagefile/mapper/ImageFileMapper.java new file mode 100644 index 0000000..c100fc5 --- /dev/null +++ b/src/main/java/com/dodo/backend/imagefile/mapper/ImageFileMapper.java @@ -0,0 +1,22 @@ +package com.dodo.backend.imagefile.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface ImageFileMapper { + + /** + * 펫 프로필 이미지 URL을 수정합니다. + * + * @param petId 수정할 펫 ID + * @param imageFileUrl 새로운 이미지 URL + * @param originalFilename 이미지 URL에서 추출한 파일명 + * @return 수정된 행 수 + */ + int updatePetProfileImage( + @Param("petId") Long petId, + @Param("imageFileUrl") String imageFileUrl, + @Param("originalFilename") String originalFilename + ); +} diff --git a/src/main/java/com/dodo/backend/imagefile/service/ImageFileService.java b/src/main/java/com/dodo/backend/imagefile/service/ImageFileService.java index c0fc2ec..596081e 100644 --- a/src/main/java/com/dodo/backend/imagefile/service/ImageFileService.java +++ b/src/main/java/com/dodo/backend/imagefile/service/ImageFileService.java @@ -1,6 +1,7 @@ package com.dodo.backend.imagefile.service; import com.dodo.backend.imagefile.dto.response.ImageFileResponse.ImageUploadResponse; +import com.dodo.backend.pet.entity.Pet; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -26,4 +27,21 @@ public interface ImageFileService { * @return 업로드 응답 DTO */ ImageUploadResponse uploadImages(List files); + + /** + * 펫 프로필 이미지 URL을 저장합니다. + * + * @param pet 이미지와 연결할 펫 + * @param imageFileUrl 저장할 이미지 URL + */ + void savePetProfileImage(Pet pet, String imageFileUrl); + + /** + * 펫 프로필 이미지 URL을 수정합니다. + * 기존 이미지 정보가 없으면 새로 저장합니다. + * + * @param pet 이미지와 연결할 펫 + * @param imageFileUrl 수정할 이미지 URL + */ + void updatePetProfileImage(Pet pet, String imageFileUrl); } diff --git a/src/main/java/com/dodo/backend/imagefile/service/ImageFileServiceImpl.java b/src/main/java/com/dodo/backend/imagefile/service/ImageFileServiceImpl.java index 5f2e7a3..f29e685 100644 --- a/src/main/java/com/dodo/backend/imagefile/service/ImageFileServiceImpl.java +++ b/src/main/java/com/dodo/backend/imagefile/service/ImageFileServiceImpl.java @@ -4,13 +4,16 @@ import com.cloudinary.utils.ObjectUtils; import com.dodo.backend.imagefile.dto.response.ImageFileResponse.ImageUploadResponse; import com.dodo.backend.imagefile.entity.ImageFile; +import com.dodo.backend.imagefile.exception.ImageFileException; +import com.dodo.backend.imagefile.mapper.ImageFileMapper; import com.dodo.backend.imagefile.repository.ImageFileRepository; -import com.dodo.backend.user.exception.UserException; +import com.dodo.backend.pet.entity.Pet; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.tika.Tika; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.function.ThrowingFunction; import org.springframework.web.multipart.MultipartFile; import java.util.Collections; @@ -20,7 +23,8 @@ import java.util.Set; import java.util.stream.Collectors; -import static com.dodo.backend.user.exception.UserErrorCode.INVALID_REQUEST; +import static com.dodo.backend.imagefile.exception.ImageFileErrorCode.IMAGE_UPLOAD_FAILED; +import static com.dodo.backend.imagefile.exception.ImageFileErrorCode.INVALID_REQUEST; /** * {@link ImageFileService}의 구현체입니다. @@ -38,18 +42,18 @@ public class ImageFileServiceImpl implements ImageFileService { ); private final ImageFileRepository imageFileRepository; + private final ImageFileMapper imageFileMapper; private final Cloudinary cloudinary; private final Tika tika = new Tika(); /** - * {@inheritDoc} + * 펫 ID 목록에 해당하는 프로필 이미지 URL을 일괄 조회합니다. *

- * 처리 과정: - *

    - *
  1. 입력된 펫 ID 목록이 비어있으면 빈 Map을 반환합니다.
  2. - *
  3. 리포지토리의 {@code findAllByPet_PetIdIn}을 호출하여 펫 ID 목록에 해당하는 이미지 파일들을 조회합니다.
  4. - *
  5. 조회된 엔티티에서 펫 ID와 이미지 URL을 추출하여 Map으로 변환합니다.
  6. - *
+ * 입력된 펫 ID 목록이 비어 있으면 DB를 조회하지 않고 빈 Map을 반환합니다. + * 조회된 이미지 파일은 펫 ID를 key, 이미지 URL을 value로 변환합니다. + * + * @param petIds 이미지 URL을 조회할 펫 ID 목록 + * @return 펫 ID와 이미지 URL을 매핑한 Map */ @Override @Transactional(readOnly = true) @@ -69,62 +73,162 @@ public Map getProfileUrlsByPetIds(List petIds) { } /** - * {@inheritDoc} + * 이미지 파일 목록을 Cloudinary에 업로드하고 업로드된 이미지 URL 목록을 반환합니다. + *

+ * 요청 파일이 비어 있거나 허용되지 않은 MIME 타입이면 잘못된 요청 예외를 발생시키고, + * 정상 파일은 개별 업로드 후 secure URL만 응답 DTO에 담습니다. + * + * @param files 업로드할 이미지 파일 목록 + * @return 업로드 성공 메시지와 이미지 URL 목록 */ @Transactional(readOnly = true) @Override public ImageUploadResponse uploadImages(List files) { if (files == null || files.isEmpty()) { - throw new UserException(INVALID_REQUEST); + throw new ImageFileException(INVALID_REQUEST); } List imageUrls = files.stream() - .map(this::uploadSingleImage) + .map(ThrowingFunction.of( + this::uploadSingleImage, + (message, exception) -> new ImageFileException(IMAGE_UPLOAD_FAILED) + )) .toList(); return ImageUploadResponse.toDto("이미지 업로드에 성공하였습니다.", imageUrls); } - private String uploadSingleImage(MultipartFile file) { + /** + * 펫 프로필 이미지 URL을 {@code image_file} 테이블에 신규 저장합니다. + *

+ * 이미지 URL이 비어 있으면 저장하지 않습니다. 파일 크기는 외부 URL만 전달받는 + * 현재 요청 구조상 알 수 없으므로 기본값 {@code 0L}로 저장합니다. + * + * @param pet 이미지와 연결할 펫 엔티티 + * @param imageFileUrl 저장할 이미지 URL + */ + @Transactional + @Override + public void savePetProfileImage(Pet pet, String imageFileUrl) { + if (pet == null || isBlank(imageFileUrl)) { + return; + } + + ImageFile imageFile = ImageFile.builder() + .pet(pet) + .imageFileUrl(imageFileUrl) + .size(0L) + .originalFilename(extractOriginalFilename(imageFileUrl)) + .build(); + + imageFileRepository.save(imageFile); + } + + /** + * 펫 프로필 이미지 URL을 수정합니다. + *

+ * 기존 이미지 행이 있으면 MyBatis mapper로 URL과 원본 파일명을 갱신하고, + * 기존 행이 없으면 신규 이미지 정보로 저장합니다. + * + * @param pet 이미지와 연결된 펫 엔티티 + * @param imageFileUrl 수정할 이미지 URL + */ + @Transactional + @Override + public void updatePetProfileImage(Pet pet, String imageFileUrl) { + if (pet == null || pet.getPetId() == null || isBlank(imageFileUrl)) { + return; + } + + int updatedRows = imageFileMapper.updatePetProfileImage( + pet.getPetId(), + imageFileUrl, + extractOriginalFilename(imageFileUrl) + ); + + if (updatedRows == 0) { + savePetProfileImage(pet, imageFileUrl); + } + } + + /** + * 단일 이미지 파일을 검증한 뒤 Cloudinary에 업로드합니다. + *

+ * 파일이 비어 있거나 허용되지 않은 MIME 타입이면 이미지 파일 도메인 예외를 발생시키고, + * 업로드 결과에 secure URL이 없으면 업로드 실패 예외를 발생시킵니다. + * + * @param file 업로드할 이미지 파일 + * @return Cloudinary에서 반환한 secure URL + * @throws Exception 파일 바이트 조회 또는 Cloudinary 업로드 중 checked exception이 발생한 경우 + */ + private String uploadSingleImage(MultipartFile file) throws Exception { if (file == null || file.isEmpty()) { - throw new UserException(INVALID_REQUEST); + throw new ImageFileException(INVALID_REQUEST); } String contentType = file.getContentType(); if (contentType == null || !ALLOWED_MIME_TYPES.contains(contentType)) { - throw new UserException(INVALID_REQUEST); + throw new ImageFileException(INVALID_REQUEST); } String detectedMimeType = detectMimeType(file); if (!ALLOWED_MIME_TYPES.contains(detectedMimeType)) { - throw new UserException(INVALID_REQUEST); + throw new ImageFileException(INVALID_REQUEST); } - try { - Map uploadResult = cloudinary.uploader().upload( - file.getBytes(), - ObjectUtils.asMap( - "folder", "images", - "resource_type", "image" - ) - ); - Object url = uploadResult.get("secure_url"); - if (url == null) { - throw new IllegalStateException("Cloudinary 응답에 secure_url이 없습니다."); - } - return Objects.toString(url); - } catch (UserException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("이미지 업로드 중 오류가 발생했습니다.", e); + Map uploadResult = cloudinary.uploader().upload( + file.getBytes(), + ObjectUtils.asMap( + "folder", "images", + "resource_type", "image" + ) + ); + Object url = uploadResult.get("secure_url"); + if (url == null) { + throw new ImageFileException(IMAGE_UPLOAD_FAILED); } + return Objects.toString(url); + } + + /** + * 파일 바이트를 기반으로 실제 MIME 타입을 감지합니다. + * + * @param file MIME 타입을 확인할 이미지 파일 + * @return 감지된 MIME 타입 + * @throws Exception 파일 바이트 조회 또는 MIME 타입 감지 중 checked exception이 발생한 경우 + */ + private String detectMimeType(MultipartFile file) throws Exception { + return tika.detect(file.getBytes()); } - private String detectMimeType(MultipartFile file) { - try { - return tika.detect(file.getBytes()); - } catch (Exception e) { - throw new UserException(INVALID_REQUEST); + /** + * 문자열이 null이거나 공백 문자열인지 확인합니다. + * + * @param value 확인할 문자열 + * @return null 또는 공백 문자열이면 true + */ + private boolean isBlank(String value) { + return value == null || value.isBlank(); + } + + /** + * 이미지 URL에서 원본 파일명을 추출합니다. + *

+ * 쿼리 파라미터는 제거하고 마지막 경로 구간을 파일명으로 사용합니다. + * 추출할 수 없으면 기본 파일명을 반환합니다. + * + * @param imageFileUrl 파일명을 추출할 이미지 URL + * @return 추출된 파일명 또는 기본 파일명 + */ + private String extractOriginalFilename(String imageFileUrl) { + int queryStart = imageFileUrl.indexOf('?'); + String urlWithoutQuery = queryStart >= 0 ? imageFileUrl.substring(0, queryStart) : imageFileUrl; + int slashIndex = urlWithoutQuery.lastIndexOf('/'); + + if (slashIndex < 0 || slashIndex == urlWithoutQuery.length() - 1) { + return "pet_profile_image"; } + + return urlWithoutQuery.substring(slashIndex + 1); } } diff --git a/src/main/java/com/dodo/backend/pet/dto/request/PetRequest.java b/src/main/java/com/dodo/backend/pet/dto/request/PetRequest.java index ba6d1d2..9c26032 100644 --- a/src/main/java/com/dodo/backend/pet/dto/request/PetRequest.java +++ b/src/main/java/com/dodo/backend/pet/dto/request/PetRequest.java @@ -36,6 +36,9 @@ public static class PetRegisterRequest { @Schema(description = "반려동물 등록번호 (선택사항, 없을 시 null)", example = "null", nullable = true) private String registrationNumber; + @Schema(description = "반려동물 프로필 이미지 URL", example = "https://example.com/images/bori.jpg") + private String imageFileUrl; + @Schema(description = "성별 (MALE, FEMALE, NEUTER)", example = "MALE") @NotNull(message = "성별은 필수입니다.") private String sex; @@ -104,6 +107,9 @@ public static class PetUpdateRequest { @Size(max = 15, message = "등록번호는 15자 이하여야 합니다.") private String registrationNumber; + @Schema(description = "변경할 반려동물 프로필 이미지 URL (선택)", example = "https://example.com/images/bori.jpg") + private String imageFileUrl; + @Schema(description = "변경할 성별 (선택, MALE, FEMALE, NEUTER)", example = "FEMALE") private String sex; diff --git a/src/main/java/com/dodo/backend/pet/service/PetServiceImpl.java b/src/main/java/com/dodo/backend/pet/service/PetServiceImpl.java index 28e3340..9892385 100644 --- a/src/main/java/com/dodo/backend/pet/service/PetServiceImpl.java +++ b/src/main/java/com/dodo/backend/pet/service/PetServiceImpl.java @@ -95,6 +95,8 @@ public PetRegisterResponse registerPet(UUID userId, PetRegisterRequest request) Pet pet = request.toEntity(); Pet savedPet = petRepository.save(pet); + imageFileService.savePetProfileImage(savedPet, request.getImageFileUrl()); + userPetService.registerUserPet(userId, savedPet, RegistrationStatus.APPROVED); log.info("펫 등록 및 유저 관계 설정 완료 - User: {}, PetId: {}", userId, savedPet.getPetId()); @@ -136,7 +138,10 @@ public PetUpdateResponse updatePet(Long petId, PetUpdateRequest request, UUID us } } - petMapper.updatePetProfileInfo(request, petId); + if (hasPetTableUpdateFields(request)) { + petMapper.updatePetProfileInfo(request, petId); + } + imageFileService.updatePetProfileImage(pet, request.getImageFileUrl()); log.info("반려동물 프로필 수정 성공 - PetId: {}", petId); @@ -540,6 +545,27 @@ public PetDeviceCheckResponse checkDeviceIdAvailability(PetDeviceCheckRequest re return PetDeviceCheckResponse.toDto("사용 가능한 디바이스 ID입니다.", true); } + /** + * {@code pet} 테이블에 반영할 필드가 있는지 확인합니다. + *

+ * {@code imageFileUrl}은 {@code image_file} 도메인에서 별도로 처리하므로 + * 이 검사 대상에 포함하지 않습니다. 이미지 URL만 수정하는 요청에서 + * {@link PetMapper#updatePetProfileInfo}를 호출하면 MyBatis의 동적 + * {@code }에 들어갈 컬럼이 없어 잘못된 SQL이 생성될 수 있습니다. + * + * @param request 반려동물 수정 요청 DTO + * @return {@code pet} 테이블 업데이트 대상 필드가 하나 이상 있으면 true + */ + private boolean hasPetTableUpdateFields(PetUpdateRequest request) { + return request.getRegistrationNumber() != null + || request.getSex() != null + || request.getAge() != null + || request.getPetName() != null + || request.getBreed() != null + || request.getReferenceHeartRate() != null + || request.getDeviceId() != null; + } + /** * 반려동물 특이사항을 생성합니다. *

diff --git a/src/main/resources/com/dodo/backend/imagefile/mapper/ImageFileMapper.xml b/src/main/resources/com/dodo/backend/imagefile/mapper/ImageFileMapper.xml new file mode 100644 index 0000000..d10e9c3 --- /dev/null +++ b/src/main/resources/com/dodo/backend/imagefile/mapper/ImageFileMapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + UPDATE image_file + + image_file_url = #{imageFileUrl}, + original_filename = #{originalFilename} + + WHERE pet_id = #{petId} + + diff --git a/src/test/java/com/dodo/backend/imagefile/service/ImageFileServiceTest.java b/src/test/java/com/dodo/backend/imagefile/service/ImageFileServiceTest.java index 33c9feb..d383c15 100644 --- a/src/test/java/com/dodo/backend/imagefile/service/ImageFileServiceTest.java +++ b/src/test/java/com/dodo/backend/imagefile/service/ImageFileServiceTest.java @@ -4,9 +4,10 @@ import com.cloudinary.Uploader; import com.dodo.backend.imagefile.dto.response.ImageFileResponse.ImageUploadResponse; import com.dodo.backend.imagefile.entity.ImageFile; +import com.dodo.backend.imagefile.exception.ImageFileException; +import com.dodo.backend.imagefile.mapper.ImageFileMapper; import com.dodo.backend.imagefile.repository.ImageFileRepository; import com.dodo.backend.pet.entity.Pet; -import com.dodo.backend.user.exception.UserException; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -46,6 +47,9 @@ class ImageFileServiceTest { @Mock private ImageFileRepository imageFileRepository; + @Mock + private ImageFileMapper imageFileMapper; + @Mock private Cloudinary cloudinary; @@ -123,6 +127,36 @@ void getProfileUrlsByPetIds_EmptyInput() { log.info("테스트 종료: 프로필 이미지 조회 (빈 리스트)"); } + @Test + @DisplayName("펫 프로필 이미지 저장 성공: 이미지 URL을 ImageFile 엔티티로 저장한다.") + void savePetProfileImage_Success() { + // given + Pet pet = Pet.builder().build(); + String imageFileUrl = "https://example.com/images/bori.jpg"; + + // when + imageFileService.savePetProfileImage(pet, imageFileUrl); + + // then + verify(imageFileRepository).save(any(ImageFile.class)); + } + + @Test + @DisplayName("펫 프로필 이미지 수정 성공: 기존 이미지 URL을 mapper로 수정한다.") + void updatePetProfileImage_Success() { + // given + Pet pet = Pet.builder().petId(1L).build(); + String imageFileUrl = "https://example.com/images/bori.jpg"; + given(imageFileMapper.updatePetProfileImage(1L, imageFileUrl, "bori.jpg")).willReturn(1); + + // when + imageFileService.updatePetProfileImage(pet, imageFileUrl); + + // then + verify(imageFileMapper).updatePetProfileImage(1L, imageFileUrl, "bori.jpg"); + verify(imageFileRepository, never()).save(any(ImageFile.class)); + } + @Test @DisplayName("이미지 업로드 성공: 파일 목록 업로드 후 URL 목록을 반환한다.") void uploadImages_Success() throws Exception { @@ -153,7 +187,7 @@ void uploadImages_Fail_InvalidFileType() { MockMultipartFile file = new MockMultipartFile("files", "a.txt", "text/plain", "text".getBytes()); // when - UserException exception = assertThrows(UserException.class, () -> + ImageFileException exception = assertThrows(ImageFileException.class, () -> imageFileService.uploadImages(List.of(file)) ); diff --git a/src/test/java/com/dodo/backend/pet/service/PetServiceTest.java b/src/test/java/com/dodo/backend/pet/service/PetServiceTest.java index 81613fb..ed00f88 100644 --- a/src/test/java/com/dodo/backend/pet/service/PetServiceTest.java +++ b/src/test/java/com/dodo/backend/pet/service/PetServiceTest.java @@ -90,6 +90,7 @@ void registerPet_Success() { .age(3) .birth(LocalDateTime.now()) .registrationNumber("1234567890") + .imageFileUrl("https://example.com/images/bori.jpg") .sex("MALE") .deviceId("DEV_123") .build(); @@ -113,6 +114,7 @@ void registerPet_Success() { verify(userPetService, times(1)).existsUser(userId); verify(petRepository, times(1)).save(any(Pet.class)); + verify(imageFileService, times(1)).savePetProfileImage(savedPet, request.getImageFileUrl()); verify(userPetService, times(1)).registerUserPet(userId, savedPet, RegistrationStatus.APPROVED); log.info("펫 등록 성공 테스트가 통과되었습니다."); } @@ -225,6 +227,7 @@ void updatePet_Success() { PetRequest.PetUpdateRequest request = PetRequest.PetUpdateRequest.builder() .petName("새로운초코") .registrationNumber("NEW-999") + .imageFileUrl("https://example.com/images/choco.jpg") .sex("FEMALE") .age(5) .build(); @@ -244,6 +247,7 @@ void updatePet_Success() { assertEquals("새로운초코", response.getPetName()); verify(petMapper, times(1)).updatePetProfileInfo(request, petId); + verify(imageFileService, times(1)).updatePetProfileImage(existingPet, request.getImageFileUrl()); log.info("펫 수정 성공 테스트가 통과되었습니다."); }