FeaturesDashboard

Dashboard (/)

SPEC #006 §2-7 · #012 PR-A(셸) · #017(실 stats 연동) · #035(분포·추이 확장) · #126(송출 요약) 정합. handoff 10-surface §4 Page 1.

Overview

로그인 직후 첫 화면. 운영사가 시스템 핵심 지표를 스캔하는 허브. 계산 가능한 시스템 지표는 GET /api/v1/admin/stats(OPERATOR-only) 실데이터로 연동(SPEC #017, #035 에서 분포·추이 확장, #126 에서 본문 “최근 방송” 송출 요약 실데이터화). 도메인이 없는 지표(장애·활동)만 정직한 “준비 중”으로 둔다 — CS 티켓·계정·매장 분포는 #035, 송출은 #126 에서 실데이터화.

데이터 소스

  • generated react-query 훅 useStats(@linkmusic/api-client, BFF /api/backend/api/v1/admin/stats 경유) — 응답 타입은 generated AdminStatsResponse 단일 소스(수기 재정의 금지).
  • 갱신 정책(SPEC #017 §4): react-query on-load + 수동 refetch. 자동 폴링 없음(QueryProvider 기본 staleTime 30s). PageHeader 의 [지금 갱신] 버튼이 stats query 를 invalidate.

6 stat 카드 (전부 실데이터 — #035 에서 placeholder 제거)

카드지표출처
활성 본사hqsByStatus.active (delta = 총 totalHqs · 온보딩 N)실데이터
전체 매장totalStores실데이터
신규 본사 (7일)newHqs7d실데이터
진행 중 임퍼소네이션activeImpersonations실데이터
미납 본사hqsByStatus.unpaid (delta = 정지 hqsByStatus.suspended)실데이터
미해결 티켓tickets.open + tickets.inProgress (delta = 긴급 urgentOpen · 미배정 unassignedOpen)실데이터 (#035)

hqsByStatus.onboarding·hqsByStatus.suspended·totalHqs 는 위 카드의 delta(보조 라벨)로 노출. 미해결 티켓 카드는 urgentOpen > 0 이면 danger tone + 펄스 도트로 강조한다.

분포·추이 카드 (#035 — div 막대, 차트 라이브러리 없음)

전용 통계 페이지 /stats(→ Stats)는 동일 분포·추이를 stat 카드/본문 없이 상세히 펼친 별도 뷰다(공유 컴포넌트 재사용, 중복 회피). 대시보드는 종합 허브로 유지.

stat 카드 아래 분포 그리드(3-col) + 전폭 추이 카드. ops-dashboard-stats 시안 정합 — 각 카드는 StatDistCard 셸(헤더 “합계 N”)로 감싸고, 상태 분포는 StackBar(누적 가로 막대 + 범례), 라벨별 절대값 비교는 MiniBar(행별 가로 막대), 30일 추이는 SignupTrend(세로 막대 + 헤더 합계/일평균)로 렌더.

카드지표컴포넌트
매장 상태 분포storesByStatus.{active, suspended, inactive}StackBar
계정 분포 (활성)accounts.{operatorsActive, hqManagersActive, storeManagersActive}MiniBar
CS 티켓 지표tickets.{unassignedOpen, urgentOpen, inProgress}MiniBar
본사 유형hqsByType.{franchise, independent}MiniBar
본사 상태 분포hqsByStatus.{active, onboarding, unpaid, suspended}StackBar
최근 30일 본사 가입 추이hqSignups30d[]SignupTrend

데이터·계약 불변 — AdminStatsResponse 의 기존 필드만 사용한다(요금제 분포는 응답에 없어 미표시). StatDistCard/StackBar 는 시안 §ops-phase4-shared 의 atom 을 admin 로컬 컴포넌트로 포트한 것.

  • 30일 축 0 채움: BE 는 가입 있는 날만 반환 → FE buildTrendPoints 가 오늘(KST) 기준 최근 30일 축(recentKstDayKeys)을 만들어 빈 날 count=0 으로 채운다(매칭은 ISO date 문자열 비교, KST 기준일). 빈 날 막대는 surface-3 트랙으로 시각 구분(시안 §TrendBars).
  • 추이 빈 상태: 30일 전부 0 이면 “최근 30일 가입 추이 없음” 표기.
  • a11y: 막대 트랙은 aria-label/title(라벨·값), 추이는 role="img" + 막대별 날짜·건수 title.

본문 카드

최근 방송 (송출 요약 — SPEC #126, 실데이터)

기존 “방송 집계 준비 중” placeholder 를 송출 요약 실데이터로 교체했다. useGetAdminStats 응답이 200 일 때만 실데이터(badge “실시간”), 로딩·에러 시에는 준비 중 placeholder (dash-broadcast-empty, badge “준비 중”)를 유지한다(빈 상태 회귀 0).

요소지표testid
요약 문구최근 7일 송출 dispatch.last7dCount건 · 도달 reach%dash-broadcast-summary
status 미니 분포dispatch.{played, pending, scheduled, canceled} (MiniBar)dash-broadcast-bar
  • 도달률 = played / (played + pending) — 분모 0 이면 ”—“(reachPercent 헬퍼).
  • 송출 추이·도달률 상세는 전용 페이지 /stats(→ Stats)에 펼쳐진다.

v1 미연결 카드(의존 도메인 미도착)

아래 카드는 시안에는 있으나 의존 도메인이 아직 없어 정직한 빈 상태(“준비 중”)로 둔다. 하드코딩 더미는 모두 제거했고, 해당 도메인 도착 시 후속 SPEC 에서 실데이터로 연결한다.

카드빈 상태 문구미도착 의존 도메인
최근 장애”장애 집계 준비 중입니다.”매장 heartbeat·장애 알림 백본(전무)
최근 활동”활동 피드 준비 중입니다.”감사 로그 집계 피드(OperatorAuditLog 는 있으나 대시보드 피드 미연결)
Topbar 장애 배지incidentCount 기본 0(배지 숨김), 가짜 “동기화 12초 전” 펄스 제거장애 도메인

CS 티켓 본문: #035 에서 기존 “대기 CS 티켓” placeholder 를 교체했고, 티켓 자체는 분포 카드(tickets.*)로 실데이터화됐다.

Implementation

// apps/admin/src/app/(protected)/page.tsx  — server 컴포넌트(셸만)
// apps/admin/src/app/(protected)/dashboard-stats.tsx  — 'use client' + useStats
 
const DashboardStats = () => {
  const { data, isLoading, isError, refetch } = useStats();
  if (isError) return <StatsError onRetry={() => void refetch()} />;
  if (isLoading || !data) return <StatGridSkeleton />;
  return (
    <>
      <StatGrid stats={data.data} />        {/* 6 실데이터 카드 */}
      <DistributionGrid stats={data.data} /> {/* #035 분포·추이 미니바 */}
      {/* + 준비 중 빈 상태 섹션들(장애·방송·활동) */}
    </>
  );
};

페이지는 server 컴포넌트로 유지하고, react-query 가 필요한 stat 그리드 + 본문 영역만 client 하위 컴포넌트(DashboardStats)로 분리한다. 분포·추이 막대는 components/dashboard/stack-bar.tsx· mini-bar.tsx·signup-trend.tsx(차트 라이브러리 없이 div 막대), 분포 카드 셸은 stat-dist-card.tsx, 30일 축 정규화는 lib/format.ts recentKstDayKeys.

States & Edge Cases

상태처리
첫 로그인 (passwordMustChange)(protected)/layout.tsx 가 server-side 로 /onboarding/change-password redirect — 대시보드 도달 X
로딩 (isLoading)6 카드 자리 스켈레톤(stats-loading, 레이아웃 시프트 회피)
에러 (isError — BFF 5xx·네트워크)“지표를 불러오지 못했습니다” + 다시 시도. 셸은 유지, 본 영역만 에러
본사/매장 0건 (초기)stat 카드 0 표시(정상 시나리오)
30일 가입 0건 (#035)추이 카드 “최근 30일 가입 추이 없음” 빈 상태

Constraints

  • OPERATOR 전용 endpoint (BE 가드). ops 셸((protected))은 OPERATOR 만 통과.
  • 실 stats 는 계산 가능한 지표만 — 장애·방송은 도메인 미존재라 placeholder ([[memory: feedback_design_only_from_handoff]]).
  • 차트 라이브러리 도입 안 함 — 분포·추이는 div/CSS 막대(ops-dashboard-stats 시안 정합, 실 구현도 동일 제약).

References

  • SPEC #006 §2-7 · #012 PR-A · #017 · #035 · #126
  • handoff 10-surface-ops-backoffice.md §4 Page 1
  • linkmusic-frontend-space/apps/admin/src/app/(protected)/page.tsx
  • linkmusic-frontend-space/apps/admin/src/app/(protected)/dashboard-stats.tsx
  • linkmusic-frontend-space/apps/admin/src/components/dashboard/mini-bar.tsx · signup-trend.tsx
  • linkmusic-frontend-space/apps/admin/src/lib/format.ts (recentKstDayKeys·kstDayKey)