Communitygithub.com

onecontroller/skills

대시보드 UI 디자인 시스템, 레이아웃 아키텍처, 미묘한 그라데이션 통계 카드, 접이식 사이드바, 브레드크럼, 날짜 필터, TanStack 데이터 테이블을 현대적인 대시보드에 구축합니다.

skills란 무엇인가요?

이 스킬은 Next.js(App Router), Tailwind CSS(v3 또는 v4), shadcn/ui를 사용하여 고급 반응형 관리 및 분석 대시보드를 구축하기 위한 포괄적인 가이드라인, 컴포넌트 아키텍처, 복사-붙여넣기 가능한 구현 패턴을 제공합니다. 특히 shadcn/ui 및 TanStack Table 설치를 포함한 필수 전제 조건 확인을 강조하여 개발 환경이 적절히 설정되었는지 확인합니다. 광범위한 디자인 시스템 토큰을 제공하며, 접이식 사이드바 및 내비게이션, 브레드크럼이 있는 페이지 헤더, 미묘한 그라데이션 통계 카드, 날짜 필터, 데이터 테이블을 포함한 핵심 대시보드 컴포넌트의 세부 구현 코드를 포함합니다. 또한 모든 컴포넌트에서 반응형 디자인, 접근성, 성능 최적화를 우선시하여 일관된 사용자 경험을 보장합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/0necontroller/skills/tree/HEAD/skills/dashboard-ui

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

대시보드 UI 스킬

Next.js(App Router), Tailwind CSS(v3 또는 v4), shadcn/ui를 사용하여 고급 반응형 관리 및 분석 대시보드를 구축하기 위한 포괄적인 가이드라인, 컴포넌트 아키텍처, 복사-붙여넣기 가능한 구현 패턴입니다.


1. 사전 요구 사항 및 shadcn/ui 컴포넌트 설치

대시보드 컴포넌트를 생성하거나 수정하기 전에 AI 에이전트는 다음 사전 요구 사항 확인을 반드시 수행해야 합니다.

1단계: shadcn/ui 설정 확인 (components.json)

프로젝트 루트에 components.json이 있는지 확인합니다.

  • components.json이 없는 경우: 블라인드로 컴포넌트를 작성하거나 설치하려고 시도하지 마세요. 사용자에게 다음과 같이 요청합니다.

    "이 프로젝트에서 shadcn/ui 구성(components.json)을 찾을 수 없습니다. 먼저 shadcn/ui를 초기화하세요(예: npx shadcn@latest init). 그런 다음 컴포넌트 디렉토리와 경로 별칭이 올바르게 구성됩니다."

  • components.json이 있는 경우: 컴포넌트 확인을 진행합니다.

2단계: 필요한 shadcn/ui 컴포넌트 설치

지정된 UI 컴포넌트 디렉토리(예: @/components/ui/)에 필요한 프리미티브가 있는지 확인합니다. 누락된 컴포넌트에 대해 npx shadcn@latest add <component>를 실행합니다.

# 핵심 레이아웃 및 사이드바
npx shadcn@latest add sidebar collapsible separator sheet tooltip avatar dropdown-menu

# 페이지 헤더 및 브레드크럼
npx shadcn@latest add breadcrumb badge button

# 통계 및 분석 카드
npx shadcn@latest add card skeleton

# 날짜 필터
npx shadcn@latest add popover calendar

# 데이터 테이블 및 폼 컨트롤
npx shadcn@latest add table input select

또는 하나의 명령으로 모든 대시보드 필수 요소를 설치합니다.

npx shadcn@latest add sidebar collapsible separator sheet tooltip avatar dropdown-menu breadcrumb badge button card skeleton popover calendar table input select

3단계: 핵심 npm 패키지 확인 및 설치

