OPEN HYPER STEP
← 목록으로 (master-project)
MASTER-PROJECT · 16 / 50
master-project
CHAPTER 16 / 50
읽기 약 2
FUNCTION

대시보드: 메인 화면 + 통계 카드


핵심 개념

통계 카드 4개·차트·최근 활동 — Server Component로 데이터 페치 + Skeleton + 에러 처리.

본문

대시보드 페이지

TSX📋 코드 (27줄)
// src/app/(app)/dashboard/page.tsx
import { Suspense } from 'react'
import { StatsCards } from './stats-cards'
import { UsageChart } from './usage-chart'
import { RecentActivity } from './recent-activity'
import { CardSkeleton } from '@/components/ui/skeleton'

export default function DashboardPage() {
  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">대시보드</h1>

      <Suspense fallback={<CardSkeleton count={4} />}>
        <StatsCards />
      </Suspense>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        <Suspense fallback={<CardSkeleton />}>
          <UsageChart />
        </Suspense>
        <Suspense fallback={<CardSkeleton />}>
          <RecentActivity />
        </Suspense>
      </div>
    </div>
  )
}

통계 카드 (Server Component)

TSX📋 코드 (48줄)
// src/app/(app)/dashboard/stats-cards.tsx
import { createClient } from '@/lib/supabase/server'
import { Sparkles, FileText, Clock, TrendingUp } from 'lucide-react'

export async function StatsCards() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return null

  const { count: totalGen } = await supabase
    .from('generations')
    .select('*', { count: 'exact', head: true })
    .eq('user_id', user.id)

  const monthStart = new Date(Date.now() - 30 * 86400 * 1000).toISOString()
  const { count: monthGen } = await supabase
    .from('generations')
    .select('*', { count: 'exact', head: true })
    .eq('user_id', user.id)
    .gte('created_at', monthStart)

  const { data: usage } = await supabase
    .from('usages')
    .select('count')
    .eq('user_id', user.id)
    .single()

  const stats = [
    { label: '총 생성', value: totalGen ?? 0, icon: Sparkles, color: 'text-purple-500' },
    { label: '이번 달', value: monthGen ?? 0, icon: FileText, color: 'text-blue-500' },
    { label: '잔여 무료', value: 5 - (usage?.count ?? 0), icon: Clock, color: 'text-green-500' },
    { label: '플랜', value: 'Free', icon: TrendingUp, color: 'text-amber-500' },
  ]

  return (
    <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
      {stats.map((s, i) => (
        <div key={i} className="border rounded-lg p-4 bg-card">
          <div className="flex items-center justify-between mb-2">
            <span className="text-sm text-muted-foreground">{s.label}</span>
            <s.icon className={`h-4 w-4 ${s.color}`} />
          </div>
          <div className="text-2xl font-bold">{s.value}</div>
        </div>
      ))}
    </div>
  )
}

차트 (Recharts)

BASH📋 코드 (1줄)
pnpm add recharts
TSX📋 코드 (19줄)
// src/app/(app)/dashboard/usage-chart.tsx
'use client'
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'

export function UsageChart({ data }: { data: { date: string; count: number }[] }) {
  return (
    <div className="border rounded-lg p-6 bg-card">
      <h3 className="font-semibold mb-4">최근 30일 사용량</h3>
      <ResponsiveContainer width="100%" height={200}>
        <LineChart data={data}>
          <XAxis dataKey="date" tick={{ fontSize: 11 }} />
          <YAxis tick={{ fontSize: 11 }} />
          <Tooltip />
          <Line type="monotone" dataKey="count" stroke="hsl(var(--primary))" strokeWidth={2} />
        </LineChart>
      </ResponsiveContainer>
    </div>
  )
}

최근 활동

TSX📋 코드 (31줄)
// src/app/(app)/dashboard/recent-activity.tsx
import { createClient } from '@/lib/supabase/server'
import { formatDistanceToNow } from 'date-fns'
import { ko } from 'date-fns/locale'

export async function RecentActivity() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  const { data: gens } = await supabase
    .from('generations')
    .select('id, prompt, created_at')
    .eq('user_id', user!.id)
    .order('created_at', { ascending: false })
    .limit(5)

  return (
    <div className="border rounded-lg p-6 bg-card">
      <h3 className="font-semibold mb-4">최근 활동</h3>
      <ul className="space-y-3">
        {gens?.map(g => (
          <li key={g.id} className="text-sm flex justify-between">
            <span className="truncate flex-1">{g.prompt}</span>
            <span className="text-xs text-muted-foreground ml-2">
              {formatDistanceToNow(new Date(g.created_at), { locale: ko, addSuffix: true })}
            </span>
          </li>
        ))}
      </ul>
    </div>
  )
}

빈 상태 (Empty State)

TSX📋 코드 (11줄)
{!gens || gens.length === 0 ? (
  <div className="text-center py-8 text-muted-foreground">
    <Sparkles className="h-12 w-12 mx-auto mb-2 opacity-50" />
    <p>아직 생성한 콘텐츠가 없어요</p>
    <Link href="/generate" className="text-primary hover:underline text-sm">
      첫 카피 생성하기 →
    </Link>
  </div>
) : (
  /* 목록 */
)}

다음 챕터

CH.17 "CRUD UI: 목록/상세/생성/수정/삭제".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 마스터 프로젝트의 대시보드 구현 부분을 분석해서
실전 적용 + 개선 우선순위 3가지를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

대시보드 구현 관련 모범 사례·안티패턴 5개를
비교 분석해서 실전 적용를 위한 추천 방안을 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 프로젝트 전체에서 대시보드 구현
최적화 가능 위치와 리스크를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 1인 개발자 시장의
대시보드 구현 트렌드와 차별화 포인트를 정리해줘.

⭐ 이것만 기억하세요
대시보드: 메인 화면 + 통계 카드 이 3가지만 확실히 잡으세요
1.Server Component + Suspense = SEO + 빠른 첫 화면
2.통계 카드 4개 + 차트 + 최근 활동 = MVP 대시보드
3.다음 챕터에서 CRUD UI


공유하기
진행도 16 / 50