/**
* 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 (
);
}
\`\`\`
### Data Fetching Island
\`\`\`tsx
// app/users/client.tsx
"use client";
import { useState, useEffect } from "react";
interface User {
id: number;
name: string;
}
export default function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/users")
.then((res) => res.json())
.then((data) => {
setUsers(data.data);
setLoading(false);
});
}, []);
if (loading) return
Loading...
;
return (
{users.map((user) => (
{user.name}
))}
);
}
\`\`\`
## Mandu.island() API (고급)
서버 데이터와 클라이언트 상태를 분리하는 page-level Island가 필요하면
\`Mandu.island({ setup, render })\`를 사용합니다. \`island("visible",
Component)\`는 지원하지 않습니다. 서버 페이지 안에 inline으로 렌더링할
영역은 \`partial()\`을 사용하세요.
\`\`\`typescript
// spec/slots/todos.client.ts
import { Mandu } from "@mandujs/core/client";
import { useState } from "react";
interface ServerData {
todos: { id: number; text: string; done: boolean }[];
}
export default Mandu.island({
setup: (serverData) => {
const [todos, setTodos] = useState(serverData.todos);
const toggle = (id: number) => {
setTodos(prev => prev.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
};
return { todos, toggle };
},
render: ({ todos, toggle }) => (