From 9d5b8ac5d9357f209334cb2afe9f4af4f4de93ff Mon Sep 17 00:00:00 2001 From: wooh Date: Fri, 22 May 2026 21:21:22 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EC=9E=90=EC=86=8C=EC=84=9C=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=EB=AC=B8=EC=9E=A5=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 첨삭 문장 상태 enum 추가 - 분석 LLM 응답에 status 필드 반영 - 문항 분석 엔티티와 응답 DTO에 status 추가 - status 값 소문자 응답 및 기본값 보정 처리 - 분석 서비스 테스트에 status 검증 추가 --- .../analysis/dto/llm/AnalysisLlmResponse.java | 1 + .../dto/response/QuestionAnalysisResponse.java | 10 ++++++++++ .../domain/analysis/entity/QuestionAnalysis.java | 7 +++++++ .../analysis/entity/QuestionAnalysisStatus.java | 8 ++++++++ .../domain/analysis/service/AnalysisAiClient.java | 2 ++ .../domain/analysis/service/AnalysisService.java | 14 ++++++++++++++ .../analysis/service/AnalysisServiceTest.java | 7 +++++++ 7 files changed, 49 insertions(+) create mode 100644 src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/QuestionAnalysisStatus.java diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/llm/AnalysisLlmResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/llm/AnalysisLlmResponse.java index 0cb253e..d8a77d8 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/llm/AnalysisLlmResponse.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/llm/AnalysisLlmResponse.java @@ -13,6 +13,7 @@ public record AnalysisLlmResponse( public record QuestionAnalysisItem( Long questionId, String sentence, + String status, String reason, String improvement ) { diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionAnalysisResponse.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionAnalysisResponse.java index c4c6f97..92a9e92 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionAnalysisResponse.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/dto/response/QuestionAnalysisResponse.java @@ -1,10 +1,12 @@ package com.jobdri.jobdri_api.domain.analysis.dto.response; import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysis; +import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysisStatus; public record QuestionAnalysisResponse( Long questionAnalysisId, String sentence, + String status, String reason, String improvement, int start, @@ -14,10 +16,18 @@ public static QuestionAnalysisResponse from(QuestionAnalysis questionAnalysis) { return new QuestionAnalysisResponse( questionAnalysis.getId(), questionAnalysis.getSentence(), + statusValue(questionAnalysis.getStatus()), questionAnalysis.getReason(), questionAnalysis.getImprovement(), questionAnalysis.getStart(), questionAnalysis.getEnd() ); } + + private static String statusValue(QuestionAnalysisStatus status) { + if (status == null) { + return QuestionAnalysisStatus.MENTIONED.name().toLowerCase(); + } + return status.name().toLowerCase(); + } } diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/QuestionAnalysis.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/QuestionAnalysis.java index 5641065..ab393b8 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/QuestionAnalysis.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/QuestionAnalysis.java @@ -32,6 +32,11 @@ public class QuestionAnalysis { @Column(nullable = false, columnDefinition = "TEXT") private String improvement; + @Enumerated(EnumType.STRING) + @Column(nullable = false, columnDefinition = "varchar(255) default 'MENTIONED'") + @Builder.Default + private QuestionAnalysisStatus status = QuestionAnalysisStatus.MENTIONED; + @Column(name = "start_index", nullable = false) private int start; @@ -44,6 +49,7 @@ public static QuestionAnalysis create( String sentence, String reason, String improvement, + QuestionAnalysisStatus status, int start, int end ) { @@ -53,6 +59,7 @@ public static QuestionAnalysis create( .sentence(sentence) .reason(reason) .improvement(improvement) + .status(status) .start(start) .end(end) .build(); diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/QuestionAnalysisStatus.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/QuestionAnalysisStatus.java new file mode 100644 index 0000000..2c99de5 --- /dev/null +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/entity/QuestionAnalysisStatus.java @@ -0,0 +1,8 @@ +package com.jobdri.jobdri_api.domain.analysis.entity; + +public enum QuestionAnalysisStatus { + PROVEN, + MENTIONED, + MISSING, + FABRICATED +} diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java index 54afd62..3318d87 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java @@ -79,6 +79,7 @@ private String buildPrompt(JobPosting jobPosting, List questions) { { "questionId": 1, "sentence": "자소서 답변 안에 실제 존재하는 정확한 부분 문자열", + "status": "mentioned", "reason": "문제 이유", "improvement": "개선 예시 문장" } @@ -131,6 +132,7 @@ private String buildPrompt(JobPosting jobPosting, List questions) { [중요 규칙] - JSON 외 텍스트, 마크다운, 코드블럭을 출력하지 않는다. - questionAnalyses의 questionId는 입력된 questionId 중 하나만 사용한다. + - questionAnalyses의 status는 proven, mentioned, missing, fabricated 중 하나만 사용한다. - sentence는 answer에 포함된 정확한 substring만 사용한다. - start/end index는 출력하지 않는다. 서버가 Java에서 계산한다. - 원문 매칭이 불확실하면 questionAnalyses에 포함하지 않는다. diff --git a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java index 96c8dbc..a787cda 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisService.java @@ -7,6 +7,7 @@ import com.jobdri.jobdri_api.domain.analysis.entity.Analysis; import com.jobdri.jobdri_api.domain.analysis.entity.Question; import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysis; +import com.jobdri.jobdri_api.domain.analysis.entity.QuestionAnalysisStatus; import com.jobdri.jobdri_api.domain.analysis.repository.AnalysisRepository; import com.jobdri.jobdri_api.domain.analysis.repository.QuestionAnalysisRepository; import com.jobdri.jobdri_api.domain.analysis.repository.QuestionRepository; @@ -137,6 +138,7 @@ private List buildQuestionAnalyses( sentence, defaultString(item.reason()), defaultString(item.improvement()), + normalizeStatus(item.status()), start, start + sentence.length() )); @@ -199,4 +201,16 @@ private String normalizeFeedback(String feedback) { private String defaultString(String value) { return value == null ? "" : value; } + + private QuestionAnalysisStatus normalizeStatus(String status) { + if (!StringUtils.hasText(status)) { + return QuestionAnalysisStatus.MENTIONED; + } + + try { + return QuestionAnalysisStatus.valueOf(status.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + return QuestionAnalysisStatus.MENTIONED; + } + } } diff --git a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java index 64ea3e6..fb9852b 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisServiceTest.java @@ -94,6 +94,7 @@ void analyzeSavesAnalysis() { new AnalysisLlmResponse.QuestionAnalysisItem( question.getId(), "Spring Boot API를 개발했습니다.", + "mentioned", "성과 지표가 없어 구체성이 약합니다.", "Spring Boot API를 개발해 응답 시간을 20% 개선했습니다." ) @@ -109,6 +110,7 @@ void analyzeSavesAnalysis() { assertThat(response.completeness()).isEqualTo(80); assertThat(response.questions()).hasSize(1); assertThat(response.questions().get(0).analyses()).hasSize(1); + assertThat(response.questions().get(0).analyses().get(0).status()).isEqualTo("mentioned"); assertThat(response.questions().get(0).analyses().get(0).start()).isEqualTo(0); assertThat(response.questions().get(0).analyses().get(0).end()) .isEqualTo("Spring Boot API를 개발했습니다.".length()); @@ -159,6 +161,7 @@ void analyzeSkipsSentenceNotInAnswer() { new AnalysisLlmResponse.QuestionAnalysisItem( question.getId(), "답변에 없는 문장입니다.", + "fabricated", "원문에 없습니다.", "원문 기반 문장으로 개선해야 합니다." ) @@ -188,6 +191,7 @@ void analyzeReplacesExistingAnalysis() { List.of(new AnalysisLlmResponse.QuestionAnalysisItem( question.getId(), "가입 완료율을 개선했습니다.", + "mentioned", "수치가 부족합니다.", "가입 완료율을 12% 개선했습니다." )) @@ -201,6 +205,7 @@ void analyzeReplacesExistingAnalysis() { List.of(new AnalysisLlmResponse.QuestionAnalysisItem( question.getId(), "API 응답 속도를 개선했습니다.", + "proven", "성과 기준이 더 필요합니다.", "API 응답 속도를 300ms 단축했습니다." )) @@ -212,6 +217,7 @@ void analyzeReplacesExistingAnalysis() { assertThat(second.analysisId()).isNotEqualTo(first.analysisId()); assertThat(second.score()).isEqualTo(88); assertThat(second.feedback()).isEqualTo("두 번째 분석"); + assertThat(second.questions().get(0).analyses().get(0).status()).isEqualTo("proven"); assertThat(analysisRepository.findByMockApplyId(mockApply.getId()).orElseThrow().getScore()).isEqualTo(88); assertThat(questionAnalysisRepository.findAllByAnalysisId(second.analysisId())).hasSize(1); assertThat(questionAnalysisRepository.findAllByAnalysisId(first.analysisId())).isEmpty(); @@ -232,6 +238,7 @@ void getAnalysis() { List.of(new AnalysisLlmResponse.QuestionAnalysisItem( question.getId(), "서비스 개선 경험이 있습니다.", + "mentioned", "구체성이 조금 부족합니다.", "서비스 개선 경험으로 전환율을 10% 높였습니다." ))