Skip to content

Commit c3a0482

Browse files
committed
feat: DTO에 score값 추가
1 parent e53ae61 commit c3a0482

File tree

3 files changed

+131
-78
lines changed

3 files changed

+131
-78
lines changed

src/main/java/Capstone/FOSSistant/global/service/customAI/IssueListServiceImpl.java

Lines changed: 121 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
@RequiredArgsConstructor
3535
@Transactional
3636
public class IssueListServiceImpl implements IssueListService {
37+
public record DifficultyResult(Tag tag, double score) {}
38+
3739
private static final double SCORE_THRESHOLD = 0.4;
3840

3941
private final IssueListRepository issueListRepository;
@@ -78,23 +80,34 @@ private List<CompletableFuture<IssueListResponseDTO.IssueResponseDTO>> classifyB
7880
// 1) Redis 캐시 및 DB 검사
7981
for (int i = 0; i < batch.size(); i++) {
8082
IssueListRequestDTO.IssueRequestDTO dto = batch.get(i);
81-
String key = "issue:" + dto.getIssueId();
83+
String issueId = dto.getIssueId();
84+
String key = "issue:" + issueId;
8285
String cached = redisTemplate.opsForValue().get(key);
8386
if (cached != null) {
8487
Tag tag = Tag.valueOf(cached.toUpperCase());
8588
// 즉시 completedFuture 리턴
8689
futures.add(
8790
CompletableFuture.completedFuture(
88-
issueListConverter.toResponseDTO(dto.getIssueId(), tag)
91+
IssueListResponseDTO.IssueResponseDTO.builder()
92+
.issueId(issueId)
93+
.difficulty(tag.toLowerCase())
94+
.score(1.0)
95+
.build()
8996
)
9097
);
9198
continue;
9299
}
93100
// DB 체크
94-
IssueList existingIssue = issueListRepository.findById(dto.getIssueId()).orElse(null);
101+
IssueList existingIssue = issueListRepository.findById(issueId).orElse(null);
95102
if (existingIssue != null) {
103+
Tag tag = existingIssue.getDifficulty();
96104
futures.add(CompletableFuture.completedFuture(
97-
issueListConverter.toResponseDTO(dto.getIssueId(), existingIssue.getDifficulty())));
105+
IssueListResponseDTO.IssueResponseDTO.builder()
106+
.issueId(issueId)
107+
.difficulty(tag.toLowerCase())
108+
.score(1.0)
109+
.build()
110+
));
98111
continue;
99112
}
100113
toClassify.add(dto);
@@ -107,32 +120,33 @@ private List<CompletableFuture<IssueListResponseDTO.IssueResponseDTO>> classifyB
107120
List<CompletableFuture<IssueClassificationResult>> classificationFutures = new ArrayList<>();
108121