이 스킬의 모든 데이터 테이블은 TanStack Table (@tanstack/react-table) 과 shadcn/ui <Table>을 결합하여 빌드됩니다. package.json에 다음 패키지가 있는지 확인합니다.

  • @tanstack/react-table: 테이블 상태 관리, 열 정의, 페이지네이션 및 정렬.
  • date-fns: 날짜 필터의 날짜 구문 분석, 형식 지정 및 산술 연산.
  • lucide-react: 표준 아이콘 세트.

누락된 경우 설치합니다.

npm install @tanstack/react-table date-fns lucide-react
# 또는: pnpm add @tanstack/react-table date-fns lucide-react
# 또는: bun add @tanstack/react-table date-fns lucide-react

2. 디자인 시스템 및 테마 기초

모든 대시보드 컴포넌트는 시맨틱 CSS 변수와 미묘하고 대비가 높은 시각적 토큰에 의존합니다.

테마 토큰 (globals.css)

:root {
  --background: 0 0% 100%;
  --foreground: 0 0% 3.9%;
  --card: 0 0% 100%;
  --card-foreground: 0 0% 3.9%;
  --popover: 0 0% 100%;
  --popover-foreground: 0 0% 3.9%;
  --primary: 330 43% 46%;
  --primary-foreground: 0 0% 98%;
  --muted: 0 0% 96.1%;
  --muted-foreground: 0 0% 45.1%;
  --border: 0 0% 89.8%;
  --radius: 0.5rem;

  /* 사이드바 토큰 */
  --sidebar: hsl(0 0% 98%);
  --sidebar-foreground: hsl(240 5.3% 26.1%);
  --sidebar-primary: hsl(240 5.9% 10%);
  --sidebar-primary-foreground: hsl(0 0% 98%);
  --sidebar-accent: hsl(240 4.8% 95.9%);
  --sidebar-accent-foreground: hsl(240 5.9% 10%);
  --sidebar-border: hsl(220 13% 91%);
  --sidebar-ring: hsl(217.2 91.2% 59.8%);
}

.dark {
  --background: 0 0% 3.9%;
  --foreground: 0 0% 98%;
  --card: 0 0% 3.9%;
  --card-foreground: 0 0% 98%;
  --muted: 0 0% 14.9%;
  --muted-foreground: 0 0% 63.9%;
  --border: 0 0% 14.9%;

  /* 다크 사이드바 토큰 */
  --sidebar: hsl(240 5.9% 10%);
  --sidebar-foreground: hsl(240 4.8% 95.9%);
  --sidebar-primary: hsl(224.3 76.3% 48%);
  --sidebar-accent: hsl(240 3.7% 15.9%);
  --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
  --sidebar-border: hsl(240 3.7% 15.9%);
}

3. 관리 레이아웃 및 사이드바 아키텍처

3.1 진입점 레이아웃 (app/admin/layout.tsx)

루트 관리 레이아웃은 페이지를 SidebarProvider로 감싸고 접이식 AppSidebar를 삽입하며 모바일 트리거로 헤더를 제어합니다.

import { AppSidebar } from '@/components/admin/sidebar/app-sidebar';
import {
  SidebarInset,
  SidebarProvider,
  SidebarTrigger
} from '@/components/ui/sidebar';

export default function AdminLayout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <AppSidebar />
      <SidebarInset>
        {/* 모바일 헤더: 작은 화면에서 상단 고정 블러, 데스크톱에서는 투명 */}
        <header className="bg-background/95 fixed top-0 right-0 left-0 z-20 flex h-16 shrink-0 items-center gap-2 border-b shadow-xs backdrop-blur transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 md:static md:border-none md:bg-transparent md:shadow-none">
          <div className="flex w-full items-center justify-between gap-2 px-4">
            <SidebarTrigger className="-ml-1" />
            <div className="font-semibold text-sm tracking-tight md:hidden">
              관리자 포털
            </div>
            <div className="w-8 md:hidden" />
          </div>
        </header>

        {/* 주요 콘텐츠 영역 */}
        <main className="flex flex-1 flex-col gap-4 p-4 pt-20 md:pt-0">
          {children}
        </main>
      </SidebarInset>
    </SidebarProvider>
  );
}

