Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions ai/src/ai_server/chain/feedback_generation_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)

Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion ai/src/ai_server/chain/feedback_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)} |")

Expand Down
2 changes: 1 addition & 1 deletion ai/src/ai_server/chain/prompts/feedback_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion ai/src/ai_server/chain/question_generation_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def _format_self_introduction(self_introduction: str | None) -> str:
# 평가 축 → 질문 설계 지침. 라벨만 넘기면 LLM 이 제각각 해석하므로 관점을 명시한다.
_FOCUS_GUIDE = {
"TECHNICAL": (
"기술 정확도 — 개념을 정확히 아는지 파고드는 질문. 사용한 기술의 동작 원리·트레이드오프·"
"직무 역량 — 개념·원리를 정확히 아는지 파고드는 질문. 개발 직군이면 사용한 기술의 동작 원리·트레이드오프·"
"대안과의 비교를 묻고, 뭉뚱그린 답으로는 통과할 수 없게 구체적인 지점을 짚으세요."
),
"LOGIC": (
Expand Down
58 changes: 48 additions & 10 deletions ai/tests/test_feedback_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async def ainvoke(self, _v):


# 평가축(dimension_name) 으로 라우팅하는 가짜 체인.
TECH = "기술 정확도·깊이"
TECH = "직무 역량·깊이"
PERSONALITY = "인성·협업 역량"
LOGIC = "논리·인과관계 명확성"
COMM = "명료성·구조화·전달력"
Expand Down Expand Up @@ -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]


Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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

Expand All @@ -373,8 +375,8 @@ async def test_all_domain_evaluators_failing_still_returns_logic_and_comm():
"커뮤니케이션·전달력 평가위원": EvaluatorResult(score=70),
},
fail_personas={
"백엔드 직군 시니어 기술 면접관",
"프론트엔드 직군 시니어 기술 면접관",
"백엔드 직군 시니어 실무 면접관",
"프론트엔드 직군 시니어 실무 면접관",
},
)
gen = PanelFeedbackGenerator(chain)
Expand All @@ -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
6 changes: 3 additions & 3 deletions ai/tests/test_feedback_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _result(**overrides) -> FeedbackResult:
panel_breakdown=[
PanelBreakdownItem(
evaluator="기술",
dimension="기술 정확도",
dimension="직무 역량",
score=82.0,
strength="개념 정확",
weakness="사례 부족",
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

// 약점 집중 재도전에서 겨냥할 평가 축. session_feedbacks 의 점수 축과 1:1.
public enum SessionFocusArea {
// technical_accuracy — 기술 정확도
// technical_accuracy — 직무 역량(개발 직군이면 기술 정확도). 컬럼명은 하위호환으로 유지.
TECHNICAL,
// logic_score — 논리력/구조
LOGIC,
Expand Down
2 changes: 1 addition & 1 deletion docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
|--------|------|------|
| 종합 피드백 | `session feedback` | 세션 종료 후 생성 |
| 종합 점수 | `overall score` | 0~100 |
| 기술 정확도 | `technical accuracy` | |
| 직무 역량 | `technical accuracy` | 컬럼·필드명은 `technical_accuracy` 유지(하위호환). 표시는 직군 중립 — 영업·인사 지원자에게 '기술 정확도'는 맞지 않는다 |
| 논리 점수 | `logic score` | |
| 커뮤니케이션 점수 | `communication score` | |
| 잘한 점 | `strengths summary` | |
Expand Down
2 changes: 1 addition & 1 deletion docs/messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/features/feedback/ui/FeedbackReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function FeedbackReport({
</div>

<div className="flex flex-1 flex-col gap-4 sm:pb-2">
<ScoreBar label="기술 정확도" score={feedback.technicalAccuracy} />
<ScoreBar label="직무 역량" score={feedback.technicalAccuracy} />
<ScoreBar label="논리력" score={feedback.logicScore} />
<ScoreBar label="전달력" score={feedback.communicationScore} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/features/history/ui/StatsSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function StatsSummary({ stats }: { stats: UserStats }) {
<div className="flex flex-col gap-3 border-t border-border pt-4">
<Eyebrow>평균 점수</Eyebrow>
<ScoreBar label="종합" score={a.overall} />
<ScoreBar label="기술 정확도" score={a.technical} />
<ScoreBar label="직무 역량" score={a.technical} />
<ScoreBar label="논리력" score={a.logic} />
<ScoreBar label="전달력" score={a.communication} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/features/interview/lib/focusArea.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('전달력')
})
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/features/interview/lib/focusArea.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// 세션의 집중 영역(SessionFocusArea) 표시명. 서버가 축을 늘려도 화면이 깨지지 않게 원본을 폴백한다.
const LABEL: Record<string, string> = {
TECHNICAL: '기술 정확도',
TECHNICAL: '직무 역량',
LOGIC: '논리력',
COMMUNICATION: '전달력',
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/widgets/home-features/ui/FeatureVisuals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function ScoringVisual() {
return (
<Card>
<div className="flex items-baseline justify-between gap-3">
<span className="text-[14px] font-semibold text-fg-strong">기술 정확도·깊이</span>
<span className="text-[14px] font-semibold text-fg-strong">직무 역량·깊이</span>
<span className="text-h5 font-bold text-fg">79</span>
</div>

Expand Down
Loading