[FE-26] 지원 대시보드 3요소

작업 내용 (설계 의도)

근거: 지원 관리 확장 FE 웹 설계 S-18 지원 대시보드 (앱 랜딩 /)(온보딩 empty 와이어프레임 포함) · 화면 목록방안 2(랜딩 전환, 사용자 확정) · 컴포넌트 트리의 대시보드 서브트리 · 신규 순수 유틸(utils/dashboard.ts) · API 연동 > 1단계의 S-18 행 · 접근성의 칸반 항목, 지원 관리 확장 TDD 1단계 — 대시보드 (FR-79).

변경 사항

  • “지금 어디까지 왔나”를 세로 스크롤 1회로 보여 주는 화면입니다. 상태별 칸반 · 다가오는 면접 · 오래 그대로인 지원 3요소를 단일 엔드포인트 1회 조회로 구성합니다(부분 실패 개념이 없습니다).
  • 이 화면의 라우트가 /dashboard에서 /(앱 랜딩)로 바뀝니다 (사용자 확정, 2026-08-08). 라우트 선언·탭 랜딩 전환은 FE-22가 소유하고, 이 티켓은 DashboardPage 본문만 채웁니다. /dashboard 별칭은 없습니다.
  • api/dashboard.tshooks/dashboard/useDashboard.ts를 신규로 둡니다.
  • utils/dashboard.ts를 신규로 둡니다 — 진행 중 4종(APPLIED·DOCUMENT_SCREENING·INTERVIEWING·OFFERED)의 count 합산과 daysUntil 표기(D-2·당일은 오늘)를 담습니다. 계약 값의 단순 합이므로 훅이 아니라 순수 유틸입니다(no-logic-in-component 대응).
  • 칸반은 가로 스크롤입니다. 컬럼 폭은 컴포넌트 지역 상수 280px로 두고 ≥md에서도 같은 값을 씁니다 — 2단 레이아웃을 만들지 않습니다. 반응형 재배치를 넣으면 컬럼 순서가 화면 폭에 따라 달라져 “왼쪽에서 오른쪽으로 진행”이라는 의미가 깨집니다.
  • 컬럼 순서는 계약의 statusBoard 배열 순서를 그대로 따릅니다. FE가 정렬하지 않습니다. 종료 4종은 count > 0일 때만 응답에 포함되므로 FE가 없는 컬럼을 만들지 않습니다.
  • 진행 중 4종은 계약상 항상 내려오므로 0건이어도 컬럼을 유지하고 “없어요” 1줄을 보여 줍니다 — 컬럼이 사라졌다 생기면 위치 기억이 깨집니다. 반대로 장기 미변경 0건이면 섹션 자체를 숨깁니다 — 좋은 상태를 빈 카드로 알릴 이유가 없습니다.
  • 랜딩 화면이므로 empty가 곧 온보딩입니다. 지원 0건이면 3섹션을 전부 숨기고 온보딩 카드 하나만 렌더합니다 — 빈 섹션 3개를 나열하면 잡음이고, 첫 사용자가 텅 빈 화면을 만나면 다음 행동을 알 수 없습니다.
    • 문구: “아직 지원한 곳이 없어요” + “관심 있는 공고를 보관함에 모아 두면, 지원한 뒤의 진행 상황이 여기에 쌓여요” (다음 행동과 그 이유를 함께 제시).
    • 단일 주 CTA [관심 공고 보관함에서 시작하기]/watchlist (accent 1곳 원칙).
    • 보조 텍스트 버튼 회사 등록하기 →/companies/new — 회사가 0곳이면 보관함도 비어 있어 이 경로가 필요하지만, 주 동선은 하나여야 하므로 위계를 한 단계 낮춥니다(1 thing per 1 page).
  • 지원이 1건이라도 생기면 온보딩 카드가 사라지고 3요소 대시보드로 자동 전환됩니다. dismiss 플래그·“온보딩 완료” 전역 상태를 두지 않습니다 — 전역 상태가 늘어나고, 지원을 전부 지운 사용자에게는 안내가 다시 필요합니다. 판정 근거는 응답 값 하나(진행 중·종료 전체 0건)입니다.
  • 카드의 allowedNextStatuses기존 TransitStatusSheet에 그대로 전달합니다. 전이 규칙을 FE가 다시 판단하지 않습니다 — 서버가 내려준 배열이 유일한 진실입니다.
  • upcomingInterviewDays·staleThresholdDays는 계약 기본값 14를 고정으로 씁니다. 사용자 조정 UI는 만들지 않습니다(요구사항에 없는 오버엔지니어링).
  • 지원 탭 랜딩이 /(이 화면)로 바뀌므로(FE-22), 대시보드 헤더 우측에 [전체 목록]/applications 링크를 둡니다. ApplicationListPage.tsx 본문은 이 티켓이 수정하지 않습니다 — 기존 S-06은 그대로 두고, 그 화면의 회사 목록 경로 상수 1줄(ApplicationListPage.tsx:14)만 FE-22가 /companies로 갱신합니다.
  • 에러는 화면 전체 ErrorState + [다시 시도]입니다.
  • 접근성: 각 칸반 컬럼은 <section aria-label="서류전형 2건">, 가로 스크롤 컨테이너는 tabIndex={0} + role="group"으로 키보드 스크롤을 허용합니다.

