[FE-42] 연락 검토 목록

작업 내용 (설계 의도)

근거: 지원 관리 확장 FE 웹 설계 S-26 연락 검토 목록 — 3단계 · 상태 표기 규칙 · 컴포넌트 트리 · Query 규약 · API 연동 > 3단계 · Single Writer per File 검증 > 단계 3 wave 2, 지원 관리 확장 TDD 3단계 — 연락 검토 화면 (FR-90)GET /api/contact-events · ContactEventListItem.

변경 사항

  • 인박스형 목록 화면(S-26)을 만듭니다. 미처리 항목이 위, 처리 완료가 아래로 접히는 토스 인박스 패턴이며, 각 행은 “누가·무엇을·언제” 한 줄 요약 + 신뢰도 신호 1개로 구성합니다.
  • 조회는 useInfiniteQuery(['contact-events', { reviewStatus, days }])로 하고 PageResponse.hasNext[더 보기]를 제어합니다. 페이지 조회 로직은 훅이 소유하고 페이지는 컨테이너로서 훅만 호출합니다.
  • 기본 세그먼트는 PENDING(검토 대기) 입니다 — 이 화면에 오는 이유가 그것이기 때문입니다. 세그먼트 상태는 useSearchParams(URL)로 두어 뒤로가기·링크 공유가 동작하게 합니다.
  • 세그먼트 라벨에 PageResponse.totalCount를 붙입니다(검토 대기 3). 건수를 클라이언트에서 배열 길이로 세지 않습니다 — 무한 쿼리는 로드된 페이지만 갖고 있어 전체 건수와 다릅니다.
  • topCandidateConfidence === null(후보 0건)은 [후보 없음] 중립 칩으로 표시해 LOW와 구분합니다. 전자는 계산 자체가 불가했던 상태이고 후자는 계산했으나 낮은 상태라, 같은 칩으로 묶으면 사용자가 “낮게 판단됐다”로 오해합니다.
  • 신뢰도 칩 ConfidenceChipwave 1의 FE-40이 소유합니다 — FE-42·FE-43이 같은 wave에서 둘 다 쓰므로 정의를 선행 wave로 올렸습니다. 이 티켓은 import만 하고 수정하지 않습니다. 다만 topCandidateConfidence === null(후보 0건)을 [후보 없음] 중립 칩으로 렌더해 LOW와 구분하는 목록 화면의 판단은 이 티켓이 소유합니다.
  • 빈 상태를 3종으로 구분합니다 — ① 검토 대기 0건은 “검토할 연락이 없어요 / 새 연락이 오면 디스코드로 알려드려요”로 긍정 상태라 CTA를 두지 않습니다, ② 반영됨·무시됨 0건은 “아직 없어요” 1줄, ③ 최근 30일 전체 0건은 Gmail 연동 안내 + [운영 열기]. 세 경우를 한 문구로 합치면 정상 상태가 문제처럼 보입니다.
  • 신뢰도 칩·상태 표기는 기존 토큰 매핑만 씁니다(높음 filled positive / 보통 outlined neutral / 낮음 filled warning / 후보 없음 중립). 신규 색 토큰 0건입니다.
  • 카드 탭 시 /contact-events/:contactEventId로 이동합니다. 라우트는 FE-41이 이미 선언했으므로 이 티켓은 링크만 겁니다.

의존

  • FE-40 — ContactEventListItem·ContactConfidenceLevel 타입, ['contact-events', …] queryKey, 목록 MSW 목(세그먼트·페이지 반영)을 사용합니다.
  • FE-41 — /contact-events 라우트와 스텁 페이지를 이 티켓이 구현으로 대체합니다.
  • BE-77 — GET /api/contact-events 구현. 계약이 SSOT이므로 MSW로 선행 착수하고 통합 검증만 BE-77 이후에 합니다.

다이어그램

처리 흐름

sequenceDiagram
    participant User as 사용자
    participant Page as ContactEventListPage
    participant Hook as useContactEvents
    participant API as GET /api/contact-events
    User->>Page: 연락 검토 열기 (기본 PENDING)
    Page->>Hook: reviewStatus·days 로 무한 조회
    Hook->>API: page=0
    API-->>Hook: PageResponse + totalCount
    Hook-->>Page: items · hasNext · totalCount
    Page-->>User: 세그먼트 건수 + 카드 목록
    User->>Page: 더 보기
    Page->>Hook: 다음 페이지 요청

컴포넌트 의존

flowchart LR
    Page[ContactEventListPage] --> Hook[useContactEvents]
    Hook --> Api[fetchContactEvents]
    Api --> Types[ContactEventListItem]
    Page --> Segment[Segment 기존 UI]
    Page --> Card[ContactEventCard]
    Card --> Chip[ConfidenceChip · FE-40 소유]
    Chip --> Util[utils/contactEvent]
    Page --> Empty[EmptyState 3종]
    Page --> Error[ErrorState]

테스트 케이스

  • 검토 대기 연락 3건이 있으면 카드 3장과 세그먼트 라벨 “검토 대기 3”이 보이고, 건수는 totalCount에서 온다.
  • 화면 진입 시 별도 조작 없이 PENDING 세그먼트가 선택돼 있고 검토 대기 목록이 조회된다.
  • 세그먼트를 반영됨으로 바꾸면 URL이 갱신되고 reviewStatus=APPLIED로 재조회되며, 뒤로가기로 PENDING으로 돌아온다.
  • topCandidateConfidence === null인 카드에는 [후보 없음] 중립 칩이 보이고 LOW(낮음) 칩과 다른 라벨로 표시된다.
  • 검토 대기 0건이면 “검토할 연락이 없어요” 긍정 빈 상태가 보이고 CTA 버튼이 렌더되지 않는다.
  • 반영됨 세그먼트가 0건이면 “아직 없어요” 1줄만 보이고 검토 대기용 긍정 문구는 보이지 않는다.
  • 최근 30일 전체가 0건이면 Gmail 연동 안내와 [운영 열기]가 보인다.
  • 목록 조회가 5xx로 실패하면 ErrorState[다시 시도]가 보이고, 재시도하면 다시 조회한다.
  • hasNext=true[더 보기]가 보이고 누르면 다음 페이지가 append되며, hasNext=false면 버튼이 렌더되지 않는다.
  • 카드를 누르면 /contact-events/{contactEventId}로 이동한다.
  • 로딩 중에는 카드 스켈레톤 3개가 보이고 빈 상태 문구는 보이지 않는다.
  • 목록 화면을 다크 모드로 렌더하면 시맨틱 토큰 class만 사용하고 하드코딩 색이 0건이다.