109122
for (IssueListRequestDTO.IssueRequestDTO dto : toClassify) {
123+
String issueId = dto.getIssueId();
124+
String key = "issue:" + issueId;
110125
// GitHub API 호출 (제목/본문)
111126
CompletableFuture<String[]> gitHubFuture = CompletableFuture.supplyAsync(
112-
() -> extractTitleAndBody(dto.getIssueId()),
127+
() -> extractTitleAndBody(issueId),
113128
classifierExecutor
114129
);
115130

116131
// GitHub 결과를 받은 뒤 AI 분류
117-
CompletableFuture<Tag> aiFuture = gitHubFuture.thenComposeAsync(parts -> {
118-
String title = parts[0];
119-
String body = parts[1];
120-
return classifyWithAI(title, body);
121-
}, classifierExecutor);
122-
123-
// AI 분류 결과를 받은 뒤 캐시 저장/DB 저장 + DTO 변환
124-
CompletableFuture<IssueClassificationResult> resultFuture = aiFuture.thenApplyAsync(tag -> {
125-
// 캐시 저장
126-
String key = "issue:" + dto.getIssueId();
127-
cacheDifficulty(key, tag);
128-
// DB 저장
129-
persistDifficulty(dto, tag);
130-
// 응답 DTO 생성
131-
IssueListResponseDTO.IssueResponseDTO responseDTO =
132-
issueListConverter.toResponseDTO(dto.getIssueId(), tag);
133-
134-
return new IssueClassificationResult(dto.getIssueId(), responseDTO);
135-
}, classifierExecutor);
132+
CompletableFuture<DifficultyResult> diffFuture = gitHubFuture
133+
.thenComposeAsync(parts -> classifyWithAI(parts[0], parts[1]), classifierExecutor);
134+
135+
CompletableFuture<IssueClassificationResult> resultFuture = diffFuture
136+
.thenApplyAsync(diffRes -> {
137+
// 캐시/DB 저장
138+
cacheDifficulty(key, diffRes.tag());
139+
persistDifficulty(dto, diffRes.tag());
140+
141+
// score 포함 DTO 생성
142+
IssueListResponseDTO.IssueResponseDTO responseDTO =
143+
IssueListResponseDTO.IssueResponseDTO.builder()
144+
.issueId(issueId)
145+
.difficulty(diffRes.tag().toLowerCase())
146+
.score(diffRes.score())
147+
.build();
148+
return new IssueClassificationResult(issueId, responseDTO);
149+
}, classifierExecutor);
136150

137151
classificationFutures.add(resultFuture);
138152
}
@@ -162,7 +176,12 @@ private List<CompletableFuture<IssueListResponseDTO.IssueResponseDTO>> classifyB
162176
}
163177
}
164178
// 실패 시 기본값 반환
165-
return issueListConverter.toResponseDTO(toClassify.get(finalIdx).getIssueId(), Tag.MISC);
179+
String issueId = toClassify.get(finalIdx).getIssueId();
180+
return IssueListResponseDTO.IssueResponseDTO.builder()
181+
.issueId(issueId)
182+
.difficulty(Tag.MISC.toLowerCase())
183+
.score(1.0)
184+
.build();
166185
}, classifierExecutor);
167186

