@mandujs/mcp 0.9.19 → 0.9.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +320 -0
  2. package/package.json +1 -1
  3. package/src/activity-monitor.ts +847 -231
  4. package/src/resources/handlers.ts +244 -0
  5. package/src/resources/skills/guides.ts +1136 -0
  6. package/src/resources/skills/index.ts +12 -0
  7. package/src/resources/skills/loader.ts +218 -0
  8. package/src/resources/skills/mandu-composition/SKILL.md +91 -0
  9. package/src/resources/skills/mandu-composition/metadata.json +13 -0
  10. package/src/resources/skills/mandu-composition/rules/_sections.md +26 -0
  11. package/src/resources/skills/mandu-composition/rules/_template.md +77 -0
  12. package/src/resources/skills/mandu-composition/rules/comp-arch-avoid-boolean-props.md +146 -0
  13. package/src/resources/skills/mandu-composition/rules/comp-arch-compound-components.md +164 -0
  14. package/src/resources/skills/mandu-composition/rules/comp-island-event.md +161 -0
  15. package/src/resources/skills/mandu-composition/rules/comp-island-slot-split.md +167 -0
  16. package/src/resources/skills/mandu-composition/rules/comp-pattern-children.md +149 -0
  17. package/src/resources/skills/mandu-composition/rules/comp-state-context-interface.md +148 -0
  18. package/src/resources/skills/mandu-composition/rules/comp-state-lift-state.md +150 -0
  19. package/src/resources/skills/mandu-deployment/SKILL.md +92 -0
  20. package/src/resources/skills/mandu-deployment/_sections.md +41 -0
  21. package/src/resources/skills/mandu-deployment/_template.md +38 -0
  22. package/src/resources/skills/mandu-deployment/metadata.json +13 -0
  23. package/src/resources/skills/mandu-deployment/rules/deploy-build-bun.md +109 -0
  24. package/src/resources/skills/mandu-deployment/rules/deploy-build-output.md +115 -0
  25. package/src/resources/skills/mandu-deployment/rules/deploy-cicd-github.md +219 -0
  26. package/src/resources/skills/mandu-deployment/rules/deploy-docker-bun.md +150 -0
  27. package/src/resources/skills/mandu-deployment/rules/deploy-docker-compose.md +223 -0
  28. package/src/resources/skills/mandu-deployment/rules/deploy-platform-fly.md +152 -0
  29. package/src/resources/skills/mandu-deployment/rules/deploy-platform-render.md +179 -0
  30. package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +323 -0
  31. package/src/resources/skills/mandu-deployment/rules/deploy-platform-vercel.md +140 -0
  32. package/src/resources/skills/mandu-fs-routes/SKILL.md +82 -0
  33. package/src/resources/skills/mandu-fs-routes/metadata.json +12 -0
  34. package/src/resources/skills/mandu-fs-routes/rules/_sections.md +36 -0
  35. package/src/resources/skills/mandu-fs-routes/rules/_template.md +69 -0
  36. package/src/resources/skills/mandu-fs-routes/rules/routes-api-methods.md +65 -0
  37. package/src/resources/skills/mandu-fs-routes/rules/routes-dynamic-param.md +93 -0
  38. package/src/resources/skills/mandu-fs-routes/rules/routes-naming-page.md +55 -0
  39. package/src/resources/skills/mandu-guard/SKILL.md +129 -0
  40. package/src/resources/skills/mandu-guard/metadata.json +12 -0
  41. package/src/resources/skills/mandu-guard/rules/_sections.md +36 -0
  42. package/src/resources/skills/mandu-guard/rules/_template.md +82 -0
  43. package/src/resources/skills/mandu-guard/rules/guard-config-rules.md +100 -0
  44. package/src/resources/skills/mandu-guard/rules/guard-layer-direction.md +76 -0
  45. package/src/resources/skills/mandu-guard/rules/guard-preset-mandu.md +81 -0
  46. package/src/resources/skills/mandu-guard/rules/guard-validate-import.md +80 -0
  47. package/src/resources/skills/mandu-hydration/SKILL.md +91 -0
  48. package/src/resources/skills/mandu-hydration/metadata.json +12 -0
  49. package/src/resources/skills/mandu-hydration/rules/_sections.md +31 -0
  50. package/src/resources/skills/mandu-hydration/rules/_template.md +72 -0
  51. package/src/resources/skills/mandu-hydration/rules/hydration-data-event.md +109 -0
  52. package/src/resources/skills/mandu-hydration/rules/hydration-directive-use-client.md +55 -0
  53. package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +113 -0
  54. package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +68 -0
  55. package/src/resources/skills/mandu-performance/SKILL.md +85 -0
  56. package/src/resources/skills/mandu-performance/metadata.json +14 -0
  57. package/src/resources/skills/mandu-performance/rules/_sections.md +31 -0
  58. package/src/resources/skills/mandu-performance/rules/_template.md +64 -0
  59. package/src/resources/skills/mandu-performance/rules/perf-async-defer-await.md +103 -0
  60. package/src/resources/skills/mandu-performance/rules/perf-async-parallel.md +95 -0
  61. package/src/resources/skills/mandu-performance/rules/perf-bun-file.md +124 -0
  62. package/src/resources/skills/mandu-performance/rules/perf-bun-serve.md +125 -0
  63. package/src/resources/skills/mandu-performance/rules/perf-bundle-imports.md +80 -0
  64. package/src/resources/skills/mandu-performance/rules/perf-bundle-island-lazy.md +145 -0
  65. package/src/resources/skills/mandu-performance/rules/perf-cache-react.md +98 -0
  66. package/src/resources/skills/mandu-performance/rules/perf-render-transitions.md +154 -0
  67. package/src/resources/skills/mandu-security/SKILL.md +87 -0
  68. package/src/resources/skills/mandu-security/metadata.json +13 -0
  69. package/src/resources/skills/mandu-security/rules/_sections.md +31 -0
  70. package/src/resources/skills/mandu-security/rules/_template.md +74 -0
  71. package/src/resources/skills/mandu-security/rules/sec-auth-guard.md +127 -0
  72. package/src/resources/skills/mandu-security/rules/sec-env-management.md +133 -0
  73. package/src/resources/skills/mandu-security/rules/sec-input-validate.md +148 -0
  74. package/src/resources/skills/mandu-security/rules/sec-protect-csrf.md +146 -0
  75. package/src/resources/skills/mandu-security/rules/sec-protect-headers.md +138 -0
  76. package/src/resources/skills/mandu-slot/SKILL.md +85 -0
  77. package/src/resources/skills/mandu-slot/metadata.json +12 -0
  78. package/src/resources/skills/mandu-slot/rules/_sections.md +36 -0
  79. package/src/resources/skills/mandu-slot/rules/_template.md +63 -0
  80. package/src/resources/skills/mandu-slot/rules/slot-basic-structure.md +38 -0
  81. package/src/resources/skills/mandu-slot/rules/slot-ctx-response.md +56 -0
  82. package/src/resources/skills/mandu-slot/rules/slot-guard-auth.md +59 -0
  83. package/src/resources/skills/mandu-slot/rules/slot-http-methods.md +64 -0
  84. package/src/resources/skills/mandu-styling/SKILL.md +118 -0
  85. package/src/resources/skills/mandu-styling/_sections.md +36 -0
  86. package/src/resources/skills/mandu-styling/_template.md +32 -0
  87. package/src/resources/skills/mandu-styling/metadata.json +13 -0
  88. package/src/resources/skills/mandu-styling/rules/style-component-compound.md +235 -0
  89. package/src/resources/skills/mandu-styling/rules/style-component-slots.md +255 -0
  90. package/src/resources/skills/mandu-styling/rules/style-component-tokens.md +205 -0
  91. package/src/resources/skills/mandu-styling/rules/style-island-animations.md +272 -0
  92. package/src/resources/skills/mandu-styling/rules/style-island-scoping.md +167 -0
  93. package/src/resources/skills/mandu-styling/rules/style-island-variants.md +221 -0
  94. package/src/resources/skills/mandu-styling/rules/style-perf-critical.md +209 -0
  95. package/src/resources/skills/mandu-styling/rules/style-perf-purge.md +192 -0
  96. package/src/resources/skills/mandu-styling/rules/style-setup-modules.md +162 -0
  97. package/src/resources/skills/mandu-styling/rules/style-setup-panda.md +164 -0
  98. package/src/resources/skills/mandu-styling/rules/style-setup-tailwind.md +161 -0
  99. package/src/resources/skills/mandu-styling/rules/style-theme-darkmode.md +229 -0
  100. package/src/resources/skills/mandu-testing/SKILL.md +99 -0
  101. package/src/resources/skills/mandu-testing/metadata.json +13 -0
  102. package/src/resources/skills/mandu-testing/rules/_sections.md +26 -0
  103. package/src/resources/skills/mandu-testing/rules/_template.md +65 -0
  104. package/src/resources/skills/mandu-testing/rules/test-component-island.md +195 -0
  105. package/src/resources/skills/mandu-testing/rules/test-e2e-playwright.md +196 -0
  106. package/src/resources/skills/mandu-testing/rules/test-mock-fetch.md +219 -0
  107. package/src/resources/skills/mandu-testing/rules/test-slot-unit.md +192 -0
  108. package/src/resources/skills/mandu-ui/SKILL.md +117 -0
  109. package/src/resources/skills/mandu-ui/_sections.md +23 -0
  110. package/src/resources/skills/mandu-ui/_template.md +32 -0
  111. package/src/resources/skills/mandu-ui/metadata.json +13 -0
  112. package/src/resources/skills/mandu-ui/rules/ui-accessibility-aria.md +232 -0
  113. package/src/resources/skills/mandu-ui/rules/ui-accessibility-focus.md +238 -0
  114. package/src/resources/skills/mandu-ui/rules/ui-composition-patterns.md +259 -0
  115. package/src/resources/skills/mandu-ui/rules/ui-island-integration.md +258 -0
  116. package/src/resources/skills/mandu-ui/rules/ui-radix-patterns.md +213 -0
  117. package/src/resources/skills/mandu-ui/rules/ui-shadcn-setup.md +209 -0
  118. package/src/resources/skills/recipes.ts +932 -0
  119. package/src/server.ts +3 -0
  120. package/src/tools/hydration.ts +8 -8
  121. package/src/tools/index.ts +1 -0
  122. package/src/tools/seo.ts +417 -0
