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
1 change: 1 addition & 0 deletions docs/api-specs/philosopher-voice-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
### 1.1 목록 조회
- `GET /api/v1/admin/philosopher-voices`
- 응답(`PhilosopherVoiceResponse[]`): `id`, `name`, `referenceId`, `voiceLabel`, `imageKey`, `note`
- **`imageKey`는 DB의 raw 저장 키가 아니라 `ResourceUrlProvider`로 변환된 호출 가능한 경로다** (다른 이미지 필드인 배틀 썸네일·옵션 이미지와 동일 규칙). 값이 있으면 그대로 `<img src>`에 써도 된다. `null`이면 이미지가 등록 안 된 것 — 프론트 기본 이미지로 대체

### 1.2 생성
- `POST /api/v1/admin/philosopher-voices`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,11 +19,12 @@
public class PhilosopherVoiceService {

private final PhilosopherVoiceRepository philosopherVoiceRepository;
private final ResourceUrlProvider resourceUrlProvider;

@Transactional(readOnly = true)
public List<PhilosopherVoiceResponse> getAll() {
return philosopherVoiceRepository.findAll().stream()
.map(PhilosopherVoiceResponse::from)
.map(this::toResponse)
.toList();
}

Expand All @@ -47,15 +50,15 @@ public PhilosopherVoiceResponse create(PhilosopherVoiceRequest request) {
.imageKey(request.imageKey())
.note(request.note())
.build());
return PhilosopherVoiceResponse.from(saved);
return toResponse(saved);
}

@Transactional
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
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
Loading