diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/scenario/response/AdminScenarioNodeResponse.java b/src/main/java/com/swyp/picke/domain/admin/dto/scenario/response/AdminScenarioNodeResponse.java index 77dbf44e..984cdd1b 100644 --- a/src/main/java/com/swyp/picke/domain/admin/dto/scenario/response/AdminScenarioNodeResponse.java +++ b/src/main/java/com/swyp/picke/domain/admin/dto/scenario/response/AdminScenarioNodeResponse.java @@ -8,8 +8,10 @@ public record AdminScenarioNodeResponse( Long nodeId, String nodeName, + Boolean isStartNode, Integer audioDuration, Long autoNextNodeId, + String autoNextNode, List scripts, List interactiveOptions ) {} diff --git a/src/main/java/com/swyp/picke/domain/admin/dto/scenario/response/AdminScenarioOptionResponse.java b/src/main/java/com/swyp/picke/domain/admin/dto/scenario/response/AdminScenarioOptionResponse.java index 241c10d2..a9bb61f6 100644 --- a/src/main/java/com/swyp/picke/domain/admin/dto/scenario/response/AdminScenarioOptionResponse.java +++ b/src/main/java/com/swyp/picke/domain/admin/dto/scenario/response/AdminScenarioOptionResponse.java @@ -6,5 +6,6 @@ public record AdminScenarioOptionResponse( String label, String title, - Long nextNodeId + Long nextNodeId, + String nextNodeName ) {} diff --git a/src/main/java/com/swyp/picke/domain/scenario/converter/ScenarioConverter.java b/src/main/java/com/swyp/picke/domain/scenario/converter/ScenarioConverter.java index ebecb95a..1d886494 100644 --- a/src/main/java/com/swyp/picke/domain/scenario/converter/ScenarioConverter.java +++ b/src/main/java/com/swyp/picke/domain/scenario/converter/ScenarioConverter.java @@ -56,6 +56,11 @@ public UserScenarioResponse toUserResponse(Scenario scenario, AudioPathType reco } public AdminScenarioDetailResponse toAdminDetailResponse(Scenario scenario) { + // 어드민은 노드 이름으로 연결을 편집하므로 ID 기반 링크를 이름으로도 내려준다. + Map nodeNameById = scenario.getNodes().stream() + .filter(node -> node.getId() != null && node.getNodeName() != null) + .collect(Collectors.toMap(ScenarioNode::getId, ScenarioNode::getNodeName, (first, second) -> first)); + return AdminScenarioDetailResponse.builder() .scenarioId(scenario.getId()) .battleId(scenario.getBattle().getId()) @@ -63,7 +68,7 @@ public AdminScenarioDetailResponse toAdminDetailResponse(Scenario scenario) { .isInteractive(scenario.getIsInteractive()) .voiceSettings(new HashMap<>(scenario.getVoiceSettings())) .nodes(scenario.getNodes().stream() - .map(this::toAdminNodeResponse) + .map(node -> toAdminNodeResponse(node, nodeNameById)) .collect(Collectors.toList())) .build(); } @@ -99,17 +104,19 @@ private ScriptResponse toUserScriptResponse(Script script) { .build(); } - private AdminScenarioNodeResponse toAdminNodeResponse(ScenarioNode node) { + private AdminScenarioNodeResponse toAdminNodeResponse(ScenarioNode node, Map nodeNameById) { return AdminScenarioNodeResponse.builder() .nodeId(node.getId()) .nodeName(node.getNodeName()) + .isStartNode(Boolean.TRUE.equals(node.getIsStartNode())) .audioDuration(node.getAudioDuration()) .autoNextNodeId(node.getAutoNextNodeId()) + .autoNextNode(nodeNameById.get(node.getAutoNextNodeId())) .scripts(node.getScripts().stream() .map(this::toAdminScriptResponse) .collect(Collectors.toList())) .interactiveOptions(node.getOptions().stream() - .map(this::toAdminOptionResponse) + .map(option -> toAdminOptionResponse(option, nodeNameById)) .collect(Collectors.toList())) .build(); } @@ -133,11 +140,12 @@ private OptionResponse toUserOptionResponse(InteractiveOption option) { .build(); } - private AdminScenarioOptionResponse toAdminOptionResponse(InteractiveOption option) { + private AdminScenarioOptionResponse toAdminOptionResponse(InteractiveOption option, Map nodeNameById) { return AdminScenarioOptionResponse.builder() .label(option.getLabel()) .title(option.getTitle()) .nextNodeId(option.getNextNodeId()) + .nextNodeName(nodeNameById.get(option.getNextNodeId())) .build(); } } \ No newline at end of file diff --git a/src/test/java/com/swyp/picke/domain/admin/controller/AdminScenarioPublishFlowIntegrationTest.java b/src/test/java/com/swyp/picke/domain/admin/controller/AdminScenarioPublishFlowIntegrationTest.java index ed7c9c8b..20b50a45 100644 --- a/src/test/java/com/swyp/picke/domain/admin/controller/AdminScenarioPublishFlowIntegrationTest.java +++ b/src/test/java/com/swyp/picke/domain/admin/controller/AdminScenarioPublishFlowIntegrationTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -111,6 +112,66 @@ void patchScenarioStatus_toPublished_triggersAudioPipeline() throws Exception { verify(scenarioAudioPipelineService, timeout(1000)).generateAndMergeAudioAsync(scenarioId); } + @Test + void getAdminScenario_returnsNodeLinksByName() throws Exception { + String adminToken = createAdminToken(); + Battle battle = createBattle(); + + mockMvc.perform(post("/api/v1/admin/scenarios") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(interactiveScenarioPayload(battle.getId())))) + .andExpect(status().isCreated()); + + mockMvc.perform(get("/api/v1/admin/battles/{battleId}/scenario", battle.getId()) + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.nodes[?(@.nodeName == 'START')].isStartNode").value(true)) + .andExpect(jsonPath("$.data.nodes[?(@.nodeName == 'START')].interactiveOptions[0].nextNodeName").value("BRANCH_A")) + .andExpect(jsonPath("$.data.nodes[?(@.nodeName == 'START')].interactiveOptions[1].nextNodeName").value("BRANCH_B")) + .andExpect(jsonPath("$.data.nodes[?(@.nodeName == 'BRANCH_A')].isStartNode").value(false)) + .andExpect(jsonPath("$.data.nodes[?(@.nodeName == 'BRANCH_A')].autoNextNode").value("CLOSING")) + .andExpect(jsonPath("$.data.nodes[?(@.nodeName == 'BRANCH_B')].autoNextNode").value("CLOSING")); + } + + private Map interactiveScenarioPayload(Long battleId) { + return Map.of( + "battleId", battleId, + "isInteractive", true, + "status", "PENDING", + "nodes", List.of( + Map.of( + "nodeName", "START", + "isStartNode", true, + "autoNextNode", "", + "scripts", List.of(script("A", "Opening")), + "interactiveOptions", List.of( + Map.of("label", "A", "title", "A side", "nextNodeName", "BRANCH_A"), + Map.of("label", "B", "title", "B side", "nextNodeName", "BRANCH_B") + ) + ), + node("BRANCH_A", "CLOSING", script("A", "Branch A")), + node("BRANCH_B", "CLOSING", script("B", "Branch B")), + node("CLOSING", "", script("NARRATOR", "Closing")) + ), + "voiceSettings", Map.of("NARRATOR", "voice-narrator") + ); + } + + private Map node(String nodeName, String autoNextNode, Map script) { + return Map.of( + "nodeName", nodeName, + "isStartNode", false, + "autoNextNode", autoNextNode, + "scripts", List.of(script), + "interactiveOptions", List.of() + ); + } + + private Map script(String speakerType, String text) { + return Map.of("speakerType", speakerType, "speakerName", speakerType, "text", text); + } + private Map scenarioPayload(Long battleId, String status) { return Map.of( "battleId", battleId,