From 3df87904e6d0acddaefe02de5c9b99559dcff577 Mon Sep 17 00:00:00 2001 From: jucheonsu Date: Thu, 16 Apr 2026 07:00:40 +0900 Subject: [PATCH] =?UTF-8?q?#190=20[Fix]=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=A0=80=EC=9E=A5/=EB=B0=9C?= =?UTF-8?q?=ED=96=89=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=95=88=EC=A0=95?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배틀/퀴즈/투표 targetDate UI 및 DTO/서비스 매핑 반영 - 퀴즈/투표 옵션 displayOrder/isCorrect 저장 경로 보완 - 배틀 이미지 hidden URL 동기화 및 EDIT 시 업로드 조건 개선 - 발행 상태 시나리오 수정 시 오디오 재합성 트리거 추가 - AdminContentCreationIntegrationTest/ScenarioServiceImplTest 보강 --- .../request/AdminBattleCreateRequest.java | 3 ++ .../request/AdminBattleUpdateRequest.java | 3 ++ .../poll/request/AdminPollCreateRequest.java | 2 + .../poll/request/AdminPollOptionRequest.java | 3 +- .../quiz/request/AdminQuizCreateRequest.java | 4 +- .../quiz/request/AdminQuizOptionRequest.java | 5 ++- .../battle/converter/BattleConverter.java | 2 + .../picke/domain/battle/entity/Battle.java | 4 +- .../battle/service/BattleServiceImpl.java | 4 ++ .../domain/poll/converter/PollConverter.java | 1 + .../picke/domain/poll/entity/PollOption.java | 2 +- .../domain/poll/service/PollServiceImpl.java | 19 ++++++++-- .../domain/quiz/converter/QuizConverter.java | 1 + .../picke/domain/quiz/entity/QuizOption.java | 2 +- .../domain/quiz/service/QuizServiceImpl.java | 20 ++++++++-- .../scenario/service/ScenarioServiceImpl.java | 22 ++++++++--- .../resources/static/js/admin/api/api-save.js | 22 ++++++++++- .../admin/components/form-battle.html | 8 ++++ .../templates/admin/components/form-quiz.html | 6 +++ .../templates/admin/components/form-vote.html | 6 +++ .../AdminContentCreationIntegrationTest.java | 31 ++++++++++------ .../service/ScenarioServiceImplTest.java | 37 +++++++++++++++++++ 22 files changed, 175 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/battle/request/AdminBattleCreateRequest.java b/src/main/java/com/swyp/picke/domain/admin/dto/battle/request/AdminBattleCreateRequest.java index 8b16512..24a3885 100644 --- a/src/main/java/com/swyp/picke/domain/admin/dto/battle/request/AdminBattleCreateRequest.java +++ b/src/main/java/com/swyp/picke/domain/admin/dto/battle/request/AdminBattleCreateRequest.java @@ -1,6 +1,7 @@ package com.swyp.picke.domain.admin.dto.battle.request; import com.swyp.picke.domain.battle.enums.BattleStatus; +import java.time.LocalDate; import java.util.List; public record AdminBattleCreateRequest( @@ -8,6 +9,8 @@ public record AdminBattleCreateRequest( String summary, String description, String thumbnailUrl, + LocalDate targetDate, + Integer audioDuration, BattleStatus status, List tagIds, List options diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/battle/request/AdminBattleUpdateRequest.java b/src/main/java/com/swyp/picke/domain/admin/dto/battle/request/AdminBattleUpdateRequest.java index 576da5b..8875c59 100644 --- a/src/main/java/com/swyp/picke/domain/admin/dto/battle/request/AdminBattleUpdateRequest.java +++ b/src/main/java/com/swyp/picke/domain/admin/dto/battle/request/AdminBattleUpdateRequest.java @@ -1,6 +1,7 @@ package com.swyp.picke.domain.admin.dto.battle.request; import com.swyp.picke.domain.battle.enums.BattleStatus; +import java.time.LocalDate; import java.util.List; public record AdminBattleUpdateRequest( @@ -8,6 +9,8 @@ public record AdminBattleUpdateRequest( String summary, String description, String thumbnailUrl, + LocalDate targetDate, + Integer audioDuration, BattleStatus status, List tagIds, List options diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/poll/request/AdminPollCreateRequest.java b/src/main/java/com/swyp/picke/domain/admin/dto/poll/request/AdminPollCreateRequest.java index d7d305e..76d3581 100644 --- a/src/main/java/com/swyp/picke/domain/admin/dto/poll/request/AdminPollCreateRequest.java +++ b/src/main/java/com/swyp/picke/domain/admin/dto/poll/request/AdminPollCreateRequest.java @@ -1,11 +1,13 @@ package com.swyp.picke.domain.admin.dto.poll.request; import com.swyp.picke.domain.poll.enums.PollStatus; +import java.time.LocalDate; import java.util.List; public record AdminPollCreateRequest( String titlePrefix, String titleSuffix, + LocalDate targetDate, PollStatus status, List options ) { diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/poll/request/AdminPollOptionRequest.java b/src/main/java/com/swyp/picke/domain/admin/dto/poll/request/AdminPollOptionRequest.java index e0b9ddb..953df8f 100644 --- a/src/main/java/com/swyp/picke/domain/admin/dto/poll/request/AdminPollOptionRequest.java +++ b/src/main/java/com/swyp/picke/domain/admin/dto/poll/request/AdminPollOptionRequest.java @@ -4,7 +4,8 @@ public record AdminPollOptionRequest( PollOptionLabel label, - String title + String title, + Integer displayOrder ) {} diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/quiz/request/AdminQuizCreateRequest.java b/src/main/java/com/swyp/picke/domain/admin/dto/quiz/request/AdminQuizCreateRequest.java index eb41491..3a52988 100644 --- a/src/main/java/com/swyp/picke/domain/admin/dto/quiz/request/AdminQuizCreateRequest.java +++ b/src/main/java/com/swyp/picke/domain/admin/dto/quiz/request/AdminQuizCreateRequest.java @@ -1,10 +1,12 @@ package com.swyp.picke.domain.admin.dto.quiz.request; import com.swyp.picke.domain.quiz.enums.QuizStatus; +import java.time.LocalDate; import java.util.List; public record AdminQuizCreateRequest( String title, + LocalDate targetDate, QuizStatus status, List options -) {} \ No newline at end of file +) {} diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/quiz/request/AdminQuizOptionRequest.java b/src/main/java/com/swyp/picke/domain/admin/dto/quiz/request/AdminQuizOptionRequest.java index 4dd94c5..37accf4 100644 --- a/src/main/java/com/swyp/picke/domain/admin/dto/quiz/request/AdminQuizOptionRequest.java +++ b/src/main/java/com/swyp/picke/domain/admin/dto/quiz/request/AdminQuizOptionRequest.java @@ -6,5 +6,6 @@ public record AdminQuizOptionRequest( QuizOptionLabel label, String text, String detailText, - Boolean isCorrect -) {} \ No newline at end of file + Boolean isCorrect, + Integer displayOrder +) {} diff --git a/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java b/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java index 71d5c27..eb58c8a 100644 --- a/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java +++ b/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java @@ -37,6 +37,8 @@ public Battle toEntity(AdminBattleCreateRequest request, User admin) { .summary(request.summary()) .description(request.description()) .thumbnailUrl(request.thumbnailUrl()) + .targetDate(request.targetDate()) + .audioDuration(request.audioDuration()) .status(request.status()) .creatorType(BattleCreatorType.ADMIN) .creator(admin) diff --git a/src/main/java/com/swyp/picke/domain/battle/entity/Battle.java b/src/main/java/com/swyp/picke/domain/battle/entity/Battle.java index e990504..0741fd9 100644 --- a/src/main/java/com/swyp/picke/domain/battle/entity/Battle.java +++ b/src/main/java/com/swyp/picke/domain/battle/entity/Battle.java @@ -109,6 +109,8 @@ public void update( String summary, String description, String thumbnailUrl, + LocalDate targetDate, + Integer audioDuration, BattleStatus status ) { if (title != null) { @@ -155,4 +157,4 @@ public void updateTargetDate(LocalDate targetDate) { this.targetDate = targetDate; } -} \ No newline at end of file +} diff --git a/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java b/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java index e8b59d6..f985e87 100644 --- a/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java +++ b/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java @@ -302,6 +302,8 @@ public AdminBattleDetailResponse createBattle(AdminBattleCreateRequest request, request.summary(), request.description(), resolvedThumbnailKey, + request.targetDate(), + request.audioDuration(), request.status() ); battle = battleRepository.save(battle); @@ -379,6 +381,8 @@ public AdminBattleDetailResponse updateBattle(Long battleId, AdminBattleUpdateRe request.summary(), request.description(), resolvedThumbnailKey, + request.targetDate(), + request.audioDuration(), request.status() ); diff --git a/src/main/java/com/swyp/picke/domain/poll/converter/PollConverter.java b/src/main/java/com/swyp/picke/domain/poll/converter/PollConverter.java index 03d74fe..18c32a9 100644 --- a/src/main/java/com/swyp/picke/domain/poll/converter/PollConverter.java +++ b/src/main/java/com/swyp/picke/domain/poll/converter/PollConverter.java @@ -25,6 +25,7 @@ public Poll toEntity(AdminPollCreateRequest request) { return Poll.builder() .titlePrefix(request.titlePrefix()) .titleSuffix(request.titleSuffix()) + .targetDate(request.targetDate()) .status(request.status()) .build(); } diff --git a/src/main/java/com/swyp/picke/domain/poll/entity/PollOption.java b/src/main/java/com/swyp/picke/domain/poll/entity/PollOption.java index c0f86e9..d15ffff 100644 --- a/src/main/java/com/swyp/picke/domain/poll/entity/PollOption.java +++ b/src/main/java/com/swyp/picke/domain/poll/entity/PollOption.java @@ -48,7 +48,7 @@ public PollOption(Poll poll, PollOptionLabel label, String title, Integer displa } - public void update(String title) { + public void update(String title, Integer displayOrder) { if (title != null) this.title = title; if (displayOrder != null) this.displayOrder = displayOrder; } diff --git a/src/main/java/com/swyp/picke/domain/poll/service/PollServiceImpl.java b/src/main/java/com/swyp/picke/domain/poll/service/PollServiceImpl.java index 4d3f995..4449e76 100644 --- a/src/main/java/com/swyp/picke/domain/poll/service/PollServiceImpl.java +++ b/src/main/java/com/swyp/picke/domain/poll/service/PollServiceImpl.java @@ -98,11 +98,14 @@ public AdminPollDetailResponse createPoll(AdminPollCreateRequest request) { List savedOptions = new ArrayList<>(); if (request.options() != null) { - for (AdminPollOptionRequest optionRequest : request.options()) { + for (int i = 0; i < request.options().size(); i++) { + AdminPollOptionRequest optionRequest = request.options().get(i); + int displayOrder = resolveDisplayOrder(optionRequest.displayOrder(), i + 1); PollOption option = PollOption.builder() .poll(poll) .label(optionRequest.label()) .title(optionRequest.title()) + .displayOrder(displayOrder) .build(); option = pollOptionRepository.save(option); savedOptions.add(option); @@ -132,7 +135,9 @@ public AdminPollDetailResponse updatePoll(Long pollId, AdminPollUpdateRequest re } Set requestedLabels = new HashSet<>(); - for (AdminPollOptionRequest optionRequest : request.options()) { + for (int i = 0; i < request.options().size(); i++) { + AdminPollOptionRequest optionRequest = request.options().get(i); + int displayOrder = resolveDisplayOrder(optionRequest.displayOrder(), i + 1); requestedLabels.add(optionRequest.label()); PollOption option = existingOptionMap.get(optionRequest.label()); @@ -141,10 +146,11 @@ public AdminPollDetailResponse updatePoll(Long pollId, AdminPollUpdateRequest re .poll(poll) .label(optionRequest.label()) .title(optionRequest.title()) + .displayOrder(displayOrder) .build(); option = pollOptionRepository.save(option); } else { - option.update(optionRequest.title()); + option.update(optionRequest.title(), displayOrder); } } @@ -168,6 +174,13 @@ public AdminPollDeleteResponse deletePoll(Long pollId) { return new AdminPollDeleteResponse(true, LocalDateTime.now()); } + private int resolveDisplayOrder(Integer requestedOrder, int fallbackOrder) { + if (requestedOrder == null || requestedOrder < 1) { + return fallbackOrder; + } + return requestedOrder; + } + private void ensureTodayPicks(LocalDate today, int requiredCount) { List todays = pollRepository.findTodayPicks(PollStatus.PUBLISHED, today, PageRequest.of(0, requiredCount)); int missingCount = requiredCount - todays.size(); diff --git a/src/main/java/com/swyp/picke/domain/quiz/converter/QuizConverter.java b/src/main/java/com/swyp/picke/domain/quiz/converter/QuizConverter.java index bdcb8bb..74d18c7 100644 --- a/src/main/java/com/swyp/picke/domain/quiz/converter/QuizConverter.java +++ b/src/main/java/com/swyp/picke/domain/quiz/converter/QuizConverter.java @@ -24,6 +24,7 @@ public class QuizConverter { public Quiz toEntity(AdminQuizCreateRequest request) { return Quiz.builder() .title(request.title()) + .targetDate(request.targetDate()) .status(request.status()) .build(); } diff --git a/src/main/java/com/swyp/picke/domain/quiz/entity/QuizOption.java b/src/main/java/com/swyp/picke/domain/quiz/entity/QuizOption.java index 85fd73e..1283efa 100644 --- a/src/main/java/com/swyp/picke/domain/quiz/entity/QuizOption.java +++ b/src/main/java/com/swyp/picke/domain/quiz/entity/QuizOption.java @@ -62,7 +62,7 @@ void assignQuiz(Quiz quiz) { this.quiz = quiz; } - public void update(String text, String detailText, Boolean isCorrect) { + public void update(String text, String detailText, Boolean isCorrect, Integer displayOrder) { if (text != null) this.text = text; if (detailText != null) this.detailText = detailText; if (isCorrect != null) this.isCorrect = isCorrect; diff --git a/src/main/java/com/swyp/picke/domain/quiz/service/QuizServiceImpl.java b/src/main/java/com/swyp/picke/domain/quiz/service/QuizServiceImpl.java index 3ee12e0..62c1525 100644 --- a/src/main/java/com/swyp/picke/domain/quiz/service/QuizServiceImpl.java +++ b/src/main/java/com/swyp/picke/domain/quiz/service/QuizServiceImpl.java @@ -98,13 +98,16 @@ public AdminQuizDetailResponse createQuiz(AdminQuizCreateRequest request) { List savedOptions = new ArrayList<>(); if (request.options() != null) { - for (AdminQuizOptionRequest optionRequest : request.options()) { + for (int i = 0; i < request.options().size(); i++) { + AdminQuizOptionRequest optionRequest = request.options().get(i); + int displayOrder = resolveDisplayOrder(optionRequest.displayOrder(), i + 1); QuizOption option = QuizOption.builder() .quiz(quiz) .label(optionRequest.label()) .text(optionRequest.text()) .detailText(optionRequest.detailText()) .isCorrect(optionRequest.isCorrect()) + .displayOrder(displayOrder) .build(); option = quizOptionRepository.save(option); savedOptions.add(option); @@ -129,7 +132,9 @@ public AdminQuizDetailResponse updateQuiz(Long quizId, AdminQuizUpdateRequest re } Set requestedLabels = new HashSet<>(); - for (AdminQuizOptionRequest optionRequest : request.options()) { + for (int i = 0; i < request.options().size(); i++) { + AdminQuizOptionRequest optionRequest = request.options().get(i); + int displayOrder = resolveDisplayOrder(optionRequest.displayOrder(), i + 1); requestedLabels.add(optionRequest.label()); QuizOption option = existingOptionMap.get(optionRequest.label()); @@ -140,13 +145,15 @@ public AdminQuizDetailResponse updateQuiz(Long quizId, AdminQuizUpdateRequest re .text(optionRequest.text()) .detailText(optionRequest.detailText()) .isCorrect(optionRequest.isCorrect()) + .displayOrder(displayOrder) .build(); option = quizOptionRepository.save(option); } else { option.update( optionRequest.text(), optionRequest.detailText(), - optionRequest.isCorrect() + optionRequest.isCorrect(), + displayOrder ); } } @@ -171,6 +178,13 @@ public AdminQuizDeleteResponse deleteQuiz(Long quizId) { return new AdminQuizDeleteResponse(true, LocalDateTime.now()); } + private int resolveDisplayOrder(Integer requestedOrder, int fallbackOrder) { + if (requestedOrder == null || requestedOrder < 1) { + return fallbackOrder; + } + return requestedOrder; + } + private void ensureTodayPicks(LocalDate today, int requiredCount) { List todays = quizRepository.findTodayPicks(QuizStatus.PUBLISHED, today, PageRequest.of(0, requiredCount)); int missingCount = requiredCount - todays.size(); diff --git a/src/main/java/com/swyp/picke/domain/scenario/service/ScenarioServiceImpl.java b/src/main/java/com/swyp/picke/domain/scenario/service/ScenarioServiceImpl.java index 11e916b..004c614 100644 --- a/src/main/java/com/swyp/picke/domain/scenario/service/ScenarioServiceImpl.java +++ b/src/main/java/com/swyp/picke/domain/scenario/service/ScenarioServiceImpl.java @@ -139,6 +139,11 @@ public void updateScenarioContent(Long scenarioId, ScenarioCreateRequest request if (mergedAudioUrl != null) s3Service.deleteFile(mergedAudioUrl); } scenario.clearAudios(); + + // 발행 상태에서 시나리오가 변경되면 merged 오디오를 다시 생성한다. + if (scenario.getStatus() == ScenarioStatus.PUBLISHED) { + triggerAudioPipeline(scenarioId); + } } } @@ -259,12 +264,17 @@ private boolean smartUpdateNodesToScenario(Scenario scenario, ScenarioCreateRequ } private void triggerAudioPipeline(Long scenarioId) { - TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { - @Override - public void afterCommit() { - audioPipelineService.generateAndMergeAudioAsync(scenarioId); - } - }); + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + audioPipelineService.generateAndMergeAudioAsync(scenarioId); + } + }); + return; + } + + audioPipelineService.generateAndMergeAudioAsync(scenarioId); } private boolean updateScriptsSmartly(ScenarioNode existingNode, java.util.List requestedScripts, Map speakerMap) { diff --git a/src/main/resources/static/js/admin/api/api-save.js b/src/main/resources/static/js/admin/api/api-save.js index d22ddec..ff5eb28 100644 --- a/src/main/resources/static/js/admin/api/api-save.js +++ b/src/main/resources/static/js/admin/api/api-save.js @@ -26,6 +26,11 @@ return Number.isNaN(parsed) ? null : parsed; }; + const setHiddenImageValue = (id, value) => { + const input = document.getElementById(id); + if (input) input.value = value || ''; + }; + const getTargetDate = (type) => { const inputIdByType = { BATTLE: 'battle-target-date', @@ -52,7 +57,9 @@ const previousStatus = PickeData.currentStatus; const targetDate = getTargetDate(currentType); const resolvedStatus = getStatus(currentType); - const shouldUploadAssets = action === 'PUBLISHED' || action === 'PUBLISH'; + const hasNewBattleImageUploads = currentType === 'BATTLE' + && !!(PickeData.uploadedFiles.thumbnail || PickeData.uploadedFiles.charA || PickeData.uploadedFiles.charB); + const shouldUploadAssets = action === 'PUBLISHED' || action === 'PUBLISH' || (action === 'EDIT' && hasNewBattleImageUploads); const shouldUploadLocalDraft = action === 'PENDING'; PickeData.currentTargetDate = targetDate; @@ -84,6 +91,14 @@ charAUrl = toUrlString(charAUrl); charBUrl = toUrlString(charBUrl); + PickeData.existingUrls.thumbnail = thumbnailUrl || null; + PickeData.existingUrls.charA = charAUrl || null; + PickeData.existingUrls.charB = charBUrl || null; + + setHiddenImageValue('battle-thumbnail-url', thumbnailUrl); + setHiddenImageValue('char-a-image-url', charAUrl); + setHiddenImageValue('char-b-image-url', charBUrl); + let payload = null; let requestUrl = ''; @@ -283,7 +298,10 @@ } if (scenarioExisted && PickeData.scenarioId) { - const shouldPatchScenarioStatus = action === 'PUBLISH' || previousStatus !== resolvedStatus; + const shouldPatchScenarioStatus = + action === 'PUBLISHED' + || action === 'PUBLISH' + || previousStatus !== resolvedStatus; if (shouldPatchScenarioStatus) { const statusRes = await fetch(`/api/v1/admin/scenarios/${PickeData.scenarioId}`, { method: 'PATCH', diff --git a/src/main/resources/templates/admin/components/form-battle.html b/src/main/resources/templates/admin/components/form-battle.html index a35d282..af48a59 100644 --- a/src/main/resources/templates/admin/components/form-battle.html +++ b/src/main/resources/templates/admin/components/form-battle.html @@ -39,6 +39,12 @@

