Skip to content

[Feat] 자기소개서 분석 Hybrid Exact 기준 구현#174

Merged
whc9999 merged 2 commits into
devfrom
feat/hybrid-exact-analysis-merge
Jul 25, 2026
Merged

[Feat] 자기소개서 분석 Hybrid Exact 기준 구현#174
whc9999 merged 2 commits into
devfrom
feat/hybrid-exact-analysis-merge

Conversation

@whc9999

@whc9999 whc9999 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

✨ 어떤 이유로 PR를 하셨나요?

  • feature 병합
  • 버그 수정(아래에 issue #를 남겨주세요)
  • 코드 개선
  • 코드 수정
  • 배포
  • 기타(아래에 자세한 내용 기입해주세요)

📋 세부 내용 - 왜 해당 PR이 필요한지 작업 내용을 자세하게 설명해주세요

  • analysis.mode=hybrid-exact 실행 모드 추가
  • Hybrid Exact에서 기존 single-pass 전체 분석 경로 실행
  • Hybrid Exact에서 기존 two-pass 전체 분석 경로 실행
  • questionAnalyses, 점수, feedback, strengths는 single-pass 결과 사용
  • missingKeywords는 two-pass 전체 결과 사용
  • 기존 missingKeywords 전용 Hybrid 최적화 경로 제거
  • Hybrid Exact merge 시 새 AnalysisLlmResponse 생성으로 필드 출처 명시
  • analysis-eval 전용 offline hybrid merge Runner 추가
  • v5-A CSV와 two-pass CSV를 caseId 기준으로 exact merge
  • CSV caseId mismatch, duplicate, 필수 JSON 파싱 실패 검증 추가
  • analysis/nlg-judge/hybrid-merge Runner 동시 실행 방지
  • Hybrid Exact merge 및 Runner 조건 테스트 추가

📸 작업 화면 스크린샷

⚠️ PR하기 전에 확인해주세요

  • 로컬테스트를 진행하셨나요?
  • 머지할 브랜치를 확인하셨나요?
  • 관련 label을 선택하셨나요?

🚨 관련 이슈 번호 [ ]

Summary by CodeRabbit

  • 새 기능

    • 단일 패스와 2단계 분석 결과를 결합하는 하이브리드 분석 모드를 추가했습니다.
    • 분석 결과 CSV를 검증하고 병합해 출력하는 기능을 제공합니다.
    • 분석 모드를 설정으로 선택할 수 있습니다.
  • 버그 수정

    • 분석 실행 방식이 동시에 활성화되는 잘못된 설정을 사전에 감지합니다.
    • 입력 CSV의 누락·중복 사례와 잘못된 JSON 형식을 검증합니다.

- analysis.mode=hybrid-exact 실행 모드 추가
- Hybrid Exact에서 기존 single-pass 전체 분석 경로 실행
- Hybrid Exact에서 기존 two-pass 전체 분석 경로 실행
- questionAnalyses, 점수, feedback, strengths는 single-pass 결과 사용
- missingKeywords는 two-pass 전체 결과 사용
- 기존 missingKeywords 전용 Hybrid 최적화 경로 제거
- Hybrid Exact merge 시 새 AnalysisLlmResponse 생성으로 필드 출처 명시
- analysis-eval 전용 offline hybrid merge Runner 추가
- v5-A CSV와 two-pass CSV를 caseId 기준으로 exact merge
- CSV caseId mismatch, duplicate, 필수 JSON 파싱 실패 검증 추가
- analysis/nlg-judge/hybrid-merge Runner 동시 실행 방지
- Hybrid Exact merge 및 Runner 조건 테스트 추가
@whc9999 whc9999 self-assigned this Jul 25, 2026
@whc9999 whc9999 added the ✨ feat New feature or request label Jul 25, 2026
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@whc9999, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 03a8da7b-7c7a-494f-b4fd-ebb4bf914a79

📥 Commits

Reviewing files that changed from the base of the PR and between 91c1a8a and ecfe9f4.

📒 Files selected for processing (6)
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationCsvSupport.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationAnalysisRunnerSafetyTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClientTest.java
📝 Walkthrough

Walkthrough

분석 모드에 HYBRID_EXACT를 추가하고, 단일·2-pass 결과를 필드별로 병합하는 기능과 평가용 CSV 병합 runner를 구현했습니다. 세 평가 플래그의 상호 배타성 검증 및 관련 테스트와 설정도 확장했습니다.

Changes

Hybrid exact 분석 및 평가 머지

Layer / File(s) Summary
분석 모드와 응답 병합
src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java, src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClientTest.java
analysis.modeSINGLE_PASS, TWO_PASS, HYBRID_EXACT로 해석하고, HYBRID_EXACT에서 단일 패스 필드와 2-pass의 missingKeywords를 조합합니다.
CSV 검증과 결과 병합
src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeService.java, src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationCsvSupport.java, src/test/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeServiceTest.java
입력 CSV의 헤더, caseId, 오류, JSON을 검증하고 병합된 CSV를 생성하며, BOM·인용 필드·개행 및 오류 사례를 테스트합니다.
평가 runner와 플래그 검증
src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeRunner.java, src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/*Runner.java, src/main/resources/application-analysis-eval.yaml, src/test/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationAnalysisRunnerSafetyTest.java
analysis-eval 프로파일의 hybrid merge 실행, 입출력 검증, 종료 코드 처리를 추가하고 세 평가 플래그의 동시 활성화를 차단합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: shinae1023

Sequence Diagram(s)

sequenceDiagram
  participant HybridExactMergeRunner
  participant HybridExactMergeService
  participant SinglePassCSV
  participant TwoPassCSV
  participant OutputCSV
  HybridExactMergeRunner->>HybridExactMergeService: merge(single-pass, two-pass, output)
  HybridExactMergeService->>SinglePassCSV: 입력 행 및 헤더 검증
  HybridExactMergeService->>TwoPassCSV: 입력 행 및 헤더 검증
  SinglePassCSV-->>HybridExactMergeService: 단일 패스 데이터
  TwoPassCSV-->>HybridExactMergeService: 2-pass 데이터
  HybridExactMergeService->>OutputCSV: 병합 행 기록
  HybridExactMergeService-->>HybridExactMergeRunner: 병합 요약 반환
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 Hybrid Exact 구현이라는 मुख्य 변경을 간결하게 잘 요약합니다.
Description check ✅ Passed 템플릿의 주요 섹션과 변경 내용이 대부분 채워져 있어 PR 설명 요구사항을 충족합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hybrid-exact-analysis-merge

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationCsvSupport.java`:
- Around line 170-187: write()와 writeRows()에 반복되는 parent 디렉터리 생성, BufferedWriter
오픈, 헤더 및 행 작성 흐름을 공통 헬퍼로 추출하세요. writeRows()는 해당 헬퍼를 사용하도록 변경하고,
EvaluationAnalysisResult 기반인 write()는 기존 타입 변환 로직을 유지한 채 통합 가능한 범위에서만 재사용하세요.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeService.java`:
- Around line 47-60: Update HybridExactMergeService.merge to also validate that
singlePassInput and twoPassInput are different paths, using the existing
validateDifferentFiles helper and a clear input label. Keep the current
validations against output unchanged.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java`:
- Around line 2167-2184: Update the IllegalStateException thrown in
resolveAnalysisMode() for unsupported analysis modes to preserve the caught
IllegalArgumentException as its cause, while keeping the existing message and
validation behavior unchanged.
- Around line 489-528: Update analyzeHybridExact to start analyzeSinglePass and
analyzeTwoPass concurrently with CompletableFuture, then merge their results
after both complete so total latency follows the slower branch rather than their
sum. Ensure llmConcurrencyLimiter permits are held and released correctly for
each asynchronous LLM operation, and propagate failures or cancellation from
either branch without leaving the other task or permit unresolved.

In
`@src/test/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationAnalysisRunnerSafetyTest.java`:
- Around line 219-266: 기존 조건부 빈 생성 및 fail-fast 테스트에 HybridExactMergeRunner 실행 실패
경로 테스트를 추가하세요. HybridExactMergeRunner.run()에서 offline merge 서비스가 예외를 던지도록 구성한 뒤,
evaluationExitCoordinator.exit("hybrid-exact-merge", 1)이 호출되고 원래 예외가 재전파되는지
검증하세요. NlgEvaluationRunner와 EvaluationAnalysisRunner의 유사한 실패 테스트 패턴을 재사용하세요.

In
`@src/test/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeServiceTest.java`:
- Around line 24-146: HybridExactMergeService 테스트에 validateSuccessRow의
errorMessage가 존재하는 입력 행을 거부하는 실패 테스트와 validateDifferentFiles가 output이
single-pass 또는 two-pass 입력과 동일할 때 거부하는 실패 테스트를 추가하세요. 각 테스트는 기존 헬퍼와 예외 검증 방식을
재사용하고, 해당 검증 경로의 명확한 예외 메시지를 확인해야 합니다.

In
`@src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClientTest.java`:
- Around line 42-160: AnalysisAiClientTest에 analyzeHybridExact() 실패 전파 테스트를
추가하세요. analyze()를 통해 mock된 openAIClient로 두 LLM 호출이 모두 실행되는지 검증하고, two-pass 호출이
예외를 던질 때 single-pass 결과를 반환하거나 삼키지 않고 동일한 예외를 그대로 전파하는지 확인하세요. 기존
resolveAnalysisMode() 및 mergeHybridExact() 테스트 범위는 변경하지 마세요.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9127dc72-033d-410f-b6c2-a3c71c86fde1

📥 Commits

Reviewing files that changed from the base of the PR and between 921b20e and 91c1a8a.

⛔ Files ignored due to path filters (2)
  • evaluation/evaluation_nlg_judge_hybrid_comparison.csv is excluded by !**/*.csv
  • evaluation/evaluation_nlg_judge_hybrid_v1.csv is excluded by !**/*.csv
📒 Files selected for processing (11)
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationAnalysisRunner.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationCsvSupport.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationRunnerFlagValidator.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeRunner.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeService.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/NlgEvaluationRunner.java
  • src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java
  • src/main/resources/application-analysis-eval.yaml
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationAnalysisRunnerSafetyTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeServiceTest.java
  • src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClientTest.java

Comment on lines +489 to +528
private AnalysisAiCallResult analyzeHybridExact(
AnalysisPromptInput promptInput,
RetrievalContext referenceContext,
JobCategoryEvaluationCriteria jobCategoryEvaluationCriteria,
String operationName
) {
AnalysisAiCallResult singlePassResult = analyzeSinglePass(
promptInput,
referenceContext,
jobCategoryEvaluationCriteria,
operationName + "-single-pass"
);
AnalysisAiCallResult twoPassResult = analyzeTwoPass(
promptInput,
referenceContext,
jobCategoryEvaluationCriteria,
operationName + "-two-pass"
);
AnalysisLlmResponse merged = mergeHybridExact(
singlePassResult.response(),
twoPassResult.response()
);
log.debug(
"Hybrid exact response merged. questionAnalysesSource=single-pass, missingKeywordsSource=two-pass, scoreSource=single-pass, singlePassQuestionAnalyses={}, twoPassQuestionAnalyses={}, mergedQuestionAnalyses={}, singlePassMissingKeywords={}, twoPassMissingKeywords={}, mergedMissingKeywords={}",
size(singlePassResult.response() == null ? null : singlePassResult.response().questionAnalyses()),
size(twoPassResult.response() == null ? null : twoPassResult.response().questionAnalyses()),
size(merged == null ? null : merged.questionAnalyses()),
size(singlePassResult.response() == null ? null : singlePassResult.response().missingKeywords()),
size(twoPassResult.response() == null ? null : twoPassResult.response().missingKeywords()),
size(merged == null ? null : merged.missingKeywords())
);
return AnalysisAiCallResult.hybridExact(
merged,
twoPassResult.rawCandidateResponse(),
twoPassResult.sanitizedCandidateResponse(),
twoPassResult.candidateReviewResponse(),
twoPassResult.candidateCallLatencyMs(),
singlePassResult.finalCallLatencyMs() + twoPassResult.finalCallLatencyMs()
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'AnalysisAiClient.java' . || true

echo "== file outline around relevant methods =="
if [ -f src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java ]; then
  wc -l src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java
  sed -n '280,540p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java | cat -n
  sed -n '1820,1885p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java | cat -n
  sed -n '2150,2260p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java | cat -n
fi

echo "== search for llmConcurrencyLimiter/analysisExecutor and call sites =="
rg -n "llmConcurrencyLimiter|analysisExecutor|hybridExact|hybrid-exact|analyze\\(" src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java || true

Repository: JobDri-Developer/BackEnd

Length of output: 22554


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== LlmConcurrencyLimiter definition/usages =="
fd 'LlmConcurrencyLimiter.java' . -x sh -c 'echo "--- {}"; wc -l "{}"; cat -n "{}"'

echo "== search all llmConcurrencyLimiter usages =="
rg -n "llmConcurrencyLimiter|LlmConcurrencyLimiter" src/main/java || true

echo "== search for CompletableFuture/parallel executor patterns in client =="
rg -n "CompletableFuture|supplyAsync|Thread|Executor|analysisExecutor|LLM" src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java || true

echo "== read remaining createStructuredResponse and related methods =="
sed -n '540,700p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java | cat -n
sed -n '850,960p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java | cat -n
sed -n '1060,1145p' src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java | cat -n

Repository: JobDri-Developer/BackEnd

Length of output: 25475


Hybrid Exact 모드는 single-pass와 two-pass를 병렬화해 응답 지연을 합산형에서 최대형으로 줄이세요.

analyzeHybridExact()analyzeSinglePass(...)를 먼저 마친 뒤 analyzeTwoPass(...)를 호출하므로, hybrid-exact API 응답은 single-pass 호출 지연 + two-pass 후보/검증 지연만큼 늘어나고 OpenAI 호출 비용도 합산됩니다. createStructuredResponse() 내부에서 llmConcurrencyLimiter.execute()가 동기 스락/허브를 하나씩 점유하므로, 두 호출을 CompletableFuture로 병렬화할 때는 동시 허가증 사용과 실패/취소 전파 방식을 함께 설계해주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.java`
around lines 489 - 528, Update analyzeHybridExact to start analyzeSinglePass and
analyzeTwoPass concurrently with CompletableFuture, then merge their results
after both complete so total latency follows the slower branch rather than their
sum. Ensure llmConcurrencyLimiter permits are held and released correctly for
each asynchronous LLM operation, and propagate failures or cancellation from
either branch without leaving the other task or permit unresolved.

Comment on lines +42 to +160
@Test
@DisplayName("analysis.mode가 없으면 기존 two-pass boolean으로 분석 모드를 해석한다")
void resolveAnalysisModeFallsBackToTwoPassBoolean() {
ReflectionTestUtils.setField(analysisAiClient, "analysisMode", "");
ReflectionTestUtils.setField(analysisAiClient, "twoPassEnabled", false);
assertThat(analysisAiClient.resolveAnalysisMode()).isEqualTo(AnalysisAiClient.AnalysisMode.SINGLE_PASS);

ReflectionTestUtils.setField(analysisAiClient, "twoPassEnabled", true);
assertThat(analysisAiClient.resolveAnalysisMode()).isEqualTo(AnalysisAiClient.AnalysisMode.TWO_PASS);
}

@Test
@DisplayName("analysis.mode가 기존 two-pass boolean보다 우선한다")
void analysisModePropertyTakesPrecedenceOverTwoPassBoolean() {
ReflectionTestUtils.setField(analysisAiClient, "twoPassEnabled", true);
ReflectionTestUtils.setField(analysisAiClient, "analysisMode", "single-pass");
assertThat(analysisAiClient.resolveAnalysisMode()).isEqualTo(AnalysisAiClient.AnalysisMode.SINGLE_PASS);

ReflectionTestUtils.setField(analysisAiClient, "analysisMode", "hybrid-exact");
assertThat(analysisAiClient.resolveAnalysisMode()).isEqualTo(AnalysisAiClient.AnalysisMode.HYBRID_EXACT);
}

@Test
@DisplayName("지원하지 않는 analysis.mode는 명확한 예외를 던진다")
void unsupportedAnalysisModeFailsFast() {
ReflectionTestUtils.setField(analysisAiClient, "analysisMode", "unknown");

assertThatThrownBy(analysisAiClient::resolveAnalysisMode)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Unsupported analysis mode: unknown");
}

@Test
@DisplayName("Hybrid Exact merge는 questionAnalyses와 점수는 single-pass, missingKeywords는 two-pass 전체 결과를 사용한다")
void mergeHybridExactUsesExplicitSources() {
AnalysisLlmResponse.QuestionAnalysisItem singleQuestionAnalysis =
new AnalysisLlmResponse.QuestionAnalysisItem(1L, "Spring Boot API를 개발했습니다.", "MENTIONED", "결과가 부족합니다.", null);
AnalysisLlmResponse.QuestionAnalysisItem twoPassQuestionAnalysis =
new AnalysisLlmResponse.QuestionAnalysisItem(1L, "장애 대응 경험이 있습니다.", "MENTIONED", "역할이 부족합니다.", null);
AnalysisLlmResponse.MissingKeywordItem singleMissingKeyword =
new AnalysisLlmResponse.MissingKeywordItem("single 누락", "mainTask");
AnalysisLlmResponse.MissingKeywordItem twoPassMissingKeyword =
new AnalysisLlmResponse.MissingKeywordItem("장애 대응 경험", "qualification");
AnalysisLlmResponse singlePassResponse = new AnalysisLlmResponse(
80,
70,
60,
"single feedback",
List.of(new AnalysisLlmResponse.HighlightItem("강점", "Spring Boot API")),
List.of(new AnalysisLlmResponse.HighlightItem("약점", "결과")),
List.of(singleMissingKeyword),
List.of(singleQuestionAnalysis)
);
AnalysisLlmResponse twoPassResponse = new AnalysisLlmResponse(
10,
20,
30,
"two-pass feedback",
List.of(new AnalysisLlmResponse.HighlightItem("two-pass 강점", "장애 대응")),
List.of(),
List.of(twoPassMissingKeyword),
List.of(twoPassQuestionAnalysis)
);

AnalysisLlmResponse merged = analysisAiClient.mergeHybridExact(
singlePassResponse,
twoPassResponse
);

assertThat(merged.jobFit()).isEqualTo(80);
assertThat(merged.impact()).isEqualTo(70);
assertThat(merged.completeness()).isEqualTo(60);
assertThat(merged.feedback()).isEqualTo("single feedback");
assertThat(merged.keyStrengths()).isEqualTo(singlePassResponse.keyStrengths());
assertThat(merged.questionAnalyses()).containsExactly(singleQuestionAnalysis);
assertThat(merged.missingKeywords()).containsExactly(twoPassMissingKeyword);
assertThat(merged.missingKeywords()).doesNotContain(singleMissingKeyword);
assertThat(merged.questionAnalyses()).doesNotContain(twoPassQuestionAnalysis);
}

@Test
@DisplayName("Hybrid Exact merge는 source 응답 리스트를 방어적으로 복사한다")
void mergeHybridExactCopiesLists() {
AnalysisLlmResponse.QuestionAnalysisItem singleQuestionAnalysis =
new AnalysisLlmResponse.QuestionAnalysisItem(1L, "Spring Boot API를 개발했습니다.", "MENTIONED", "결과가 부족합니다.", null);
AnalysisLlmResponse.MissingKeywordItem twoPassMissingKeyword =
new AnalysisLlmResponse.MissingKeywordItem("장애 대응 경험", "qualification");
AnalysisLlmResponse singlePassResponse = new AnalysisLlmResponse(
80,
70,
60,
"single feedback",
new java.util.ArrayList<>(List.of(new AnalysisLlmResponse.HighlightItem("강점", "Spring Boot API"))),
new java.util.ArrayList<>(),
new java.util.ArrayList<>(),
new java.util.ArrayList<>(List.of(singleQuestionAnalysis))
);
AnalysisLlmResponse twoPassResponse = new AnalysisLlmResponse(
10,
20,
30,
"two-pass feedback",
new java.util.ArrayList<>(),
new java.util.ArrayList<>(),
new java.util.ArrayList<>(List.of(twoPassMissingKeyword)),
new java.util.ArrayList<>()
);

AnalysisLlmResponse merged = analysisAiClient.mergeHybridExact(singlePassResponse, twoPassResponse);

assertThat(merged.keyStrengths()).containsExactlyElementsOf(singlePassResponse.keyStrengths());
assertThat(merged.questionAnalyses()).containsExactly(singleQuestionAnalysis);
assertThat(merged.missingKeywords()).containsExactly(twoPassMissingKeyword);
assertThatThrownBy(() -> merged.questionAnalyses().add(singleQuestionAnalysis))
.isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> merged.missingKeywords().add(twoPassMissingKeyword))
.isInstanceOf(UnsupportedOperationException.class);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

analyzeHybridExact() 실패 전파 경로에 대한 테스트가 빠져 있습니다.

resolveAnalysisMode()mergeHybridExact()는 꼼꼼히 테스트됐지만, 실제로 두 LLM 호출을 묶는 analyzeHybridExact()(private)의 동작—예를 들어 two-pass 쪽 호출이 예외를 던졌을 때 single-pass 결과가 조용히 버려지고 예외가 그대로 전파되는지, 혹은 두 호출이 실제로 모두 실행되는지—를 검증하는 테스트가 없습니다. 이 메서드는 두 개의 독립적인 외부 API 호출을 결합하는 지점이라 실패 복구/부분 실패 시나리오 커버리지가 특히 중요합니다.

analyze()를 통해 mock된 openAIClient로 간접 테스트하거나, 패키지 접근 제한을 완화해 직접 테스트하는 방향을 검토해주세요.

As per path instructions, "테스트가 동시성, 재시도, timeout, 실패 복구, 멱등성 같은 비동기 시나리오를 충분히 검증하는지 중점적으로 확인하세요. 해피패스만 있는 경우 경계 조건과 회귀 위험을 지적하세요."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClientTest.java`
around lines 42 - 160, AnalysisAiClientTest에 analyzeHybridExact() 실패 전파 테스트를
추가하세요. analyze()를 통해 mock된 openAIClient로 두 LLM 호출이 모두 실행되는지 검증하고, two-pass 호출이
예외를 던질 때 single-pass 결과를 반환하거나 삼키지 않고 동일한 예외를 그대로 전파하는지 확인하세요. 기존
resolveAnalysisMode() 및 mergeHybridExact() 테스트 범위는 변경하지 마세요.

Source: Path instructions

- 평가 CSV writer의 공통 파일 생성 및 writer 처리 흐름 추출
- Hybrid Exact offline merge 입력 경로 동일성 검증 추가
- unsupported analysis.mode 예외 cause 보존
- Hybrid merge Runner 실패 시 종료 요청 및 예외 전파 테스트 추가
- Hybrid merge error row 및 overwrite 방지 테스트 추가
- 병렬화 요구는 Hybrid Exact 기준 구현 제약과 충돌하여 미반영
@whc9999
whc9999 merged commit e5deb1c into dev Jul 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant