[BE-80] 매칭 근거 API 노출 — 공고 상세 근거 배열 · 목록 matchScore (FR-86 / C-2)
작업 내용 (설계 의도)
근거 TDD: 20260808-지원관리-확장-tdd.md — “3단계 — 매칭 근거 노출 (C-2)“
변경 사항
BE-69가 job_posting_match_field_evidences에 필드별 근거를 저장하지만 API 응답 필드가 없어 FR-86의 “어느 필드에서 일치했는지 근거를 저장·표시한다” 중 표시가 성립하지 않습니다. senior-fe가 FE-47을 이 계약 확정 전까지 착수 금지로 묶어 둔 상태입니다.
- 공고 상세 응답 확장 —
matchFieldEvidences[]+matchScore+matchThreshold를 추가합니다.matchThreshold(판정 당시 임계치)를 함께 내리는 이유는 판정 재현성입니다. 임계치 상수가 나중에 바뀌어도 과거 판정을 “그때 기준으로는 통과였다”고 설명할 수 있습니다.jobKeywordGroupName을 함께 내려 FE가 그룹 id를 라벨로 바꾸는 조회를 하지 않게 합니다.
- 교차 목록은
matchScore·matched만 채웁니다 — 근거 배열은 상세에서만 노출합니다. 목록 20건 × 그룹 × 필드만큼 응답이 커지는 것을 막습니다. 2-1. 미매칭 공고에서도 근거 배열이 채워집니다 (A-2) — BE-69가match_score > 0이면 전부 저장하므로, 상세 화면이 “제목 0 + 태그 35 = 50 미달”을 설명할 수 있습니다. 배열이 비는 경우는 어느 필드에서도 걸리지 않은 공고(matchScore: 0) 뿐입니다. 2-2.matched·excludedByKeyword를 함께 노출합니다 —matchScore >= matchThreshold비교로는 매칭 여부를 판정할 수 없습니다(제외 키워드가 매칭을 이깁니다, FR-23).matched가 SSOT이고,excludedByKeyword: { keywordId, keyword } | null로 미매칭 사유가 “제외 키워드”인지 “점수 미달”인지 구분합니다. - 단계별 점진 노출 계약 준수 — BE-52(1단계)가
matchScore: null로, 이 티켓이 실제 값으로 채웁니다. 3단계 미배포 상태에서matchFieldEvidences는 빈 배열입니다. - N+1 금지 — 상세는 단건이라 조회 1회, 목록은
jobPostingId집합으로 배치 조회 1회입니다. - 크로스 컨텍스트 조합은 application 레이어 — matching이 소유한 근거를 posting 응답에 얹는 것이므로
application/posting매퍼가MatchResultQueryDomainService에서 받아 조립합니다. posting 도메인이 matching 타입을 참조하지 않습니다.
파일 소유: application/posting/{JobPostingDetailResponse, JobPostingResponseMapper, CrossCompanyPostingResponseMapper}.kt를 수정합니다. 같은 wave(BE-74·75·76)는 각각 application/matching·application/contact·application/notification만 건드리므로 파일이 겹치지 않습니다.
롤백: matching.field-weighted-scope 플래그 OFF면 matchScore가 갱신되지 않고 근거도 쌓이지 않아 빈 배열이 내려갑니다(FE 하위 호환).
의존
- BE-69 (필드별 가중치 매칭 · 근거 저장)
다이어그램
처리 흐름
sequenceDiagram participant FE as web(SPA) participant C as JobPostingApiController participant U as GetJobPostingUseCase participant P as JobPostingQueryService participant M as MatchResultQueryDomainService participant Map as JobPostingResponseMapper FE->>C: GET /api/companies/{cid}/job-postings/{id} C->>U: execute(command) U->>P: getBy(jobPostingId) U->>M: findEvidencesBy(jobPostingId) M-->>U: 필드별 근거 + matchScore + matchThreshold U->>Map: 조립 (근거 배열 + 점수) Map-->>C: JobPostingDetailResponse C-->>FE: 200
클래스 의존
flowchart LR subgraph Application["application/posting"] UC[GetJobPostingUseCase] Detail[JobPostingDetailResponse] Mapper[JobPostingResponseMapper] ListMapper[CrossCompanyPostingResponseMapper] end subgraph Domain["domain"] PQ[JobPostingQueryService] MQ[MatchResultQueryDomainService] Evidence[MatchFieldEvidence] end UC --> Mapper UC --> PQ UC --> MQ Mapper --> Detail ListMapper --> MQ MQ --> Evidence
테스트 케이스
- 제목에서 매칭된 공고 상세에
matchField=TITLE,weightPoints=60근거가 노출된다 - 태그 + 본문으로 매칭된 공고에 근거 2건(
SOURCE_TAG35 /DESCRIPTION_BODY20)이 노출된다 - 본문 근거의
occurrenceCount가 유효 출현 횟수(2 이상)로 노출된다 - 근거의
snippet이 200자를 넘지 않는다 (경계값) jobKeywordGroupName이 그룹 표시명으로 채워진다matchScore와matchThreshold가 판정 당시 값으로 함께 노출된다- 미매칭 공고(점수 35 < 임계 50)의 상세에 근거 배열이 채워진다 (A-2)
matchScore: 0인 공고는matchFieldEvidences: []다 (근거 없음)- 평가되지 않은 공고는
matchScore: null,matchFieldEvidences: []다 (미배포 —0과 구분됨) - 제외 키워드로 미매칭된 공고는
matched: false이고excludedByKeyword가 채워진다 - 점수 미달로 미매칭된 공고는
matched: false이고excludedByKeyword: null이다 - 매칭 성립 공고는
matched: true이고excludedByKeyword: null이다 - 교차 목록 응답에
matchScore가 채워지고 근거 배열은 포함되지 않는다 - 교차 목록 20건에 대해 매칭 근거 조회가 1회만 발생한다 (N+1 방지)
matching.field-weighted-scope플래그 OFF면 근거가 빈 배열이다 (하위 호환)- 기존 공고 상세 응답 필드가 변경되지 않는다 (회귀 — 추가만)