242 lines
7.7 KiB
Markdown
242 lines
7.7 KiB
Markdown
---
|
|
id: wiki-2026-0508-saas-대시보드-및-이커머스-레이아웃-구축
|
|
title: SaaS 대시보드 및 이커머스 레이아웃 구축
|
|
category: 10_Wiki/Topics
|
|
status: verified
|
|
canonical_id: self
|
|
aliases: [SaaS Dashboard Layout, Ecommerce Layout, Admin Layout]
|
|
duplicate_of: none
|
|
source_trust_level: A
|
|
confidence_score: 0.9
|
|
verification_status: applied
|
|
tags: [frontend, layout, saas, dashboard, ecommerce, nextjs, shadcn]
|
|
raw_sources: []
|
|
last_reinforced: 2026-05-10
|
|
github_commit: pending
|
|
tech_stack:
|
|
language: TypeScript
|
|
framework: Next.js 16 / Tailwind v4 / shadcn-ui
|
|
---
|
|
|
|
# SaaS 대시보드 및 이커머스 레이아웃 구축
|
|
|
|
## 매 한 줄
|
|
> **"매 SaaS 의 sidebar+content shell + 매 Ecommerce 의 PLP/PDP/Cart"**. 2026 stack 매 Next.js 16 (App Router) + Tailwind v4 + shadcn-ui + Radix Primitive + TanStack Query. 매 Server Component 로 data fetch, Client Component 로 interactivity.
|
|
|
|
## 매 핵심
|
|
|
|
### 매 SaaS Dashboard 패턴
|
|
- **Shell**: sidebar (collapsible) + topbar (search/user menu) + content area.
|
|
- **Navigation**: 매 nested section, persistent state, breadcrumb.
|
|
- **Data widgets**: 매 stat card, chart (Recharts/Tremor v3), table (TanStack Table).
|
|
- **Multi-tenant**: 매 workspace switcher.
|
|
|
|
### 매 Ecommerce 패턴
|
|
- **PLP (Product List Page)**: 매 grid + filter sidebar + sort + pagination/infinite scroll.
|
|
- **PDP (Product Detail Page)**: 매 image gallery + variant selector + add-to-cart.
|
|
- **Cart/Checkout**: 매 cart drawer + multi-step checkout + Stripe Elements.
|
|
- **Search**: 매 typeahead + facet filtering (Algolia/MeiliSearch).
|
|
|
|
### 매 Stack (2026)
|
|
- Framework: Next.js 16 App Router / Remix 3 / Astro 5.
|
|
- UI: shadcn-ui v2 + Radix + Tailwind v4 (Oxide engine).
|
|
- Data: TanStack Query 5 + Server Action / RSC.
|
|
- Forms: react-hook-form + Zod.
|
|
- Tables: TanStack Table v8.
|
|
- Charts: Tremor v3 / Recharts.
|
|
|
|
### 매 응용
|
|
1. SaaS admin: Linear-style sidebar.
|
|
2. Ecommerce: Shopify Hydrogen / Next.js Commerce 템플릿.
|
|
3. Internal tool: Retool-like 매 form/table heavy.
|
|
|
|
## 💻 패턴
|
|
|
|
### App Router Layout (Next.js 16)
|
|
```tsx
|
|
// app/(dashboard)/layout.tsx
|
|
import { Sidebar } from '@/components/sidebar';
|
|
import { Topbar } from '@/components/topbar';
|
|
|
|
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
const user = await getCurrentUser();
|
|
return (
|
|
<div className="grid h-screen grid-cols-[260px_1fr]">
|
|
<Sidebar />
|
|
<div className="flex flex-col overflow-hidden">
|
|
<Topbar user={user} />
|
|
<main className="flex-1 overflow-auto p-6">{children}</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### shadcn-ui Sidebar (collapsible)
|
|
```tsx
|
|
'use client';
|
|
import { Sidebar, SidebarContent, SidebarMenu, SidebarMenuItem } from '@/components/ui/sidebar';
|
|
import { Home, Users, Settings } from 'lucide-react';
|
|
|
|
const items = [
|
|
{ title: 'Home', url: '/', icon: Home },
|
|
{ title: 'Users', url: '/users', icon: Users },
|
|
{ title: 'Settings', url: '/settings', icon: Settings },
|
|
];
|
|
|
|
export function AppSidebar() {
|
|
return (
|
|
<Sidebar collapsible="icon">
|
|
<SidebarContent>
|
|
<SidebarMenu>
|
|
{items.map((item) => (
|
|
<SidebarMenuItem key={item.title}>
|
|
<a href={item.url}>
|
|
<item.icon className="size-4" />
|
|
<span>{item.title}</span>
|
|
</a>
|
|
</SidebarMenuItem>
|
|
))}
|
|
</SidebarMenu>
|
|
</SidebarContent>
|
|
</Sidebar>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Stat Card Grid
|
|
```tsx
|
|
function StatCards({ stats }: { stats: Stat[] }) {
|
|
return (
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
{stats.map((s) => (
|
|
<div key={s.id} className="rounded-lg border bg-card p-6">
|
|
<div className="text-sm text-muted-foreground">{s.label}</div>
|
|
<div className="mt-2 text-3xl font-semibold">{s.value}</div>
|
|
<div className="mt-1 text-sm text-emerald-600">{s.delta}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### TanStack Table (server pagination)
|
|
```tsx
|
|
'use client';
|
|
import { useReactTable, getCoreRowModel } from '@tanstack/react-table';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
export function UsersTable() {
|
|
const [page, setPage] = useState(0);
|
|
const { data } = useQuery({
|
|
queryKey: ['users', page],
|
|
queryFn: () => fetch(`/api/users?page=${page}`).then(r => r.json()),
|
|
});
|
|
|
|
const table = useReactTable({
|
|
data: data?.users ?? [],
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
manualPagination: true,
|
|
pageCount: data?.totalPages,
|
|
});
|
|
|
|
return <table>...</table>;
|
|
}
|
|
```
|
|
|
|
### Ecommerce PLP (RSC + filter)
|
|
```tsx
|
|
// app/products/page.tsx
|
|
export default async function ProductsPage({ searchParams }: { searchParams: Promise<{ category?: string; sort?: string }> }) {
|
|
const params = await searchParams;
|
|
const products = await getProducts({ category: params.category, sort: params.sort });
|
|
return (
|
|
<div className="grid grid-cols-[240px_1fr] gap-6">
|
|
<FilterSidebar />
|
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
|
|
{products.map(p => <ProductCard key={p.id} product={p} />)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Cart Drawer (Zustand + shadcn Sheet)
|
|
```tsx
|
|
'use client';
|
|
import { create } from 'zustand';
|
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
|
|
|
const useCart = create<{ items: Item[]; add: (i: Item) => void; remove: (id: string) => void }>((set) => ({
|
|
items: [],
|
|
add: (i) => set((s) => ({ items: [...s.items, i] })),
|
|
remove: (id) => set((s) => ({ items: s.items.filter(x => x.id !== id) })),
|
|
}));
|
|
|
|
export function CartDrawer() {
|
|
const items = useCart((s) => s.items);
|
|
return (
|
|
<Sheet>
|
|
<SheetTrigger>Cart ({items.length})</SheetTrigger>
|
|
<SheetContent>
|
|
{items.map(i => <div key={i.id}>{i.name}</div>)}
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Server Action (Add to cart)
|
|
```tsx
|
|
// app/actions/cart.ts
|
|
'use server';
|
|
import { cookies } from 'next/headers';
|
|
|
|
export async function addToCart(productId: string, qty: number) {
|
|
const cart = (await cookies()).get('cart');
|
|
// ... persist
|
|
return { success: true };
|
|
}
|
|
```
|
|
|
|
## 매 결정 기준
|
|
| 상황 | Approach |
|
|
|---|---|
|
|
| New SaaS dashboard | Next.js 16 + shadcn + Tailwind v4 |
|
|
| Ecommerce | Next.js Commerce 또는 Shopify Hydrogen |
|
|
| Internal admin | Refine.dev / Retool |
|
|
| Mobile-first ecommerce | Astro + Solid Islands |
|
|
| Real-time dashboard | Next.js + WebSocket (Pusher/Ably) |
|
|
| Heavy data viz | Tremor + Recharts |
|
|
|
|
**기본값**: 매 SaaS 매 Next.js + shadcn-ui + Tailwind v4. 매 Ecommerce 매 Next.js Commerce 템플릿 fork.
|
|
|
|
## 🔗 Graph
|
|
- 부모: [[Frontend_Architecture]] · [[Layout_Pattern]]
|
|
- 변형: [[Admin_Dashboard]] · [[PLP_PDP]] · [[Cart_Checkout]]
|
|
- 응용: [[Linear_App]] · [[Shopify_Hydrogen]] · [[Vercel_Commerce]]
|
|
- Adjacent: [[Next.js]] · [[Tailwind_v4]] · [[shadcn-ui]] · [[TanStack_Query]]
|
|
|
|
## 🤖 LLM 활용
|
|
**언제**: greenfield SaaS / ecommerce 의 layout shell 구축, design system 적용.
|
|
**언제 X**: 매 micro-frontend / heavy SSR-disabled 의 SPA — 매 다른 stack (Vite + React Router) 적합.
|
|
|
|
## ❌ 안티패턴
|
|
- **모든 component 'use client'**: 매 RSC 의 benefit 상실 — server-first.
|
|
- **inline style + Tailwind 혼재**: 매 inconsistency — design token 통일.
|
|
- **Custom UI library scratch**: 매 shadcn-ui copy-paste 가 maintenance 우위.
|
|
- **Cart in localStorage only**: 매 cross-device sync 불가 — server-side cart + cookie.
|
|
- **No skeleton during loading**: 매 CLS / UX 저하 — Suspense + skeleton.
|
|
|
|
## 🧪 검증 / 중복
|
|
- Verified (Next.js 16 docs, shadcn-ui v2, Tailwind v4, Vercel Commerce).
|
|
- 신뢰도 A.
|
|
|
|
## 🕓 Changelog
|
|
| 날짜 | 변경 |
|
|
|---|---|
|
|
| 2026-05-08 | Phase 1 |
|
|
| 2026-05-10 | Manual cleanup — Next.js 16 / shadcn / Tailwind v4 modern stack |
|