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 3318d87..710b3bb 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 @@ -81,7 +81,7 @@ private String buildPrompt(JobPosting jobPosting, List questions) { "sentence": "자소서 답변 안에 실제 존재하는 정확한 부분 문자열", "status": "mentioned", "reason": "문제 이유", - "improvement": "개선 예시 문장" + "improvement": "사용자가 그대로 붙여 넣을 수 있는 완성된 개선 예시 문장" } ] } @@ -134,6 +134,12 @@ private String buildPrompt(JobPosting jobPosting, List questions) { - questionAnalyses의 questionId는 입력된 questionId 중 하나만 사용한다. - questionAnalyses의 status는 proven, mentioned, missing, fabricated 중 하나만 사용한다. - sentence는 answer에 포함된 정확한 substring만 사용한다. + - improvement는 첨삭 조언이 아니라 sentence를 대체할 수 있는 완성된 예시 문장이어야 한다. + - improvement에는 "하세요.", "해주세요.", "해야 합니다.", "필요합니다."로 끝나는 지시문을 쓰지 않는다. + - improvement에는 "추가하세요.", "보완하세요.", "수정해주세요.", "명확히 해야 합니다." 같은 첨삭 조언 표현을 쓰지 않는다. + - improvement는 반드시 한국어 평서문으로 작성하고, 가능하면 수치/성과/행동을 포함한다. + - 좋은 improvement 예: "저는 쿼리 실행 계획을 분석해 누락된 인덱스를 추가했고, 평균 응답 시간을 1.8초에서 0.6초로 단축했습니다." + - 나쁜 improvement 예: "성과 수치를 추가하여 문제 해결의 효과를 명확히 하세요." - start/end index는 출력하지 않는다. 서버가 Java에서 계산한다. - 원문 매칭이 불확실하면 questionAnalyses에 포함하지 않는다. """.formatted( 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 8d66f33..abd654b 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 @@ -149,7 +149,7 @@ private List buildQuestionAnalyses( analysis, sentence, defaultString(item.reason()), - defaultString(item.improvement()), + normalizeImprovement(item.improvement()), normalizeStatus(item.status()), start, start + sentence.length() @@ -219,6 +219,32 @@ private String defaultString(String value) { return value == null ? "" : value; } + private String normalizeImprovement(String improvement) { + if (!StringUtils.hasText(improvement)) { + return ""; + } + + String normalized = improvement.trim(); + if (isInstructionLikeImprovement(normalized)) { + return ""; + } + return normalized; + } + + private boolean isInstructionLikeImprovement(String improvement) { + return improvement.endsWith("하세요.") + || improvement.endsWith("해주세요.") + || improvement.endsWith("해 주세요.") + || improvement.endsWith("하십시오.") + || improvement.endsWith("해주십시오.") + || improvement.endsWith("해 주십시오.") + || improvement.endsWith("해야 합니다.") + || improvement.endsWith("필요합니다.") + || improvement.matches(".*[을를]\\s+(추가|보완|수정)하\\s*(세요|십시오).*") + || improvement.matches(".*명확히\\s+(하\\s*(세요|십시오|야 합니다)|해\\s*(주세요|주십시오|야 합니다)).*") + || improvement.matches("^(추가|보완|수정).*(하세요|해주세요|해 주세요|하십시오|해주십시오|해 주십시오)(\\.|$)"); + } + private QuestionAnalysisStatus normalizeStatus(String status) { if (!StringUtils.hasText(status)) { 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 58c88be..a6acfb5 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 @@ -236,6 +236,127 @@ void analyzeSkipsSentenceNotInAnswer() { assertThat(questionAnalysisRepository.findAllByAnalysisId(analysis.getId())).isEmpty(); } + @Test + @DisplayName("LLM improvement가 첨삭 지시문이면 빈 값으로 저장한다") + void analyzeNormalizesInstructionLikeImprovement() { + User user = saveUser("analysis-instruction-improvement@example.com"); + MockApply mockApply = saveMockApply(user); + Question question = saveQuestion(mockApply, "문제 해결 경험", "저는 로그를 확인하고 쿼리 실행 계획을 분석했습니다."); + when(analysisAiClient.analyze(any(), any())).thenReturn(new AnalysisLlmResponse( + 64, + 70, + 55, + 67, + "개선 예시 문장 검증입니다.", + List.of(new AnalysisLlmResponse.QuestionAnalysisItem( + question.getId(), + "저는 로그를 확인하고 쿼리 실행 계획을 분석했습니다.", + "mentioned", + "성과 수치가 부족합니다.", + "성과 수치를 추가하여 문제 해결의 효과를 명확히 하세요." + )) + )); + + AnalysisResponse response = analysisService.analyze(user, mockApply.getId()); + + assertThat(response.questions().get(0).analyses()).hasSize(1); + assertThat(response.questions().get(0).analyses().get(0).improvement()).isEmpty(); + } + + @Test + @DisplayName("완성된 평서문 improvement는 그대로 저장한다") + void analyzePreservesDeclarativeImprovement() { + User user = saveUser("analysis-declarative-improvement@example.com"); + MockApply mockApply = saveMockApply(user); + Question question = saveQuestion(mockApply, "문제 해결 경험", "저는 로그를 확인했습니다."); + String validImprovement = "저는 로그를 분석하고 누락된 인덱스를 추가하여 응답 시간을 1.8초에서 0.6초로 단축했습니다."; + when(analysisAiClient.analyze(any(), any())).thenReturn(new AnalysisLlmResponse( + 70, + 75, + 65, + 70, + "평서문 검증입니다.", + List.of(new AnalysisLlmResponse.QuestionAnalysisItem( + question.getId(), + "저는 로그를 확인했습니다.", + "mentioned", + "성과가 부족합니다.", + validImprovement + )) + )); + + AnalysisResponse response = analysisService.analyze(user, mockApply.getId()); + + assertThat(response.questions().get(0).analyses()).hasSize(1); + assertThat(response.questions().get(0).analyses().get(0).improvement()) + .isEqualTo(validImprovement); + } + + @Test + @DisplayName("띄어쓰기가 포함된 첨삭 지시문도 빈 값으로 저장한다") + void analyzeNormalizesSpacedInstructionLikeImprovement() { + User user = saveUser("analysis-spaced-instruction-improvement@example.com"); + MockApply mockApply = saveMockApply(user); + Question question = saveQuestion(mockApply, "문제 해결 경험", "저는 로그를 확인했습니다."); + when(analysisAiClient.analyze(any(), any())).thenReturn(new AnalysisLlmResponse( + 64, + 70, + 55, + 67, + "띄어쓰기 지시문 검증입니다.", + List.of(new AnalysisLlmResponse.QuestionAnalysisItem( + question.getId(), + "저는 로그를 확인했습니다.", + "mentioned", + "성과가 부족합니다.", + "성과 수치를 추가해 주세요." + )) + )); + + AnalysisResponse response = analysisService.analyze(user, mockApply.getId()); + + assertThat(response.questions().get(0).analyses()).hasSize(1); + assertThat(response.questions().get(0).analyses().get(0).improvement()).isEmpty(); + } + + @Test + @DisplayName("하십시오와 해주십시오 형태의 첨삭 지시문도 빈 값으로 저장한다") + void analyzeNormalizesFormalInstructionLikeImprovement() { + User user = saveUser("analysis-formal-instruction-improvement@example.com"); + MockApply mockApply = saveMockApply(user); + Question firstQuestion = saveQuestion(mockApply, "문제 해결 경험", "저는 로그를 확인했습니다."); + Question secondQuestion = saveQuestion(mockApply, "성과 경험", "저는 API 응답 속도를 개선했습니다."); + when(analysisAiClient.analyze(any(), any())).thenReturn(new AnalysisLlmResponse( + 64, + 70, + 55, + 67, + "격식체 지시문 검증입니다.", + List.of( + new AnalysisLlmResponse.QuestionAnalysisItem( + firstQuestion.getId(), + "저는 로그를 확인했습니다.", + "mentioned", + "성과가 부족합니다.", + "문장을 명확히 하십시오." + ), + new AnalysisLlmResponse.QuestionAnalysisItem( + secondQuestion.getId(), + "저는 API 응답 속도를 개선했습니다.", + "mentioned", + "성과 수치가 부족합니다.", + "수정해주십시오." + ) + ) + )); + + AnalysisResponse response = analysisService.analyze(user, mockApply.getId()); + + assertThat(response.questions()).hasSize(2); + assertThat(response.questions().get(0).analyses().get(0).improvement()).isEmpty(); + assertThat(response.questions().get(1).analyses().get(0).improvement()).isEmpty(); + } + @Test @DisplayName("재분석 시 기존 분석과 문항 분석을 새 결과로 교체한다") void analyzeReplacesExistingAnalysis() {