[FE-47] 매칭 근거 표시
계약 확정 + 범위 확대 확정 — 착수 가능합니다 (C-2 수용 + 게이트 ② 결정 ⓑ, 2026-08-09). 공고 상세 응답에
matchFieldEvidences[]·matchScore·matchThreshold·matched·excludedByKeyword가 추가됐고 BE-80이 신설됐습니다. 미매칭 공고에도 근거를 표시하는 ⓑ가 확정되어 초안의 게이트 대기가 해제됩니다 — 확대 비용은 우려했던 6.7배가 아니라 dba 실측 +32.6%(3년 51,600행 / 22MB) 이고, 본문 보유 공고가 13.8%(914/6,633) 뿐이라 구조적 상한이 있습니다. 범위 확대로 티켓 사이즈는 M → L입니다. 타입·유틸은 FE-40이 소유하므로 이 티켓은 import해 화면만 만듭니다.
작업 내용 (설계 의도)
근거: 지원 관리 확장 FE 웹 설계 S-04 확장 (3단계) — 매칭 근거(미매칭 와이어프레임 2종 · matched SSOT 표 · 미매칭 2분기 표 · matchScore 3-state 표) · API 연동 > 3단계 · 상태 표기 규칙(매칭 근거 발췌의 일치 구간) · 컴포넌트 트리 · 신규 순수 유틸(utils/matchEvidence.ts) · Release Scenario(matching.field-weighted-scope OFF 대응) · Testing Plan 49·50·50-a~50-d, 지원 관리 확장 TDD 매칭/미매칭의 구분 — matched가 SSOT입니다 · 3단계 — 매칭 근거 노출 (C-2) · 3단계 — 필드 가중 매칭(테이블 job_posting_match_field_evidences).
변경 사항
- 최상단 규칙 —
matched가 판정 SSOT입니다. 매칭 여부는 오직matched: boolean으로 판단하고,matchScore >= matchThreshold비교를 코드·테스트 어디에도 쓰지 않습니다. 제외 키워드가 매칭을 이기므로(FR-23) 점수 60인데matched: false인 공고가 실제로 존재합니다 — 점수로 판정하면 그 공고가 “매칭됨”으로 표시돼 사용자에게 거짓말을 합니다.matchScore·matchThreshold는 “얼마나 모자랐나”를 설명하는 수치일 뿐입니다. - 판정은
utils/matchEvidence.ts#resolveMatchOutcome(FE-40 소유)을 import만 해서 씁니다. 이 컴포넌트는 5가지 상태(NOT_DEPLOYED·NO_EVIDENCE·MATCHED·UNMATCHED_BELOW_THRESHOLD·UNMATCHED_EXCLUDED)를 받아 렌더만 하고, 판정 로직을 스스로 만들지 않습니다(no-logic-in-component). 유틸 파일은 수정하지 않습니다. - 공고 상세(S-04)의 기존 매칭 박스를 확장해 “이렇게 계산했어요” 근거 목록을 보여 줍니다. 판정 결과(매칭됨/아님)만 있으면 사용자가 왜 이 공고가 걸렸는지 알 수 없어 키워드 설정을 고칠 근거가 생기지 않습니다.
- 미매칭 공고도 근거를 표시합니다(ⓑ 확정). “왜 안 걸렸는지”를 설명할 수 있어야 사용자가 키워드를 고칠지 판단합니다. 미매칭은 사유에 따라 2분기로 렌더가 갈립니다.
| 조건 | 렌더 |
|---|---|
excludedByKeyword != null | ”제외 키워드 ‘{keyword}‘에 걸렸어요” + “점수와 무관하게 제외됩니다”. 점수 줄·근거 배열을 렌더하지 않습니다 |
excludedByKeyword === null | ”점수 {matchScore} / 임계 {matchThreshold}” + 근거 배열 + “N점이 더 필요해요”(matchThreshold - matchScore) |
- 제외 키워드 분기에서 점수와 근거를 감추는 이유는 점수가 몇 점이든 결과가 바뀌지 않기 때문입니다. “점수 60 / 임계 50”과 “매칭 안 됨”을 나란히 보여 주면 사용자가 화면을 오류로 읽습니다.
matchScore3-state로 분기합니다 —matchFieldEvidences === []만 보면 세 상태가 뭉개집니다.
| 값 | 의미 | 렌더 |
|---|---|---|
matchScore === null | 미배포 (3단계 배포 전·재평가 전) | 근거 섹션 자체를 렌더하지 않고 기존 matched 표시로 폴백 |
matchScore === 0 | 근거 없음 (어느 필드에도 안 걸림) | “일치한 항목이 없어요” — 미배포 폴백과 다른 문구 |
matchScore > 0 | 근거 있음 | 근거 배열 렌더 (매칭·미매칭 공통) |
- 미매칭 공고에서는 근거 배열에 없는 필드를
0으로 표기합니다(“제목 0 · 일치한 표현이 없어요”). 걸린 필드만 배열로 오므로, 빠진 필드를 그냥 감추면 “어디서 모자랐는지”가 화면에 남지 않습니다(설계 미매칭 와이어프레임 참조). - 확정 계약 필드명을 그대로 씁니다 —
matchField(TITLE·SOURCE_TAG·DESCRIPTION_BODY) ·jobKeywordGroupId·jobKeywordGroupName·weightPoints·occurrenceCount·snippet(최대 200자). 초안에서 역제안했던 이름(field·keywordGroupName·weight)은 채택되지 않았으므로 코드·테스트 어디에도 남기지 않습니다. matchScore와matchThreshold를 항상 함께 노출해 “왜 매칭됐는가”가 재현 가능하게 만듭니다(75 / 임계 50). 임계치가 나중에 바뀌어도 과거 판정을 설명할 수 있어야 하므로, 판정 당시 임계치를 화면이 그대로 보여 줍니다. 점수만 보여 주면 그 값이 통과인지 판단할 기준이 없고, 임계치를 상수로 박으면 변경 후 과거 판정이 거짓말이 됩니다.- 가중치는 제목 60 / 태그 35 / 본문 20이고 임계는 50입니다. 이 값은 서버가
weightPoints·matchThreshold로 내려 주므로 FE가 상수로 갖지 않습니다 — 표기 검증용 기준일 뿐입니다. jobKeywordGroupName이 함께 오므로 FE가 그룹 id를 라벨로 매핑하지 않습니다. 프런트에 id→이름 사전을 두면 그룹 이름이 바뀔 때마다 화면이 낡습니다.occurrenceCount는 유효 출현 횟수이며 본문 신호는 2회 이상이어야 인정됩니다. 그래서DESCRIPTION_BODY근거에만 횟수를 병기합니다 — 제목·태그에 “1회”를 붙이면 의미 없는 숫자가 화면을 채웁니다.- 근거는 필드 단위로 나열합니다 — 필드 라벨 ·
+weightPoints· (본문일 때) 횟수 ·jobKeywordGroupName· 발췌. 발췌 안의 일치 구간은<mark>로 감싸고--accent-subtle배경 +--accent-on-subtle텍스트를 씁니다(기존 토큰 매핑, 신규 색 토큰 0건). matchScore === null(미배포)일 때만 기존matched: boolean표시로 폴백하고 근거 섹션을 렌더하지 않습니다. 3단계 배포 전(matching.field-weighted-scopeOFF)과 재평가 이전 공고가 이 상태입니다. 0점으로 대체하면 “매칭 점수 0”이라는 거짓 정보가 됩니다.- 교차 목록(S-15)에는
matchScore·matched만 내려오므로 근거 목록은 공고 상세에서만 렌더합니다. - 이 컴포넌트는 props만 받는 순수 프레젠테이션입니다. 데이터 조회는 공고 상세 페이지가 하고, 공고 상세 배선은 wave 3의 FE-48이 수행합니다(같은 wave 파일 충돌 회피).
- 타입·판정 유틸은 FE-40이 소유합니다(
MatchFieldEvidenceResponse+ 상세 응답 확장 +utils/matchEvidence.ts). 이 티켓은 import만 하고types/api.ts·utils/matchEvidence.ts를 수정하지 않습니다. - 롤백: BE-80 배포가 늦어지면
matchScore: null이 내려와 근거 박스가 렌더되지 않고 기존matched표시가 그대로 동작하므로, 공고 상세는 정상입니다.
의존
- BE-80 — 매칭 근거 API 노출(
matchFieldEvidences[]·matchScore·matchThreshold·matched·excludedByKeyword, 미매칭 공고 포함). C-2 수용 + 게이트 ② ⓑ로 확정된 티켓이며 통합 검증의 선행 조건입니다. - BE-69 — 필드 가중치 매칭 근거 저장 구현(
job_posting_match_field_evidences). BE-80이 노출할 데이터의 원천입니다. - FE-40 —
MatchFieldEvidenceResponse·matched·excludedByKeyword타입,utils/matchEvidence.ts#resolveMatchOutcome, 5종 상태 MSW 목(매칭 / 미매칭-점수 미달 / 미매칭-제외 키워드 /matchScore: 0/matchScore: null+matchScore: 60·matched: false)을 소유합니다. 이 티켓은 import만 하고 수정하지 않습니다. - 화면 배선은 FE-48이 이어받습니다.
다이어그램
처리 흐름
sequenceDiagram participant User as 사용자 participant Page as JobPostingDetailPage participant Api as 공고 상세 API participant Box as MatchEvidenceBox participant Util as resolveMatchOutcome User->>Page: 공고 상세 열기 Page->>Api: GET 공고 상세 Api-->>Page: matched · matchScore · matchThreshold · excludedByKeyword · 근거 배열 Page->>Box: props 전달 Box->>Util: 5상태 판정 요청 Util-->>Box: NOT_DEPLOYED 또는 NO_EVIDENCE 또는 MATCHED 또는 미매칭 2종 Box-->>User: NOT_DEPLOYED 면 기존 matched 폴백 Box-->>User: UNMATCHED_EXCLUDED 면 제외 키워드 문구만 Box-->>User: 그 외에는 점수·임계 + 필드별 근거 목록
컴포넌트 의존
flowchart LR Detail[JobPostingDetailPage · FE-48 배선] --> Box[MatchEvidenceBox] Box --> Types[MatchFieldEvidenceResponse · FE-40 소유] Box --> Outcome[resolveMatchOutcome · FE-40 소유] Outcome --> Fallback[NOT_DEPLOYED · matched 폴백] Outcome --> NoEvidence[NO_EVIDENCE · 일치한 항목이 없어요] Outcome --> Excluded[UNMATCHED_EXCLUDED · 제외 키워드 문구만] Outcome --> Scored[MATCHED 또는 점수 미달] Scored --> Score[matchScore 슬래시 matchThreshold 동시 표기] Scored --> Short[부족 점수 · N점이 더 필요해요] Scored --> Row[근거 행 · matchField 라벨 · weightPoints] Row --> Count[occurrenceCount · DESCRIPTION_BODY 만 병기] Row --> Group[jobKeywordGroupName 그대로 표기] Row --> Zero[근거 없는 필드 0 표기] Box --> Mark[snippet 일치 구간 mark · accent-subtle]
테스트 케이스
matchScore=75·matchThreshold=50이면 “점수 75 / 임계 50”이 함께 보이고 매칭됨 표시가 같이 렌더된다.- 근거 3건(
TITLE60·SOURCE_TAG35·DESCRIPTION_BODY20)이 있으면 필드 라벨·가중치·발췌가 각각 보인다. DESCRIPTION_BODY근거에만occurrenceCount가 병기되고,TITLE·SOURCE_TAG근거에는 횟수가 보이지 않는다.- 각 근거에
jobKeywordGroupName이 그대로 표기되고, 화면이jobKeywordGroupId를 라벨로 변환하지 않는다. - 발췌 안의 일치 구간이
<mark>로 감싸져 강조되고 하드코딩 색 없이 accent 계열 토큰만 사용한다. matchScore === null(3단계 배포 전)이면 근거 목록과 점수 줄이 렌더되지 않고 기존matched: boolean표시로 폴백한다(설계 50).matchScore === 0이면 “일치한 항목이 없어요”가 보이고, 미배포 폴백과 다른 표시가 된다(설계 50-a).matched: false+excludedByKeyword != null이면 “제외 키워드 ‘인턴’에 걸렸어요”가 보이고 점수 줄·근거 배열이 렌더되지 않는다(설계 50-b).matched: false+excludedByKeyword === null이면 점수·임계치와 근거 배열, 부족 점수 “15점이 더 필요해요”가 함께 보인다(설계 50-c).matchScore: 60+matchThreshold: 50+matched: false이면 “매칭 안 됨”으로 표시된다 — 점수가 임계치를 넘어도matched를 따른다(설계 50-d).- 미매칭 공고에서 근거 배열에 없는 필드가
0으로 표기되고 “일치한 표현이 없어요”가 함께 보인다. - 컴포넌트가
matchScore와matchThreshold를 직접 비교하지 않고resolveMatchOutcome결과로만 분기한다. - 200자 발췌가 잘리지 않고 렌더되며 레이아웃이 무너지지 않는다.
- 근거 박스를 다크 모드로 렌더하면
<mark>하이라이트를 포함해 시맨틱 토큰 class만 사용하고 하드코딩 색이 0건이다.