From 4cc34187af4897b06e5d9d3c3a59b767892601de Mon Sep 17 00:00:00 2001 From: jmj Date: Fri, 19 Jun 2026 11:05:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A9=80=ED=8B=B0=20=EB=A9=B4=EC=A0=91?= =?UTF-8?q?=EA=B4=80=20=ED=8C=A8=EB=84=90=20=E2=80=94=20=EB=8B=A4=EC=A7=81?= =?UTF-8?q?=EA=B5=B0=20=EC=A7=88=EB=AC=B8=EB=B9=84=EC=A4=91=20=EA=B0=80?= =?UTF-8?q?=EC=A4=91=20(Phase=202B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다직군 세션에서 직군별 기술 평가위원을 두고, 직군별 질문 수 비중으로 technical_accuracy 를 가중평균. 단일/레거시는 기존대로(단일 평가위원). - AI 질문생성: 각 질문에 job_category 태그(세션 직군 중 하나, 비면 Core 폴백) - Core: session_question_pool.job_category 컬럼(V16) + 콜백 저장, generate.feedback 에 domainQuestionCounts(사용된 일반질문의 직군별 개수) 전달 (interview_messages·메시지 팩토리는 무변경 — 주제=메인질문 단위 가중) - AI 패널: 직군 수만큼 기술 평가위원 생성→질문수 가중평균, 논리·전달은 공통. 분해(panel_breakdown)에 직군별 항목으로 노출(프론트는 N개 자동 렌더, 무변경) - 테스트: 다직군 가중평균(80*3+40*1)/4=70 검증 등 검증: AI 250 / 백엔드 BUILD OK / openapi 불변(응답 계약 유지) / 프론트 무변경. Co-Authored-By: Claude Opus 4.8 --- .../chain/feedback_generation_chain.py | 116 +++++++++++++----- .../chain/prompts/question_generation.py | 3 +- .../messaging/consumers/feedback_consumer.py | 1 + ai/src/ai_server/model/messages/feedback.py | 2 + ai/src/ai_server/model/messages/questions.py | 3 + ai/tests/test_feedback_panel.py | 36 ++++++ .../application/QuestionsCallbackService.java | 6 +- .../application/SessionFeedbackRequester.java | 11 +- .../dto/GenerateFeedbackPayload.java | 4 +- .../dto/QuestionsCallbackPayload.java | 1 + .../session/domain/SessionQuestionPool.java | 17 ++- .../domain/SessionQuestionPoolRepository.java | 3 + ...V16__add_job_category_to_question_pool.sql | 3 + .../QuestionsCallbackServiceTest.java | 8 +- .../SessionFeedbackRequesterTest.java | 1 + 15 files changed, 171 insertions(+), 44 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V16__add_job_category_to_question_pool.sql diff --git a/ai/src/ai_server/chain/feedback_generation_chain.py b/ai/src/ai_server/chain/feedback_generation_chain.py index c3b4fa73..72cb0b6a 100644 --- a/ai/src/ai_server/chain/feedback_generation_chain.py +++ b/ai/src/ai_server/chain/feedback_generation_chain.py @@ -43,6 +43,7 @@ async def generate( rag_context: str, voice_analysis_summary: str, score_basis: str = "(없음)", + domain_question_counts: dict[str, int] | None = None, ) -> FeedbackResult: ... @@ -61,6 +62,7 @@ async def generate( rag_context: str, voice_analysis_summary: str = "", score_basis: str = "(없음)", + domain_question_counts: dict[str, int] | None = None, ) -> FeedbackResult: result = await self._chain.ainvoke( { @@ -136,6 +138,19 @@ class _EvaluatorSpec: dimension_guide: str +_TECH_GUIDE = ( + "- 기술 정확성, 깊이, trade-off, 근거를 봅니다. 질문의 '기대 신호'를 " + "답변이 얼마나 짚었는지를 핵심 근거로 삼습니다." +) + +_DOMAIN_KO = { + "FRONTEND": "프론트엔드", + "BACKEND": "백엔드", + "INFRA": "인프라", + "DBA": "DBA", +} + + def _domain_spec(job_category: str, mode: str) -> _EvaluatorSpec: # PERSONALITY 모드는 기술 평가자를 인성·협업 평가자로 교체(사용자 결정). if (mode or "").upper() == "PERSONALITY": @@ -149,18 +164,39 @@ def _domain_spec(job_category: str, mode: str) -> _EvaluatorSpec: "기술 정확도는 평가하지 않습니다." ), ) + ko = _DOMAIN_KO.get((job_category or "").upper(), job_category) return _EvaluatorSpec( key="technical", label="기술", - persona=f"{job_category} 직군 시니어 기술 면접관", + persona=f"{ko} 직군 시니어 기술 면접관", dimension_name="기술 정확도·깊이", - dimension_guide=( - "- 기술 정확성, 깊이, trade-off, 근거를 봅니다. 질문의 '기대 신호'를 " - "답변이 얼마나 짚었는지를 핵심 근거로 삼습니다." - ), + dimension_guide=_TECH_GUIDE, ) +def _domain_specs_weighted( + job_category: str, mode: str, domain_question_counts: dict[str, int] +) -> list[tuple[_EvaluatorSpec, float]]: + """직군별 기술 평가위원 + 가중치(질문 수). PERSONALITY/단일/레거시는 단일 평가위원.""" + if (mode or "").upper() == "PERSONALITY" or not domain_question_counts: + return [(_domain_spec(job_category, mode), 1.0)] + specs: list[tuple[_EvaluatorSpec, float]] = [] + for dom, cnt in domain_question_counts.items(): + ko = _DOMAIN_KO.get((dom or "").upper(), dom) + weight = float(cnt) if cnt and cnt > 0 else 1.0 + specs.append(( + _EvaluatorSpec( + key=f"tech:{dom}", + label=ko, + persona=f"{ko} 직군 시니어 기술 면접관", + dimension_name="기술 정확도·깊이", + dimension_guide=_TECH_GUIDE, + ), + weight, + )) + return specs or [(_domain_spec(job_category, mode), 1.0)] + + _LOGIC_SPEC = _EvaluatorSpec( key="logic", label="논리", @@ -262,8 +298,11 @@ async def generate( rag_context: str, voice_analysis_summary: str = "", score_basis: str = "(없음)", + domain_question_counts: dict[str, int] | None = None, ) -> FeedbackResult: - specs = [_domain_spec(job_category, mode), _LOGIC_SPEC, _COMM_SPEC] + domain_specs = _domain_specs_weighted(job_category, mode, domain_question_counts or {}) + # 평가위원 순서: 직군 기술 평가위원(N) + 논리 + 전달. + specs = [s for s, _ in domain_specs] + [_LOGIC_SPEC, _COMM_SPEC] shared = { "job_category": job_category, "mode": mode, @@ -290,52 +329,67 @@ async def generate( return_exceptions=True, ) - results: dict[str, EvaluatorResult] = {} + # 위치 기준 매핑(직군이 여러 개라 key 가 겹치지 않게). + results: list[EvaluatorResult] = [] for spec, r in zip(specs, raw): if isinstance(r, EvaluatorResult): - results[spec.key] = r + results.append(r) else: - log.warning( - "feedback.panel.evaluator_failed", - evaluator=spec.key, - error=str(r), - ) - results[spec.key] = EvaluatorResult() + log.warning("feedback.panel.evaluator_failed", evaluator=spec.key, error=str(r)) + results.append(EvaluatorResult()) - tech = results["technical"] - logic = results["logic"] - comm = results["communication"] - domain_label = specs[0].label + n_domain = len(domain_specs) + domain_results = list(zip([s for s, _ in domain_specs], [w for _, w in domain_specs], results[:n_domain])) + logic = results[n_domain] + comm = results[n_domain + 1] + # technical_accuracy = 직군 평가위원 점수의 질문수 가중평균. + technical_accuracy = _weighted_overall([(r.score, w) for _, w, r in domain_results]) overall = _weighted_overall( [ - (tech.score, self._w_tech), + (technical_accuracy, self._w_tech), (logic.score, self._w_logic), (comm.score, self._w_comm), ] ) - strengths = _merge_notes( - [(domain_label, tech.strength), ("논리", logic.strength), ("전달", comm.strength)] - ) - weaknesses = _merge_notes( - [(domain_label, tech.weakness), ("논리", logic.weakness), ("전달", comm.weakness)] - ) - keywords = _dedup_keywords(tech.keywords + logic.keywords + comm.keywords) + + note_items = [(spec.label, r.strength) for spec, _, r in domain_results] + note_items += [("논리", logic.strength), ("전달", comm.strength)] + strengths = _merge_notes(note_items) + weak_items = [(spec.label, r.weakness) for spec, _, r in domain_results] + weak_items += [("논리", logic.weakness), ("전달", comm.weakness)] + weaknesses = _merge_notes(weak_items) + + all_keywords: list[str] = [] + for _, _, r in domain_results: + all_keywords += r.keywords + all_keywords += logic.keywords + comm.keywords + keywords = _dedup_keywords(all_keywords) breakdown = [ PanelBreakdownItem( evaluator=spec.label, dimension=spec.dimension_name, - score=res.score, - strength=res.strength, - weakness=res.weakness, + score=r.score, + strength=r.strength, + weakness=r.weakness, ) - for spec, res in zip(specs, (tech, logic, comm)) + for spec, _, r in domain_results ] + for spec, r in ((_LOGIC_SPEC, logic), (_COMM_SPEC, comm)): + breakdown.append( + PanelBreakdownItem( + evaluator=spec.label, + dimension=spec.dimension_name, + score=r.score, + strength=r.strength, + weakness=r.weakness, + ) + ) return FeedbackResult( overall_score=overall, - technical_accuracy=tech.score, + technical_accuracy=technical_accuracy, logic_score=logic.score, communication_score=comm.score, strengths_summary=strengths, diff --git a/ai/src/ai_server/chain/prompts/question_generation.py b/ai/src/ai_server/chain/prompts/question_generation.py index d5bc7e7e..e3ec5fdf 100644 --- a/ai/src/ai_server/chain/prompts/question_generation.py +++ b/ai/src/ai_server/chain/prompts/question_generation.py @@ -26,7 +26,8 @@ "요구 사항:\n" "1. 정확히 {max_questions}개의 질문을 생성하되, **서로 다른 주제/영역**을 다룹니다 " "(각 질문이 일반질문 1개의 씨앗이 되고 뒤에 꼬리질문이 붙으므로 같은 주제 반복 금지).\n" - "2. 각 질문에 적절한 category 를 부여합니다.\n" + "2. 각 질문에 적절한 category 를 부여하고, job_category 를 직군({job_categories}) 중 " + "그 질문이 겨냥하는 **하나**로 지정합니다(직군이 하나면 모두 그 값).\n" "3. 직군({job_categories})·면접 모드({mode}) 에 맞는 비중으로 카테고리 분배:\n" " - TECHNICAL: CS_FUNDAMENTAL + TECH_CHOICE + PROJECT_DEEP_DIVE 중심.\n" " - PERSONALITY: BEHAVIORAL 중심.\n" diff --git a/ai/src/ai_server/messaging/consumers/feedback_consumer.py b/ai/src/ai_server/messaging/consumers/feedback_consumer.py index 2c8a88d0..cd2d4e8f 100644 --- a/ai/src/ai_server/messaging/consumers/feedback_consumer.py +++ b/ai/src/ai_server/messaging/consumers/feedback_consumer.py @@ -97,6 +97,7 @@ async def handle(self, message: AbstractIncomingMessage) -> None: score_basis=score_basis, rag_context=rag_context, voice_analysis_summary=voice_analysis_summary, + domain_question_counts=req.domain_question_counts, ) payload = FeedbackCallbackPayload( diff --git a/ai/src/ai_server/model/messages/feedback.py b/ai/src/ai_server/model/messages/feedback.py index 675de679..19979fde 100644 --- a/ai/src/ai_server/model/messages/feedback.py +++ b/ai/src/ai_server/model/messages/feedback.py @@ -60,6 +60,8 @@ class GenerateFeedbackRequest(BaseModel): messages: list[FeedbackMessageItem] = Field(default_factory=list) context_document_ids: list[int] = Field(default_factory=list) voice_analysis_summary: VoiceAnalysisSummary | None = None + # 다직군 패널 가중: 사용된 일반질문의 직군별 개수. 비면 단일 직군 평가. + domain_question_counts: dict[str, int] = Field(default_factory=dict) class PanelBreakdownItem(BaseModel): diff --git a/ai/src/ai_server/model/messages/questions.py b/ai/src/ai_server/model/messages/questions.py index 5536bef7..ce49d978 100644 --- a/ai/src/ai_server/model/messages/questions.py +++ b/ai/src/ai_server/model/messages/questions.py @@ -48,6 +48,9 @@ class GeneratedQuestion(BaseModel): category: QuestionCategory question: str + # 이 질문이 겨냥한 직군. 세션의 job_categories 중 하나. 다직군 패널 가중에 사용. + # LLM 이 비우면 Core 가 대표 직군으로 폴백. + job_category: JobCategory | None = None # 질문이 근거한 자료 인용(PROJECT/TECH 는 필수). 라이브 화면에 힌트로 노출. target_evidence: str = "" # 좋은 답이 드러내야 할 것 — 내부 평가용. 라이브 비노출(정답 유출 방지). diff --git a/ai/tests/test_feedback_panel.py b/ai/tests/test_feedback_panel.py index 4894c0ba..a33547eb 100644 --- a/ai/tests/test_feedback_panel.py +++ b/ai/tests/test_feedback_panel.py @@ -87,6 +87,42 @@ async def test_personality_mode_swaps_domain_to_behavioral(): assert "[인성]" in r.strengths_summary +class _PersonaChain: + """persona 내용으로 라우팅(다직군 기술 평가위원은 dimension 이 같아 persona 로 구분).""" + + async def ainvoke(self, v): + p = v["persona"] + if "백엔드" in p: + return EvaluatorResult(score=80, strength="BE 강점") + if "프론트엔드" in p: + return EvaluatorResult(score=40, strength="FE 강점") + if "논리" in p: + return EvaluatorResult(score=60) + return EvaluatorResult(score=50) # 커뮤니케이션 + + +@pytest.mark.asyncio +async def test_multi_domain_weighted_by_question_counts(): + gen = PanelFeedbackGenerator(_PersonaChain()) + r = await gen.generate( + job_category="BACKEND", + mode="TECHNICAL", + total_question_count=4, + end_reason="POOL_EXHAUSTED", + transcript="t", + rag_context="(none)", + domain_question_counts={"BACKEND": 3, "FRONTEND": 1}, + ) + # technical = (80*3 + 40*1)/4 = 70 + assert r.technical_accuracy == 70 + assert r.logic_score == 60 + assert r.communication_score == 50 + # 직군 평가위원 2명 + 논리 + 전달 = 4 + assert [b.evaluator for b in r.panel_breakdown] == ["백엔드", "프론트엔드", "논리", "전달"] + # overall = 0.5*70 + 0.25*60 + 0.25*50 = 62.5 → 62 (은행가 반올림) + assert r.overall_score == 62 + + @pytest.mark.asyncio async def test_keyword_dedup(): r = await _run( diff --git a/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java b/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java index 7525344c..dc1f42ce 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java @@ -90,9 +90,13 @@ private void applyInitialQuestion(InterviewSession session, QuestionsCallbackPay return; } int idx = 0; + String fallbackJobCategory = session.getJobCategory().name(); for (GeneratedQuestion q : questions) { + String jobCategory = (q.jobCategory() != null && !q.jobCategory().isBlank()) + ? q.jobCategory() : fallbackJobCategory; poolRepository.save(SessionQuestionPool.of( - session.getId(), idx++, q.question(), q.category(), q.targetEvidence(), q.expectedSignal())); + session.getId(), idx++, q.question(), q.category(), jobCategory, + q.targetEvidence(), q.expectedSignal())); } poolRepository.findFirstBySessionIdAndUsedFalseOrderByIdxAsc(session.getId()) .ifPresent(first -> insertGeneralFromPool(session, first, "INITIAL_QUESTION_READY")); diff --git a/backend/src/main/java/com/stackup/stackup/session/application/SessionFeedbackRequester.java b/backend/src/main/java/com/stackup/stackup/session/application/SessionFeedbackRequester.java index e6c7fa0d..45081787 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/SessionFeedbackRequester.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/SessionFeedbackRequester.java @@ -46,6 +46,7 @@ public class SessionFeedbackRequester { private final SessionContextRepository contextRepository; private final SessionFeedbackRepository feedbackRepository; private final MessageVoiceAnalysisRepository voiceAnalysisRepository; + private final com.stackup.stackup.session.domain.SessionQuestionPoolRepository poolRepository; @Transactional(propagation = Propagation.REQUIRES_NEW) @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) @@ -68,6 +69,13 @@ public void onSessionEnded(SessionEndedEvent event) { .map(c -> c.getDocument().getId()) .toList(); + Map domainQuestionCounts = new java.util.LinkedHashMap<>(); + String fallbackJobCategory = session.getJobCategory().name(); + for (var pool : poolRepository.findBySessionIdAndUsedTrue(event.sessionId())) { + String jc = pool.getJobCategory() != null ? pool.getJobCategory() : fallbackJobCategory; + domainQuestionCounts.merge(jc, 1, Integer::sum); + } + GenerateFeedbackPayload payload = new GenerateFeedbackPayload( session.getId(), session.getMode().name(), @@ -76,7 +84,8 @@ public void onSessionEnded(SessionEndedEvent event) { event.reason(), messages, contextDocumentIds, - summarizeVoiceAnalysis(event.sessionId()) + summarizeVoiceAnalysis(event.sessionId()), + domainQuestionCounts ); publisher.publishToAi( diff --git a/backend/src/main/java/com/stackup/stackup/session/application/dto/GenerateFeedbackPayload.java b/backend/src/main/java/com/stackup/stackup/session/application/dto/GenerateFeedbackPayload.java index abb0be9d..d2cb6569 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/dto/GenerateFeedbackPayload.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/dto/GenerateFeedbackPayload.java @@ -13,7 +13,9 @@ public record GenerateFeedbackPayload( String endReason, List messages, List contextDocumentIds, - VoiceAnalysisSummary voiceAnalysisSummary + VoiceAnalysisSummary voiceAnalysisSummary, + // 다직군 패널 가중: 사용된 일반질문의 직군별 개수(예: {"BACKEND":3,"FRONTEND":2}). 비면 단일. + Map domainQuestionCounts ) { public record MessageItem( diff --git a/backend/src/main/java/com/stackup/stackup/session/application/dto/QuestionsCallbackPayload.java b/backend/src/main/java/com/stackup/stackup/session/application/dto/QuestionsCallbackPayload.java index 626c66a4..38e55d7b 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/dto/QuestionsCallbackPayload.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/dto/QuestionsCallbackPayload.java @@ -26,6 +26,7 @@ public boolean isFollowup() { public record GeneratedQuestion( String category, String question, + String jobCategory, // 이 질문이 겨냥한 직군(다직군 가중용). null=대표 직군 폴백. String targetEvidence, // 질문 근거(자료 인용). 라이브 노출. String expectedSignal // 좋은 답이 드러낼 것. 내부 저장만(라이브 비노출). ) { diff --git a/backend/src/main/java/com/stackup/stackup/session/domain/SessionQuestionPool.java b/backend/src/main/java/com/stackup/stackup/session/domain/SessionQuestionPool.java index 9dd68190..92e7f2d0 100644 --- a/backend/src/main/java/com/stackup/stackup/session/domain/SessionQuestionPool.java +++ b/backend/src/main/java/com/stackup/stackup/session/domain/SessionQuestionPool.java @@ -33,6 +33,10 @@ public class SessionQuestionPool extends BaseTimeEntity { @Column(length = 40) private String category; + // 이 질문이 겨냥한 직군(다직군 패널 가중용). null = 대표 직군 폴백. + @Column(name = "job_category", length = 30) + private String jobCategory; + @Column(name = "target_evidence", columnDefinition = "text") private String targetEvidence; @@ -42,19 +46,22 @@ public class SessionQuestionPool extends BaseTimeEntity { @Column(nullable = false) private boolean used = false; - private SessionQuestionPool(Long sessionId, int idx, String question, - String category, String targetEvidence, String expectedSignal) { + private SessionQuestionPool(Long sessionId, int idx, String question, String category, + String jobCategory, String targetEvidence, String expectedSignal) { this.sessionId = sessionId; this.idx = idx; this.question = question; this.category = category; + this.jobCategory = jobCategory; this.targetEvidence = targetEvidence; this.expectedSignal = expectedSignal; } - public static SessionQuestionPool of(Long sessionId, int idx, String question, - String category, String targetEvidence, String expectedSignal) { - return new SessionQuestionPool(sessionId, idx, question, category, targetEvidence, expectedSignal); + public static SessionQuestionPool of(Long sessionId, int idx, String question, String category, + String jobCategory, String targetEvidence, + String expectedSignal) { + return new SessionQuestionPool(sessionId, idx, question, category, jobCategory, + targetEvidence, expectedSignal); } public void markUsed() { diff --git a/backend/src/main/java/com/stackup/stackup/session/domain/SessionQuestionPoolRepository.java b/backend/src/main/java/com/stackup/stackup/session/domain/SessionQuestionPoolRepository.java index b4f62927..e4aeb1f8 100644 --- a/backend/src/main/java/com/stackup/stackup/session/domain/SessionQuestionPoolRepository.java +++ b/backend/src/main/java/com/stackup/stackup/session/domain/SessionQuestionPoolRepository.java @@ -14,6 +14,9 @@ public interface SessionQuestionPoolRepository extends JpaRepository findBySessionIdAndUsedTrue(Long sessionId); + // 중복 질문 회피용: 주어진 세션들에서 출제된 질문 텍스트(최신순). @Query("select p.question from SessionQuestionPool p " + "where p.sessionId in :sessionIds order by p.createdAt desc") diff --git a/backend/src/main/resources/db/migration/V16__add_job_category_to_question_pool.sql b/backend/src/main/resources/db/migration/V16__add_job_category_to_question_pool.sql new file mode 100644 index 00000000..bcb23648 --- /dev/null +++ b/backend/src/main/resources/db/migration/V16__add_job_category_to_question_pool.sql @@ -0,0 +1,3 @@ +-- 멀티 면접관 패널(다직군 가중): 풀의 각 일반질문이 겨냥한 직군 태그. +-- 피드백 시 사용된 질문을 직군별로 집계해 평가위원 가중에 쓴다. null = 대표 직군 폴백. +ALTER TABLE session_question_pool ADD COLUMN job_category varchar(30); diff --git a/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java b/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java index a32695f6..fb0bfcfc 100644 --- a/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java +++ b/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java @@ -47,8 +47,8 @@ void apply_poolCallbackSeedsPoolAndInsertsFirstQuestion() { InterviewSession session = sessionFixture(11L, SessionStatus.READY); QuestionsCallbackEnvelope env = poolEnvelope(11L, List.of( new GeneratedQuestion("PROJECT_DEEP_DIVE", "Introduce yourself", - "이력서: 결제 시스템", "협업/문제해결 깊이"), - new GeneratedQuestion("TECH", "JPA?", null, null) + "BACKEND", "이력서: 결제 시스템", "협업/문제해결 깊이"), + new GeneratedQuestion("TECH", "JPA?", null, null, null) )); when(processedMessageRepository.existsById("m-1")).thenReturn(false); when(sessionRepository.findById(11L)).thenReturn(Optional.of(session)); @@ -57,7 +57,7 @@ void apply_poolCallbackSeedsPoolAndInsertsFirstQuestion() { when(poolRepository.findFirstBySessionIdAndUsedFalseOrderByIdxAsc(11L)).thenReturn( Optional.of(com.stackup.stackup.session.domain.SessionQuestionPool.of( 11L, 0, "Introduce yourself", "PROJECT_DEEP_DIVE", - "이력서: 결제 시스템", "협업/문제해결 깊이"))); + "BACKEND", "이력서: 결제 시스템", "협업/문제해결 깊이"))); when(messageRepository.save(any(InterviewMessage.class))).thenAnswer(inv -> { InterviewMessage m = inv.getArgument(0); ReflectionTestUtils.setField(m, "id", 500L); @@ -121,7 +121,7 @@ void apply_followupAutoEndsSessionAtMaxQuestions() { @Test void apply_skipsDuplicateMessageId() { QuestionsCallbackEnvelope env = - poolEnvelope(11L, List.of(new GeneratedQuestion("X", "Q", null, null))); + poolEnvelope(11L, List.of(new GeneratedQuestion("X", "Q", null, null, null))); when(processedMessageRepository.existsById("m-1")).thenReturn(true); service.apply(env); diff --git a/backend/src/test/java/com/stackup/stackup/session/application/SessionFeedbackRequesterTest.java b/backend/src/test/java/com/stackup/stackup/session/application/SessionFeedbackRequesterTest.java index bc18b36d..d9208daa 100644 --- a/backend/src/test/java/com/stackup/stackup/session/application/SessionFeedbackRequesterTest.java +++ b/backend/src/test/java/com/stackup/stackup/session/application/SessionFeedbackRequesterTest.java @@ -45,6 +45,7 @@ class SessionFeedbackRequesterTest { @Mock SessionContextRepository contextRepository; @Mock SessionFeedbackRepository feedbackRepository; @Mock MessageVoiceAnalysisRepository voiceAnalysisRepository; + @Mock com.stackup.stackup.session.domain.SessionQuestionPoolRepository poolRepository; @InjectMocks SessionFeedbackRequester requester; @Test