From 827b2658a0430b1c53161a307dba635fea6530f8 Mon Sep 17 00:00:00 2001 From: jmj Date: Tue, 29 Sep 2026 11:45:06 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=8F=89=EA=B0=80=20=EC=B6=95=C2=B7?= =?UTF-8?q?=ED=8F=89=EA=B0=80=EC=9C=84=EC=9B=90=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20=EC=A7=81=EA=B5=B0=20=EC=A4=91=EB=A6=BD=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #266 에서 남겨 둔 것. 영업 지원자의 피드백이 이렇게 나오고 있었다: 페르소나 "영업·영업관리 직군 시니어 **기술** 면접관" 평가 축 이름 "**기술** 정확도·깊이" 화면 표시 "기술 정확도" **표시 문제가 아니다.** 페르소나와 축 이름은 프롬프트에 들어가 평가의 틀을 정한다 — 기술 면접관에게 기술 정확도를 매기라고 시키면 영업 지원자의 점수 근거가 그쪽으로 끌려간다. 직군별 실제 관점(_DOMAIN_TECH_GUIDE)은 이미 구분돼 있는데 그 위에 덮인 이름이 그걸 되돌리고 있었다. persona "{직군} 직군 시니어 실무 면접관" dimension_name "직무 역량·깊이" 화면 표시 "직무 역량" DB 컬럼(technical_accuracy)·API 필드는 그대로 둔다. 값의 의미는 처음부터 "직군 패널이 매긴 직무 역량" 이었고 틀린 건 이름뿐이라, 스키마를 흔들 이유가 없다. 용어집에 대응을 적어 뒀다. ## 곁가지 — 단일 직군 평가위원 라벨 "기술" 이던 라벨이 직군명("백엔드")이 된다. 다직군 경로는 원래 직군명을 썼으므로 이제 단일/다직군이 일관되고, 요약의 "[백엔드] 설계 깊이" 가 "[기술] 설계 깊이" 보다 구체적이다. ## 회귀 테스트 - 20개 직군 **전부** 에 대해 페르소나에 "기술 면접관" 이, 축 이름에 "기술" 이 들어가지 않는지 - 영업 스펙이 자연스럽게 읽히고 **평가 관점은 여전히 영업의 것**인지 ("고객 문제 파악") - 중립화가 개발 직군의 관점까지 뭉개지 않았는지 (백엔드에 "트랜잭션") AI 437 / 백엔드 353 / 프론트 201 통과. --- .../chain/feedback_generation_chain.py | 15 +++-- ai/src/ai_server/chain/feedback_report.py | 2 +- .../chain/prompts/feedback_generation.py | 2 +- .../chain/question_generation_chain.py | 2 +- ai/tests/test_feedback_panel.py | 58 +++++++++++++++---- ai/tests/test_feedback_report.py | 6 +- .../session/domain/SessionFocusArea.java | 2 +- docs/glossary.md | 2 +- docs/messaging.md | 2 +- .../features/feedback/ui/FeedbackReport.tsx | 2 +- .../src/features/history/ui/StatsSummary.tsx | 2 +- .../features/interview/lib/focusArea.test.ts | 2 +- .../src/features/interview/lib/focusArea.ts | 2 +- .../home-features/ui/FeatureVisuals.tsx | 2 +- 14 files changed, 71 insertions(+), 30 deletions(-) diff --git a/ai/src/ai_server/chain/feedback_generation_chain.py b/ai/src/ai_server/chain/feedback_generation_chain.py index 16da4467..f7bf4388 100644 --- a/ai/src/ai_server/chain/feedback_generation_chain.py +++ b/ai/src/ai_server/chain/feedback_generation_chain.py @@ -296,15 +296,18 @@ def _domain_spec(job_category: str, mode: str) -> _EvaluatorSpec: dimension_name="인성·협업 역량", dimension_guide=( "- 협업/갈등 해결, 성장 경험, 태도, 자기주도성을 봅니다. " - "기술 정확도는 평가하지 않습니다." + "직무 역량은 평가하지 않습니다." ), ) ko = _DOMAIN_KO.get((job_category or "").upper(), job_category) return _EvaluatorSpec( key="technical", - label="기술", - persona=f"{ko} 직군 시니어 기술 면접관", - dimension_name="기술 정확도·깊이", + label=ko, + # "기술 면접관" 이 아니다. 영업·인사 지원자를 기술 면접관이 기술 정확도로 채점하면 + # **표시가 아니라 채점이 틀어진다** — 프롬프트가 평가의 틀을 정하기 때문이다. + # 직군별 실제 관점은 dimension_guide(_DOMAIN_TECH_GUIDE)가 이미 구분한다. + persona=f"{ko} 직군 시니어 실무 면접관", + dimension_name="직무 역량·깊이", dimension_guide=_tech_guide_for(job_category), ) @@ -324,8 +327,8 @@ def _domain_specs_weighted( _EvaluatorSpec( key=f"tech:{dom}", label=ko, - persona=f"{ko} 직군 시니어 기술 면접관", - dimension_name="기술 정확도·깊이", + persona=f"{ko} 직군 시니어 실무 면접관", + dimension_name="직무 역량·깊이", dimension_guide=_tech_guide_for(dom), ), weight, diff --git a/ai/src/ai_server/chain/feedback_report.py b/ai/src/ai_server/chain/feedback_report.py index 632386cb..28f545aa 100644 --- a/ai/src/ai_server/chain/feedback_report.py +++ b/ai/src/ai_server/chain/feedback_report.py @@ -53,7 +53,7 @@ def _render_scores(lines: list[str], result: FeedbackResult) -> None: lines.append("| 항목 | 점수 |") lines.append("| --- | ---: |") lines.append(f"| 종합 | {_fmt_score(result.overall_score)} |") - lines.append(f"| 기술 정확도 | {_fmt_score(result.technical_accuracy)} |") + lines.append(f"| 직무 역량 | {_fmt_score(result.technical_accuracy)} |") lines.append(f"| 논리 | {_fmt_score(result.logic_score)} |") lines.append(f"| 전달력 | {_fmt_score(result.communication_score)} |") diff --git a/ai/src/ai_server/chain/prompts/feedback_generation.py b/ai/src/ai_server/chain/prompts/feedback_generation.py index 89cea40e..dfbbc93e 100644 --- a/ai/src/ai_server/chain/prompts/feedback_generation.py +++ b/ai/src/ai_server/chain/prompts/feedback_generation.py @@ -6,7 +6,7 @@ "당신은 IT 직군 면접 평가관입니다. 지원자의 모든 답변을 종합해 객관적이고 건설적인 피드백을 한국어로 작성합니다.\n" "- 점수 (0~100 정수형, 산정 불가 시 null):\n" " - overall_score: 종합 점수\n" - " - technical_accuracy: 기술 정확도\n" + " - technical_accuracy: 직무 역량(개발 직군이면 기술 정확도)\n" " - logic_score: 논리·인과관계 명확성\n" " - communication_score: 답변의 명료성·구조화\n" "- 요약:\n" diff --git a/ai/src/ai_server/chain/question_generation_chain.py b/ai/src/ai_server/chain/question_generation_chain.py index 1be16a3a..9dfa7be5 100644 --- a/ai/src/ai_server/chain/question_generation_chain.py +++ b/ai/src/ai_server/chain/question_generation_chain.py @@ -32,7 +32,7 @@ def _format_self_introduction(self_introduction: str | None) -> str: # 평가 축 → 질문 설계 지침. 라벨만 넘기면 LLM 이 제각각 해석하므로 관점을 명시한다. _FOCUS_GUIDE = { "TECHNICAL": ( - "기술 정확도 — 개념을 정확히 아는지 파고드는 질문. 사용한 기술의 동작 원리·트레이드오프·" + "직무 역량 — 개념·원리를 정확히 아는지 파고드는 질문. 개발 직군이면 사용한 기술의 동작 원리·트레이드오프·" "대안과의 비교를 묻고, 뭉뚱그린 답으로는 통과할 수 없게 구체적인 지점을 짚으세요." ), "LOGIC": ( diff --git a/ai/tests/test_feedback_panel.py b/ai/tests/test_feedback_panel.py index 4f41c31b..50a048ed 100644 --- a/ai/tests/test_feedback_panel.py +++ b/ai/tests/test_feedback_panel.py @@ -20,7 +20,7 @@ async def ainvoke(self, _v): # 평가축(dimension_name) 으로 라우팅하는 가짜 체인. -TECH = "기술 정확도·깊이" +TECH = "직무 역량·깊이" PERSONALITY = "인성·협업 역량" LOGIC = "논리·인과관계 명확성" COMM = "명료성·구조화·전달력" @@ -67,10 +67,12 @@ async def test_weighted_overall_and_dimension_mapping(): assert r.communication_score == 40 # 0.5*80 + 0.25*60 + 0.25*40 = 65 assert r.overall_score == 65 - assert "[기술]" in r.strengths_summary and "[논리]" in r.strengths_summary + # 평가위원 라벨이 "기술" → 직군명("백엔드")이 됐다. 다직군 경로는 원래 직군명을 + # 썼으므로 이제 단일/다직군이 일관되고, 표시도 더 구체적이다. + assert "[백엔드]" in r.strengths_summary and "[논리]" in r.strengths_summary assert set(r.improvement_keywords) == {"JPA", "trade-off", "STAR"} # 평가위원별 분해 - assert [b.evaluator for b in r.panel_breakdown] == ["기술", "논리", "전달"] + assert [b.evaluator for b in r.panel_breakdown] == ["백엔드", "논리", "전달"] assert [b.score for b in r.panel_breakdown] == [80, 60, 40] @@ -273,7 +275,7 @@ def test_literal_matches_db_check_constraint(): def test_domain_spec_uses_domain_specific_guide(): spec = _domain_spec("FRONTEND", "TECHNICAL") - assert spec.persona == "프론트엔드 직군 시니어 기술 면접관" + assert spec.persona == "프론트엔드 직군 시니어 실무 면접관" assert "렌더링" in spec.dimension_guide @@ -315,8 +317,8 @@ async def ainvoke(self, v): rag_context="(none)", domain_question_counts={"BACKEND": 3, "FRONTEND": 1}, ) - be_guide = chain.guides_by_persona["백엔드 직군 시니어 기술 면접관"] - fe_guide = chain.guides_by_persona["프론트엔드 직군 시니어 기술 면접관"] + be_guide = chain.guides_by_persona["백엔드 직군 시니어 실무 면접관"] + fe_guide = chain.guides_by_persona["프론트엔드 직군 시니어 실무 면접관"] assert be_guide != fe_guide assert "트랜잭션" in be_guide assert "렌더링" in fe_guide @@ -345,7 +347,7 @@ async def test_one_evaluator_failure_does_not_break_the_others(): "논리·문제해결 평가위원": EvaluatorResult(score=60), "커뮤니케이션·전달력 평가위원": EvaluatorResult(score=40), }, - fail_personas={"백엔드 직군 시니어 기술 면접관"}, + fail_personas={"백엔드 직군 시니어 실무 면접관"}, ) gen = PanelFeedbackGenerator(chain) r = await gen.generate( @@ -360,7 +362,7 @@ async def test_one_evaluator_failure_does_not_break_the_others(): assert r.technical_accuracy is None # (60*0.25 + 40*0.25) / 0.5 = 50 assert r.overall_score == 50 - tech_item = next(b for b in r.panel_breakdown if b.evaluator == "기술") + tech_item = next(b for b in r.panel_breakdown if b.evaluator == "백엔드") assert tech_item.score is None assert tech_item.detail == _EVAL_FAILED_DETAIL @@ -373,8 +375,8 @@ async def test_all_domain_evaluators_failing_still_returns_logic_and_comm(): "커뮤니케이션·전달력 평가위원": EvaluatorResult(score=70), }, fail_personas={ - "백엔드 직군 시니어 기술 면접관", - "프론트엔드 직군 시니어 기술 면접관", + "백엔드 직군 시니어 실무 면접관", + "프론트엔드 직군 시니어 실무 면접관", }, ) gen = PanelFeedbackGenerator(chain) @@ -394,3 +396,39 @@ async def test_all_domain_evaluators_failing_still_returns_logic_and_comm(): item = next(b for b in r.panel_breakdown if b.evaluator == label) assert item.score is None assert item.detail == _EVAL_FAILED_DETAIL + + +# ── 직군 중립 명명 ─────────────────────────────────────────────────────────── +# +# 영업·인사 지원자를 "기술 면접관" 이 "기술 정확도" 로 채점하면 **표시가 아니라 채점이 +# 틀어진다** — 페르소나와 평가 축 이름이 프롬프트에 들어가 평가의 틀을 정하기 때문이다. +# 서비스 대상이 취준생 전반이 된 이상 기본 명명은 중립이어야 한다. + + +def test_persona_and_axis_are_not_engineering_flavoured(): + from ai_server.chain.feedback_generation_chain import _domain_spec + from ai_server.model.messages.job_category import JobCategory + from typing import get_args + + for category in get_args(JobCategory): + spec = _domain_spec(category, "TECHNICAL") + assert "기술 면접관" not in spec.persona, f"{category}: {spec.persona}" + assert "기술" not in spec.dimension_name, f"{category}: {spec.dimension_name}" + + +def test_non_engineering_domain_spec_reads_naturally(): + from ai_server.chain.feedback_generation_chain import _domain_spec + + sales = _domain_spec("SALES", "TECHNICAL") + assert sales.persona == "영업·영업관리 직군 시니어 실무 면접관" + assert sales.dimension_name == "직무 역량·깊이" + # 직군별 실제 관점은 그대로 달라야 한다 — 이름만 중립으로 바꾼 게 아니다. + assert "고객 문제 파악" in sales.dimension_guide + + +def test_engineering_domain_still_gets_engineering_guide(): + """중립 명명이 개발 직군의 평가 관점까지 뭉개면 안 된다.""" + from ai_server.chain.feedback_generation_chain import _domain_spec + + backend = _domain_spec("BACKEND", "TECHNICAL") + assert "트랜잭션" in backend.dimension_guide diff --git a/ai/tests/test_feedback_report.py b/ai/tests/test_feedback_report.py index 59375a1e..6cbef32c 100644 --- a/ai/tests/test_feedback_report.py +++ b/ai/tests/test_feedback_report.py @@ -50,7 +50,7 @@ def _result(**overrides) -> FeedbackResult: panel_breakdown=[ PanelBreakdownItem( evaluator="기술", - dimension="기술 정확도", + dimension="직무 역량", score=82.0, strength="개념 정확", weakness="사례 부족", @@ -89,10 +89,10 @@ def test_renders_full_report_sections(): assert "- 종료 사유: 질문 수 도달" in md # 점수 표: 85.0 → "85", 82.5 는 유지, None 은 —. assert "| 종합 | 85 |" in md - assert "| 기술 정확도 | 82.5 |" in md + assert "| 직무 역량 | 82.5 |" in md assert "| 전달력 | — |" in md # 패널. - assert "### 기술 — 기술 정확도 (82점)" in md + assert "### 기술 — 직무 역량 (82점)" in md assert "> 점수 근거: 예시 부재 감점" in md # 요약·키워드·플랜·하이라이트. assert "## 강점 요약" in md and "ACID 4요소를 명확히 답변." in md diff --git a/backend/src/main/java/com/stackup/stackup/session/domain/SessionFocusArea.java b/backend/src/main/java/com/stackup/stackup/session/domain/SessionFocusArea.java index c8057692..e5f5e894 100644 --- a/backend/src/main/java/com/stackup/stackup/session/domain/SessionFocusArea.java +++ b/backend/src/main/java/com/stackup/stackup/session/domain/SessionFocusArea.java @@ -2,7 +2,7 @@ // 약점 집중 재도전에서 겨냥할 평가 축. session_feedbacks 의 점수 축과 1:1. public enum SessionFocusArea { - // technical_accuracy — 기술 정확도 + // technical_accuracy — 직무 역량(개발 직군이면 기술 정확도). 컬럼명은 하위호환으로 유지. TECHNICAL, // logic_score — 논리력/구조 LOGIC, diff --git a/docs/glossary.md b/docs/glossary.md index e78c163f..f7430929 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -59,7 +59,7 @@ |--------|------|------| | 종합 피드백 | `session feedback` | 세션 종료 후 생성 | | 종합 점수 | `overall score` | 0~100 | -| 기술 정확도 | `technical accuracy` | | +| 직무 역량 | `technical accuracy` | 컬럼·필드명은 `technical_accuracy` 유지(하위호환). 표시는 직군 중립 — 영업·인사 지원자에게 '기술 정확도'는 맞지 않는다 | | 논리 점수 | `logic score` | | | 커뮤니케이션 점수 | `communication score` | | | 잘한 점 | `strengths summary` | | diff --git a/docs/messaging.md b/docs/messaging.md index 4712154a..7ba62402 100644 --- a/docs/messaging.md +++ b/docs/messaging.md @@ -510,7 +510,7 @@ { "messageId": 203, "modelAnswer": "이 질문에 강한 답변 예시…", "answerRewrite": "내 답변을 이렇게 고치면…", "coachingComment": "결론을 먼저 말하세요." } ], "panelBreakdown": [ - { "evaluator": "백엔드", "dimension": "기술 정확도·깊이", "score": 80.0, "detail": "...", "scoreRationale": "..." }, + { "evaluator": "백엔드", "dimension": "직무 역량·깊이", "score": 80.0, "detail": "...", "scoreRationale": "..." }, { "evaluator": "첫인상", "dimension": "자기소개 전달력·구성·직무적합성", "score": 78.0, "detail": "...", "scoreRationale": "..." } ], "reportS3Key": "feedback/99/report.md" diff --git a/frontend/src/features/feedback/ui/FeedbackReport.tsx b/frontend/src/features/feedback/ui/FeedbackReport.tsx index 65fa2b18..4856bdf8 100644 --- a/frontend/src/features/feedback/ui/FeedbackReport.tsx +++ b/frontend/src/features/feedback/ui/FeedbackReport.tsx @@ -115,7 +115,7 @@ export function FeedbackReport({
- +
diff --git a/frontend/src/features/history/ui/StatsSummary.tsx b/frontend/src/features/history/ui/StatsSummary.tsx index 3184f42c..20ac1ef2 100644 --- a/frontend/src/features/history/ui/StatsSummary.tsx +++ b/frontend/src/features/history/ui/StatsSummary.tsx @@ -15,7 +15,7 @@ export function StatsSummary({ stats }: { stats: UserStats }) {
평균 점수 - +
diff --git a/frontend/src/features/interview/lib/focusArea.test.ts b/frontend/src/features/interview/lib/focusArea.test.ts index 15dd3e10..eda089c4 100644 --- a/frontend/src/features/interview/lib/focusArea.test.ts +++ b/frontend/src/features/interview/lib/focusArea.test.ts @@ -3,7 +3,7 @@ import { focusAreaLabel } from './focusArea' describe('focusAreaLabel', () => { it('알려진 축은 한국어 표시명으로 바꾼다', () => { - expect(focusAreaLabel('TECHNICAL')).toBe('기술 정확도') + expect(focusAreaLabel('TECHNICAL')).toBe('직무 역량') expect(focusAreaLabel('LOGIC')).toBe('논리력') expect(focusAreaLabel('COMMUNICATION')).toBe('전달력') }) diff --git a/frontend/src/features/interview/lib/focusArea.ts b/frontend/src/features/interview/lib/focusArea.ts index 5d2515de..50e9a107 100644 --- a/frontend/src/features/interview/lib/focusArea.ts +++ b/frontend/src/features/interview/lib/focusArea.ts @@ -1,6 +1,6 @@ // 세션의 집중 영역(SessionFocusArea) 표시명. 서버가 축을 늘려도 화면이 깨지지 않게 원본을 폴백한다. const LABEL: Record = { - TECHNICAL: '기술 정확도', + TECHNICAL: '직무 역량', LOGIC: '논리력', COMMUNICATION: '전달력', } diff --git a/frontend/src/widgets/home-features/ui/FeatureVisuals.tsx b/frontend/src/widgets/home-features/ui/FeatureVisuals.tsx index 37b0abb1..1e1a86bd 100644 --- a/frontend/src/widgets/home-features/ui/FeatureVisuals.tsx +++ b/frontend/src/widgets/home-features/ui/FeatureVisuals.tsx @@ -73,7 +73,7 @@ export function ScoringVisual() { return (
- 기술 정확도·깊이 + 직무 역량·깊이 79