[FE-43] 연락 검토 상세 · 반영 결정
작업 내용 (설계 의도)
근거: 지원 관리 확장 FE 웹 설계 S-27 연락 검토 상세·반영 — 3단계 (가장 복잡한 화면) · 상태 표기 규칙 · 컴포넌트 트리 · Query 규약(무효화 매핑) · API 연동 > 3단계 · 접근성 · Testing Plan > 반드시 커버할 실패·엣지 경로 #35~#42, 지원 관리 확장 TDD 3단계 — 연락 검토 화면 (FR-90)의 GET /api/contact-events/{contactEventId} · POST /api/contact-events/{contactEventId}/decisions · ContactEventDetailResponse.
변경 사항
- 3단계에서 가장 복잡한 화면입니다. 원문 확인 → 시스템 제안 확인 → 사용자 결정의 3단 세로 흐름으로 배치하고, 마지막에 단일 주 CTA
반영하기를 두며 부차 액션무시는 텍스트 버튼으로 위계를 낮춥니다. 페이지는 컨테이너로서 훅만 호출하고, 원문·파싱·후보·결정폼·결과 5개 하위 블록은 props만 받습니다. - ④ 상태 선택지는 선택된 후보의
allowedNextStatuses로 제한합니다. 후보를 바꾸면 선택지를 다시 계산하고, 이전 선택이 새 목록에 없으면 초기화합니다. FE가 전이 규칙을 자체 판단하지 않습니다 — 서버의canTransitTo결과가 유일한 진실입니다. terminal === true인 후보는 라디오를disabled처리하고 숨기지 않습니다. 왜 선택할 수 없는지 사유를 함께 보여야 사용자가 “후보가 없다”와 “있는데 종료됐다”를 구분합니다. 접근성은disabled+aria-disabled+ 사유를aria-describedby로 연결합니다.autoSelectedJobApplicationId === null(자동 선택 불가)이면 라디오를 전부 미선택으로 시작하고 수동 선택 안내를 중립 톤으로 띄웁니다. 선택 전에는반영하기가disabled입니다. 임의로 1번 후보를 선택해 두면 사용자가 확인 없이 잘못된 지원 건에 반영합니다.parse === null(파싱 실패)이면 ② 섹션만 warning-subtle 안내로 바꾸고 ③·④는 정상 노출합니다 — 파싱 실패가 반영을 막지 않습니다.- 후보 0건이면 ③ 섹션을 “연결할 지원 건을 찾지 못했어요” +
[지원 목록 열기]로 바꾸고반영하기를disabled,무시만 활성화합니다. 후보 전원이terminal이면 “이미 종료됐어요” 안내 +무시만 활성입니다. - 원문은 기본 6줄 미리보기 +
[전체 보기]로 펼칩니다. 최대 20000자를 처음부터 렌더하면 스크롤이 무너집니다. - 첨부는 메타데이터만 표시합니다 (B-8 확정).
recruitment_contact_event_attachments테이블은 파일명·MIME·크기 메타만 저장하고 바이너리를 보관하지 않으므로 다운로드 링크 자체가 존재하지 않습니다 — “다운로드를 제공하지 않는다”가 아니라 줄 대상이 없습니다. 파일명 + 유형 칩 +utils/fileSize로 포맷한 크기를 나열하고, 목록 하단에 “원본은 메일에서 확인해 주세요” 1줄을 둡니다. 이벤트당 최대 20건이라 접기·페이지네이션 없이 전량 렌더합니다(20건은 세로로 흐르는 목록이 스크롤을 무너뜨리지 않는 규모입니다). - 면접 회차는 번호를 편집시키지 않고 라벨만 편집하게 합니다 — 계약이
nextInterviewRoundNumber를 주지만 결정 요청의interview에는roundLabel만 있습니다. 기본 라벨은suggestedRoundLabel ?? "{nextInterviewRoundNumber}차 면접"으로 조립합니다(utils/contactEvent). - 결정 폼은 지역
useState이며 화면 이탈 시 보존하지 않습니다 — 계약에 초안 저장 API가 없어 보존하면 서버와 어긋난 상태가 남습니다. 낙관적 업데이트는 적용하지 않습니다(상태 전이·면접 생성이 서버 파생 결과라 화면이 거짓말을 하게 됩니다). - 409 3종을 각각 다르게 처리합니다 —
CONTACT_EVENT_ALREADY_DECIDED는 화면을 무효화해 결과 요약으로 전환하고 CTA를 제거,CONTACT_EVENT_TARGET_TERMINAL은 해당 후보 카드에 인라인 + 재선택 유도,TRANSITION_NOT_ALLOWED는 ④ 상태 선택 영역에 인라인.404 APPLICATION_NOT_FOUND는 토스트 + 후보 목록 무효화입니다. - 반영 성공 시 토스트만 띄우고 이 화면에 머물러 결과 요약을 보여 줍니다(이동 여부를 묻지 않습니다). 무효화 대상은
['contact-events']접두사 +['applications', id]+['dashboard']이며, 대시보드 검토 대기 배너(FE-48)가 같은 쿼리를 공유해 자동으로 갱신됩니다. decision !== null(이미 결정됨)로 진입하면 ③·④를 결과 요약으로 대체하고 CTA를 렌더하지 않습니다.ConfidenceChip은 wave 1의 FE-40이 소유하며 이 티켓은 import만 하고 수정하지 않습니다(같은 wave 의존 회피).
의존
- FE-40 —
ContactEventDetailResponse·ContactDecisionRequest타입,['contact-events', contactEventId]queryKey, 상세·결정 MSW 목(409 3종·후보 변형 fixture)을 사용합니다. - FE-41 —
/contact-events/:contactEventId라우트와 스텁 페이지를 이 티켓이 구현으로 대체합니다. - FE-40 —
ConfidenceChip을 import합니다(정의는 wave 1 소유, 이 티켓은 수정 금지). - BE-77 — 상세 조회·결정 API 구현. MSW로 선행 착수하고 통합 검증만 이후에 합니다.
다이어그램
처리 흐름
sequenceDiagram participant User as 사용자 participant Page as ContactEventDetailPage participant Form as ContactDecisionForm participant Mutation as useDecideContactEvent participant API as POST decisions Page-->>User: 원문·파싱·후보 3단 렌더 User->>Form: 후보 선택 Form-->>User: allowedNextStatuses 로 상태 선택지 재계산 User->>Form: 상태·면접·메모 입력 후 반영하기 Form->>Mutation: 결정 요청 Mutation->>API: decision payload API-->>Mutation: 200 상세 또는 409 3종 Mutation-->>Page: 결과 요약 전환 또는 인라인 에러
컴포넌트 의존
flowchart LR Page[ContactEventDetailPage] --> Hook[useContactEvent] Page --> Source[ContactSourceSection] Page --> Parse[ContactParseSection] Page --> Candidates[ContactCandidateSection] Candidates --> Card[ContactCandidateCard] Card --> Chip[ConfidenceChip · FE-40 소유] Page --> Form[ContactDecisionForm] Form --> Mutation[useDecideContactEvent] Page --> Result[ContactDecisionResult] Form --> Util[utils/contactEvent]
테스트 케이스
- 후보 2건 중 자동 선택된 후보가 라디오로 선택된 상태로 렌더되고, 상태 선택지가 그 후보의
allowedNextStatuses와 정확히 일치한다. - 상태를 고르고
반영하기를 누르면 결정 요청이 전송되고 성공 토스트와 결과 요약이 보이며 CTA가 사라진다. - 후보가 0건이면
반영하기가 비활성이고무시만 활성이다. autoSelectedJobApplicationId === null이면 라디오가 전부 미선택으로 시작하고 수동 선택 안내가 보이며반영하기가 비활성이다.- 모든 후보가
terminal: true이면 반영 불가 안내가 보이고무시만 활성이며, 각 후보 라디오에aria-disabled와 사유가 연결된다. - 후보를 다른 항목으로 바꾸면 상태 선택지가 새 후보의
allowedNextStatuses로 갱신되고 이전 선택이 초기화된다. parse === null이면 파싱 실패 안내가 보이되 후보 선택과 반영이 여전히 가능하다.- 결정 요청이
409 CONTACT_EVENT_ALREADY_DECIDED를 반환하면 결과 요약으로 전환되고 CTA가 사라진다. - 결정 요청이
409 TRANSITION_NOT_ALLOWED를 반환하면 상태 선택 영역에 인라인 에러가 보이고 화면이 결과 요약으로 바뀌지 않는다. - 결정 요청이
409 CONTACT_EVENT_TARGET_TERMINAL을 반환하면 해당 후보 카드에 인라인 에러가 보이고 다른 후보를 다시 고를 수 있다. - 본문이 20000자면 기본 6줄 미리보기가 보이고
[전체 보기]를 누르면 전체가 펼쳐진다. - 첨부가 있으면 파일명·유형·크기 메타데이터만 보이고 다운로드 링크가 렌더되지 않으며 원본 확인 안내 문구가 보인다.
- 첨부가 20건이면 접기·더보기 없이 20건이 전량 렌더되고 다운로드 링크가 0건이다.
- 면접 등록을 체크하면 회차 라벨 기본값이
suggestedRoundLabel이고, 없으면 “{nextInterviewRoundNumber}차 면접”으로 채워진다. decision !== null인 연락으로 진입하면 후보·결정 영역 대신 결과 요약이 보이고 CTA가 렌더되지 않는다.- 상세 조회가 404면 “연락을 찾을 수 없어요”가 보이고
ErrorState재시도 흐름과 구분된다. - 상세 화면을 다크 모드로 렌더하면 시맨틱 토큰 class만 사용하고 하드코딩 색이 0건이다.