From generate-frontend-projects
Next.js App Router + Supabase + React Query + TailwindCSS + shadcn/ui 웹 프로젝트 스캐폴딩 스킬. (client)/(provider)/(admin) 라우트 그룹 구조, 페이지별 components/ 폴더, React Query 훅 패턴(reactquery/도메인/), 공통 훅(common/), 공통 UI 컴포넌트, Supabase SSR 인증, Axios 클라이언트, QueryProvider, Sonner 토스트를 자동 생성. Trigger when: 사용자가 Next.js 프로젝트 생성/초기화, Next.js 템플릿 구조 설정, 웹 프로젝트 스캐폴딩, App Router 폴더 구조 생성, React Query + Supabase 셋업, 프론트엔드 프로젝트 템플릿 요청.
How this skill is triggered — by the user, by Claude, or both
Slash command
/generate-frontend-projects:generate-frontend-projectsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Next.js App Router + Supabase + React Query 기반 프로덕션 수준의 웹 프로젝트를 스캐폴딩합니다.
Next.js App Router + Supabase + React Query 기반 프로덕션 수준의 웹 프로젝트를 스캐폴딩합니다.
스킬 시작 전 사용자에게 확인:
(client) 그룹에 어떤 페이지가 필요한지 (예: community, blog, shop)npm install @tanstack/react-query @tanstack/react-query-devtools axios react-hook-form @hookform/resolvers zod sonner
shadcn/ui 미설치 시:
npx shadcn@latest init
npx shadcn@latest add button input card badge label checkbox dropdown-menu
전체 폴더 구조는 references/folder-structure.md를 참고하라.
| 그룹 | 레이아웃 | URL 패턴 |
|---|---|---|
(client) | Header + Footer | /페이지명 |
(provider) | 사이드바 | /provider/페이지명 |
(admin) | 사이드바 | /admin/페이지명 |
핵심 원칙:
page.tsx와 components/가 동일 위상으로 존재page.tsx에서 직접 로직보다 components/의 컴포넌트를 조합hooks/reactquery/[domain]/apis.ts 안에 정의import axios from "axios";
export const axiosClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || "",
headers: { "Content-Type": "application/json" },
withCredentials: true,
});
axiosClient.interceptors.response.use(
(response) => response,
(error) => Promise.reject(error)
);
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState } from "react";
export function QueryProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() => new QueryClient({
defaultOptions: { queries: { staleTime: 60 * 1000, retry: 1 } },
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
export { QueryProvider } from "./QueryProvider";
import type { Metadata } from "next";
import { ThemeProvider } from "next-themes";
import { QueryProvider } from "@/lib/providers";
import { Toaster } from "sonner";
import "./globals.css";
export const metadata: Metadata = {
title: "프로젝트명",
description: "프로젝트 설명",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<QueryProvider>{children}</QueryProvider>
</ThemeProvider>
<Toaster richColors position="top-right" />
</body>
</html>
);
}
import { redirect } from "next/navigation";
export default function RootPage() {
redirect("/root");
}
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
const navItems = [
{ label: "홈", href: "/root" },
// 사용자 도메인에 맞게 추가
];
export function Header() {
const pathname = usePathname();
return (
<header className="sticky top-0 z-40 w-full border-b border-border bg-background/80 backdrop-blur">
<div className="mx-auto flex h-14 max-w-screen-lg items-center justify-between px-4">
<Link href="/root" className="text-lg font-bold">Logo</Link>
<nav className="flex items-center gap-6">
{navItems.map((item) => (
<Link key={item.href} href={item.href}
className={cn("text-sm transition-colors hover:text-foreground",
pathname === item.href ? "font-semibold text-foreground" : "text-muted-foreground"
)}>
{item.label}
</Link>
))}
</nav>
<Link href="/login"
className="rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90">
로그인
</Link>
</div>
</header>
);
}
export function Footer() {
return (
<footer className="border-t border-border py-8">
<div className="mx-auto max-w-screen-lg px-4 text-center text-sm text-muted-foreground">
<p>© {new Date().getFullYear()} 프로젝트명. All rights reserved.</p>
</div>
</footer>
);
}
// app/(client)/layout.tsx
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";
export default function ClientLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</div>
);
}
// app/(provider)/layout.tsx
import Link from "next/link";
const providerNav = [
{ label: "대시보드", href: "/provider/dashboard" },
{ label: "내 콘텐츠", href: "/provider/contents" },
{ label: "통계", href: "/provider/status" },
{ label: "설정", href: "/provider/settings" },
];
export default function ProviderLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen">
<aside className="w-56 flex-shrink-0 border-r border-border bg-muted/40">
<div className="p-4">
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Provider</p>
<nav className="mt-4 space-y-1">
{providerNav.map((item) => (
<Link key={item.href} href={item.href}
className="block rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground">
{item.label}
</Link>
))}
</nav>
</div>
</aside>
<main className="flex-1">{children}</main>
</div>
);
}
// app/(admin)/layout.tsx
// providerNav와 동일한 패턴, /admin/* 경로로 변경
훅 작성 패턴은 references/hooks-pattern.md를 참고하라.
hooks/reactquery/[도메인]/apis.ts — API 함수 + 타입hooks/reactquery/[도메인]/keys.ts — 캐시 키 팩토리hooks/reactquery/[도메인]/queries.ts — useQuery 훅hooks/reactquery/[도메인]/mutations.ts — useMutation 훅hooks/reactquery/[도메인]/index.ts — re-exporthooks/common/ 아래에 생성. 상세 구현은 references/hooks-pattern.md 참고.
hooks/common/
├── useModal.ts # useState 기반 open/close/toggle
├── useAuth.ts # useMe() 래핑, isAuthenticated 제공
├── useToast.ts # sonner toast 헬퍼 (success/error/info/warning)
├── useDebounce.ts # value + delay → debounced value
├── usePagination.ts # page/limit + goToPage/next/prev/reset
├── useLocalStorage.ts # JSON 직렬화 포함 localStorage
├── useOutsideClick.ts # ref + mousedown 이벤트 외부 클릭 감지
└── index.ts # 전체 re-export
상세 구현은 references/common-components.md 참고.
components/common/
├── SearchBar/ # 검색창 + clear 버튼
├── Pagination/ # ellipsis 포함 페이지네이터
├── Modal/ # ESC 닫기, scroll lock
├── EmptyState/ # 빈 상태 플레이스홀더
└── LoadingSpinner/ # 스피너 + LoadingPage
import { z } from "zod";
export const loginSchema = z.object({
email: z.string().email("올바른 이메일을 입력해주세요"),
password: z.string().min(8, "비밀번호는 최소 8자 이상이어야 합니다"),
});
export const signupSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
password: z.string().min(8),
confirmPassword: z.string(),
}).refine((d) => d.password === d.confirmPassword, {
message: "비밀번호가 일치하지 않습니다",
path: ["confirmPassword"],
});
export type LoginFormValues = z.infer<typeof loginSchema>;
export type SignupFormValues = z.infer<typeof signupSchema>;
React Hook Form + zodResolver + useLogin mutation 연결. 에러 메시지는 errors.field.message로 표시. isPending 동안 버튼 비활성화.
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
},
},
}
);
}
# .env.local
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
NEXT_PUBLIC_API_URL=http://localhost:8000
lib/axios/client.ts 생성lib/providers/QueryProvider.tsx 생성app/layout.tsx — QueryProvider + Toaster 포함(client), (provider), (admin) 레이아웃 생성hooks/reactquery/[도메인]/ 생성hooks/common/ 생성components/common/ 공통 UI 생성components/layout/Header, Footer 생성components/forms/schema/auth.ts 생성.env.local 환경 변수 템플릿 제공npm run build 빌드 성공 확인"use client" 추가Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub auraworks/my-marketplace --plugin generate-frontend-projects