Skip to content

Commit 88224f3

Browse files
committed
feat(evaluation): 1차 수정본(최소 기능 부착 완료)
1 parent 6270864 commit 88224f3

File tree

3 files changed

+176
-1
lines changed

3 files changed

+176
-1
lines changed

backend/src/main/java/com/backend/domain/analysis/service/AnalysisService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.backend.domain.analysis.service;
22

3+
import com.backend.domain.evaluation.service.EvaluationService;
34
import com.backend.domain.repository.dto.response.RepositoryData;
45
import com.backend.domain.analysis.entity.AnalysisResult;
56
import com.backend.domain.analysis.repository.AnalysisResultRepository;
@@ -20,6 +21,8 @@
2021
public class AnalysisService {
2122
private final RepositoryService repositoryService;
2223
private final AnalysisResultRepository analysisResultRepository;
24+
private final EvaluationService evaluationService; // ★ 추가
25+
2326

2427
@Transactional
2528
public void analyze(String githubUrl) {
@@ -32,7 +35,8 @@ public void analyze(String githubUrl) {
3235

3336
log.info("🫠 ResponseData: {}", repositoryData);
3437
// TODO: AI 평가
35-
// EvaluationResult evaluation = evaluationService.evaluate(repositoryData);
38+
evaluationService.evaluateAndSave(repositoryData); // ★ 이 한 줄로 끝!
39+
3640

3741
// TODO: AI 평가 저장
3842
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.backend.domain.evaluation.dto;
2+
3+
import java.util.List;
4+
5+
public class EvaluationDto {
6+
7+
/** AI가 반환해야 할 점수 묶음 */
8+
public record Scores(
9+
int readme, // 0~25
10+
int test, // 0~25
11+
int commit, // 0~25
12+
int cicd // 0~25
13+
) {}
14+
15+
/** AI가 반환해야 할 전체 결과 */
16+
public record AiResult(
17+
String summary,
18+
List<String> strengths,
19+
List<String> improvements,
20+
Scores scores
21+
) {}
22+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package com.backend.domain.evaluation.service;
2+
3+
import com.backend.domain.analysis.entity.AnalysisResult;
4+
import com.backend.domain.analysis.entity.Score;
5+
import com.backend.domain.analysis.repository.AnalysisResultRepository;
6+
import com.backend.domain.evaluation.dto.EvaluationDto;
7+
import com.backend.domain.evaluation.dto.EvaluationDto.AiResult;
8+
import com.backend.domain.evaluation.dto.EvaluationDto.Scores;
9+
import com.backend.domain.evaluation.service.AiService;
10+
import com.backend.domain.evaluation.dto.AiDto; // 이미 존재하는 DTO(complete 요청용)
11+
import com.backend.domain.repository.dto.response.RepositoryData;
12+
import com.backend.domain.repository.entity.Repositories;
13+
import com.backend.domain.repository.repository.RepositoryJpaRepository;
14+
import com.backend.global.exception.BusinessException;
15+
import com.backend.global.exception.ErrorCode;
16+
import com.fasterxml.jackson.core.type.TypeReference;
17+
import com.fasterxml.jackson.databind.ObjectMapper;
18+
import lombok.RequiredArgsConstructor;
19+
import lombok.extern.slf4j.Slf4j;
20+
import org.springframework.stereotype.Service;
21+
import org.springframework.transaction.annotation.Transactional;
22+
23+
import java.time.LocalDateTime;
24+
import java.util.List;
25+
import java.util.Objects;
26+
import java.util.regex.Matcher;
27+
import java.util.regex.Pattern;
28+
29+
@Slf4j
30+
@Service
31+
@RequiredArgsConstructor
32+
public class EvaluationService {
33+
34+
private final AiService aiService; // OpenAI 호출 래퍼(이미 프로젝트에 있음)
35+
private final ObjectMapper objectMapper = new ObjectMapper(); // JSON 파싱용
36+
private final RepositoryJpaRepository repositoryJpaRepository; // Repo 엔티티 조회
37+
private final AnalysisResultRepository analysisResultRepository; // 결과 저장
38+
39+
/**
40+
* 평가 + 저장까지 한 번에 처리합니다.
41+
* @param data (RepositoryData) 수집된 깃허브 종합 데이터
42+
* @return 저장된 분석결과 ID(Long)
43+
*/
44+
@Transactional
45+
public Long evaluateAndSave(RepositoryData data) {
46+
// 1) AI 호출
47+
AiResult ai = callAiAndParse(data);
48+
49+
// 2) Repositories 엔티티 찾기 (URL로 조회)
50+
String url = data.getRepositoryUrl();
51+
Repositories repo = repositoryJpaRepository.findByHtmlUrl(url)
52+
.orElseThrow(() -> new BusinessException(ErrorCode.GITHUB_REPO_NOT_FOUND));
53+
54+
// 3) 엔티티 생성 (AnalysisResult + Score)
55+
AnalysisResult analysis = AnalysisResult.builder()
56+
.repositories(repo)
57+
.summary(safe(ai.summary()))
58+
.strengths(joinBullets(ai.strengths()))
59+
.improvements(joinBullets(ai.improvements()))
60+
.createDate(LocalDateTime.now())
61+
.build();
62+
63+
Score score = Score.builder()
64+
.analysisResult(analysis) // ★ Score가 연관관계의 주인일 가능성이 높음
65+
.readmeScore(ai.scores().readme())
66+
.testScore(ai.scores().test())
67+
.commitScore(ai.scores().commit())
68+
.cicdScore(ai.scores().cicd())
69+
.build();
70+
71+
// AnalysisResult ←→ Score 양쪽 세팅이 필요하다면,
72+
// AnalysisResult에 세터가 없을 수 있어도 JPA가 연관 찾아옵니다(지연 로딩).
73+
// 만약 세터가 있다면 아래 한 줄도 함께 두세요.
74+
// analysis.setScore(score);
75+
76+
// 4) 저장 (cascade = ALL 이라면 Score도 함께 저장됩니다)
77+
AnalysisResult saved = analysisResultRepository.save(analysis);
78+
log.info("✅ Evaluation saved. analysisResultId={}", saved.getId());
79+
return saved.getId();
80+
}
81+
82+
/**
83+
* 실제 AI 호출부
84+
* @param data (RepositoryData)
85+
* @return (EvaluationDto.AiResult) AI 결과
86+
*/
87+
public AiResult callAiAndParse(RepositoryData data) {
88+
try {
89+
// content: 원본 데이터(JSON으로 넘기면 모델이 읽기 쉽습니다)
90+
String content = objectMapper.writeValueAsString(data);
91+
92+
// prompt: 모델에게 "이 스키마로만 JSON"을 만들라고 강하게 지시
93+
String prompt = """
94+
You are a senior software engineering reviewer.
95+
Analyze the given GitHub repository data and return ONLY a valid JSON. No commentary.
96+
97+
Scoring: total 100 (README 0~25, TEST 0~25, COMMIT 0~25, CICD 0~25).
98+
Consider test folders, CI configs(e.g., .github/workflows), commit frequency/messages, README depth, etc.
99+
100+
JSON schema:
101+
{
102+
"summary": "one-paragraph summary in Korean",
103+
"strengths": ["...","..."],
104+
"improvements": ["...","..."],
105+
"scores": { "readme": 0, "test": 0, "commit": 0, "cicd": 0 }
106+
}
107+
""";
108+
109+
AiDto.CompleteResponse res =
110+
aiService.complete(new AiDto.CompleteRequest(content, prompt));
111+
112+
String raw = res.result();
113+
String json = extractJson(raw);
114+
// {"summary":...,"strengths":[...],...} → AiResult로 역직렬화
115+
return objectMapper.readValue(json, new TypeReference<AiResult>() {});
116+
} catch (Exception e) {
117+
log.error("AI evaluation failed", e);
118+
throw new BusinessException(ErrorCode.INTERNAL_ERROR);
119+
}
120+
}
121+
122+
/** 모델이 앞/뒤에 텍스트를 붙여도 JSON 본문만 뽑아냅니다. */
123+
private String extractJson(String text) {
124+
if (text == null) throw new IllegalArgumentException("AI result is null");
125+
// ```json ... ``` 같은 포맷도 대비
126+
String cleaned = text.replaceAll("```json", "```").trim();
127+
128+
// 가장 바깥 { ... } 블록을 정규식으로 추출
129+
Pattern p = Pattern.compile("\\{.*}", Pattern.DOTALL);
130+
Matcher m = p.matcher(cleaned);
131+
if (m.find()) return m.group();
132+
133+
// 못 찾으면 그대로 시도(모델이 운좋게 순수 JSON만 준 경우)
134+
return cleaned;
135+
}
136+
137+
private String joinBullets(List<String> list) {
138+
if (list == null || list.isEmpty()) return "";
139+
return list.stream()
140+
.filter(Objects::nonNull)
141+
.map(String::trim)
142+
.filter(s -> !s.isEmpty())
143+
.map(s -> "- " + s)
144+
.reduce((a, b) -> a + "\n" + b)
145+
.orElse("");
146+
}
147+
148+
private String safe(String s) { return s == null ? "" : s.trim(); }
149+
}

0 commit comments

Comments
 (0)