diff --git a/docs/api-specs/philosopher-voice-api.md b/docs/api-specs/philosopher-voice-api.md index 26bdc9a0..648948e5 100644 --- a/docs/api-specs/philosopher-voice-api.md +++ b/docs/api-specs/philosopher-voice-api.md @@ -13,6 +13,7 @@ ### 1.1 목록 조회 - `GET /api/v1/admin/philosopher-voices` - 응답(`PhilosopherVoiceResponse[]`): `id`, `name`, `referenceId`, `voiceLabel`, `imageKey`, `note` + - **`imageKey`는 DB의 raw 저장 키가 아니라 `ResourceUrlProvider`로 변환된 호출 가능한 경로다** (다른 이미지 필드인 배틀 썸네일·옵션 이미지와 동일 규칙). 값이 있으면 그대로 ``에 써도 된다. `null`이면 이미지가 등록 안 된 것 — 프론트 기본 이미지로 대체 ### 1.2 생성 - `POST /api/v1/admin/philosopher-voices` diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/philosophervoice/response/PhilosopherVoiceResponse.java b/src/main/java/com/swyp/picke/domain/admin/dto/philosophervoice/response/PhilosopherVoiceResponse.java index ab894014..e51907e1 100644 --- a/src/main/java/com/swyp/picke/domain/admin/dto/philosophervoice/response/PhilosopherVoiceResponse.java +++ b/src/main/java/com/swyp/picke/domain/admin/dto/philosophervoice/response/PhilosopherVoiceResponse.java @@ -10,13 +10,17 @@ public record PhilosopherVoiceResponse( String imageKey, String note ) { - public static PhilosopherVoiceResponse from(PhilosopherVoice entity) { + /** + * @param imageUrl {@code entity.getImageKey()} 를 {@code ResourceUrlProvider.getImageUrl(FileCategory.PHILOSOPHER, ...)} + * 로 변환한 호출 가능한 경로. raw key 를 그대로 내려주지 않는다(다른 이미지 필드와 동일한 규칙). + */ + public static PhilosopherVoiceResponse from(PhilosopherVoice entity, String imageUrl) { return new PhilosopherVoiceResponse( entity.getId(), entity.getName(), entity.getReferenceId(), entity.getVoiceLabel(), - entity.getImageKey(), + imageUrl, entity.getNote() ); } diff --git a/src/main/java/com/swyp/picke/domain/scenario/service/PhilosopherVoiceService.java b/src/main/java/com/swyp/picke/domain/scenario/service/PhilosopherVoiceService.java index 5a4449a4..37420b05 100644 --- a/src/main/java/com/swyp/picke/domain/scenario/service/PhilosopherVoiceService.java +++ b/src/main/java/com/swyp/picke/domain/scenario/service/PhilosopherVoiceService.java @@ -6,6 +6,8 @@ import com.swyp.picke.domain.scenario.repository.PhilosopherVoiceRepository; import com.swyp.picke.global.common.exception.CustomException; import com.swyp.picke.global.common.exception.ErrorCode; +import com.swyp.picke.global.infra.s3.enums.FileCategory; +import com.swyp.picke.global.infra.s3.util.ResourceUrlProvider; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -17,11 +19,12 @@ public class PhilosopherVoiceService { private final PhilosopherVoiceRepository philosopherVoiceRepository; + private final ResourceUrlProvider resourceUrlProvider; @Transactional(readOnly = true) public List getAll() { return philosopherVoiceRepository.findAll().stream() - .map(PhilosopherVoiceResponse::from) + .map(this::toResponse) .toList(); } @@ -47,7 +50,7 @@ public PhilosopherVoiceResponse create(PhilosopherVoiceRequest request) { .imageKey(request.imageKey()) .note(request.note()) .build()); - return PhilosopherVoiceResponse.from(saved); + return toResponse(saved); } @Transactional @@ -55,7 +58,7 @@ public PhilosopherVoiceResponse update(Long id, PhilosopherVoiceRequest request) PhilosopherVoice entity = philosopherVoiceRepository.findById(id) .orElseThrow(() -> new CustomException(ErrorCode.PHILOSOPHER_VOICE_NOT_FOUND)); entity.update(request.referenceId(), request.voiceLabel(), request.imageKey(), request.note()); - return PhilosopherVoiceResponse.from(entity); + return toResponse(entity); } @Transactional @@ -65,4 +68,13 @@ public void delete(Long id) { } philosopherVoiceRepository.deleteById(id); } + + /** + * imageKey는 DB에 raw 저장 키로 있다가, 응답 시 다른 이미지 필드(썸네일/옵션 이미지)와 + * 동일하게 호출 가능한 경로로 변환해서 나간다({@link ResourceUrlProvider}). + */ + private PhilosopherVoiceResponse toResponse(PhilosopherVoice entity) { + String imageUrl = resourceUrlProvider.getImageUrl(FileCategory.PHILOSOPHER, entity.getImageKey()); + return PhilosopherVoiceResponse.from(entity, imageUrl); + } } diff --git a/src/test/java/com/swyp/picke/domain/scenario/service/PhilosopherVoiceServiceTest.java b/src/test/java/com/swyp/picke/domain/scenario/service/PhilosopherVoiceServiceTest.java index f1935832..c198a288 100644 --- a/src/test/java/com/swyp/picke/domain/scenario/service/PhilosopherVoiceServiceTest.java +++ b/src/test/java/com/swyp/picke/domain/scenario/service/PhilosopherVoiceServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -13,7 +14,10 @@ import com.swyp.picke.domain.scenario.repository.PhilosopherVoiceRepository; import com.swyp.picke.global.common.exception.CustomException; import com.swyp.picke.global.common.exception.ErrorCode; +import com.swyp.picke.global.infra.s3.enums.FileCategory; +import com.swyp.picke.global.infra.s3.util.ResourceUrlProvider; import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -26,9 +30,19 @@ class PhilosopherVoiceServiceTest { @Mock private PhilosopherVoiceRepository philosopherVoiceRepository; + @Mock + private ResourceUrlProvider resourceUrlProvider; + @InjectMocks private PhilosopherVoiceService philosopherVoiceService; + @BeforeEach + void setUp() { + // raw key를 그대로 돌려주는 기본 스텁(변환 여부 자체를 검증하는 테스트만 다르게 재정의) + lenient().when(resourceUrlProvider.getImageUrl(any(), any())) + .thenAnswer(invocation -> invocation.getArgument(1)); + } + private PhilosopherVoice entity(String name, String referenceId) { return PhilosopherVoice.builder() .name(name) @@ -103,6 +117,22 @@ private PhilosopherVoice entity(String name, String referenceId) { assertThat(found.getReferenceId()).isEqualTo("voice-new"); } + @Test + void 응답의_imageKey는_raw_저장키가_아니라_ResourceUrlProvider가_변환한_값이다() { + when(philosopherVoiceRepository.existsByName("루소")).thenReturn(false); + when(philosopherVoiceRepository.save(any(PhilosopherVoice.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + when(resourceUrlProvider.getImageUrl(FileCategory.PHILOSOPHER, "images/philosophers/rousseau.png")) + .thenReturn("https://dev.picke.store/api/v1/resources/images/PHILOSOPHER/rousseau.png"); + + PhilosopherVoiceResponse response = philosopherVoiceService.create( + new PhilosopherVoiceRequest("루소", "voice-x", null, "images/philosophers/rousseau.png", null)); + + assertThat(response.imageKey()) + .isEqualTo("https://dev.picke.store/api/v1/resources/images/PHILOSOPHER/rousseau.png") + .isNotEqualTo("images/philosophers/rousseau.png"); + } + @Test void create_및_update가_imageKey를_저장한다() { when(philosopherVoiceRepository.existsByName("루소")).thenReturn(false);