From d289905ab82e23eff6c31cbf18e15ee991b11b6f Mon Sep 17 00:00:00 2001 From: johnbosco0414 Date: Wed, 15 Oct 2025 03:31:57 +0900 Subject: [PATCH] =?UTF-8?q?[FIX]=20=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=94=94=EB=B2=84=EA=B9=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/DecisionLineRepository.java | 3 + .../scenario/service/ScenarioService.java | 4 +- .../ai/client/text/GeminiJsonTextClient.java | 1 + .../ai/client/text/GeminiTextClient.java | 19 ++++--- .../ai/config/BaseScenarioAiProperties.java | 12 ++++ .../config/DecisionScenarioAiProperties.java | 12 ++++ .../ai/dto/result/BaseScenarioResult.java | 6 ++ .../ai/dto/result/DecisionScenarioResult.java | 9 +++ .../global/ai/prompt/BaseScenarioPrompt.java | 25 +++++---- .../ai/prompt/DecisionScenarioPrompt.java | 25 +++++---- .../back/global/ai/service/AiServiceImpl.java | 55 ++++++++++++++++--- back/src/main/resources/application.yml | 16 ++++-- .../scenario/service/ScenarioServiceTest.java | 14 ++--- 13 files changed, 152 insertions(+), 49 deletions(-) diff --git a/back/src/main/java/com/back/domain/node/repository/DecisionLineRepository.java b/back/src/main/java/com/back/domain/node/repository/DecisionLineRepository.java index 1be7704..44e5745 100644 --- a/back/src/main/java/com/back/domain/node/repository/DecisionLineRepository.java +++ b/back/src/main/java/com/back/domain/node/repository/DecisionLineRepository.java @@ -20,5 +20,8 @@ public interface DecisionLineRepository extends JpaRepository findWithUserById(Long id); + @EntityGraph(attributePaths = {"user", "baseLine", "baseLine.baseNodes"}) + Optional findWithUserAndBaseLineById(Long id); + void deleteByBaseLine_Id(Long baseLineId); } \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/service/ScenarioService.java b/back/src/main/java/com/back/domain/scenario/service/ScenarioService.java index b75f429..aed2000 100644 --- a/back/src/main/java/com/back/domain/scenario/service/ScenarioService.java +++ b/back/src/main/java/com/back/domain/scenario/service/ScenarioService.java @@ -102,8 +102,8 @@ private ScenarioValidationResult validateScenarioCreation( ScenarioCreateRequest request, @Nullable DecisionNodeNextRequest lastDecision) { - // DecisionLine 존재 여부 확인 (User EAGER 로딩) - DecisionLine decisionLine = decisionLineRepository.findWithUserById(request.decisionLineId()) + // DecisionLine 존재 여부 확인 (User, BaseLine, BaseLine.baseNodes EAGER 로딩) + DecisionLine decisionLine = decisionLineRepository.findWithUserAndBaseLineById(request.decisionLineId()) .orElseThrow(() -> new ApiException(ErrorCode.DECISION_LINE_NOT_FOUND)); // 권한 검증 diff --git a/back/src/main/java/com/back/global/ai/client/text/GeminiJsonTextClient.java b/back/src/main/java/com/back/global/ai/client/text/GeminiJsonTextClient.java index e550435..778aeaa 100644 --- a/back/src/main/java/com/back/global/ai/client/text/GeminiJsonTextClient.java +++ b/back/src/main/java/com/back/global/ai/client/text/GeminiJsonTextClient.java @@ -57,6 +57,7 @@ public CompletableFuture generateText(String prompt) { @Override public CompletableFuture generateText(AiRequest aiRequest) { + log.info("[CLIENT] GeminiJsonTextClient (2.0) is being used."); if (aiRequest == null || aiRequest.prompt() == null) { return CompletableFuture.failedFuture(new AiParsingException("Prompt is null")); } diff --git a/back/src/main/java/com/back/global/ai/client/text/GeminiTextClient.java b/back/src/main/java/com/back/global/ai/client/text/GeminiTextClient.java index 1e46115..9c00eab 100644 --- a/back/src/main/java/com/back/global/ai/client/text/GeminiTextClient.java +++ b/back/src/main/java/com/back/global/ai/client/text/GeminiTextClient.java @@ -45,11 +45,12 @@ public CompletableFuture generateText(String prompt) { @Override public CompletableFuture generateText(AiRequest aiRequest) { + log.info("[CLIENT] GeminiTextClient (2.5) is being used."); return webClient .post() .uri("/v1beta/models/{model}:generateContent", textAiConfig.getModel()) .contentType(MediaType.APPLICATION_JSON) - .bodyValue(createGeminiRequest(aiRequest.prompt(), aiRequest.maxTokens())) + .bodyValue(createGeminiRequest(aiRequest)) .retrieve() .onStatus(HttpStatusCode::isError, this::handleErrorResponse) .bodyToMono(GeminiResponse.class) @@ -64,17 +65,17 @@ public CompletableFuture generateText(AiRequest aiRequest) { .toFuture(); } - private Map createGeminiRequest(String prompt, int maxTokens) { + private Map createGeminiRequest(AiRequest aiRequest) { + // AiRequest로부터 generationConfig를 가져와 사용 + java.util.Map generationConfig = new java.util.HashMap<>(aiRequest.parameters()); + // maxTokens는 AiRequest의 전용 필드에서 가져와 확실히 설정 + generationConfig.put("maxOutputTokens", aiRequest.maxTokens()); + return Map.of( "contents", List.of( - Map.of("parts", List.of(Map.of("text", prompt))) + Map.of("parts", List.of(Map.of("text", aiRequest.prompt()))) ), - "generationConfig", Map.of( - "temperature", 0.8, // 시나리오 생성용 창의성 향상 (0.7 → 0.8) - "topK", 3, // 성능 최적화 (40 → 3, 10-15% 속도 향상) - "topP", 0.95, - "maxOutputTokens", maxTokens // AiRequest의 maxTokens 사용 - ) + "generationConfig", generationConfig ); } diff --git a/back/src/main/java/com/back/global/ai/config/BaseScenarioAiProperties.java b/back/src/main/java/com/back/global/ai/config/BaseScenarioAiProperties.java index e71ecb9..456c0fc 100644 --- a/back/src/main/java/com/back/global/ai/config/BaseScenarioAiProperties.java +++ b/back/src/main/java/com/back/global/ai/config/BaseScenarioAiProperties.java @@ -11,9 +11,21 @@ public class BaseScenarioAiProperties { private int maxOutputTokens = 1000; private int timeoutSeconds = 60; + // Generation Config (AI 응답 품질 제어) + private double temperature = 0.7; + private double topP = 0.9; + private int topK = 40; + // getters/setters public int getMaxOutputTokens() { return maxOutputTokens; } public void setMaxOutputTokens(int maxOutputTokens) { this.maxOutputTokens = maxOutputTokens; } public int getTimeoutSeconds() { return timeoutSeconds; } public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; } + + public double getTemperature() { return temperature; } + public void setTemperature(double temperature) { this.temperature = temperature; } + public double getTopP() { return topP; } + public void setTopP(double topP) { this.topP = topP; } + public int getTopK() { return topK; } + public void setTopK(int topK) { this.topK = topK; } } diff --git a/back/src/main/java/com/back/global/ai/config/DecisionScenarioAiProperties.java b/back/src/main/java/com/back/global/ai/config/DecisionScenarioAiProperties.java index 84050b8..96f829c 100644 --- a/back/src/main/java/com/back/global/ai/config/DecisionScenarioAiProperties.java +++ b/back/src/main/java/com/back/global/ai/config/DecisionScenarioAiProperties.java @@ -11,9 +11,21 @@ public class DecisionScenarioAiProperties { private int maxOutputTokens = 1200; private int timeoutSeconds = 60; + // Generation Config (AI 응답 품질 제어) + private double temperature = 0.7; + private double topP = 0.9; + private int topK = 40; + // getters/setters public int getMaxOutputTokens() { return maxOutputTokens; } public void setMaxOutputTokens(int maxOutputTokens) { this.maxOutputTokens = maxOutputTokens; } public int getTimeoutSeconds() { return timeoutSeconds; } public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; } + + public double getTemperature() { return temperature; } + public void setTemperature(double temperature) { this.temperature = temperature; } + public double getTopP() { return topP; } + public void setTopP(double topP) { this.topP = topP; } + public int getTopK() { return topK; } + public void setTopK(int topK) { this.topK = topK; } } diff --git a/back/src/main/java/com/back/global/ai/dto/result/BaseScenarioResult.java b/back/src/main/java/com/back/global/ai/dto/result/BaseScenarioResult.java index 14a706f..c2e638e 100644 --- a/back/src/main/java/com/back/global/ai/dto/result/BaseScenarioResult.java +++ b/back/src/main/java/com/back/global/ai/dto/result/BaseScenarioResult.java @@ -31,6 +31,9 @@ public record Indicator( * indicators 배열을 Map로 변환 */ public Map indicatorScores() { + if (indicators == null) { + return java.util.Collections.emptyMap(); + } return indicators.stream() .collect(java.util.stream.Collectors.toMap( ind -> Type.valueOf(ind.type), @@ -42,6 +45,9 @@ public Map indicatorScores() { * indicators 배열을 Map로 변환 */ public Map indicatorAnalysis() { + if (indicators == null) { + return java.util.Collections.emptyMap(); + } return indicators.stream() .collect(java.util.stream.Collectors.toMap( ind -> Type.valueOf(ind.type), diff --git a/back/src/main/java/com/back/global/ai/dto/result/DecisionScenarioResult.java b/back/src/main/java/com/back/global/ai/dto/result/DecisionScenarioResult.java index 764db98..57c7edd 100644 --- a/back/src/main/java/com/back/global/ai/dto/result/DecisionScenarioResult.java +++ b/back/src/main/java/com/back/global/ai/dto/result/DecisionScenarioResult.java @@ -42,6 +42,9 @@ public record Comparison( * indicators 배열을 Map로 변환 */ public Map indicatorScores() { + if (indicators == null) { + return java.util.Collections.emptyMap(); + } return indicators.stream() .collect(java.util.stream.Collectors.toMap( ind -> Type.valueOf(ind.type), @@ -53,6 +56,9 @@ public Map indicatorScores() { * indicators 배열을 Map로 변환 */ public Map indicatorAnalysis() { + if (indicators == null) { + return java.util.Collections.emptyMap(); + } return indicators.stream() .collect(java.util.stream.Collectors.toMap( ind -> Type.valueOf(ind.type), @@ -64,6 +70,9 @@ public Map indicatorAnalysis() { * comparisons 배열을 Map로 변환 */ public Map comparisonResults() { + if (comparisons == null) { + return java.util.Collections.emptyMap(); + } return comparisons.stream() .collect(java.util.stream.Collectors.toMap( Comparison::type, diff --git a/back/src/main/java/com/back/global/ai/prompt/BaseScenarioPrompt.java b/back/src/main/java/com/back/global/ai/prompt/BaseScenarioPrompt.java index 3e93af5..f5f1a01 100644 --- a/back/src/main/java/com/back/global/ai/prompt/BaseScenarioPrompt.java +++ b/back/src/main/java/com/back/global/ai/prompt/BaseScenarioPrompt.java @@ -34,7 +34,7 @@ public class BaseScenarioPrompt { ## 현재 삶 정보 베이스라인: {baselineDescription} - ## 현재 분기점들 + ## 과거 주요 인생 기록 {baseNodes} ## 요구사항 (JSON 형식) @@ -99,13 +99,13 @@ public static String generatePrompt(BaseLine baseLine) { BaseNode node = baseNodes.get(i); int actualYear = birthYear + node.getAgeYear() - 1; // 실제 연도 계산 - baseNodesInfo.append(String.format("%d. 카테고리: %s | 나이: %d세 (%d년) | 상황: %s | 결정: %s\n", + baseNodesInfo.append(String.format("%d. 카테고리: %s | 나이: %d세 (%d년) | 사건: %s | 결과: %s\n", i + 1, node.getCategory() != null ? node.getCategory().name() : "없음", node.getAgeYear(), actualYear, - node.getSituation() != null ? node.getSituation() : "상황 없음", - node.getDecision() != null ? node.getDecision() : "결정 없음")); + node.getSituation() != null ? node.getSituation() : "사건 없음", + node.getDecision() != null ? node.getDecision() : "결과 없음")); // 가장 최근 노드의 연도를 시나리오 기준 연도로 사용 if (i == baseNodes.size() - 1) { @@ -117,13 +117,18 @@ public static String generatePrompt(BaseLine baseLine) { int currentYear = java.time.LocalDate.now().getYear(); int userCurrentAge = currentYear - birthYear + 1; - // BaseNode들의 실제 연도들을 타임라인 연도로 사용 + // 맨 처음과 맨 끝 노드를 제외한 중간 노드들의 연도만 타임라인에 사용 StringBuilder timelineYears = new StringBuilder(); - for (int i = 0; i < baseNodes.size(); i++) { - BaseNode node = baseNodes.get(i); - int actualYear = birthYear + node.getAgeYear() - 1; - if (i > 0) timelineYears.append(", "); - timelineYears.append('"').append(actualYear).append('"').append(": \"제목 (5단어 이내)\""); + if (baseNodes.size() > 2) { + java.util.List intermediateNodes = baseNodes.subList(1, baseNodes.size() - 1); + for (int i = 0; i < intermediateNodes.size(); i++) { + BaseNode node = intermediateNodes.get(i); + int actualYear = birthYear + node.getAgeYear() - 1; + if (i > 0) { + timelineYears.append(", "); + } + timelineYears.append('"').append(actualYear).append('"').append(": \"해당 연도 요약 (5단어 이내)\""); + } } // 사용자 정보 추출 (null-safe) diff --git a/back/src/main/java/com/back/global/ai/prompt/DecisionScenarioPrompt.java b/back/src/main/java/com/back/global/ai/prompt/DecisionScenarioPrompt.java index 9a42393..0e7f063 100644 --- a/back/src/main/java/com/back/global/ai/prompt/DecisionScenarioPrompt.java +++ b/back/src/main/java/com/back/global/ai/prompt/DecisionScenarioPrompt.java @@ -39,7 +39,7 @@ public class DecisionScenarioPrompt { 설명: {baseDescription} 지표: {baseIndicators} - ## 대안 선택 경로 + ## 새로운 선택 기록 {decisionNodes} ## 요구사항 (JSON 형식) @@ -116,12 +116,12 @@ public static String generatePrompt(DecisionLine decisionLine, Scenario baseScen int actualYear = birthYear + node.getAgeYear() - 1; // 실제 연도 계산 decisionNodesInfo.append(String.format( - "%d단계 선택 (%d세, %d년):\n상황: %s\n결정: %s\n\n", + "%d단계 선택 (%d세, %d년):\n사건: %s\n선택: %s\n\n", i + 1, node.getAgeYear(), actualYear, - node.getSituation() != null ? node.getSituation() : "상황 정보 없음", - node.getDecision() != null ? node.getDecision() : "결정 정보 없음" + node.getSituation() != null ? node.getSituation() : "사건 정보 없음", + node.getDecision() != null ? node.getDecision() : "선택 정보 없음" )); } @@ -146,13 +146,18 @@ public static String generatePrompt(DecisionLine decisionLine, Scenario baseScen int careerScore = getScoreByType(baseSceneTypes, "직업"); int healthScore = getScoreByType(baseSceneTypes, "건강"); - // DecisionNode들의 실제 연도들을 타임라인 연도로 사용 + // 맨 처음과 맨 끝 노드를 제외한 중간 노드들의 연도만 타임라인에 사용 StringBuilder timelineYears = new StringBuilder(); - for (int i = 0; i < decisionNodes.size(); i++) { - DecisionNode node = decisionNodes.get(i); - int actualYear = birthYear + node.getAgeYear() - 1; - if (i > 0) timelineYears.append(", "); - timelineYears.append('"').append(actualYear).append('"').append(": \"제목 (5단어 이내)\""); + if (decisionNodes.size() > 2) { + java.util.List intermediateNodes = decisionNodes.subList(1, decisionNodes.size() - 1); + for (int i = 0; i < intermediateNodes.size(); i++) { + DecisionNode node = intermediateNodes.get(i); + int actualYear = birthYear + node.getAgeYear() - 1; + if (i > 0) { + timelineYears.append(", "); + } + timelineYears.append('"').append(actualYear).append('"').append(": \"해당 연도 요약 (5단어 이내)\""); + } } // 사용자 정보 추출 (null-safe) diff --git a/back/src/main/java/com/back/global/ai/service/AiServiceImpl.java b/back/src/main/java/com/back/global/ai/service/AiServiceImpl.java index 0bdd817..0c3c161 100644 --- a/back/src/main/java/com/back/global/ai/service/AiServiceImpl.java +++ b/back/src/main/java/com/back/global/ai/service/AiServiceImpl.java @@ -35,11 +35,11 @@ */ @Service -@RequiredArgsConstructor @Slf4j public class AiServiceImpl implements AiService { - private final @Qualifier("gemini25TextClient") TextAiClient textAiClient; + private final TextAiClient scenarioClient; + private final TextAiClient situationClient; private final ObjectMapper objectMapper; private final SceneTypeRepository sceneTypeRepository; private final SituationAiProperties situationAiProperties; @@ -48,6 +48,26 @@ public class AiServiceImpl implements AiService { private final ImageAiClient imageAiClient; private final com.back.global.storage.StorageService storageService; + public AiServiceImpl(@Qualifier("gemini25TextClient") TextAiClient scenarioClient, + @Qualifier("gemini20JsonClient") TextAiClient situationClient, + ObjectMapper objectMapper, + SceneTypeRepository sceneTypeRepository, + SituationAiProperties situationAiProperties, + BaseScenarioAiProperties baseScenarioAiProperties, + DecisionScenarioAiProperties decisionScenarioAiProperties, + ImageAiClient imageAiClient, + com.back.global.storage.StorageService storageService) { + this.scenarioClient = scenarioClient; + this.situationClient = situationClient; + this.objectMapper = objectMapper; + this.sceneTypeRepository = sceneTypeRepository; + this.situationAiProperties = situationAiProperties; + this.baseScenarioAiProperties = baseScenarioAiProperties; + this.decisionScenarioAiProperties = decisionScenarioAiProperties; + this.imageAiClient = imageAiClient; + this.storageService = storageService; + } + @Override public CompletableFuture generateBaseScenario(BaseLine baseLine) { if (baseLine == null) { @@ -65,10 +85,21 @@ public CompletableFuture generateBaseScenario(BaseLine baseL // Step 2: AI 호출 및 파싱 int maxTokens = baseScenarioAiProperties.getMaxOutputTokens(); log.info("Using maxOutputTokens: {} for base scenario generation", maxTokens); - AiRequest request = new AiRequest(baseScenarioPrompt, Map.of(), maxTokens); - return textAiClient.generateText(request) + + // JSON 모드 강제 + 구조화된 응답 유도 (application.yml에서 관리) + Map generationConfig = Map.of( + "temperature", baseScenarioAiProperties.getTemperature(), + "topP", baseScenarioAiProperties.getTopP(), + "topK", baseScenarioAiProperties.getTopK(), + "candidateCount", 1, + "response_mime_type", "application/json" // JSON 모드 강제 + ); + + AiRequest request = new AiRequest(baseScenarioPrompt, generationConfig, maxTokens); + return scenarioClient.generateText(request) .thenApply(aiResponse -> { try { + log.info("Raw AI response for BaseLine ID: {}: {}", baseLine.getId(), aiResponse); log.debug("Received AI response for BaseLine ID: {}, length: {}", baseLine.getId(), aiResponse.length()); // Remove markdown code block wrappers (```json ... ```) @@ -131,10 +162,20 @@ public CompletableFuture generateDecisionScenario(Decisi log.debug("Generated decision scenario prompt for DecisionLine ID: {}", decisionLine.getId()); // Step 2: AI 호출 및 파싱 - AiRequest request = new AiRequest(newScenarioPrompt, Map.of(), decisionScenarioAiProperties.getMaxOutputTokens()); - return textAiClient.generateText(request) + // JSON 모드 강제 + 구조화된 응답 유도 (application.yml에서 관리) + Map generationConfig = Map.of( + "temperature", decisionScenarioAiProperties.getTemperature(), + "topP", decisionScenarioAiProperties.getTopP(), + "topK", decisionScenarioAiProperties.getTopK(), + "candidateCount", 1, + "response_mime_type", "application/json" // JSON 모드 강제 + ); + + AiRequest request = new AiRequest(newScenarioPrompt, generationConfig, decisionScenarioAiProperties.getMaxOutputTokens()); + return scenarioClient.generateText(request) .thenApply(aiResponse -> { try { + log.info("Raw AI response for DecisionLine ID: {}: {}", decisionLine.getId(), aiResponse); log.debug("Received AI response for DecisionLine ID: {}, length: {}", decisionLine.getId(), aiResponse.length()); // Remove markdown code block wrappers (```json ... ```) @@ -215,7 +256,7 @@ public CompletableFuture generateSituation(List previousNo // Step 2: AI 호출 및 상황 텍스트 추출 AiRequest request = new AiRequest(situationPrompt, Map.of(), situationAiProperties.getMaxOutputTokens()); - return textAiClient.generateText(request) + return situationClient.generateText(request) .thenApply(aiResponse -> { try { log.debug("Received AI response for situation generation, length: {}", diff --git a/back/src/main/resources/application.yml b/back/src/main/resources/application.yml index 045dab9..828a65a 100644 --- a/back/src/main/resources/application.yml +++ b/back/src/main/resources/application.yml @@ -92,8 +92,8 @@ ai: model20: gemini-2.0-flash base-url: https://generativelanguage.googleapis.com timeout-seconds: 70 # AI 응답 대기 시간 (시나리오 생성: 30-40초 + 여유 30초) - max-retries: 0 # 재시도 비활성화 (타임아웃 방지) - retry-delay-seconds: 2 # 재시도 간격 (초) + max-retries: 2 # 재시도 횟수 (총 3번 시도, 일시 오류 복구) + retry-delay-seconds: 3 # 재시도 간격 (초, 지수 백오프 권장) max-context-tokens: 8192 image: enabled: true @@ -114,10 +114,18 @@ ai: timeout-seconds: 30 # 상황 생성 타임아웃 (30초, 실시간 응답용) base-scenario: maxOutputTokens: 16384 # 8192 → 16384 (gemini-2.5-flash 최대 65536, 충분한 여유) - timeout-seconds: 60 # 베이스 시나리오 생성 타임아웃 (실제: 30-40초 + 여유) + timeout-seconds: 90 # 베이스 시나리오 생성 타임아웃 (실제: 30-40초 + 여유) + # Generation Config (AI 응답 품질 제어) + temperature: 0.7 # 창의성 vs 일관성 (0.0=결정론적, 1.0=창의적) + topP: 0.9 # 누적 확률 기준 필터링 (0.9=상위 90%) + topK: 5 # 상위 K개 토큰만 고려 (다양성 확보) decision-scenario: maxOutputTokens: 16384 # 8192 -> 16384 (gemini-2.5-flash 최대 65536, 충분한 여유) - timeout-seconds: 60 # 결정 시나리오 생성 타임아웃 (실제: 30-40초 + 여유) + timeout-seconds: 90 # 결정 시나리오 생성 타임아웃 (실제: 30-40초 + 여유) + # Generation Config (AI 응답 품질 제어) + temperature: 0.7 # 창의성 vs 일관성 (0.0=결정론적, 1.0=창의적) + topP: 0.9 # 누적 확률 기준 필터링 (0.9=상위 90%) + topK: 5 # 상위 K개 토큰만 고려 (다양성 확보) embedding: dim: 768 diff --git a/back/src/test/java/com/back/domain/scenario/service/ScenarioServiceTest.java b/back/src/test/java/com/back/domain/scenario/service/ScenarioServiceTest.java index 0970d2d..fab851e 100644 --- a/back/src/test/java/com/back/domain/scenario/service/ScenarioServiceTest.java +++ b/back/src/test/java/com/back/domain/scenario/service/ScenarioServiceTest.java @@ -80,7 +80,7 @@ class CreateScenarioTests { ReflectionTestUtils.setField(savedScenario, "id", 1001L); // 실제 ScenarioService 구현에 맞춘 모킹 - given(decisionLineRepository.findWithUserById(decisionLineId)) + given(decisionLineRepository.findWithUserAndBaseLineById(decisionLineId)) .willReturn(Optional.of(mockDecisionLine)); given(scenarioRepository.findByDecisionLineId(decisionLineId)) .willReturn(Optional.empty()); // 기존 시나리오 없음 @@ -100,7 +100,7 @@ class CreateScenarioTests { assertThat(result.message()).isEqualTo("시나리오 생성이 시작되었습니다."); // 동기 부분의 핵심 비즈니스 로직만 검증 - verify(decisionLineRepository).findWithUserById(decisionLineId); + verify(decisionLineRepository).findWithUserAndBaseLineById(decisionLineId); verify(scenarioRepository).findByDecisionLineId(decisionLineId); verify(scenarioRepository).save(any(Scenario.class)); @@ -116,7 +116,7 @@ class CreateScenarioTests { Long decisionLineId = 999L; ScenarioCreateRequest request = new ScenarioCreateRequest(decisionLineId); - given(decisionLineRepository.findWithUserById(decisionLineId)) + given(decisionLineRepository.findWithUserAndBaseLineById(decisionLineId)) .willReturn(Optional.empty()); // When & Then @@ -124,7 +124,7 @@ class CreateScenarioTests { .isInstanceOf(ApiException.class) .hasFieldOrPropertyWithValue("errorCode", ErrorCode.DECISION_LINE_NOT_FOUND); - verify(decisionLineRepository).findWithUserById(decisionLineId); + verify(decisionLineRepository).findWithUserAndBaseLineById(decisionLineId); verify(scenarioRepository, never()).save(any()); } @@ -145,7 +145,7 @@ class CreateScenarioTests { .build(); ReflectionTestUtils.setField(mockDecisionLine, "id", decisionLineId); - given(decisionLineRepository.findWithUserById(decisionLineId)) + given(decisionLineRepository.findWithUserAndBaseLineById(decisionLineId)) .willReturn(Optional.of(mockDecisionLine)); // When & Then @@ -153,7 +153,7 @@ class CreateScenarioTests { .isInstanceOf(ApiException.class) .hasFieldOrPropertyWithValue("errorCode", ErrorCode.HANDLE_ACCESS_DENIED); - verify(decisionLineRepository).findWithUserById(decisionLineId); + verify(decisionLineRepository).findWithUserAndBaseLineById(decisionLineId); verify(scenarioRepository, never()).save(any()); } @@ -181,7 +181,7 @@ class CreateScenarioTests { .build(); ReflectionTestUtils.setField(existingScenario, "id", 999L); - given(decisionLineRepository.findWithUserById(decisionLineId)) + given(decisionLineRepository.findWithUserAndBaseLineById(decisionLineId)) .willReturn(Optional.of(mockDecisionLine)); given(scenarioRepository.findByDecisionLineId(decisionLineId)) .willReturn(Optional.of(existingScenario)); // 기존 PENDING 시나리오 존재