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);