3.2 앱 사이드바 (components/admin/sidebar/app-sidebar.tsx)

collapsible="icon"과 함께 variant="inset"을 사용하여 아이콘 전용 도크로 축소합니다.

'use client';

import {
  PieChart,
  ShoppingBag,
  Users,
  Settings2,
  Package
} from 'lucide-react';
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarHeader,
  SidebarRail
} from '@/components/ui/sidebar';
import { NavMain } from './nav-main';
import { NavUser } from './nav-user';
import { TeamSwitcher } from './team-switcher';

const navData = [
  {
    title: '대시보드',
    url: '/admin',
    icon: PieChart,
    items: [{ title: '개요', url: '/admin' }]
  },
  {
    title: '주문',
    url: '/admin/orders',
    icon: ShoppingBag,
    items: [
      { title: '모든 주문', url: '/admin/orders' },
      { title: '거래 내역', url: '/admin/orders/transactions' }
    ]
  },
  {
    title: '재고',
    url: '/admin/inventory',
    icon: Package,
    items: [
      { title: '제품', url: '/admin/inventory/products' },
      { title: '재고 부족', url: '/admin/inventory/low-stock' }
    ]
  },
  {
    title: '사용자',
    url: '/admin/users',
    icon: Users,
    items: [{ title: '고객', url: '/admin/users' }]
  },
  {
    title: '설정',
    url: '/admin/settings',
    icon: Settings2,
    items: [{ title: '일반', url: '/admin/settings' }]
  }
];

export function AppSidebar(props: React.ComponentProps<typeof Sidebar>) {
  return (
    <Sidebar variant="inset" collapsible="icon" {...props}>
      <SidebarHeader>
        <TeamSwitcher />
      </SidebarHeader>
      <SidebarContent>
        <NavMain items={navData} />
      </SidebarContent>
      <SidebarFooter>
        <NavUser user={{ name: '관리자', email: 'admin@company.com' }} />
      </SidebarFooter>
      <SidebarRail />
    </Sidebar>
  );
}

3.3 접이식 내비게이션 (활성 상태 및 카운터 알약 포함) (nav-main.tsx)

활성 링크를 강조 표시하고, 부드럽게 쉐브론을 회전시키며, tabular-nums로 알림 카운터를 표시합니다.

'use client';

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { ChevronRight, type LucideIcon } from 'lucide-react';
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger
} from '@/components/ui/collapsible';
import {
  SidebarGroup,
  SidebarGroupLabel,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem
} from '@/components/ui/sidebar';

interface NavItem {
  title: string;
  url: string;
  icon?: LucideIcon;
  count?: number;
  items?: { title: string; url: string; count?: number }[];
}

export function NavMain({ items }: { items: NavItem[] }) {
  const pathname = usePathname();

  const isPathActive = (target: string) => {
    if (!target) return false;
    if (pathname === target) return true;
    if (target === '/admin') return false;
    return pathname.startsWith(`${target}/`);
  };

  return (
    <SidebarGroup>
      <SidebarGroupLabel>플랫폼</SidebarGroupLabel>
      <SidebarMenu>
        {items.map((item) => {
          const subItemActive = item.items?.some((s) => isPathActive(s.url));
          const isActive = subItemActive || isPathActive(item.url);

          return (
            <Collapsible
              key={item.title}
              asChild
              defaultOpen={isActive}
              className="group/collapsible"
            >
              <SidebarMenuItem>
                <CollapsibleTrigger asChild>
                  <SidebarMenuButton tooltip={item.title} isActive={isActive}>
                    {item.icon && <item.icon />}
                    <span>{item.title}</span>
                    {item.count && item.count > 0 && (
                      <span className="bg-primary/10 text-primary mr-1 ml-auto flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[11px] font-semibold tabular-nums">
                        {item.count}
                      </span>
                    )}
                    <ChevronRight
                      className={`transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 ${
                        item.count ? '' : 'ml-auto'
                      }`}
                    />
                  </SidebarMenuButton>
                </CollapsibleTrigger>
                <CollapsibleContent>
                  <SidebarMenuSub>
                    {item.items?.map((subItem) => {
                      const subIsActive = isPathActive(subItem.url);
                      return (
                        <SidebarMenuSubItem key={subItem.title}>
                          <SidebarMenuSubButton asChild isActive={subIsActive}>
                            <Link
                              href={subItem.url}
                              className="flex w-full items-center justify-between"
                            >
                              <span>{subItem.title}</span>
                              {subItem.count && subItem.count > 0 && (
                                <span className="bg-primary ml-auto flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium text-white tabular-nums">
                                  {subItem.count}
                                </span>
                              )}
                            </Link>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                      );
                    })}
                  </SidebarMenuSub>
                </CollapsibleContent>
              </SidebarMenuItem>
            </Collapsible>
          );
        })}
      </SidebarMenu>
    </SidebarGroup>
  );
}

