[FE-46] 59점 상한 해제 컨트롤
작업 내용 (설계 의도)
근거: 지원 관리 확장 FE 웹 설계 S-24 지원 추천도 · 상태 표기 규칙(59점 상한 적용·해제됨) · 컴포넌트 트리 · Query 규약(낙관적 업데이트 2건 · 무효화 매핑) · API 연동 > 3단계 · Single Writer per File 검증 > 단계 3 wave 2, 지원 관리 확장 TDD 3단계 — 가중치 수정·상한 해제 (FR-97)의 POST·DELETE /api/job-postings/{jobPostingId}/recommendation/cap-release.
변경 사항
- 필수 기술 미충족으로 적용된 59점 상한을 사용자가 해제·재적용할 수 있는 컨트롤(
CapReleaseControl)을 만듭니다. 상한은 시스템 판단이지만 사용자가 “그래도 지원하겠다”고 결정할 수 있어야 하므로 토글 형태의 명시적 액션으로 둡니다. - 신규 파일만 추가합니다 —
components/recommendation/에 컨트롤 1개,api/recommendation/capRelease.ts,hooks/recommendation/useRecommendationCap.ts. FE-36이 만든 기존 4개 컴포넌트(RecommendationScoreCard·RecommendationAxisList·RecommendationCapNotice·RecommendationGradeChip)는 수정하지 않습니다 — 같은 wave의 파일 충돌을 피하기 위한 제약입니다. - 공고 상세 배선은 이 티켓이 하지 않습니다.
RecommendationSection에 컨트롤을 꽂는 작업은 wave 3의 FE-48이 수행합니다. 이 티켓은 props만 받는 독립 컴포넌트와 mutation 훅까지를 완결합니다. - 낙관적 업데이트를 적용합니다(설계가 허용한 2건 중 하나). 단일 토글이라 결과가 명확하고 서버 파생 필드가 화면에서 즉시 필요한 값이 아니므로,
onMutate에서 추천도 쿼리 캐시를 스냅샷 → 낙관적 반영 →onError에서 복원 + “상한 해제를 저장하지 못했어요” 토스트로 롤백합니다. capReleased === true여도capReason을 계속 노출합니다(FR-97 명시 요구). 표시는 중립 톤(“상한 해제됨 · 원래 사유: {capReason}“)입니다. 사유를 지우면 왜 상한이 걸렸는지 기록이 사라져 다음 판단이 불가능해집니다.- 상태 표기는 기존 토큰 매핑만 씁니다 — 상한 적용 중(
capApplied && !capReleased)은 filled warning, 해제됨은 outlined neutral + 원래 사유 병기. 신규 색 토큰 0건입니다. - 해제·재적용 성공 시
['job-postings', id, 'recommendation']과['job-postings','cross-company']를 무효화해 보관함 카드의 등급·점수까지 일치시킵니다. 상세만 갱신하면 목록의 점수가 옛 값으로 남습니다. capApplied === false(상한이 적용되지 않은 공고)에서는 컨트롤을 렌더하지 않습니다. 할 일이 없는 액션을 노출하면 화면 위계가 흐려집니다.- 롤백: 해제는
DELETE로 즉시 재적용되므로 파괴적이지 않습니다. 요청 실패 시 낙관적 캐시가 원래 값으로 복원돼 화면과 서버가 어긋나지 않습니다.
의존
- FE-40 — 상한 해제 API의 응답(
RecommendationResponse) 소비와 상한 해제·재적용 MSW 목을 사용합니다. - FE-36 (단계 2) — 추천도 표시 컴포넌트와 같은 디렉토리를 쓰지만 기존 파일을 수정하지 않고 신규 파일만 추가합니다.
- BE-72 —
POST·DELETE .../recommendation/cap-release구현. - 라우트 신설이 없어 FE-41에 의존하지 않습니다. 화면 배선은 FE-48이 이어받습니다.
다이어그램
처리 흐름
sequenceDiagram participant User as 사용자 participant Control as CapReleaseControl participant Hook as useRecommendationCap participant Cache as Query 캐시 participant API as cap-release API User->>Control: 상한 해제 Control->>Hook: 해제 요청 Hook->>Cache: 스냅샷 후 낙관적 반영 Hook->>API: POST cap-release API-->>Hook: 200 재계산 결과 또는 실패 Hook->>Cache: 실패 시 스냅샷 복원 Hook-->>User: 실패 토스트 또는 갱신된 점수
컴포넌트 의존
flowchart LR Section[RecommendationSection · FE-48 배선] --> Control[CapReleaseControl] Control --> Hook[useRecommendationCap] Hook --> Api[releaseCap · restoreCap] Hook --> Keys[queryKeys recommendation] Control --> Notice[RecommendationCapNotice · 미수정] Control --> Util[utils/recommendation]
테스트 케이스
capApplied && !capReleased이면 상한 경고와[상한 해제]버튼이 보인다.[상한 해제]를 누르면 화면이 즉시 해제 상태로 바뀌고 해제 요청이 전송된다.- 해제 요청이 실패하면 화면이 상한 적용 상태로 롤백되고 “상한 해제를 저장하지 못했어요” 토스트가 보인다.
capReleased === true이면 “상한 해제됨”과 원래capReason이 함께 중립 톤으로 보인다.- 해제된 상태에서 재적용을 누르면 상한 적용 상태로 돌아가고 재적용 요청이 전송된다.
- 재적용 요청이 실패하면 해제 상태로 롤백되고 실패 토스트가 보인다.
capApplied === false이면 컨트롤이 렌더되지 않는다.- 해제 성공 후 추천도 쿼리와 교차 회사 목록 쿼리가 무효화된다.
- 요청 진행 중에는 토글이 비활성이라 중복 요청이 발생하지 않는다.
- 컨트롤을 다크 모드로 렌더하면 시맨틱 토큰 class만 사용하고 하드코딩 색이 0건이다.