[FE-36] 추천도 표시 컴포넌트

작업 내용 (설계 의도)

근거: 지원 관리 확장 FE 웹 설계 S-24 지원 추천도·상태 표기 규칙·컴포넌트 트리·Query 규약·접근성·Testing Plan > 반드시 커버할 실패·엣지 경로(27·28·29), 지원 관리 확장 TDD 2단계 — 지원 추천도 API 계약(GET /api/job-postings/{id}/recommendation).

변경 사항

  • 추천도 표시 컴포넌트 4종을 components/recommendation/에 신설합니다 — RecommendationGradeChip(등급 칩), RecommendationScoreCard(점수·게이지), RecommendationAxisList(축별 근거), RecommendationCapNotice(59점 상한 안내).
  • 4종 모두 props만 받는 순수 프레젠테이션입니다. 데이터 조회는 소비 화면(보관함 카드·공고 상세 섹션)이 하고, 컴포넌트는 서버 상태를 스스로 가져오지 않습니다. 같은 컴포넌트를 목록과 상세 두 문맥에서 쓰기 위한 전제입니다.
  • 조회 훅 useRecommendation과 API 함수는 이 티켓이 소유합니다. staleTime: 5 * 60_000 을 적용합니다 — 재평가는 사용자 명시 액션(프로필 확정·일괄 재평가)으로만 발생하므로 30초 기본값으로 재조회할 이유가 없습니다.
  • 등급 4종을 기존 Chipfill × tone 조합으로만 표현합니다. 신규 색 토큰 0건, --accent 원색 미사용입니다 — STRONGLY_RECOMMENDED는 filled positive, RECOMMENDED는 outlined positive, REVIEW는 filled neutral, NOT_RECOMMENDED는 none(text-tertiary)입니다. 점수를 크게 쓰되 색으로 겁주지 않는 것이 원칙이고, accent는 화면당 1곳(주 CTA)에만 씁니다.
  • judgeable === false인 축은 점수·게이지를 렌더하지 않습니다. 대신 행 전체를 surface-muted + text-tertiary로 낮추고 “판단 불가”와 정규화 사실을 문장으로 설명합니다(“공고에 근무 조건 정보가 없어 나머지 축으로 나눠 계산했어요”). 이 설명이 없으면 가중치 합이 안 맞는 점수가 마술처럼 보입니다.
  • configuredWeight → normalizedWeight를 나란히 노출합니다. 사용자가 45%로 설정한 축이 51%로 계산된 사실을 숨기지 않아야 점수를 신뢰할 수 있습니다.
  • capReleased === true여도 capReason을 계속 노출합니다(FR-97 명시 요구). 표현은 중립 톤(“상한 해제됨 · 원래 사유: …”)으로 낮추되 사유 자체를 지우지 않습니다. capApplied && !capReleased일 때만 warning 톤 배너입니다.
  • notEvaluableReason 2종은 원인이 달라 문구와 액션이 다릅니다JD_UNAVAILABLE은 사용자가 할 일이 없으므로 안내만 하고, PROFILE_NOT_CONFIRMED[프로필 만들기] CTA를 붙여 S-23으로 보냅니다. 둘을 같은 “평가 불가”로 합치면 해결 가능한 상황에서 사용자가 막힙니다.
  • 축 충족 3종(FULL/PARTIAL/NONE)은 형태(채움/테두리/없음)로 구분하되 텍스트 라벨(“충족”/“일부”/“없음”)을 항상 병기합니다. 색·형태만으로 정보를 전달하지 않습니다.
  • 게이지는 role="meter" + aria-valuenow/aria-valuemin/aria-valuemax/aria-label을 갖고 숫자 텍스트를 병기합니다. 트랙은 --border, 채움은 --positive 기존 토큰입니다.
  • 라벨·비율 계산은 utils/recommendation.ts(FE-30 소유)에 위임합니다 — 컴포넌트는 렌더만 합니다.
  • 공고 상세·보관함 카드에 이 컴포넌트를 배선하는 작업은 wave 3의 FE-38이 수행합니다. 이 티켓은 컴포넌트와 조회 훅까지만 소유해 같은 wave의 파일 충돌을 피합니다.

의존

  • FE-30 — RecommendationResponse·RecommendationAxis·RecommendationGrade·RecommendationAxisKey 타입, 추천도 queryKey, utils/recommendation.ts, 분기 픽스처(judgeable false·cap 2종·notEvaluable 2종).
  • BE-63 — 추천도 평가·조회 API 구현.
  • BE-65 — 재평가·기준 버전 계약(등급·상한 값의 산출 근거).

다이어그램

처리 흐름

sequenceDiagram
    participant Page as 소비 화면
    participant Hook as useRecommendation
    participant Api as GET recommendation
    participant Card as RecommendationScoreCard
    participant Axis as RecommendationAxisList
    Page->>Hook: jobPostingId로 조회(staleTime 5분)
    Hook->>Api: GET
    Api-->>Page: RecommendationResponse
    Page->>Card: 점수·등급·상한 props
    Page->>Axis: axes props

컴포넌트 의존

flowchart LR
    Hook[useRecommendation] --> Api[fetchRecommendation]
    Api --> Types[RecommendationResponse]
    Card[RecommendationScoreCard] --> Grade[RecommendationGradeChip]
    Card --> Meter[role=meter 게이지]
    Card --> Cap[RecommendationCapNotice]
    Axis[RecommendationAxisList] --> Util[utils/recommendation]
    Grade --> Chip[기존 Chip fill x tone]
    Card --> Util

테스트 케이스

  • totalScore: 92·grade: 'STRONGLY_RECOMMENDED'이면 점수 숫자와 “강력 추천” 칩이 함께 보인다.
  • 등급 4종이 각각 filled positive·outlined positive·filled neutral·none 형태로 렌더되고 --accent 원색 클래스를 사용하지 않는다.
  • judgeable: false인 축은 게이지가 렌더되지 않고 “판단 불가”와 정규화 사실 문장이 보인다.
  • judgeable: true인 축은 configuredWeightnormalizedWeight가 나란히 보이고 충족률 게이지가 렌더된다.
  • 축 항목의 FULL·PARTIAL·NONE이 각각 “충족”·“일부”·“없음” 텍스트 라벨과 함께 보이고 PARTIALreason이 함께 보인다.
  • capApplied: true·capReleased: false이면 warning 톤 상한 배너와 capReason이 보인다.
  • capReleased: true이면 “상한 해제됨”과 원래 상한 사유가 함께 중립 톤으로 보인다.
  • notEvaluableReason: 'JD_UNAVAILABLE'이면 안내 문구만 보이고 [프로필 만들기] CTA가 보이지 않는다.
  • notEvaluableReason: 'PROFILE_NOT_CONFIRMED'이면 다른 문구와 함께 [프로필 만들기] CTA가 보인다.
  • totalScore: null이면 점수 숫자와 게이지가 렌더되지 않고 0으로 대체되지 않는다.
  • 게이지가 role="meter"aria-valuenow·aria-valuemin·aria-valuemax를 가지고 숫자 텍스트를 병기한다.
  • useRecommendationstaleTime이 5분으로 설정되어 재마운트 시 즉시 재조회가 발생하지 않는다.
  • 컴포넌트 4종이 조회 훅 없이 props만으로 렌더된다(서버 요청이 발생하지 않는다).
  • 컴포넌트 4종이 .dark 클래스 환경에서 렌더되고 하드코딩 색을 0건 사용한다.