+ + + +
+ +
@@ -63,6 +69,7 @@

선택 A 철학자 이미지 +
@@ -86,6 +93,7 @@

선택 B 철학자 이미지 +
diff --git a/src/main/resources/templates/admin/components/form-quiz.html b/src/main/resources/templates/admin/components/form-quiz.html index 1274c88..00cbd4f 100644 --- a/src/main/resources/templates/admin/components/form-quiz.html +++ b/src/main/resources/templates/admin/components/form-quiz.html @@ -11,6 +11,11 @@

+
+ + +
+
@@ -52,3 +57,4 @@

+
+ + +
+
@@ -49,3 +54,4 @@

option.getLabel().name().equals("A")).findFirst().orElseThrow(); @@ -171,14 +172,15 @@ void createBattle_persistsAllMappedFields() throws Exception { } @Test - @DisplayName("관리자가 퀴즈를 생성할 때 현재 500을 반환한다") + @DisplayName("관리자가 퀴즈를 생성할 때 필드가 저장된다") void createQuiz_persistsAllMappedFields() throws Exception { User admin = createAdminUser(); String adminToken = jwtProvider.createAccessToken(admin.getId(), "ADMIN"); + LocalDate targetDate = LocalDate.now().plusDays(1); Map payload = Map.of( "title", "퀴즈 제목", - "targetDate", LocalDate.now().plusDays(1).toString(), + "targetDate", targetDate.toString(), "status", "PENDING", "options", List.of( Map.of( @@ -202,20 +204,24 @@ void createQuiz_persistsAllMappedFields() throws Exception { .header("Authorization", "Bearer " + adminToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(payload))) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.statusCode").value(500)); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.quizId").exists()) + .andExpect(jsonPath("$.data.targetDate").value(targetDate.toString())) + .andExpect(jsonPath("$.data.options[0].displayOrder").value(1)) + .andExpect(jsonPath("$.data.options[1].displayOrder").value(2)); } @Test - @DisplayName("관리자가 투표를 생성할 때 현재 500을 반환한다") + @DisplayName("관리자가 투표를 생성할 때 필드가 저장된다") void createPoll_persistsAllMappedFields() throws Exception { User admin = createAdminUser(); String adminToken = jwtProvider.createAccessToken(admin.getId(), "ADMIN"); + LocalDate targetDate = LocalDate.now().plusDays(2); Map payload = Map.of( "titlePrefix", "당신은", "titleSuffix", "어느 쪽인가요?", - "targetDate", LocalDate.now().plusDays(2).toString(), + "targetDate", targetDate.toString(), "status", "PENDING", "options", List.of( Map.of( @@ -235,8 +241,11 @@ void createPoll_persistsAllMappedFields() throws Exception { .header("Authorization", "Bearer " + adminToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(payload))) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.statusCode").value(500)); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.pollId").exists()) + .andExpect(jsonPath("$.data.targetDate").value(targetDate.toString())) + .andExpect(jsonPath("$.data.options[0].displayOrder").value(1)) + .andExpect(jsonPath("$.data.options[1].displayOrder").value(2)); } @Test diff --git a/src/test/java/com/swyp/picke/domain/scenario/service/ScenarioServiceImplTest.java b/src/test/java/com/swyp/picke/domain/scenario/service/ScenarioServiceImplTest.java index 2e60d4b..2d409aa 100644 --- a/src/test/java/com/swyp/picke/domain/scenario/service/ScenarioServiceImplTest.java +++ b/src/test/java/com/swyp/picke/domain/scenario/service/ScenarioServiceImplTest.java @@ -161,6 +161,43 @@ void updateScenarioContent_voiceChanged_invalidatesOnlyAffectedSpeakerChunks_and verify(s3Service).deleteFile("s3://merged/common-old.mp3"); } + @Test + void updateScenarioContent_whenPublishedAndModified_triggersAudioPipeline() { + Scenario scenario = createScenario(); + scenario.updateStatus(ScenarioStatus.PUBLISHED); + ScenarioNode startNode = createNode("START", true); + Script script = createScript(SpeakerType.NARRATOR, "NARRATOR", "old-line", "s3://chunks/script-old.mp3"); + startNode.addScript(script); + scenario.addNode(startNode); + scenario.addAudioUrl(AudioPathType.COMMON, "s3://merged/common-old.mp3"); + scenario.replaceVoiceSettings(Map.of(SpeakerType.NARRATOR, "voice-narrator")); + + when(scenarioRepository.findById(3L)).thenReturn(Optional.of(scenario)); + when(battleOptionRepository.findByBattle(scenario.getBattle())).thenReturn(List.of()); + + ScenarioCreateRequest request = new ScenarioCreateRequest( + 1L, + false, + ScenarioStatus.PUBLISHED, + List.of( + new NodeRequest( + "START", + true, + "", + List.of(new ScriptRequest("NARRATOR", SpeakerType.NARRATOR, "new-line")), + List.of() + ) + ), + Map.of(SpeakerType.NARRATOR, "voice-narrator") + ); + + scenarioService.updateScenarioContent(3L, request); + + verify(s3Service).deleteFile("s3://chunks/script-old.mp3"); + verify(s3Service).deleteFile("s3://merged/common-old.mp3"); + verify(audioPipelineService).generateAndMergeAudioAsync(3L); + } + private Scenario createScenario() { Battle battle = Battle.builder() .title("battle")