대시보드

온라인 쇼핑몰의 관리 화면입니다. 기간을 바꾸면 숫자 카드와 그래프가 그 기간의 데이터로 바뀝니다.

대시보드

핵심 포인트

  1. 1

    숫자는 원본 데이터에서 계산하기

    매출, 주문 수 같은 합계는 하루하루의 데이터를 reduce로 더해서 구합니다. 기간을 바꿔도 다시 계산하기만 하면 맞습니다.

  2. 2

    그래프는 라이브러리에 맡기기

    막대, 눈금, 마우스를 올렸을 때 나오는 숫자는 Recharts가 그립니다. 우리는 "어떤 데이터의 어떤 항목을 그릴지"만 알려 주면 됩니다.

  3. 3

    늘었는지 줄었는지는 색과 기호로 보여 주기

    이전 기간과 비교해 늘었으면 초록색 ▲, 줄었으면 빨간색 ▼를 표시합니다. 숫자를 읽지 않아도 좋고 나쁨을 한눈에 알 수 있습니다.

사용한 도구

누르면 제2부의 비교 페이지로

코드 고치기

코드를 고치면 오른쪽 미리보기가 바로 바뀝니다.

import { useState } from "react"
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
import { Button } from "./components/ui/button"

// 일별 매출과 주문 수 (실제 서비스에서는 서버에서 가져옴). 오늘을 10/4로 가정
const weekDays = [
  { day: "9/28", sales: 1313000, orders: 38 },
  { day: "9/29", sales: 1105000, orders: 33 },
  { day: "9/30", sales: 1222000, orders: 36 },
  { day: "10/1", sales: 1521000, orders: 44 },
  { day: "10/2", sales: 845000, orders: 27 },
  { day: "10/3", sales: 1859000, orders: 61 },
  { day: "10/4", sales: 2041000, orders: 64 },
]

// 최근 30일 (9/5~10/4) = 9/5~9/27의 23일 + 위의 최근 7일 (같은 날은 같은 숫자가 되도록)
// 23일분은 정해진 식으로 생성 (데모용, 외울 필요 없음)
const monthDays = [
  ...Array.from({ length: 23 }, (_, i) => ({
    day: `9/${i + 5}`,
    sales: 949000 + ((i * 37) % 11) * 104000 + (i % 7 <= 1 ? 611000 : 0),
    orders: 30 + ((i * 37) % 11) * 3 + (i % 7 <= 1 ? 18 : 0),
  })),
  ...weekDays,
]

// 기간별 데이터. previous는 '직전 기간'의 숫자 (증감 비교용)
const periods = {
  week: { label: "최근 7일", days: weekDays, members: 48, previous: { sales: 8840000, orders: 286, average: 31200, members: 40 } },
  month: { label: "최근 30일", days: monthDays, members: 186, previous: { sales: 42900000, orders: 1390, average: 31200, members: 201 } },
}

// 최근 주문
const recentOrders = [
  { id: "A-1052", name: "김민준", amount: 110500, status: "배송 완료" },
  { id: "A-1051", name: "이서연", amount: 39000, status: "준비 중" },
  { id: "A-1050", name: "박지훈", amount: 201500, status: "배송 완료" },
  { id: "A-1049", name: "최수아", amount: 26000, status: "준비 중" },
  { id: "A-1048", name: "정하준", amount: 65000, status: "배송 완료" },
]

// 금액 표기 (1313000 → ₩1,313,000)
function yen(value) {
  return "₩" + value.toLocaleString()
}

// 직전 기간 대비 몇 % 늘었는지 (줄었으면 마이너스). 예: 현재 120, 이전 100 → 20
function changePercent(value, previous) {
  return Math.round(((value - previous) / previous) * 100)
}

