Skip to content

Commit 4171315

Browse files
committed
[FIX]: 시나리오생성디버그
1 parent 8cdb7a2 commit 4171315

File tree

15 files changed

+344
-118
lines changed

15 files changed

+344
-118
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ uploads/
2222

2323
# 테스트 이미지 경로 (LocalStorageServiceTest)
2424
test-uploads/
25-
./test-uploads/
25+
./test-uploads/
26+
27+
### Environment Variables ###
28+
.env

back/.gitignore

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ out/
3939
### Environment Variables ###
4040
.env
4141

42-
# AI 생성 이미지 저장 경로
43-
uploads/
44-
./uploads/
45-
4642
# 테스트 이미지 경로 (LocalStorageServiceTest)
4743
test-uploads/
4844
./test-uploads/

back/src/main/java/com/back/domain/scenario/repository/ScenarioRepository.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public interface ScenarioRepository extends JpaRepository<Scenario, Long> {
2323
@Query("SELECT s FROM Scenario s LEFT JOIN FETCH s.baseLine bl LEFT JOIN FETCH bl.scenarios WHERE s.id = :id AND s.user.id = :userId")
2424
Optional<Scenario> findByIdAndUserId(@Param("id") Long id, @Param("userId") Long userId);
2525

26+
// 시나리오 상태 폴링용 조회 (권한 검증 포함, 최소 데이터만 로딩)
27+
@Query("SELECT s FROM Scenario s WHERE s.id = :id AND s.user.id = :userId")
28+
Optional<Scenario> findByIdAndUserIdForStatusCheck(@Param("id") Long id, @Param("userId") Long userId);
29+
2630
// 베이스 시나리오 조회
2731
Optional<Scenario> findByBaseLineIdAndDecisionLineIsNull(Long baseLineId);
2832

@@ -33,6 +37,22 @@ public interface ScenarioRepository extends JpaRepository<Scenario, Long> {
3337
@Query("SELECT s FROM Scenario s WHERE s.decisionLine.id = :decisionLineId")
3438
Optional<Scenario> findByDecisionLineId(@Param("decisionLineId") Long decisionLineId);
3539

40+
// AI 생성용 시나리오 조회 - 1단계: DecisionLine + DecisionNodes
41+
@Query("SELECT DISTINCT s FROM Scenario s " +
42+
"LEFT JOIN FETCH s.decisionLine dl " +
43+
"LEFT JOIN FETCH dl.decisionNodes dn " +
44+
"WHERE s.id = :id")
45+
Optional<Scenario> findByIdWithDecisionNodes(@Param("id") Long id);
46+
47+
// AI 생성용 시나리오 조회 - 2단계: BaseLine + BaseNodes + User
48+
@Query("SELECT DISTINCT s FROM Scenario s " +
49+
"LEFT JOIN FETCH s.decisionLine dl " +
50+
"LEFT JOIN FETCH dl.baseLine bl " +
51+
"LEFT JOIN FETCH bl.baseNodes bn " +
52+
"LEFT JOIN FETCH bl.user " +
53+
"WHERE s.id = :id")
54+
Optional<Scenario> findByIdWithDecisionLineAndBaseLine(@Param("id") Long id);
55+
3656
// DecisionLine 기반 시나리오 조회 (권한 검증 포함, baseLine.scenarios fetch)
3757
@Query("SELECT s FROM Scenario s LEFT JOIN FETCH s.baseLine bl LEFT JOIN FETCH bl.scenarios WHERE s.decisionLine.id = :decisionLineId AND s.user.id = :userId")
3858
Optional<Scenario> findByDecisionLineIdAndUserId(

back/src/main/java/com/back/domain/scenario/service/ScenarioService.java

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ private ScenarioValidationResult validateScenarioCreation(
108108
ScenarioCreateRequest request,
109109
@Nullable DecisionNodeNextRequest lastDecision) {
110110

111-
// DecisionLine 존재 여부 확인
112-
DecisionLine decisionLine = decisionLineRepository.findById(request.decisionLineId())
111+
// DecisionLine 존재 여부 확인 (User EAGER 로딩)
112+
DecisionLine decisionLine = decisionLineRepository.findWithUserById(request.decisionLineId())
113113
.orElseThrow(() -> new ApiException(ErrorCode.DECISION_LINE_NOT_FOUND));
114114

115115
// 권한 검증
@@ -312,17 +312,20 @@ public void processScenarioGenerationAsync(Long scenarioId) {
312312
// 1. 상태를 PROCESSING으로 업데이트 (별도 트랜잭션)
313313
scenarioTransactionService.updateScenarioStatus(scenarioId, ScenarioStatus.PROCESSING, null);
314314

315-
// 2. AI 시나리오 생성 (트랜잭션 외부에서 실행)
316-
AiScenarioGenerationResult result = executeAiGeneration(scenarioId);
315+
// 2. AI 생성에 필요한 모든 데이터를 트랜잭션 내에서 미리 로드
316+
Scenario scenarioWithData = scenarioTransactionService.prepareScenarioData(scenarioId);
317317

318-
// 3. 결과 저장 및 완료 상태 업데이트 (별도 트랜잭션)
318+
// 3. AI 시나리오 생성 (트랜잭션 외부에서 실행)
319+
AiScenarioGenerationResult result = executeAiGeneration(scenarioWithData);
320+
321+
// 4. 결과 저장 및 완료 상태 업데이트 (별도 트랜잭션)
319322
scenarioTransactionService.saveAiResult(scenarioId, result);
320323
scenarioTransactionService.updateScenarioStatus(scenarioId, ScenarioStatus.COMPLETED, null);
321324

322325
log.info("Scenario generation completed successfully for ID: {}", scenarioId);
323326

324327
} catch (Exception e) {
325-
// 4. 실패 상태 업데이트 (별도 트랜잭션)
328+
// 5. 실패 상태 업데이트 (별도 트랜잭션)
326329
scenarioTransactionService.updateScenarioStatus(scenarioId, ScenarioStatus.FAILED,
327330
"시나리오 생성 실패: " + e.getMessage());
328331
log.error("Scenario generation failed for ID: {}, error: {}",
@@ -331,11 +334,8 @@ public void processScenarioGenerationAsync(Long scenarioId) {
331334
}
332335

333336
// AI 호출 전용 메서드 (트랜잭션 없음)
334-
private AiScenarioGenerationResult executeAiGeneration(Long scenarioId) {
335-
Scenario scenario = scenarioRepository.findById(scenarioId)
336-
.orElseThrow(() -> new ApiException(ErrorCode.SCENARIO_NOT_FOUND));
337-
338-
// AI 호출 로직 (트랜잭션 외부에서 실행)
337+
private AiScenarioGenerationResult executeAiGeneration(Scenario scenario) {
338+
// AI 호출 로직 (미리 로드된 데이터 사용)
339339
DecisionLine decisionLine = scenario.getDecisionLine();
340340
BaseLine baseLine = decisionLine.getBaseLine();
341341

@@ -347,7 +347,7 @@ private AiScenarioGenerationResult executeAiGeneration(Long scenarioId) {
347347
.generateDecisionScenario(decisionLine, baseScenario)
348348
.orTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
349349
.exceptionally(ex -> {
350-
log.error("Decision scenario generation timeout or error for scenario ID: {}", scenarioId, ex);
350+
log.error("Decision scenario generation timeout or error for scenario ID: {}", scenario.getId(), ex);
351351
throw new ApiException(ErrorCode.AI_REQUEST_TIMEOUT, "시나리오 생성 시간 초과 (60초)");
352352
})
353353
.join();
@@ -365,12 +365,12 @@ private Scenario ensureBaseScenarioExists(BaseLine baseLine) {
365365
private Scenario createBaseScenario(BaseLine baseLine) {
366366
log.info("Creating base scenario for BaseLine ID: {}", baseLine.getId());
367367

368-
// 1. AI 호출 with 타임아웃 (60초)
368+
// 1. AI 호출 with 타임아웃 (180초 - 테스트용)
369369
BaseScenarioResult aiResult = aiService.generateBaseScenario(baseLine)
370-
.orTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
370+
.orTimeout(180, java.util.concurrent.TimeUnit.SECONDS)
371371
.exceptionally(ex -> {
372372
log.error("Base scenario generation timeout or error for BaseLine ID: {}", baseLine.getId(), ex);
373-
throw new ApiException(ErrorCode.AI_REQUEST_TIMEOUT, "베이스 시나리오 생성 시간 초과 (60초)");
373+
throw new ApiException(ErrorCode.AI_REQUEST_TIMEOUT, "베이스 시나리오 생성 시간 초과 (180초)");
374374
})
375375
.join();
376376

@@ -394,7 +394,7 @@ private Scenario createBaseScenario(BaseLine baseLine) {
394394
@Transactional(readOnly = true)
395395
public ScenarioStatusResponse getScenarioStatus(Long scenarioId, Long userId) {
396396
// 권한 검증 및 조회
397-
Scenario scenario = scenarioRepository.findByIdAndUserId(scenarioId, userId)
397+
Scenario scenario = scenarioRepository.findByIdAndUserIdForStatusCheck(scenarioId, userId)
398398
.orElseThrow(() -> new ApiException(ErrorCode.SCENARIO_NOT_FOUND));
399399

400400
// DTO 변환 및 반환

back/src/main/java/com/back/domain/scenario/service/ScenarioTransactionService.java

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.back.domain.scenario.repository.SceneTypeRepository;
1010
import com.back.global.ai.dto.result.BaseScenarioResult;
1111
import com.back.global.ai.dto.result.DecisionScenarioResult;
12+
import com.back.global.ai.exception.AiParsingException;
1213
import com.back.global.ai.service.AiService;
1314
import com.back.global.exception.ApiException;
1415
import com.back.global.exception.ErrorCode;
@@ -164,24 +165,30 @@ private void handleImageGeneration(Scenario scenario, String imagePrompt) {
164165
}
165166

166167
private int calculateTotalScore(Map<Type, Integer> indicatorScores) {
168+
if (indicatorScores == null || indicatorScores.isEmpty()) {
169+
log.warn("indicatorScores is null or empty, returning 0");
170+
return 0;
171+
}
167172
return indicatorScores.values().stream()
168173
.mapToInt(Integer::intValue)
169174
.sum();
170175
}
171176

172177
private void createBaseSceneTypes(Scenario scenario, BaseScenarioResult aiResult) {
173-
List<SceneType> sceneTypes = aiResult.indicatorScores()
174-
.entrySet().stream()
175-
.map(entry -> {
176-
Type type = entry.getKey();
177-
int point = entry.getValue();
178-
String analysis = aiResult.indicatorAnalysis().get(type);
178+
if (aiResult.indicators() == null || aiResult.indicators().isEmpty()) {
179+
log.warn("No indicator scores available for scenario {}", scenario.getId());
180+
return;
181+
}
182+
183+
List<SceneType> sceneTypes = aiResult.indicators().stream()
184+
.map(indicator -> {
185+
Type type = Type.valueOf(indicator.type());
179186

180187
return com.back.domain.scenario.entity.SceneType.builder()
181188
.scenario(scenario)
182189
.type(type)
183-
.point(point) // AI가 분석한 실제 점수
184-
.analysis(analysis != null ? analysis : "현재 " + type.name() + " 상황 분석")
190+
.point(indicator.point()) // AI가 분석한 실제 점수
191+
.analysis(indicator.analysis() != null ? indicator.analysis() : "현재 " + type.name() + " 상황 분석")
185192
.build();
186193
})
187194
.toList();
@@ -190,18 +197,19 @@ private void createBaseSceneTypes(Scenario scenario, BaseScenarioResult aiResult
190197
}
191198

192199
private void createDecisionSceneTypes(Scenario scenario, DecisionScenarioResult aiResult) {
193-
List<com.back.domain.scenario.entity.SceneType> sceneTypes = aiResult.indicatorScores()
194-
.entrySet().stream()
195-
.map(entry -> {
196-
Type type = entry.getKey();
197-
int point = entry.getValue();
198-
String analysis = aiResult.indicatorAnalysis().get(type);
200+
if (aiResult.indicators() == null || aiResult.indicators().isEmpty()) {
201+
throw new AiParsingException("Indicator scores are missing in AI response for scenario " + scenario.getId());
202+
}
203+
204+
List<com.back.domain.scenario.entity.SceneType> sceneTypes = aiResult.indicators().stream()
205+
.map(indicator -> {
206+
Type type = Type.valueOf(indicator.type());
199207

200208
return com.back.domain.scenario.entity.SceneType.builder()
201209
.scenario(scenario)
202210
.type(type)
203-
.point(point)
204-
.analysis(analysis != null ? analysis : "분석 정보를 가져올 수 없습니다.")
211+
.point(indicator.point())
212+
.analysis(indicator.analysis() != null ? indicator.analysis() : "분석 정보를 가져올 수 없습니다.")
205213
.build();
206214
})
207215
.toList();
@@ -210,10 +218,12 @@ private void createDecisionSceneTypes(Scenario scenario, DecisionScenarioResult
210218
}
211219

212220
private void createSceneCompare(Scenario scenario, DecisionScenarioResult aiResult) {
213-
List<SceneCompare> compares = aiResult.comparisonResults()
214-
.entrySet().stream()
215-
.map(entry -> {
216-
String resultTypeStr = entry.getKey().toUpperCase();
221+
if (aiResult.comparisons() == null || aiResult.comparisons().isEmpty()) {
222+
throw new AiParsingException("Comparison results are missing in AI response for scenario " + scenario.getId());
223+
}
224+
List<SceneCompare> compares = aiResult.comparisons().stream()
225+
.map(comparison -> {
226+
String resultTypeStr = comparison.type().toUpperCase();
217227
com.back.domain.scenario.entity.SceneCompareResultType resultType;
218228
try {
219229
resultType = com.back.domain.scenario.entity.SceneCompareResultType.valueOf(resultTypeStr);
@@ -225,7 +235,7 @@ private void createSceneCompare(Scenario scenario, DecisionScenarioResult aiResu
225235
return SceneCompare.builder()
226236
.scenario(scenario)
227237
.resultType(resultType)
228-
.compareResult(entry.getValue())
238+
.compareResult(comparison.analysis())
229239
.build();
230240
})
231241
.toList();
@@ -240,4 +250,21 @@ private void updateBaseLineTitle(BaseLine baseLine, String baselineTitle) {
240250
log.info("BaseLine title updated for ID: {}", baseLine.getId());
241251
}
242252
}
253+
254+
@Transactional(readOnly = true)
255+
public Scenario prepareScenarioData(Long scenarioId) {
256+
// MultipleBagFetchException을 피하기 위해 별도 쿼리로 각 컬렉션을 초기화
257+
// 1. DecisionNodes 초기화
258+
Scenario scenario = scenarioRepository.findById(scenarioId)
259+
.orElseThrow(() -> new ApiException(ErrorCode.SCENARIO_NOT_FOUND));
260+
scenario.getDecisionLine().getDecisionNodes().size(); // 프록시 초기화
261+
262+
// 2. BaseNodes 초기화
263+
scenario.getBaseLine().getBaseNodes().size(); // 프록시 초기화
264+
265+
// 3. User 엔티티 초기화 (새로운 오류 방지)
266+
scenario.getUser().getMbti(); // User 프록시 초기화
267+
268+
return scenario;
269+
}
243270
}

back/src/main/java/com/back/global/ai/client/image/StableDiffusionImageClient.java

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -118,31 +118,42 @@ private Mono<String> extractImageData(String response) {
118118
try {
119119
JsonNode rootNode = objectMapper.readTree(response);
120120

121-
// Stability AI 공식 응답 구조: { "artifacts": [{ "base64": "..." }] }
122-
if (rootNode.has("artifacts") && rootNode.get("artifacts").isArray()) {
121+
// 1. 단순 응답 구조 체크: {"image": "..."}
122+
if (rootNode.has("image")) {
123+
String base64Data = rootNode.get("image").asText();
124+
if (base64Data != null && !base64Data.isEmpty()) {
125+
log.info("Image generated successfully (simple structure). Base64 length: {}", base64Data.length());
126+
return Mono.just(base64Data);
127+
}
128+
}
129+
130+
// 2. 공식 응답 구조 체크: { "artifacts": [{...}] }
131+
if (rootNode.has("artifacts") && rootNode.get("artifacts").isArray() && !rootNode.get("artifacts").isEmpty()) {
123132
JsonNode firstArtifact = rootNode.get("artifacts").get(0);
124133

125-
// finishReason 검증
126-
if (firstArtifact.has("finishReason")) {
127-
String finishReason = firstArtifact.get("finishReason").asText();
128-
if (!"SUCCESS".equals(finishReason)) {
129-
log.error("Image generation failed with reason: {}", finishReason);
130-
return Mono.error(new AiServiceException(
131-
ErrorCode.AI_GENERATION_FAILED,
132-
"Image generation failed: " + finishReason
133-
));
134+
if (firstArtifact != null) {
135+
// finishReason 검증
136+
if (firstArtifact.has("finishReason")) {
137+
String finishReason = firstArtifact.get("finishReason").asText();
138+
if (!"SUCCESS".equals(finishReason)) {
139+
log.error("Image generation failed with reason: {}", finishReason);
140+
return Mono.error(new AiServiceException(
141+
ErrorCode.AI_GENERATION_FAILED,
142+
"Image generation failed: " + finishReason
143+
));
144+
}
134145
}
135-
}
136146

137-
// Base64 데이터 추출
138-
if (firstArtifact.has("base64")) {
139-
String base64Data = firstArtifact.get("base64").asText();
140-
log.info("Image generated successfully. Base64 length: {}", base64Data.length());
141-
return Mono.just(base64Data);
147+
// Base64 데이터 추출
148+
if (firstArtifact.has("base64")) {
149+
String base64Data = firstArtifact.get("base64").asText();
150+
log.info("Image generated successfully (artifacts structure). Base64 length: {}", base64Data.length());
151+
return Mono.just(base64Data);
152+
}
142153
}
143154
}
144155

145-
// 응답 구조가 예상과 다를 경우
156+
// 두 구조 모두 해당하지 않을 경우
146157
log.error("Unexpected Stable Diffusion API response structure: {}", response);
147158
return Mono.error(new AiServiceException(
148159
ErrorCode.AI_GENERATION_FAILED,

0 commit comments

Comments
 (0)