fe-scaffold
SKILL.md
FE Scaffolding
$ARGUMENTS를 파싱하여 해당하는 타입의 보일러플레이트를 생성한다.
지원 타입
| 타입 | 설명 | 예시 |
|---|---|---|
component |
React 컴포넌트 + 테스트 | /fe-scaffold component UserProfile |
page |
Next.js App Router 페이지 | /fe-scaffold page dashboard |
api |
Route Handler (API) | /fe-scaffold api users |
hook |
커스텀 훅 + 테스트 | /fe-scaffold hook useDebounce |
store |
Zustand 스토어 | /fe-scaffold store auth |
feature |
기능 모듈 (컴포넌트 + 훅 + 타입 + 테스트) | /fe-scaffold feature checkout |
form |
React Hook Form + Zod 스키마 폼 | /fe-scaffold form LoginForm |
인자가 없으면 사용자에게 어떤 타입을 생성할지 질문한다.
템플릿 규칙
component
파일 생성 위치: src/components/[이름]/[이름].tsx + [이름].test.tsx
// src/components/[Name]/[Name].tsx
import { cn } from "@/lib/utils";
interface [Name]Props {
className?: string;
children?: React.ReactNode;
}
function [Name]({ className, children }: [Name]Props) {
return (
<div className={cn("", className)}>
{children}
</div>
);
}
export { [Name] };
export type { [Name]Props };
// src/components/[Name]/[Name].test.tsx
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { [Name] } from "./[Name]";
describe("[Name]", () => {
it("renders children", () => {
render(<[Name]>test</[Name]>);
expect(screen.getByText("test")).toBeInTheDocument();
});
});
page
파일 생성 위치: src/app/[이름]/page.tsx + layout.tsx (필요 시) + loading.tsx
// src/app/[name]/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "[Name]",
description: "[Name] page",
};
export default function [Name]Page() {
return (
<main>
<h1>[Name]</h1>
</main>
);
}
// src/app/[name]/loading.tsx
import { Skeleton } from "@/components/ui/skeleton";
export default function [Name]Loading() {
return <Skeleton className="h-screen w-full" />;
}
api
파일 생성 위치: src/app/api/[이름]/route.ts
// src/app/api/[name]/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
try {
// TODO: implement
return NextResponse.json({ data: [] });
} catch (error) {
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// TODO: implement
return NextResponse.json({ data: body }, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
}
}
hook
파일 생성 위치: src/hooks/[이름].ts + [이름].test.ts
// src/hooks/[useName].ts
import { useCallback, useState } from "react";
function [useName]() {
// TODO: implement
return {};
}
export { [useName] };
// src/hooks/[useName].test.ts
import { renderHook, act } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { [useName] } from "./[useName]";
describe("[useName]", () => {
it("should work", () => {
const { result } = renderHook(() => [useName]());
expect(result.current).toBeDefined();
});
});
store
파일 생성 위치: src/stores/[이름]Store.ts
// src/stores/[name]Store.ts
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface [Name]State {
// TODO: define state
}
interface [Name]Actions {
// TODO: define actions
reset: () => void;
}
const initial[Name]State: [Name]State = {
// TODO: initial values
};
const use[Name]Store = create<[Name]State & [Name]Actions>()(
devtools(
(set) => ({
...initial[Name]State,
reset: () => set(initial[Name]State),
}),
{ name: "[name]-store" }
)
);
export { use[Name]Store };
export type { [Name]State, [Name]Actions };
feature
기능 모듈 전체를 생성한다. 위치: src/components/[feature]/
src/components/[feature]/
├── [Feature].tsx # 메인 컴포넌트
├── [Feature].test.tsx # 테스트
├── use[Feature].ts # 기능 전용 훅
├── [feature].types.ts # 타입 정의
└── index.ts # barrel export
form
React Hook Form + Zod 기반 폼 컴포넌트를 생성한다.
// src/components/[Name]/[Name].tsx
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const [name]Schema = z.object({
// TODO: define schema
email: z.string().email(),
});
type [Name]Values = z.infer<typeof [name]Schema>;
interface [Name]Props {
onSubmit: (values: [Name]Values) => void;
}
function [Name]({ onSubmit }: [Name]Props) {
const form = useForm<[Name]Values>({
resolver: zodResolver([name]Schema),
defaultValues: {
email: "",
},
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="email@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
export { [Name] };
export type { [Name]Values };
실행 규칙
$ARGUMENTS에서 타입과 이름을 파싱- 프로젝트의 기존 구조를 Glob/Read로 확인하여 실제 경로 패턴에 맞춤
- 이미 존재하는 파일이 있으면 덮어쓰지 않고 사용자에게 확인
- 파일 생성 후 생성된 파일 목록을 출력
- shadcn/ui 컴포넌트가 필요한데 없으면 설치 명령어 안내
Weekly Installs
2
Repository
ingpdw/pdw-fe-dev-toolFirst Seen
Feb 7, 2026
Security Audits
Installed on
mcpjam2
openhands2
replit2
junie2
windsurf2
zencoder2