export default function App() {
  // ① 선택한 기간 ("week" = 최근 7일, "month" = 최근 30일), state에 저장
  //const period = "week"
  const [period, setPeriod] = useState("week")

  const current = periods[period] // 선택한 기간의 데이터

  // ② 기간 합계
  //const sales = 1144000 // 일별 매출(d.sales)을 모두 더한 금액
  const sales = current.days.reduce((sum, d) => sum + d.sales, 0)
  //const orders = 303 // 일별 주문 수(d.orders)를 모두 더한 수
  const orders = current.days.reduce((sum, d) => sum + d.orders, 0)
  const average = Math.round(sales / orders) // 객단가 = 매출 ÷ 주문 수

  // 숫자 카드 4개의 데이터 (label = 이름, text = 크게 보여 줄 글자, change = 증감률 %)
  const stats = [
    { label: "매출", text: yen(sales), change: changePercent(sales, current.previous.sales) },
    { label: "주문 수", text: orders.toLocaleString() + "건", change: changePercent(orders, current.previous.orders) },
    { label: "객단가", text: yen(average), change: changePercent(average, current.previous.average) },
    { label: "신규 회원", text: current.members + "명", change: changePercent(current.members, current.previous.members) },
  ]

  return (
    <div className="mx-auto my-6 flex w-full max-w-4xl flex-col gap-6 px-4 text-foreground">
      {/* 제목과 기간 버튼 */}
      <div className="flex flex-wrap items-center justify-between gap-3">
        <h1 className="text-xl font-semibold">대시보드</h1>
        <div className="flex gap-2">
          {/* ③ 누르면 기간 변경. 선택한 쪽은 채움(default), 나머지는 외곽선(outline) */}
          <Button size="sm" variant={period === "week" ? "default" : "outline"} onClick={() => setPeriod("week")}>
            최근 7일
          </Button>
          <Button size="sm" variant={period === "month" ? "default" : "outline"} onClick={() => setPeriod("month")}>
            최근 30일
          </Button>
        </div>
      </div>

      {/* ④ 숫자 카드: map으로 stats 4개 나열 */}

      <div className="grid grid-cols-2 gap-3 md:grid-cols-4">
        {stats.map((stat) => (
          <div key={stat.label} className="rounded-xl border bg-card p-4 text-card-foreground">
            <p className="text-xs text-muted-foreground">{stat.label}</p>
            <p className="mt-1 text-xl font-semibold tabular-nums">{stat.text}</p>
            {/* ⑤ 늘었으면(change가 0 이상) 초록 ▲, 줄었으면 빨강 ▼ */}
            {/* <p className="mt-1 text-xs text-emerald-600"> */}
            <p className={stat.change >= 0 ? "mt-1 text-xs text-emerald-600" : "mt-1 text-xs text-red-600"}>
              {stat.change >= 0 ? "▲" : "▼"} {Math.abs(stat.change)}%
              <span className="text-muted-foreground"> 직전 기간 대비</span>
            </p>
          </div>
        ))}
      </div>


      {/* 매출 그래프 (Recharts가 data로 넘긴 목록을 막대로 그림) */}
      <div className="rounded-xl border bg-card p-4 text-card-foreground">
        <p className="text-sm font-medium">매출 추이</p>
        <div className="mt-4 h-56">
          <ResponsiveContainer width="100%" height="100%">
            <BarChart data={current.days}>
              <CartesianGrid vertical={false} stroke="var(--border)" />
              <XAxis dataKey="day" tickLine={false} axisLine={false} fontSize={12} />
              <YAxis tickLine={false} axisLine={false} fontSize={12} width={40} tickFormatter={(v) => v / 10000 + "만"} />
              <Tooltip formatter={(v) => [yen(v), "매출"]} cursor={{ fill: "var(--muted)" }} />
              <Bar dataKey="sales" fill="var(--primary)" radius={[4, 4, 0, 0]} />
            </BarChart>
          </ResponsiveContainer>
        </div>
      </div>

      {/* 최근 주문 */}
      <div className="rounded-xl border bg-card p-4 text-card-foreground">
        <p className="text-sm font-medium">최근 주문</p>
        <ul className="mt-3 divide-y text-sm">
          {recentOrders.map((order) => (
            <li key={order.id} className="flex items-center gap-3 py-2.5">
              <span className="w-16 text-muted-foreground tabular-nums">{order.id}</span>
              <span className="flex-1">{order.name}</span>
              <span className="tabular-nums">{yen(order.amount)}</span>
              <span
                // ⑥ '배송 완료'면 초록, 그 외(준비 중)는 노랑
                className={order.status === "배송 완료" ? "w-16 rounded-full bg-emerald-100 py-0.5 text-center text-xs text-emerald-700" : "w-16 rounded-full bg-amber-100 py-0.5 text-center text-xs text-amber-700"}
              >
                {order.status}
              </span>
            </li>
          ))}
        </ul>
      </div>
    </div >
  )
}