168187
futures.add(batchIndex, wrapperFuture);
@@ -193,41 +212,48 @@ private String[] extractTitleAndBody(String issueUrl) {
193212
* AI 서버에 단건 분류 요청을 보내고, JSON 문자열을 Tag로 파싱하여 CompletableFuture<Tag>로 반환합니다.
194213
* 내부적으로 WebClient Mono → toFuture()로 변환되며, 파싱 단계는 classifierExecutor에서 실행됩니다.
195214
*/
196-
public CompletableFuture<Tag> classifyWithAI(String title, String body) {
215+
public CompletableFuture<DifficultyResult> classifyWithAI(String title, String body) {
197216
long start = System.currentTimeMillis();
198217

199218
return aiClassifierClient.classify(title, body)
200-
.timeout(Duration.ofSeconds(20))
201-
// 1) 네트워크/타임아웃 에러
219+
.timeout(Duration.ofSeconds(30))
202220
.onErrorResume(TimeoutException.class, e -> {
203-
log.error("[{}] AI 호출 타임아웃 ({}ms) — issue: {}",
204-
"NETWORK", System.currentTimeMillis() - start, title, e);
205-
// fallback JSON
221+
log.error("[NETWORK] AI 호출 타임아웃: {}ms — {}", System.currentTimeMillis()-start, title, e);
206222
return aiClassifierClient.defaultSingleResult();
207223
})
208224
.onErrorResume(WebClientResponseException.class, e -> {
209-
log.error("[{}] AI HTTP 에러: {} {} — issue: {}",
210-
"HTTP", e.getRawStatusCode(), e.getStatusText(), title, e);
225+
log.error("[HTTP] AI HTTP 에러 {} {} — {}", e.getRawStatusCode(), e.getStatusText(), title, e);
211226
return aiClassifierClient.defaultSingleResult();
212227
})
213228
.onErrorResume(e -> {
214-
log.error("[{}] AI 호출 예외: {} — issue: {}",
215-
"UNKNOWN_CALL", e.getMessage(), title, e);
229+
log.error("[UNKNOWN_CALL] AI 호출 예외 — {}", title, e);
216230
return aiClassifierClient.defaultSingleResult();
217231
})
218232
.toFuture()
219233
.thenApplyAsync(jsonResult -> {
220234
try {
221-
// 2) JSON 파싱
222-
Tag tag = parseJsonToTag(jsonResult);
223-
log.info("[{}] 분류 완료: {} ({}ms) — issue: {}",
224-
"SUCCESS", tag, System.currentTimeMillis() - start, title);
225-
return tag;
226-
} catch (Exception parseEx) {
227-
// 3) 파싱 오류
228-
log.error("[{}] AI 응답 파싱 실패 ({}ms) — issue: {}, json={}",
229-
"PARSE_ERROR", System.currentTimeMillis() - start, title, jsonResult, parseEx);
230-
return Tag.UNKNOWN;
235+
JsonNode first = new ObjectMapper()
236+
.readTree(jsonResult)
237+
.get("results")
238+
.get(0);
239+
240+
String diff = first.get("difficulty").asText("misc").toLowerCase();
241+
double score = first.get("score").asDouble(0.0);
242+
243+
Tag tag = switch (diff) {
244+
case "easy" -> Tag.EASY;
245+
case "medium" -> Tag.MEDIUM;
246+
case "hard" -> Tag.HARD;
247+
case "misc" -> Tag.MISC;
248+
default -> Tag.UNKNOWN;
249+
};
250+
251+
log.info("[SUCCESS] {} → {} (score={}) {}ms", title, tag, score, System.currentTimeMillis()-start);
252+
return new DifficultyResult(tag, score);
253+
254+
} catch (Exception ex) {
255+
log.error("[PARSE_ERROR] AI 응답 파싱 실패 — {}", title, ex);
256+
return new DifficultyResult(Tag.UNKNOWN, 0.0);
231257
}
232258
}, classifierExecutor);
233259
}
@@ -345,36 +371,54 @@ private static class IssueClassificationResult {
345371
/**
346372
* 단건 분류 요청 (캐시/DB/AI 순)
347373
*/
348-
public CompletableFuture<IssueListResponseDTO.IssueResponseDTO> classify(IssueListRequestDTO.IssueRequestDTO dto) {
349-
String key = "issue:" + dto.getIssueId();
350-
return CompletableFuture.supplyAsync(() -> {
351-
// DB 우선 체크
352-
IssueList existing = issueListRepository.findById(dto.getIssueId()).orElse(null);
353-
if (existing != null) {
354-
return "DB:" + existing.getDifficulty().name().toLowerCase();
355-
}
356-
String redisCached = redisTemplate.opsForValue().get(key);
357-
return redisCached != null ? "REDIS:" + redisCached : null;
358-
}, classifierExecutor).thenCompose(source -> {
359-
if (source != null) {
360-
if (source.startsWith("DB:")) {
361-
Tag tag = Tag.valueOf(source.substring(3).toUpperCase());
362-
return CompletableFuture.completedFuture(issueListConverter.toResponseDTO(dto.getIssueId(), tag));
363-
} else { // REDIS case
364-
Tag tag = Tag.valueOf(source.substring(6).toUpperCase());
365-
return CompletableFuture.completedFuture(issueListConverter.toResponseDTO(dto.getIssueId(), tag));
366-
}
367-
}
368-
// GitHub fetch + AI classify
369-
CompletableFuture<Tag> tagFuture = CompletableFuture.supplyAsync(
370-
() -> extractTitleAndBody(dto.getIssueId()), classifierExecutor
371-
).thenComposeAsync(parts -> classifyWithAI(parts[0], parts[1]), classifierExecutor);
372-
373-
return tagFuture.thenApplyAsync(tag -> {
374-
cacheDifficulty(key, tag);
375-
persistDifficulty(dto, tag);
376-
return issueListConverter.toResponseDTO(dto.getIssueId(), tag);
377-
}, classifierExecutor);
378-
});
379-
}
374+
// public CompletableFuture<IssueListResponseDTO.IssueResponseDTO> classify(IssueListRequestDTO.IssueRequestDTO dto) {
375+
// String issueId = dto.getIssueId();
376+
// String key = "issue:" + issueId;
377+
// return CompletableFuture.supplyAsync(() -> {
378+
// // DB 우선 체크
379+
// IssueList existing = issueListRepository.findById(issueId).orElse(null);
380+
// if (existing != null) {
381+
// Tag tag = existing.getDifficulty();
382+
// return "DB:" + tag.name().toLowerCase();
383+
// }
384+
// String redisCached = redisTemplate.opsForValue().get(key);
385+
// return redisCached != null ? "REDIS:" + redisCached : null;
386+
// }, classifierExecutor).thenCompose(source -> {
387+
// if (source != null) {
388+
// if (source.startsWith("DB:")) {
389+
// Tag tag = Tag.valueOf(source.substring(3).toUpperCase());
390+
// return CompletableFuture.completedFuture(
391+
// IssueListResponseDTO.IssueResponseDTO.builder()
392+
// .issueId(issueId)
393+
// .difficulty(tag.toLowerCase())
394+
// .score(1.0)
395+
// .build()
396+
// );
397+
// } else { // REDIS case
398+
// Tag tag = Tag.valueOf(source.substring(6).toUpperCase());
399+
// return CompletableFuture.completedFuture(
400+
// IssueListResponseDTO.IssueResponseDTO.builder()
401+
// .issueId(issueId)
402+
// .difficulty(tag.toLowerCase())
403+
// .score(1.0)
404+
// .build()
405+
// );
406+
// }
407+
// }
408+
// // GitHub fetch + AI classify
409+
// CompletableFuture<DifficultyResult> diffFuture = CompletableFuture.supplyAsync(
410+
// () -> extractTitleAndBody(issueId), classifierExecutor
411+
// ).thenComposeAsync(parts -> classifyWithAI(parts[0], parts[1]), classifierExecutor);
412+
//
413+
// return diffFuture.thenApplyAsync(diffRes -> {
414+
// cacheDifficulty(key, diffRes.tag());
415+
// persistDifficulty(dto, diffRes.tag());
416+
// return IssueListResponseDTO.IssueResponseDTO.builder()
417+
// .issueId(issueId)
418+
// .difficulty(diffRes.tag().toLowerCase())
419+
// .score(diffRes.score())
420+
// .build();
421+
// }, classifierExecutor);
422+
// });
423+
// }
380424
}

src/main/java/Capstone/FOSSistant/global/service/llm/IssueDetailService.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,16 @@ public CompletableFuture<IssueGuideResponseDTO> processGeminiResponse(Object[] a
147147
}
148148

149149
// 2) 미분류 상태면 AI 분류 서비스로 태그 결정 (비동기)
150+
150151
return issueListService.classifyWithAI(title, body)
151-
.thenApplyAsync(tag -> saveDetail(issueId, title, tag, gemini, relatedLinks),
152+
.thenApplyAsync(diffRes ->
153+
saveDetail(
154+
issueId,
155+
title,
156+
diffRes.tag(), // 태그는 .tag()
157+
gemini,
158+
relatedLinks
159+
),
152160
classifierExecutor);
153161
}
154162

src/main/java/Capstone/FOSSistant/global/web/dto/IssueList/IssueListResponseDTO.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ public class IssueListResponseDTO {
2222
public static class IssueResponseDTO {
2323
private String issueId;
2424
private String difficulty;
25+
private Double score;
2526
}
2627
}

0 commit comments

Comments
 (0)