의존

  • FE-20 — DashboardResponse·DashboardApplicationCard 타입, ['dashboard'] queryKey, 대시보드 MSW 목.
  • FE-22 — /(앱 랜딩) 라우트와 DashboardPage 스텁, 지원 탭 랜딩 전환, 회사 목록 /companies 이동.
  • BE-51 — 대시보드 API. 통합 검증은 BE-51 완료 후 수행합니다.

다이어그램

처리 흐름

sequenceDiagram
    participant User as 사용자
    participant Page as DashboardPage
    participant Query as useDashboard
    participant Util as utils/dashboard
    participant Sheet as TransitStatusSheet
    Page->>Query: GET /api/dashboard
    Query-->>Page: statusBoard · 면접 · 장기 미변경
    Page->>Util: 진행 중 4종 count 합산
    Util-->>Page: 진행 중 N건
    User->>Page: 상태 변경 탭
    Page->>Sheet: allowedNextStatuses 그대로 전달

컴포넌트 의존

flowchart LR
    Page[DashboardPage] --> Hook[useDashboard]
    Hook --> Api[api/dashboard]
    Page --> Summary[DashboardSummary]
    Summary --> Util[utils/dashboard]
    Page --> Board[StatusBoardSection]
    Board --> Column[StatusBoardColumn]
    Column --> Card[DashboardApplicationCard]
    Card --> Sheet[TransitStatusSheet 기존]
    Page --> Interview[UpcomingInterviewSection]
    Page --> Stale[StaleApplicationSection]
    Page --> Onboard["OnboardingCard (지원 0건)"]
    Onboard --> Watchlist["/watchlist"]
    Onboard --> NewCompany["/companies/new"]

테스트 케이스

  • 대시보드 응답이 있으면 상단 요약의 진행 중 건수가 진행 중 4종 count의 합과 일치한다.
  • 칸반 컬럼이 응답의 statusBoard 배열 순서 그대로 렌더된다.
  • 진행 중 4종이 전부 0건이어도 컬럼 4개가 유지되고 각각 “없어요”가 보인다.
  • 종료 상태가 응답에 없으면 해당 컬럼이 렌더되지 않는다.
  • 장기 미변경이 0건이면 해당 섹션이 렌더되지 않는다.
  • 다가오는 면접이 0건이면 섹션은 유지되고 “예정된 면접이 없어요”가 보인다.
  • 지원이 0건이면 온보딩 카드 1개만 보이고 칸반·다가오는 면접·장기 미변경 3섹션이 전부 렌더되지 않는다.
  • 지원이 1건 이상이면 온보딩 카드가 렌더되지 않고 3요소 대시보드가 보인다.
  • 온보딩 카드의 주 CTA를 누르면 /watchlist로 이동한다.
  • 온보딩 카드의 보조 텍스트 버튼을 누르면 /companies/new로 이동한다.
  • 온보딩 노출 여부가 dismiss 플래그나 전역 스토어가 아니라 응답의 지원 건수만으로 결정된다.
  • 카드에서 상태 변경을 열면 시트의 선택지가 그 카드의 allowedNextStatuses 배열과 정확히 일치한다.
  • daysUntil이 0이면 “오늘”, 2면 “D-2”로 표기된다.
  • 칸반 컬럼이 가로 스크롤 컨테이너 안에 배치되고 화면 폭이 커져도 2단으로 재배치되지 않는다.
  • 조회 실패 시 화면 전체 ErrorState[다시 시도]가 보인다.
  • 로딩 중에는 요약·칸반·섹션 스켈레톤이 보이고 실제 카드가 렌더되지 않는다.
  • 헤더의 [전체 목록]/applications로 이동한다.
  • 각 칸반 컬럼에 상태 라벨과 건수를 담은 aria-label이 설정된다.
  • 대시보드를 다크 모드로 렌더하면 시맨틱 토큰 class만 사용하고 하드코딩 색이 0건이다.