4. 페이지 헤더, 브레드크럼 및 제목 계층 구조

모든 페이지는 계층적 브레드크럼, 접근 가능한 <h1> 제목, 부제목, 작업/필터 도구 모음을 결합한 깔끔한 헤더 행으로 시작합니다.

import Link from 'next/link';
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator
} from '@/components/ui/breadcrumb';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';

interface PageHeaderProps {
  breadcrumbs?: { title: string; href?: string }[];
  title: string;
  subtitle?: string;
  statusBadge?: React.ReactNode;
  children?: React.ReactNode; // 작업 버튼 또는 필터
}

export function PageHeader({
  breadcrumbs,
  title,
  subtitle,
  statusBadge,
  children
}: PageHeaderProps) {
  return (
    <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
      <div>
        {breadcrumbs && breadcrumbs.length > 0 && (
          <Breadcrumb className="mb-2">
            <BreadcrumbList>
              {breadcrumbs.map((crumb, idx) => {
                const isLast = idx === breadcrumbs.length - 1;
                return (
                  <span key={crumb.title} className="inline-flex items-center gap-1.5">
                    <BreadcrumbItem>
                      {isLast || !crumb.href ? (
                        <BreadcrumbPage>{crumb.title}</BreadcrumbPage>
                      ) : (
                        <BreadcrumbLink asChild>
                          <Link href={crumb.href}>{crumb.title}</Link>
                        </BreadcrumbLink>
                      )}
                    </BreadcrumbItem>
                    {!isLast && <BreadcrumbSeparator />}
                  </span>
                );
              })}
            </BreadcrumbList>
          </Breadcrumb>
        )}

        <div className="flex items-center gap-3">
          <h1 className="text-2xl font-semibold tracking-tight md:text-3xl md:font-bold">
            {title}
          </h1>
          {statusBadge}
        </div>

        {subtitle && (
          <p className="text-muted-foreground text-sm mt-1">
            {subtitle}
          </p>
        )}
      </div>

      {children && (
        <div className="flex flex-wrap items-center gap-2">
          {children}
        </div>
      )}
    </div>
  );
}

5. 미묘한 그라데이션이 있는 통계 카드

5.1 그라데이션 및 컨테이너 쿼리 패턴

시각적 혼란 없이 고급스러운 느낌을 얻으려면:

  1. 라이트 모드에서 상위 와일드카드 선택기 *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:bg-gradient-to-t를 적용합니다.
  2. 다크 모드 카드의 경우 깔끔하고 무광택인 dark:*:data-[slot=card]:bg-card로 대체합니다.
  3. 카드에 @container/card를 추가하여 제목 활자 크기를 동적으로 조정합니다(@[250px]/card:text-3xl).
  4. 추세 표시기를 표시합니다.

...

[번역을 위해 잘림]

관련 스킬