/** * Mandu MCP Skills - Recipes * 작업별 step-by-step 레시피 */ export const RECIPE_ADD_API_ROUTE = `# API 라우트 추가하기 ## 목표 새로운 REST API 엔드포인트를 추가합니다. ## Step 1: 파일 생성 \`app/api/{name}/route.ts\` 파일을 생성합니다. 예시: 사용자 API를 만들려면 \`app/api/users/route.ts\` ## Step 2: 핸들러 작성 \`\`\`typescript // app/api/users/route.ts // GET /api/users export function GET() { const users = [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, ]; return Response.json({ data: users }); } // POST /api/users export async function POST(request: Request) { const body = await request.json(); // 유효성 검사 if (!body.name) { return Response.json( { error: "name is required" }, { status: 400 } ); } const newUser = { id: Date.now(), ...body }; return Response.json( { data: newUser }, { status: 201 } ); } \`\`\` ## Step 3: 개발 서버 실행 \`\`\`bash bun run dev \`\`\` ## Step 4: 테스트 \`\`\`bash # GET 요청 curl http://localhost:3000/api/users # POST 요청 curl -X POST http://localhost:3000/api/users \\ -H "Content-Type: application/json" \\ -d '{"name": "Charlie"}' \`\`\` ## 추가 메서드 필요에 따라 다른 HTTP 메서드도 추가할 수 있습니다: \`\`\`typescript export function PUT(request: Request) { ... } export function PATCH(request: Request) { ... } export function DELETE(request: Request) { ... } \`\`\` ## 동적 라우트 특정 ID의 사용자를 처리하려면: \`\`\`typescript // app/api/users/[id]/route.ts export function GET( request: Request, { params }: { params: { id: string } } ) { const { id } = params; return Response.json({ userId: id }); } export function DELETE( request: Request, { params }: { params: { id: string } } ) { const { id } = params; // 삭제 로직 return new Response(null, { status: 204 }); } \`\`\` ## 완료! API가 다음 URL에서 사용 가능합니다: - GET/POST: \`http://localhost:3000/api/users\` - GET/DELETE: \`http://localhost:3000/api/users/:id\` `; export const RECIPE_ADD_PAGE = `# 페이지 추가하기 ## 목표 새로운 페이지를 추가합니다. ## Step 1: 파일 생성 \`app/{path}/page.tsx\` 파일을 생성합니다. 예시: - \`app/about/page.tsx\` → \`/about\` - \`app/products/page.tsx\` → \`/products\` - \`app/blog/posts/page.tsx\` → \`/blog/posts\` ## Step 2: 페이지 컴포넌트 작성 \`\`\`tsx // app/about/page.tsx export default function AboutPage() { return (

About Us

Welcome to our company!

Our Mission

Building great software.

Contact

Email: hello@example.com

); } \`\`\` ## Step 3: 메타데이터 추가 (선택) \`\`\`tsx // app/about/page.tsx export const metadata = { title: "About Us | My App", description: "Learn about our company and mission", }; export default function AboutPage() { return (

About Us

{/* ... */}
); } \`\`\` ## Step 4: 확인 \`\`\`bash bun run dev # http://localhost:3000/about 접속 \`\`\` ## 동적 페이지 URL 파라미터를 받는 페이지: \`\`\`tsx // app/users/[id]/page.tsx interface Props { params: { id: string }; } export default function UserPage({ params }: Props) { return (

User Profile

User ID: {params.id}

); } \`\`\` ## 스타일 추가 \`\`\`tsx // app/about/page.tsx export default function AboutPage() { return (

About Us

Welcome!

); } \`\`\` 또는 CSS 파일 사용: \`\`\`tsx import "./about.css"; export default function AboutPage() { return (

About Us

); } \`\`\` ## 레이아웃 적용 페이지에 레이아웃을 적용하려면 같은 폴더에 \`layout.tsx\` 생성: \`\`\`tsx // app/about/layout.tsx export default function AboutLayout({ children }: { children: React.ReactNode }) { return (
{children}
); } \`\`\` ## 완료! 페이지가 \`http://localhost:3000/about\`에서 사용 가능합니다. `; export const RECIPE_ADD_AUTH = `# 인증 추가하기 ## 목표 API에 인증(Authentication)을 추가합니다. ## Step 1: 인증 유틸리티 생성 \`\`\`typescript // app/lib/auth.ts export interface User { id: number; email: string; name: string; } // 간단한 토큰 검증 (실제로는 JWT 등 사용) export function verifyToken(token: string): User | null { // 예시: "Bearer user_1" 형식 if (token.startsWith("Bearer user_")) { const userId = parseInt(token.replace("Bearer user_", "")); return { id: userId, email: \`user\${userId}@example.com\`, name: \`User \${userId}\`, }; } return null; } export function getAuthUser(request: Request): User | null { const authHeader = request.headers.get("Authorization"); if (!authHeader) return null; return verifyToken(authHeader); } \`\`\` ## Step 2: 인증이 필요한 API 작성 \`\`\`typescript // app/api/me/route.ts import { getAuthUser } from "@/app/lib/auth"; export function GET(request: Request) { const user = getAuthUser(request); if (!user) { return Response.json( { error: "Unauthorized" }, { status: 401 } ); } return Response.json({ user }); } \`\`\` ## Step 3: 미들웨어로 공통 처리 (선택) \`\`\`typescript // app/api/protected/route.ts import { getAuthUser } from "@/app/lib/auth"; // 인증 체크 헬퍼 function requireAuth(request: Request) { const user = getAuthUser(request); if (!user) { return { error: Response.json({ error: "Unauthorized" }, { status: 401 }), user: null, }; } return { error: null, user }; } export function GET(request: Request) { const { error, user } = requireAuth(request); if (error) return error; return Response.json({ message: \`Hello, \${user!.name}!\`, user, }); } export async function POST(request: Request) { const { error, user } = requireAuth(request); if (error) return error; const body = await request.json(); return Response.json({ message: "Created by " + user!.name, data: body, }); } \`\`\` ## Step 4: Slot을 사용한 인증 (권장) Mandu.filling()의 guard를 사용하면 더 깔끔합니다: \`\`\`typescript // spec/slots/protected.slot.ts import { Mandu } from "@mandujs/core"; import { getAuthUser, type User } from "@/app/lib/auth"; export default Mandu.filling() .onRequest((ctx) => { // 모든 요청에서 사용자 확인 const user = getAuthUser(ctx.req); if (user) { ctx.set("user", user); } }) .guard((ctx) => { const user = ctx.get("user"); if (!user) { return ctx.unauthorized("로그인이 필요합니다"); } }) .get((ctx) => { const user = ctx.get("user"); return ctx.ok({ user }); }) .post(async (ctx) => { const user = ctx.get("user"); const body = await ctx.body(); return ctx.created({ createdBy: user!.name, data: body, }); }); \`\`\` ## Step 5: 테스트 \`\`\`bash # 인증 없이 (401 에러) curl http://localhost:3000/api/me # 인증 포함 (성공) curl http://localhost:3000/api/me \\ -H "Authorization: Bearer user_1" # POST 요청 curl -X POST http://localhost:3000/api/protected \\ -H "Authorization: Bearer user_1" \\ -H "Content-Type: application/json" \\ -d '{"title": "Hello"}' \`\`\` ## 로그인 API 추가 \`\`\`typescript // app/api/login/route.ts export async function POST(request: Request) { const { email, password } = await request.json(); // 실제로는 DB에서 사용자 확인 if (email === "admin@example.com" && password === "password") { return Response.json({ token: "Bearer user_1", user: { id: 1, email, name: "Admin" }, }); } return Response.json( { error: "Invalid credentials" }, { status: 401 } ); } \`\`\` ## 완료! 인증 시스템이 추가되었습니다: - \`POST /api/login\` - 로그인 - \`GET /api/me\` - 현재 사용자 정보 - 보호된 API는 \`Authorization\` 헤더 필요 `; export const RECIPE_ADD_ISLAND = `# Island 컴포넌트 추가하기 ## 목표 인터랙티브한 Island 컴포넌트를 페이지에 추가합니다. ## Step 1: 클라이언트 컴포넌트 생성 \`"use client"\` 지시어를 사용하여 클라이언트 컴포넌트를 만듭니다. \`\`\`tsx // app/counter/client.tsx "use client"; import { useState } from "react"; interface CounterProps { initial?: number; step?: number; } export default function Counter({ initial = 0, step = 1 }: CounterProps) { const [count, setCount] = useState(initial); return (

Interactive Counter

{count}

); } \`\`\` ## Step 2: 서버 페이지에서 partial로 사용 \`\`\`tsx // app/counter/page.tsx import { partial } from "@mandujs/core/client"; import Counter from "./client"; const CounterPartial = partial({ component: Counter, priority: "visible", }); export default function CounterPage() { return (

Counter Demo

아래 카운터는 클라이언트에서 hydration됩니다.

{/* Inline client region */}

이 텍스트는 정적 HTML입니다.

); } \`\`\` ## Step 3: 확인 \`\`\`bash bun run dev # http://localhost:3000/counter 접속 \`\`\` 버튼을 클릭하면 카운터가 동작합니다! ## 더 복잡한 Island 예제 ### Form Island \`\`\`tsx // app/contact/client.tsx "use client"; import { useState } from "react"; export default function ContactForm() { const [formData, setFormData] = useState({ name: "", email: "", message: "" }); const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setStatus("loading"); try { const res = await fetch("/api/contact", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(formData), }); if (res.ok) { setStatus("success"); setFormData({ name: "", email: "", message: "" }); } else { setStatus("error"); } } catch { setStatus("error"); } }; return (
setFormData({ ...formData, name: e.target.value })} required />
setFormData({ ...formData, email: e.target.value })} required />