@@ -0,0 +1,932 @@
1
+ /**
2
+ * Mandu MCP Skills - Recipes
3
+ * 작업별 step-by-step 레시피
4
+ */
5
+
6
+ export const RECIPE_ADD_API_ROUTE = `# API 라우트 추가하기
7
+
8
+ ## 목표
9
+ 새로운 REST API 엔드포인트를 추가합니다.
10
+
11
+ ## Step 1: 파일 생성
12
+
13
+ \`app/api/{name}/route.ts\` 파일을 생성합니다.
14
+
15
+ 예시: 사용자 API를 만들려면 \`app/api/users/route.ts\`
16
+
17
+ ## Step 2: 핸들러 작성
18
+
19
+ \`\`\`typescript
20
+ // app/api/users/route.ts
21
+
22
+ // GET /api/users
23
+ export function GET() {
24
+ const users = [
25
+ { id: 1, name: "Alice" },
26
+ { id: 2, name: "Bob" },
27
+ ];
28
+ return Response.json({ data: users });
29
+ }
30
+
31
+ // POST /api/users
32
+ export async function POST(request: Request) {
33
+ const body = await request.json();
34
+
35
+ // 유효성 검사
36
+ if (!body.name) {
37
+ return Response.json(
38
+ { error: "name is required" },
39
+ { status: 400 }
40
+ );
41
+ }
42
+
43
+ const newUser = { id: Date.now(), ...body };
44
+ return Response.json(
45
+ { data: newUser },
46
+ { status: 201 }
47
+ );
48
+ }
49
+ \`\`\`
50
+
51
+ ## Step 3: 개발 서버 실행
52
+
53
+ \`\`\`bash
54
+ bun run dev
55
+ \`\`\`
56
+
57
+ ## Step 4: 테스트
58
+
59
+ \`\`\`bash
60
+ # GET 요청
61
+ curl http://localhost:3000/api/users
62
+
63
+ # POST 요청
64
+ curl -X POST http://localhost:3000/api/users \\
65
+ -H "Content-Type: application/json" \\
66
+ -d '{"name": "Charlie"}'
67
+ \`\`\`
68
+
69
+ ## 추가 메서드
70
+
71
+ 필요에 따라 다른 HTTP 메서드도 추가할 수 있습니다:
72
+
73
+ \`\`\`typescript
74
+ export function PUT(request: Request) { ... }
75
+ export function PATCH(request: Request) { ... }
76
+ export function DELETE(request: Request) { ... }
77
+ \`\`\`
78
+
79
+ ## 동적 라우트
80
+
81
+ 특정 ID의 사용자를 처리하려면:
82
+
83
+ \`\`\`typescript
84
+ // app/api/users/[id]/route.ts
85
+
86
+ export function GET(
87
+ request: Request,
88
+ { params }: { params: { id: string } }
89
+ ) {
90
+ const { id } = params;
91
+ return Response.json({ userId: id });
92
+ }
93
+
94
+ export function DELETE(
95
+ request: Request,
96
+ { params }: { params: { id: string } }
97
+ ) {
98
+ const { id } = params;
99
+ // 삭제 로직
100
+ return new Response(null, { status: 204 });
101
+ }
102
+ \`\`\`
103
+
104
+ ## 완료!
105
+
106
+ API가 다음 URL에서 사용 가능합니다:
107
+ - GET/POST: \`http://localhost:3000/api/users\`
108
+ - GET/DELETE: \`http://localhost:3000/api/users/:id\`
109
+ `;
110
+
111
+ export const RECIPE_ADD_PAGE = `# 페이지 추가하기
112
+
113
+ ## 목표
114
+ 새로운 페이지를 추가합니다.
115
+
116
+ ## Step 1: 파일 생성
117
+
118
+ \`app/{path}/page.tsx\` 파일을 생성합니다.
119
+
120
+ 예시:
121
+ - \`app/about/page.tsx\` → \`/about\`
122
+ - \`app/products/page.tsx\` → \`/products\`
123
+ - \`app/blog/posts/page.tsx\` → \`/blog/posts\`
124
+
125
+ ## Step 2: 페이지 컴포넌트 작성
126
+
127
+ \`\`\`tsx
128
+ // app/about/page.tsx
129
+
130
+ export default function AboutPage() {
131
+ return (
132
+ <div>
133
+ <h1>About Us</h1>
134
+ <p>Welcome to our company!</p>
135
+
136
+ <section>
137
+ <h2>Our Mission</h2>
138
+ <p>Building great software.</p>
139
+ </section>
140
+
141
+ <section>
142
+ <h2>Contact</h2>
143
+ <p>Email: hello@example.com</p>
144
+ </section>
145
+ </div>
146
+ );
147
+ }
148
+ \`\`\`
149
+
150
+ ## Step 3: 메타데이터 추가 (선택)
151
+
152
+ \`\`\`tsx
153
+ // app/about/page.tsx
154
+
155
+ export const metadata = {
156
+ title: "About Us | My App",
157
+ description: "Learn about our company and mission",
158
+ };
159
+
160
+ export default function AboutPage() {
161
+ return (
162
+ <div>
163
+ <h1>About Us</h1>
164
+ {/* ... */}
165
+ </div>
166
+ );
167
+ }
168
+ \`\`\`
169
+
170
+ ## Step 4: 확인
171
+
172
+ \`\`\`bash
173
+ bun run dev
174
+ # http://localhost:3000/about 접속
175
+ \`\`\`
176
+
177
+ ## 동적 페이지
178
+
179
+ URL 파라미터를 받는 페이지:
180
+
181
+ \`\`\`tsx
182
+ // app/users/[id]/page.tsx
183
+
184
+ interface Props {
185
+ params: { id: string };
186
+ }
187
+
188
+ export default function UserPage({ params }: Props) {
189
+ return (
190
+ <div>
191
+ <h1>User Profile</h1>
192
+ <p>User ID: {params.id}</p>
193
+ </div>
194
+ );
195
+ }
196
+ \`\`\`
197
+
198
+ ## 스타일 추가
199
+
200
+ \`\`\`tsx
201
+ // app/about/page.tsx
202
+
203
+ export default function AboutPage() {
204
+ return (
205
+ <div style={{ maxWidth: "800px", margin: "0 auto", padding: "20px" }}>
206
+ <h1 style={{ color: "#333" }}>About Us</h1>
207
+ <p style={{ lineHeight: 1.6 }}>Welcome!</p>
208
+ </div>
209
+ );
210
+ }
211
+ \`\`\`
212
+
213
+ 또는 CSS 파일 사용:
214
+
215
+ \`\`\`tsx
216
+ import "./about.css";
217
+
218
+ export default function AboutPage() {
219
+ return (
220
+ <div className="about-container">
221
+ <h1 className="about-title">About Us</h1>
222
+ </div>
223
+ );
224
+ }
225
+ \`\`\`
226
+
227
+ ## 레이아웃 적용
228
+
229
+ 페이지에 레이아웃을 적용하려면 같은 폴더에 \`layout.tsx\` 생성:
230
+
231
+ \`\`\`tsx
232
+ // app/about/layout.tsx
233
+
234
+ export default function AboutLayout({ children }: { children: React.ReactNode }) {
235
+ return (
236
+ <div className="about-layout">
237
+ <nav>About Section Nav</nav>
238
+ <main>{children}</main>
239
+ </div>
240
+ );
241
+ }
242
+ \`\`\`
243
+
244
+ ## 완료!
245
+
246
+ 페이지가 \`http://localhost:3000/about\`에서 사용 가능합니다.
247
+ `;
248
+
249
+ export const RECIPE_ADD_AUTH = `# 인증 추가하기
250
+
251
+ ## 목표
252
+ API에 인증(Authentication)을 추가합니다.
253
+
254
+ ## Step 1: 인증 유틸리티 생성
255
+
256
+ \`\`\`typescript
257
+ // app/lib/auth.ts
258
+
259
+ export interface User {
260
+ id: number;
261
+ email: string;
262
+ name: string;
263
+ }
264
+
265
+ // 간단한 토큰 검증 (실제로는 JWT 등 사용)
266
+ export function verifyToken(token: string): User | null {
267
+ // 예시: "Bearer user_1" 형식
268
+ if (token.startsWith("Bearer user_")) {
269
+ const userId = parseInt(token.replace("Bearer user_", ""));
270
+ return {
271
+ id: userId,
272
+ email: \`user\${userId}@example.com\`,
273
+ name: \`User \${userId}\`,
274
+ };
275
+ }
276
+ return null;
277
+ }
278
+
279
+ export function getAuthUser(request: Request): User | null {
280
+ const authHeader = request.headers.get("Authorization");
281
+ if (!authHeader) return null;
282
+ return verifyToken(authHeader);
283
+ }
284
+ \`\`\`
285
+
286
+ ## Step 2: 인증이 필요한 API 작성
287
+
288
+ \`\`\`typescript
289
+ // app/api/me/route.ts
290
+
291
+ import { getAuthUser } from "@/app/lib/auth";
292
+
293
+ export function GET(request: Request) {
294
+ const user = getAuthUser(request);
295
+
296
+ if (!user) {
297
+ return Response.json(
298
+ { error: "Unauthorized" },
299
+ { status: 401 }
300
+ );
301
+ }
302
+
303
+ return Response.json({ user });
304
+ }
305
+ \`\`\`
306
+
307
+ ## Step 3: 미들웨어로 공통 처리 (선택)
308
+
309
+ \`\`\`typescript
310
+ // app/api/protected/route.ts
311
+
312
+ import { getAuthUser } from "@/app/lib/auth";
313
+
314
+ // 인증 체크 헬퍼
315
+ function requireAuth(request: Request) {
316
+ const user = getAuthUser(request);
317
+ if (!user) {
318
+ return {
319
+ error: Response.json({ error: "Unauthorized" }, { status: 401 }),
320
+ user: null,
321
+ };
322
+ }
323
+ return { error: null, user };
324
+ }
325
+
326
+ export function GET(request: Request) {
327
+ const { error, user } = requireAuth(request);
328
+ if (error) return error;
329
+
330
+ return Response.json({
331
+ message: \`Hello, \${user!.name}!\`,
332
+ user,
333
+ });
334
+ }
335
+
336
+ export async function POST(request: Request) {
337
+ const { error, user } = requireAuth(request);
338
+ if (error) return error;
339
+
340
+ const body = await request.json();
341
+ return Response.json({
342
+ message: "Created by " + user!.name,
343
+ data: body,
344
+ });
345
+ }
346
+ \`\`\`
347
+
348
+ ## Step 4: Slot을 사용한 인증 (권장)
349
+
350
+ Mandu.filling()의 guard를 사용하면 더 깔끔합니다:
351
+
352
+ \`\`\`typescript
353
+ // spec/slots/protected.slot.ts
354
+
355
+ import { Mandu } from "@mandujs/core";
356
+ import { getAuthUser, type User } from "@/app/lib/auth";
357
+
358
+ export default Mandu.filling()
359
+ .onRequest((ctx) => {
360
+ // 모든 요청에서 사용자 확인
361
+ const user = getAuthUser(ctx.req);
362
+ if (user) {
363
+ ctx.set("user", user);
364
+ }
365
+ })
366
+ .guard((ctx) => {
367
+ const user = ctx.get<User>("user");
368
+ if (!user) {
369
+ return ctx.unauthorized("로그인이 필요합니다");
370
+ }
371
+ })
372
+ .get((ctx) => {
373
+ const user = ctx.get<User>("user");
374
+ return ctx.ok({ user });
375
+ })
376
+ .post(async (ctx) => {
377
+ const user = ctx.get<User>("user");
378
+ const body = await ctx.body();
379
+ return ctx.created({
380
+ createdBy: user!.name,
381
+ data: body,
382
+ });
383
+ });
384
+ \`\`\`
385
+
386
+ ## Step 5: 테스트
387
+
388
+ \`\`\`bash
389
+ # 인증 없이 (401 에러)
390
+ curl http://localhost:3000/api/me
391
+
392
+ # 인증 포함 (성공)
393
+ curl http://localhost:3000/api/me \\
394
+ -H "Authorization: Bearer user_1"
395
+
396
+ # POST 요청
397
+ curl -X POST http://localhost:3000/api/protected \\
398
+ -H "Authorization: Bearer user_1" \\
399
+ -H "Content-Type: application/json" \\
400
+ -d '{"title": "Hello"}'
401
+ \`\`\`
402
+
403
+ ## 로그인 API 추가
404
+
405
+ \`\`\`typescript
406
+ // app/api/login/route.ts
407
+
408
+ export async function POST(request: Request) {
409
+ const { email, password } = await request.json();
410
+
411
+ // 실제로는 DB에서 사용자 확인
412
+ if (email === "admin@example.com" && password === "password") {
413
+ return Response.json({
414
+ token: "Bearer user_1",
415
+ user: { id: 1, email, name: "Admin" },
416
+ });
417
+ }
418
+
419
+ return Response.json(
420
+ { error: "Invalid credentials" },
421
+ { status: 401 }
422
+ );
423
+ }
424
+ \`\`\`
425
+
426
+ ## 완료!
427
+
428
+ 인증 시스템이 추가되었습니다:
429
+ - \`POST /api/login\` - 로그인
430
+ - \`GET /api/me\` - 현재 사용자 정보
431
+ - 보호된 API는 \`Authorization\` 헤더 필요
432
+ `;
433
+
434
+ export const RECIPE_ADD_ISLAND = `# Island 컴포넌트 추가하기
435
+
436
+ ## 목표
437
+ 인터랙티브한 Island 컴포넌트를 페이지에 추가합니다.
438
+
439
+ ## Step 1: 클라이언트 컴포넌트 생성
440
+
441
+ \`"use client"\` 지시어를 사용하여 클라이언트 컴포넌트를 만듭니다.
442
+
443
+ \`\`\`tsx
444
+ // app/counter/client.tsx
445
+
446
+ "use client";
447
+
448
+ import { useState } from "react";
449
+
450
+ interface CounterProps {
451
+ initial?: number;
452
+ step?: number;
453
+ }
454
+
455
+ export default function Counter({ initial = 0, step = 1 }: CounterProps) {
456
+ const [count, setCount] = useState(initial);
457
+
458
+ return (
459
+ <div style={{ padding: "20px", border: "1px solid #ccc", borderRadius: "8px" }}>
460
+ <h2>Interactive Counter</h2>
461
+ <p style={{ fontSize: "2rem", fontWeight: "bold" }}>{count}</p>
462
+ <div style={{ display: "flex", gap: "10px" }}>
463
+ <button onClick={() => setCount(c => c - step)}>-{step}</button>
464
+ <button onClick={() => setCount(initial)}>Reset</button>
465
+ <button onClick={() => setCount(c => c + step)}>+{step}</button>
466
+ </div>
467
+ </div>
468
+ );
469
+ }
470
+ \`\`\`
471
+
472
+ ## Step 2: 페이지에서 사용
473
+
474
+ \`\`\`tsx
475
+ // app/counter/page.tsx
476
+
477
+ import Counter from "./client";
478
+
479
+ export default function CounterPage() {
480
+ return (
481
+ <div style={{ padding: "20px" }}>
482
+ <h1>Counter Demo</h1>
483
+ <p>아래 카운터는 클라이언트에서 hydration됩니다.</p>
484
+
485
+ {/* Island 컴포넌트 */}
486
+ <Counter initial={10} step={5} />
487
+
488
+ <p style={{ marginTop: "20px", color: "#666" }}>
489
+ 이 텍스트는 정적 HTML입니다.
490
+ </p>
491
+ </div>
492
+ );
493
+ }
494
+ \`\`\`
495
+
496
+ ## Step 3: 확인
497
+
498
+ \`\`\`bash
499
+ bun run dev
500
+ # http://localhost:3000/counter 접속
501
+ \`\`\`
502
+
503
+ 버튼을 클릭하면 카운터가 동작합니다!
504
+
505
+ ## 더 복잡한 Island 예제
506
+
507
+ ### Form Island
508
+
509
+ \`\`\`tsx
510
+ // app/contact/client.tsx
511
+
512
+ "use client";
513
+
514
+ import { useState } from "react";
515
+
516
+ export default function ContactForm() {
517
+ const [formData, setFormData] = useState({ name: "", email: "", message: "" });
518
+ const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
519
+
520
+ const handleSubmit = async (e: React.FormEvent) => {
521
+ e.preventDefault();
522
+ setStatus("loading");
523
+
524
+ try {
525
+ const res = await fetch("/api/contact", {
526
+ method: "POST",
527
+ headers: { "Content-Type": "application/json" },
528
+ body: JSON.stringify(formData),
529
+ });
530
+
531
+ if (res.ok) {
532
+ setStatus("success");
533
+ setFormData({ name: "", email: "", message: "" });
534
+ } else {
535
+ setStatus("error");
536
+ }
537
+ } catch {
538
+ setStatus("error");
539
+ }
540
+ };
541
+
542
+ return (
543
+ <form onSubmit={handleSubmit}>
544
+ <div>
545
+ <label>Name</label>
546
+ <input
547
+ type="text"
548
+ value={formData.name}
549
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
550
+ required
551
+ />
552
+ </div>
553
+ <div>
554
+ <label>Email</label>
555
+ <input
556
+ type="email"
557
+ value={formData.email}
558
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
559
+ required
560
+ />
561
+ </div>
562
+ <div>
563
+ <label>Message</label>
564
+ <textarea
565
+ value={formData.message}
566
+ onChange={(e) => setFormData({ ...formData, message: e.target.value })}
567
+ required
568
+ />
569
+ </div>
570
+ <button type="submit" disabled={status === "loading"}>
571
+ {status === "loading" ? "Sending..." : "Send"}
572
+ </button>
573
+ {status === "success" && <p style={{ color: "green" }}>Sent!</p>}
574
+ {status === "error" && <p style={{ color: "red" }}>Failed to send</p>}
575
+ </form>
576
+ );
577
+ }
578
+ \`\`\`
579
+
580
+ ### Data Fetching Island
581
+
582
+ \`\`\`tsx
583
+ // app/users/client.tsx
584
+
585
+ "use client";
586
+
587
+ import { useState, useEffect } from "react";
588
+
589
+ interface User {
590
+ id: number;
591
+ name: string;
592
+ }
593
+
594
+ export default function UserList() {
595
+ const [users, setUsers] = useState<User[]>([]);
596
+ const [loading, setLoading] = useState(true);
597
+
598
+ useEffect(() => {
599
+ fetch("/api/users")
600
+ .then((res) => res.json())
601
+ .then((data) => {
602
+ setUsers(data.data);
603
+ setLoading(false);
604
+ });
605
+ }, []);
606
+
607
+ if (loading) return <p>Loading...</p>;
608
+
609
+ return (
610
+ <ul>
611
+ {users.map((user) => (
612
+ <li key={user.id}>{user.name}</li>
613
+ ))}
614
+ </ul>
615
+ );
616
+ }
617
+ \`\`\`
618
+
619
+ ## Mandu.island() API (고급)
620
+
621
+ 서버 데이터와 클라이언트 상태를 분리하려면:
622
+
623
+ \`\`\`typescript
624
+ // spec/slots/todos.client.ts
625
+
626
+ import { Mandu } from "@mandujs/core/client";
627
+ import { useState } from "react";
628
+
629
+ interface ServerData {
630
+ todos: { id: number; text: string; done: boolean }[];
631
+ }
632
+
633
+ export default Mandu.island<ServerData>({
634
+ setup: (serverData) => {
635
+ const [todos, setTodos] = useState(serverData.todos);
636
+
637
+ const toggle = (id: number) => {
638
+ setTodos(prev => prev.map(t =>
639
+ t.id === id ? { ...t, done: !t.done } : t
640
+ ));
641
+ };
642
+
643
+ return { todos, toggle };
644
+ },
645
+
646
+ render: ({ todos, toggle }) => (
647
+ <ul>
648
+ {todos.map(todo => (
649
+ <li key={todo.id} onClick={() => toggle(todo.id)}>
650
+ {todo.done ? "✅" : "⬜"} {todo.text}
651
+ </li>
652
+ ))}
653
+ </ul>
654
+ ),
655
+ });
656
+ \`\`\`
657
+
658
+ ## 완료!
659
+
660
+ Island 컴포넌트가 추가되었습니다:
661
+ - 정적 HTML 부분은 서버에서 렌더링
662
+ - 인터랙티브 부분만 클라이언트에서 hydration
663
+ - JavaScript 번들 크기 최소화
664
+ `;
665
+
666
+ export const RECIPE_ADD_DATABASE = `# 데이터베이스 연결하기
667
+
668
+ ## 목표
669
+ API에 데이터베이스를 연결합니다.
670
+
671
+ ## Step 1: 데이터베이스 클라이언트 설치
672
+
673
+ \`\`\`bash
674
+ # SQLite (간단한 시작)
675
+ bun add better-sqlite3
676
+
677
+ # 또는 PostgreSQL
678
+ bun add pg
679
+
680
+ # 또는 Prisma (ORM)
681
+ bun add prisma @prisma/client
682
+ \`\`\`
683
+
684
+ ## Step 2: 데이터베이스 설정
685
+
686
+ ### SQLite 예시
687
+
688
+ \`\`\`typescript
689
+ // app/lib/db.ts
690
+
691
+ import Database from "better-sqlite3";
692
+ import path from "path";
693
+
694
+ const db = new Database(path.join(process.cwd(), "data.db"));
695
+
696
+ // 테이블 생성
697
+ db.exec(\`
698
+ CREATE TABLE IF NOT EXISTS users (
699
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
700
+ name TEXT NOT NULL,
701
+ email TEXT UNIQUE NOT NULL,
702
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
703
+ )
704
+ \`);
705
+
706
+ export default db;
707
+ \`\`\`
708
+
709
+ ### Prisma 예시
710
+
711
+ \`\`\`bash
712
+ # Prisma 초기화
713
+ bunx prisma init
714
+ \`\`\`
715
+
716
+ \`\`\`prisma
717
+ // prisma/schema.prisma
718
+
719
+ generator client {
720
+ provider = "prisma-client-js"
721
+ }
722
+
723
+ datasource db {
724
+ provider = "sqlite"
725
+ url = "file:./dev.db"
726
+ }
727
+
728
+ model User {
729
+ id Int @id @default(autoincrement())
730
+ name String
731
+ email String @unique
732
+ createdAt DateTime @default(now())
733
+ }
734
+ \`\`\`
735
+
736
+ \`\`\`bash
737
+ # 마이그레이션
738
+ bunx prisma migrate dev --name init
739
+ \`\`\`
740
+
741
+ \`\`\`typescript
742
+ // app/lib/db.ts
743
+
744
+ import { PrismaClient } from "@prisma/client";
745
+
746
+ const prisma = new PrismaClient();
747
+
748
+ export default prisma;
749
+ \`\`\`
750
+
751
+ ## Step 3: API에서 사용
752
+
753
+ ### SQLite 직접 사용
754
+
755
+ \`\`\`typescript
756
+ // app/api/users/route.ts
757
+
758
+ import db from "@/app/lib/db";
759
+
760
+ export function GET() {
761
+ const users = db.prepare("SELECT * FROM users").all();
762
+ return Response.json({ data: users });
763
+ }
764
+
765
+ export async function POST(request: Request) {
766
+ const { name, email } = await request.json();
767
+
768
+ try {
769
+ const result = db
770
+ .prepare("INSERT INTO users (name, email) VALUES (?, ?)")
771
+ .run(name, email);
772
+
773
+ return Response.json(
774
+ { id: result.lastInsertRowid, name, email },
775
+ { status: 201 }
776
+ );
777
+ } catch (error) {
778
+ return Response.json(
779
+ { error: "Email already exists" },
780
+ { status: 400 }
781
+ );
782
+ }
783
+ }
784
+ \`\`\`
785
+
786
+ ### Prisma 사용
787
+
788
+ \`\`\`typescript
789
+ // app/api/users/route.ts
790
+
791
+ import prisma from "@/app/lib/db";
792
+
793
+ export async function GET() {
794
+ const users = await prisma.user.findMany({
795
+ orderBy: { createdAt: "desc" },
796
+ });
797
+ return Response.json({ data: users });
798
+ }
799
+
800
+ export async function POST(request: Request) {
801
+ const { name, email } = await request.json();
802
+
803
+ try {
804
+ const user = await prisma.user.create({
805
+ data: { name, email },
806
+ });
807
+ return Response.json(user, { status: 201 });
808
+ } catch (error) {
809
+ return Response.json(
810
+ { error: "Email already exists" },
811
+ { status: 400 }
812
+ );
813
+ }
814
+ }
815
+ \`\`\`
816
+
817
+ ## Step 4: Slot에서 사용
818
+
819
+ \`\`\`typescript
820
+ // spec/slots/users.slot.ts
821
+
822
+ import { Mandu } from "@mandujs/core";
823
+ import prisma from "@/app/lib/db";
824
+
825
+ export default Mandu.filling()
826
+ .get(async (ctx) => {
827
+ const { page = "1", limit = "10" } = ctx.query;
828
+ const skip = (parseInt(page) - 1) * parseInt(limit);
829
+
830
+ const [users, total] = await Promise.all([
831
+ prisma.user.findMany({
832
+ skip,
833
+ take: parseInt(limit),
834
+ orderBy: { createdAt: "desc" },
835
+ }),
836
+ prisma.user.count(),
837
+ ]);
838
+
839
+ return ctx.ok({
840
+ data: users,
841
+ pagination: {
842
+ page: parseInt(page),
843
+ limit: parseInt(limit),
844
+ total,
845
+ },
846
+ });
847
+ })
848
+ .post(async (ctx) => {
849
+ const body = await ctx.body<{ name: string; email: string }>();
850
+
851
+ if (!body.name || !body.email) {
852
+ return ctx.error("name과 email이 필요합니다");
853
+ }
854
+
855
+ try {
856
+ const user = await prisma.user.create({
857
+ data: body,
858
+ });
859
+ return ctx.created({ data: user });
860
+ } catch {
861
+ return ctx.error("이미 존재하는 이메일입니다");
862
+ }
863
+ });
864
+ \`\`\`
865
+
866
+ ## Step 5: 테스트
867
+
868
+ \`\`\`bash
869
+ # 사용자 생성
870
+ curl -X POST http://localhost:3000/api/users \\
871
+ -H "Content-Type: application/json" \\
872
+ -d '{"name": "Alice", "email": "alice@example.com"}'
873
+
874
+ # 사용자 목록
875
+ curl http://localhost:3000/api/users
876
+
877
+ # 페이지네이션
878
+ curl "http://localhost:3000/api/users?page=1&limit=5"
879
+ \`\`\`
880
+
881
+ ## 완료!
882
+
883
+ 데이터베이스가 연결되었습니다:
884
+ - SQLite 또는 PostgreSQL 선택
885
+ - Prisma ORM으로 타입 안전한 쿼리
886
+ - API에서 CRUD 작업 가능
887
+ `;
888
+
889
+ // 모든 레시피 목록
890
+ export const RECIPES = {
891
+ "add-api-route": RECIPE_ADD_API_ROUTE,
892
+ "add-page": RECIPE_ADD_PAGE,
893
+ "add-auth": RECIPE_ADD_AUTH,
894
+ "add-island": RECIPE_ADD_ISLAND,
895
+ "add-database": RECIPE_ADD_DATABASE,
896
+ } as const;
897
+
898
+ export type RecipeId = keyof typeof RECIPES;
899
+
900
+ export function getRecipe(id: string): string | null {
901
+ return RECIPES[id as RecipeId] || null;
902
+ }
903
+
904
+ export function listRecipes(): { id: string; title: string; description: string }[] {
905
+ return [
906
+ {
907
+ id: "add-api-route",
908
+ title: "API 라우트 추가하기",
909
+ description: "새로운 REST API 엔드포인트를 추가하는 방법",
910
+ },
911
+ {
912
+ id: "add-page",
913
+ title: "페이지 추가하기",
914
+ description: "새로운 페이지를 추가하는 방법",
915
+ },
916
+ {
917
+ id: "add-auth",
918
+ title: "인증 추가하기",
919
+ description: "API에 인증(Authentication)을 추가하는 방법",
920
+ },
921
+ {
922
+ id: "add-island",
923
+ title: "Island 컴포넌트 추가하기",
924
+ description: "인터랙티브한 Island 컴포넌트를 추가하는 방법",
925
+ },
926
+ {
927
+ id: "add-database",
928
+ title: "데이터베이스 연결하기",
929
+ description: "SQLite/PostgreSQL/Prisma로 데이터베이스를 연결하는 방법",
930
+ },
931
+ ];
932
+ }