[Feat] 자기소개서 분석 Hybrid Exact 기준 구현#174
Conversation
- 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 조건 테스트 추가
|
Warning Review limit reached
Next review available in: 49 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthrough분석 모드에 ChangesHybrid exact 분석 및 평가 머지
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: 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: 병합 요약 반환
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (2)
evaluation/evaluation_nlg_judge_hybrid_comparison.csvis excluded by!**/*.csvevaluation/evaluation_nlg_judge_hybrid_v1.csvis excluded by!**/*.csv
📒 Files selected for processing (11)
src/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationAnalysisRunner.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationCsvSupport.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationRunnerFlagValidator.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeRunner.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeService.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/evaluation/NlgEvaluationRunner.javasrc/main/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClient.javasrc/main/resources/application-analysis-eval.yamlsrc/test/java/com/jobdri/jobdri_api/domain/analysis/evaluation/EvaluationAnalysisRunnerSafetyTest.javasrc/test/java/com/jobdri/jobdri_api/domain/analysis/evaluation/HybridExactMergeServiceTest.javasrc/test/java/com/jobdri/jobdri_api/domain/analysis/service/AnalysisAiClientTest.java
| 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() | ||
| ); | ||
| } |
There was a problem hiding this comment.
🚀 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 || trueRepository: 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 -nRepository: 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.
| @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); | ||
| } | ||
|
|
There was a problem hiding this comment.
🩺 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 기준 구현 제약과 충돌하여 미반영
✨ 어떤 이유로 PR를 하셨나요?
📋 세부 내용 - 왜 해당 PR이 필요한지 작업 내용을 자세하게 설명해주세요
📸 작업 화면 스크린샷
🚨 관련 이슈 번호 [ ]
Summary by CodeRabbit
새 기능
버그 수정