@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,1136 @@
1
+ /**
2
+ * Mandu MCP Skills - Guides
3
+ * 에이전트가 Mandu를 학습할 수 있는 가이드 문서
4
+ */
5
+
6
+ export const GUIDE_SLOT = `# Mandu Slot 작성 가이드
7
+
8
+ ## 개요
9
+
10
+ Slot은 비즈니스 로직을 작성하는 파일입니다. \`Mandu.filling()\` API를 사용합니다.
11
+
12
+ ## 파일 위치
13
+
14
+ \`\`\`
15
+ spec/slots/{name}.slot.ts # 서버 로직
16
+ spec/slots/{name}.client.ts # 클라이언트 로직 (Island)
17
+ \`\`\`
18
+
19
+ ## 기본 구조
20
+
21
+ \`\`\`typescript
22
+ import { Mandu } from "@mandujs/core";
23
+
24
+ export default Mandu.filling()
25
+ .get((ctx) => {
26
+ return ctx.ok({ message: "Hello!" });
27
+ });
28
+ \`\`\`
29
+
30
+ ## HTTP 메서드
31
+
32
+ \`\`\`typescript
33
+ export default Mandu.filling()
34
+ .get((ctx) => ctx.ok({ data: [] })) // GET
35
+ .post(async (ctx) => { // POST
36
+ const body = await ctx.body();
37
+ return ctx.created({ data: body });
38
+ })
39
+ .put(async (ctx) => { ... }) // PUT
40
+ .patch(async (ctx) => { ... }) // PATCH
41
+ .delete((ctx) => ctx.noContent()); // DELETE
42
+ \`\`\`
43
+
44
+ ## Context API
45
+
46
+ ### 응답 메서드
47
+
48
+ | 메서드 | HTTP 상태 | 설명 |
49
+ |--------|-----------|------|
50
+ | \`ctx.ok(data)\` | 200 | 성공 |
51
+ | \`ctx.created(data)\` | 201 | 생성됨 |
52
+ | \`ctx.noContent()\` | 204 | 내용 없음 |
53
+ | \`ctx.error(message)\` | 400 | 잘못된 요청 |
54
+ | \`ctx.unauthorized(message)\` | 401 | 인증 필요 |
55
+ | \`ctx.forbidden(message)\` | 403 | 권한 없음 |
56
+ | \`ctx.notFound(message)\` | 404 | 찾을 수 없음 |
57
+ | \`ctx.fail(message)\` | 500 | 서버 오류 |
58
+
59
+ ### 요청 데이터
60
+
61
+ \`\`\`typescript
62
+ // Request body (POST, PUT, PATCH)
63
+ const body = await ctx.body<{ name: string }>();
64
+
65
+ // URL 파라미터 (/users/:id)
66
+ const { id } = ctx.params;
67
+
68
+ // Query string (?page=1&limit=10)
69
+ const { page, limit } = ctx.query;
70
+
71
+ // Headers
72
+ const authHeader = ctx.headers.get("authorization");
73
+ \`\`\`
74
+
75
+ ### 상태 저장/조회
76
+
77
+ \`\`\`typescript
78
+ // 저장
79
+ ctx.set("user", { id: 1, name: "Alice" });
80
+
81
+ // 조회
82
+ const user = ctx.get<User>("user");
83
+ \`\`\`
84
+
85
+ ## 가드 (인증/권한)
86
+
87
+ \`\`\`typescript
88
+ export default Mandu.filling()
89
+ .guard((ctx) => {
90
+ const user = ctx.get("user");
91
+ if (!user) {
92
+ return ctx.unauthorized("로그인이 필요합니다");
93
+ }
94
+ // void 반환 시 계속 진행
95
+ })
96
+ .get((ctx) => {
97
+ const user = ctx.get("user");
98
+ return ctx.ok({ user });
99
+ });
100
+ \`\`\`
101
+
102
+ ## 라이프사이클 훅
103
+
104
+ \`\`\`typescript
105
+ export default Mandu.filling()
106
+ .onRequest((ctx) => {
107
+ // 요청 시작 시
108
+ ctx.set("startTime", Date.now());
109
+ })
110
+ .beforeHandle((ctx) => {
111
+ // 핸들러 전 (가드 역할)
112
+ })
113
+ .afterHandle((ctx, res) => {
114
+ // 핸들러 후
115
+ return res;
116
+ })
117
+ .afterResponse((ctx) => {
118
+ // 응답 후 (로깅 등)
119
+ console.log("Duration:", Date.now() - ctx.get("startTime"));
120
+ })
121
+ .get((ctx) => ctx.ok({ data: [] }));
122
+ \`\`\`
123
+
124
+ ## 전체 예제: CRUD API
125
+
126
+ \`\`\`typescript
127
+ import { Mandu } from "@mandujs/core";
128
+
129
+ interface User {
130
+ id: number;
131
+ name: string;
132
+ email: string;
133
+ }
134
+
135
+ const users: User[] = [];
136
+
137
+ export default Mandu.filling()
138
+ .guard((ctx) => {
139
+ const apiKey = ctx.headers.get("x-api-key");
140
+ if (apiKey !== "secret") {
141
+ return ctx.unauthorized("Invalid API key");
142
+ }
143
+ })
144
+ .get((ctx) => {
145
+ const { page = "1", limit = "10" } = ctx.query;
146
+ const start = (parseInt(page) - 1) * parseInt(limit);
147
+ const items = users.slice(start, start + parseInt(limit));
148
+ return ctx.ok({ data: items, total: users.length });
149
+ })
150
+ .post(async (ctx) => {
151
+ const body = await ctx.body<{ name: string; email: string }>();
152
+
153
+ if (!body.name || !body.email) {
154
+ return ctx.error("name과 email이 필요합니다");
155
+ }
156
+
157
+ const newUser: User = {
158
+ id: users.length + 1,
159
+ ...body,
160
+ };
161
+ users.push(newUser);
162
+
163
+ return ctx.created({ data: newUser });
164
+ });
165
+ \`\`\`
166
+ `;
167
+
168
+ export const GUIDE_FS_ROUTES = `# Mandu FS Routes 가이드
169
+
170
+ ## 개요
171
+
172
+ FS Routes는 파일 시스템 기반 라우팅입니다. \`app/\` 폴더의 파일 구조가 URL이 됩니다.
173
+
174
+ ## 기본 규칙
175
+
176
+ | 파일 경로 | URL |
177
+ |-----------|-----|
178
+ | \`app/page.tsx\` | \`/\` |
179
+ | \`app/about/page.tsx\` | \`/about\` |
180
+ | \`app/users/page.tsx\` | \`/users\` |
181
+ | \`app/api/health/route.ts\` | \`/api/health\` |
182
+
183
+ ## 특수 파일
184
+
185
+ | 파일명 | 용도 |
186
+ |--------|------|
187
+ | \`page.tsx\` | 페이지 컴포넌트 |
188
+ | \`route.ts\` | API 핸들러 |
189
+ | \`layout.tsx\` | 레이아웃 (하위 페이지 감싸기) |
190
+ | \`loading.tsx\` | 로딩 UI |
191
+ | \`error.tsx\` | 에러 UI |
192
+ | \`slot.ts\` | 서버 비즈니스 로직 |
193
+ | \`client.tsx\` | 클라이언트 인터랙티브 컴포넌트 |
194
+
195
+ ## 동적 라우트
196
+
197
+ ### 단일 파라미터
198
+
199
+ \`\`\`
200
+ app/users/[id]/page.tsx → /users/123, /users/456
201
+ \`\`\`
202
+
203
+ \`\`\`tsx
204
+ export default function UserPage({ params }: { params: { id: string } }) {
205
+ return <h1>User ID: {params.id}</h1>;
206
+ }
207
+ \`\`\`
208
+
209
+ ### Catch-all
210
+
211
+ \`\`\`
212
+ app/docs/[...slug]/page.tsx → /docs/a, /docs/a/b, /docs/a/b/c
213
+ \`\`\`
214
+
215
+ \`\`\`tsx
216
+ export default function DocsPage({ params }: { params: { slug: string[] } }) {
217
+ return <h1>Path: {params.slug.join("/")}</h1>;
218
+ }
219
+ \`\`\`
220
+
221
+ ### Optional Catch-all
222
+
223
+ \`\`\`
224
+ app/shop/[[...slug]]/page.tsx → /shop, /shop/a, /shop/a/b
225
+ \`\`\`
226
+
227
+ ## 라우트 그룹
228
+
229
+ 괄호로 감싸면 URL에 포함되지 않음:
230
+
231
+ \`\`\`
232
+ app/(auth)/login/page.tsx → /login
233
+ app/(auth)/register/page.tsx → /register
234
+ app/(dashboard)/home/page.tsx → /home
235
+ \`\`\`
236
+
237
+ ## API 라우트
238
+
239
+ ### 기본 구조
240
+
241
+ \`\`\`typescript
242
+ // app/api/users/route.ts
243
+
244
+ export function GET() {
245
+ return Response.json({ users: [] });
246
+ }
247
+
248
+ export async function POST(request: Request) {
249
+ const body = await request.json();
250
+ return Response.json({ created: body }, { status: 201 });
251
+ }
252
+
253
+ export function DELETE() {
254
+ return new Response(null, { status: 204 });
255
+ }
256
+ \`\`\`
257
+
258
+ ### 지원 메서드
259
+
260
+ - \`GET\`, \`POST\`, \`PUT\`, \`PATCH\`, \`DELETE\`, \`HEAD\`, \`OPTIONS\`
261
+
262
+ ## 페이지 컴포넌트
263
+
264
+ ### 기본 구조
265
+
266
+ \`\`\`tsx
267
+ // app/page.tsx
268
+
269
+ export default function Home() {
270
+ return (
271
+ <div>
272
+ <h1>Welcome to Mandu!</h1>
273
+ </div>
274
+ );
275
+ }
276
+ \`\`\`
277
+
278
+ ### 메타데이터
279
+
280
+ \`\`\`tsx
281
+ export const metadata = {
282
+ title: "Home | My App",
283
+ description: "Welcome page",
284
+ };
285
+
286
+ export default function Home() {
287
+ return <h1>Home</h1>;
288
+ }
289
+ \`\`\`
290
+
291
+ ## 레이아웃
292
+
293
+ \`\`\`tsx
294
+ // app/layout.tsx
295
+
296
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
297
+ return (
298
+ <html>
299
+ <head>
300
+ <title>My App</title>
301
+ </head>
302
+ <body>
303
+ <nav>...</nav>
304
+ <main>{children}</main>
305
+ <footer>...</footer>
306
+ </body>
307
+ </html>
308
+ );
309
+ }
310
+ \`\`\`
311
+
312
+ ### 중첩 레이아웃
313
+
314
+ \`\`\`
315
+ app/
316
+ ├── layout.tsx # 루트 레이아웃
317
+ ├── page.tsx
318
+ └── dashboard/
319
+ ├── layout.tsx # 대시보드 레이아웃 (루트 안에 중첩)
320
+ └── page.tsx
321
+ \`\`\`
322
+
323
+ ## 프로젝트 구조 예시
324
+
325
+ \`\`\`
326
+ app/
327
+ ├── page.tsx # /
328
+ ├── layout.tsx # 루트 레이아웃
329
+ ├── about/
330
+ │ └── page.tsx # /about
331
+ ├── users/
332
+ │ ├── page.tsx # /users
333
+ │ └── [id]/
334
+ │ └── page.tsx # /users/:id
335
+ ├── api/
336
+ │ ├── health/
337
+ │ │ └── route.ts # /api/health
338
+ │ └── users/
339
+ │ ├── route.ts # /api/users
340
+ │ └── [id]/
341
+ │ └── route.ts # /api/users/:id
342
+ └── (auth)/
343
+ ├── login/
344
+ │ └── page.tsx # /login
345
+ └── register/
346
+ └── page.tsx # /register
347
+ \`\`\`
348
+ `;
349
+
350
+ export const GUIDE_HYDRATION = `# Mandu Island Hydration 가이드
351
+
352
+ ## 개요
353
+
354
+ Island Hydration은 페이지의 일부분만 클라이언트에서 인터랙티브하게 만드는 기술입니다.
355
+ 대부분의 페이지는 정적 HTML로 유지하고, 필요한 부분만 JavaScript를 로드합니다.
356
+
357
+ ## 장점
358
+
359
+ - **빠른 초기 로드**: 대부분 정적 HTML
360
+ - **적은 JavaScript**: 필요한 부분만 로드
361
+ - **SEO 친화적**: 완전한 HTML 콘텐츠
362
+
363
+ ## Hydration 전략
364
+
365
+ | 전략 | 설명 | 사용 사례 |
366
+ |------|------|----------|
367
+ | \`none\` | JavaScript 없음 | 순수 정적 페이지 |
368
+ | \`island\` | 부분 hydration (기본값) | 정적 + 인터랙티브 혼합 |
369
+ | \`full\` | 전체 hydration | SPA 스타일 페이지 |
370
+
371
+ ## Hydration 우선순위
372
+
373
+ | 우선순위 | 로드 시점 | 사용 사례 |
374
+ |----------|----------|----------|
375
+ | \`immediate\` | 페이지 로드 시 | 중요한 인터랙션 |
376
+ | \`visible\` | 뷰포트 진입 시 (기본값) | 스크롤 아래 콘텐츠 |
377
+ | \`idle\` | 브라우저 유휴 시 | 비중요 기능 |
378
+ | \`interaction\` | 사용자 상호작용 시 | 클릭해야 활성화 |
379
+
380
+ ## Island 만들기
381
+
382
+ ### 1. 클라이언트 컴포넌트 작성
383
+
384
+ \`\`\`tsx
385
+ // app/counter/client.tsx
386
+
387
+ "use client";
388
+
389
+ import { useState } from "react";
390
+
391
+ export default function Counter({ initial = 0 }: { initial?: number }) {
392
+ const [count, setCount] = useState(initial);
393
+
394
+ return (
395
+ <div>
396
+ <p>Count: {count}</p>
397
+ <button onClick={() => setCount(c => c - 1)}>-</button>
398
+ <button onClick={() => setCount(c => c + 1)}>+</button>
399
+ </div>
400
+ );
401
+ }
402
+ \`\`\`
403
+
404
+ ### 2. 페이지에서 사용
405
+
406
+ \`\`\`tsx
407
+ // app/counter/page.tsx
408
+
409
+ import Counter from "./client";
410
+
411
+ export default function CounterPage() {
412
+ return (
413
+ <div>
414
+ <h1>Counter Demo</h1>
415
+ <p>이 텍스트는 정적 HTML입니다.</p>
416
+
417
+ {/* 이 부분만 hydration됩니다 */}
418
+ <Counter initial={10} />
419
+ </div>
420
+ );
421
+ }
422
+ \`\`\`
423
+
424
+ ## Mandu.island() API
425
+
426
+ 고급 Island 패턴을 위한 API:
427
+
428
+ \`\`\`typescript
429
+ // spec/slots/todos.client.ts
430
+
431
+ import { Mandu } from "@mandujs/core/client";
432
+ import { useState, useCallback } from "react";
433
+
434
+ interface TodosData {
435
+ todos: { id: number; text: string; done: boolean }[];
436
+ }
437
+
438
+ export default Mandu.island<TodosData>({
439
+ // Setup: 서버 데이터로 클라이언트 상태 초기화
440
+ setup: (serverData) => {
441
+ const [todos, setTodos] = useState(serverData.todos);
442
+
443
+ const addTodo = useCallback((text: string) => {
444
+ setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
445
+ }, []);
446
+
447
+ const toggleTodo = useCallback((id: number) => {
448
+ setTodos(prev => prev.map(t =>
449
+ t.id === id ? { ...t, done: !t.done } : t
450
+ ));
451
+ }, []);
452
+
453
+ return { todos, addTodo, toggleTodo };
454
+ },
455
+
456
+ // Render: 순수 렌더링 로직
457
+ render: ({ todos, addTodo, toggleTodo }) => (
458
+ <div>
459
+ <ul>
460
+ {todos.map(todo => (
461
+ <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
462
+ {todo.done ? "✅" : "⬜"} {todo.text}
463
+ </li>
464
+ ))}
465
+ </ul>
466
+ <button onClick={() => addTodo("New Todo")}>Add</button>
467
+ </div>
468
+ ),
469
+
470
+ // 선택: 에러 UI
471
+ errorBoundary: (error, reset) => (
472
+ <div>
473
+ <p>Error: {error.message}</p>
474
+ <button onClick={reset}>Retry</button>
475
+ </div>
476
+ ),
477
+
478
+ // 선택: 로딩 UI
479
+ loading: () => <p>Loading...</p>,
480
+ });
481
+ \`\`\`
482
+
483
+ ## Island 간 통신
484
+
485
+ \`\`\`typescript
486
+ import { useIslandEvent } from "@mandujs/core/client";
487
+
488
+ // Island A: 이벤트 발송
489
+ const { emit } = useIslandEvent<{ count: number }>("counter-update");
490
+ emit({ count: 42 });
491
+
492
+ // Island B: 이벤트 수신
493
+ useIslandEvent<{ count: number }>("counter-update", (data) => {
494
+ console.log("Received:", data.count);
495
+ });
496
+ \`\`\`
497
+
498
+ ## 클라이언트 훅
499
+
500
+ \`\`\`typescript
501
+ import {
502
+ useServerData,
503
+ useHydrated,
504
+ useIslandEvent,
505
+ } from "@mandujs/core/client";
506
+
507
+ // SSR 데이터 접근
508
+ const data = useServerData<UserData>("user", defaultValue);
509
+
510
+ // Hydration 완료 여부
511
+ const isHydrated = useHydrated();
512
+ \`\`\`
513
+
514
+ ## 빌드
515
+
516
+ \`\`\`bash
517
+ # 클라이언트 번들 빌드
518
+ bun run build
519
+
520
+ # 개발 모드 (HMR 포함)
521
+ bun run dev
522
+ \`\`\`
523
+ `;
524
+
525
+ export const GUIDE_GUARD = `# Mandu Guard 가이드
526
+
527
+ ## 개요
528
+
529
+ Mandu Guard는 아키텍처 규칙을 강제하는 시스템입니다.
530
+ 레이어 간 의존성을 검사하고 위반을 실시간으로 감지합니다.
531
+
532
+ ## 사용법
533
+
534
+ \`\`\`bash
535
+ # 아키텍처 검사
536
+ bunx mandu guard arch
537
+
538
+ # 실시간 감시
539
+ bunx mandu guard arch --watch
540
+
541
+ # CI 모드 (위반 시 exit 1)
542
+ bunx mandu guard arch --ci
543
+
544
+ # 특정 프리셋 사용
545
+ bunx mandu guard arch --preset fsd
546
+ \`\`\`
547
+
548
+ ## 프리셋
549
+
550
+ | 프리셋 | 설명 | 사용 사례 |
551
+ |--------|------|----------|
552
+ | \`mandu\` | FSD + Clean 하이브리드 (기본값) | 풀스택 프로젝트 |
553
+ | \`fsd\` | Feature-Sliced Design | 프론트엔드 중심 |
554
+ | \`clean\` | Clean Architecture | 백엔드 중심 |
555
+ | \`hexagonal\` | Hexagonal/Ports & Adapters | 도메인 중심 |
556
+ | \`atomic\` | Atomic Design | UI 컴포넌트 라이브러리 |
557
+
558
+ ## Mandu 프리셋 레이어
559
+
560
+ ### 프론트엔드 (FSD)
561
+
562
+ \`\`\`
563
+ app # 최상위: 앱 진입점
564
+
565
+ pages # 페이지 컴포넌트
566
+
567
+ widgets # 복합 UI 블록
568
+
569
+ features # 기능 단위
570
+
571
+ entities # 비즈니스 엔티티
572
+
573
+ shared # 공유 유틸리티
574
+ \`\`\`
575
+
576
+ ### 백엔드 (Clean)
577
+
578
+ \`\`\`
579
+ api # 최상위: API 진입점
580
+
581
+ application # 유스케이스
582
+
583
+ domain # 비즈니스 로직
584
+
585
+ infra # 인프라 (DB, 외부 API)
586
+
587
+ core # 핵심 유틸리티
588
+
589
+ shared # 공유
590
+ \`\`\`
591
+
592
+ ## 규칙
593
+
594
+ ### 의존성 방향
595
+
596
+ - 상위 레이어 → 하위 레이어 ✅
597
+ - 하위 레이어 → 상위 레이어 ❌
598
+
599
+ \`\`\`typescript
600
+ // ✅ OK: features → entities
601
+ import { User } from "@/entities/user";
602
+
603
+ // ❌ VIOLATION: entities → features
604
+ import { useAuth } from "@/features/auth";
605
+ \`\`\`
606
+
607
+ ### 같은 레이어 내
608
+
609
+ - 같은 레이어 내 다른 모듈 import ❌ (일반적으로)
610
+ - shared 레이어는 예외
611
+
612
+ ## 검사 규칙
613
+
614
+ | 규칙 ID | 설명 |
615
+ |---------|------|
616
+ | \`LAYER_VIOLATION\` | 레이어 의존성 위반 |
617
+ | \`GENERATED_DIRECT_EDIT\` | generated 파일 직접 수정 |
618
+ | \`WRONG_SLOT_LOCATION\` | 잘못된 slot 파일 위치 |
619
+ | \`SLOT_NAMING\` | slot 파일 이름 규칙 위반 |
620
+ | \`FORBIDDEN_IMPORT\` | 금지된 import (fs, child_process 등) |
621
+
622
+ ## 설정
623
+
624
+ 프로젝트 루트에 \`mandu.config.ts\` 또는 \`.mandu/guard.json\`:
625
+
626
+ \`\`\`typescript
627
+ // mandu.config.ts
628
+ export default {
629
+ guard: {
630
+ preset: "mandu",
631
+ rules: {
632
+ // 특정 규칙 비활성화
633
+ "LAYER_VIOLATION": "warn", // error | warn | off
634
+ },
635
+ ignore: [
636
+ "**/test/**",
637
+ "**/*.test.ts",
638
+ ],
639
+ },
640
+ };
641
+ \`\`\`
642
+
643
+ ## MCP 도구
644
+
645
+ \`\`\`typescript
646
+ // 아키텍처 검사
647
+ mandu_guard_check()
648
+
649
+ // 파일 위치 검사
650
+ mandu_check_location({ filePath: "src/features/auth/index.ts" })
651
+
652
+ // import 검사
653
+ mandu_check_import({
654
+ fromFile: "src/features/auth/index.ts",
655
+ importPath: "@/entities/user"
656
+ })
657
+
658
+ // 아키텍처 규칙 조회
659
+ mandu_get_architecture()
660
+ \`\`\`
661
+
662
+ ## 실시간 감시
663
+
664
+ \`\`\`bash
665
+ # CLI로 시작
666
+ bunx mandu guard arch --watch
667
+
668
+ # 또는 MCP로 시작
669
+ mandu_watch_start()
670
+ \`\`\`
671
+
672
+ ### 감시 이벤트
673
+
674
+ 파일 변경 시 자동으로:
675
+ 1. 아키텍처 규칙 검사
676
+ 2. 위반 감지 시 경고
677
+ 3. MCP push notification (에이전트에게 알림)
678
+
679
+ ## 리포트
680
+
681
+ \`\`\`bash
682
+ # Markdown 리포트 생성
683
+ bunx mandu guard arch --output report.md --report-format markdown
684
+
685
+ # JSON 리포트
686
+ bunx mandu guard arch --output report.json --report-format json
687
+ \`\`\`
688
+
689
+ ## 자동 수정
690
+
691
+ 일부 위반은 자동 수정 가능:
692
+
693
+ \`\`\`bash
694
+ bunx mandu guard arch --auto-correct
695
+ \`\`\`
696
+
697
+ 또는 MCP:
698
+
699
+ \`\`\`typescript
700
+ mandu_doctor({ autoFix: true })
701
+ \`\`\`
702
+
703
+ ## 폴더 구조 예시 (Mandu 프리셋)
704
+
705
+ \`\`\`
706
+ src/
707
+ ├── app/ # 앱 진입점
708
+ │ └── main.tsx
709
+ ├── pages/ # 페이지
710
+ │ ├── home/
711
+ │ └── users/
712
+ ├── widgets/ # 복합 UI
713
+ │ ├── header/
714
+ │ └── sidebar/
715
+ ├── features/ # 기능
716
+ │ ├── auth/
717
+ │ └── cart/
718
+ ├── entities/ # 엔티티
719
+ │ ├── user/
720
+ │ └── product/
721
+ ├── shared/ # 공유
722
+ │ ├── ui/
723
+ │ ├── lib/
724
+ │ └── config/
725
+ └── api/ # 백엔드 API
726
+ ├── application/
727
+ ├── domain/
728
+ └── infra/
729
+ \`\`\`
730
+ `;
731
+
732
+ export const GUIDE_SEO = `# Mandu SEO 가이드
733
+
734
+ ## 개요
735
+
736
+ Mandu SEO 모듈은 Next.js Metadata API 패턴을 따릅니다.
737
+ 정적/동적 메타데이터, Open Graph, Twitter Cards, JSON-LD 구조화 데이터를 지원합니다.
738
+
739
+ ## 정적 메타데이터
740
+
741
+ \`\`\`typescript
742
+ // app/layout.tsx
743
+ import type { Metadata } from '@mandujs/core'
744
+
745
+ export const metadata: Metadata = {
746
+ metadataBase: new URL('https://example.com'),
747
+ title: {
748
+ template: '%s | My Site',
749
+ default: 'My Site',
750
+ },
751
+ description: 'Welcome to my site',
752
+ openGraph: {
753
+ siteName: 'My Site',
754
+ type: 'website',
755
+ },
756
+ }
757
+
758
+ export default function RootLayout({ children }) {
759
+ return <html><body>{children}</body></html>
760
+ }
761
+ \`\`\`
762
+
763
+ ## 동적 메타데이터
764
+
765
+ \`\`\`typescript
766
+ // app/blog/[slug]/page.tsx
767
+ import type { Metadata, MetadataParams } from '@mandujs/core'
768
+
769
+ export async function generateMetadata({ params }: MetadataParams): Promise<Metadata> {
770
+ const post = await getPost(params.slug)
771
+
772
+ return {
773
+ title: post.title,
774
+ description: post.excerpt,
775
+ openGraph: {
776
+ title: post.title,
777
+ images: [post.coverImage],
778
+ },
779
+ }
780
+ }
781
+ \`\`\`
782
+
783
+ ## 타이틀 템플릿
784
+
785
+ \`\`\`typescript
786
+ // 루트 레이아웃
787
+ export const metadata: Metadata = {
788
+ title: {
789
+ template: '%s | My Site', // 자식 타이틀이 %s에 삽입
790
+ default: 'Home | My Site', // 기본값
791
+ },
792
+ }
793
+
794
+ // 페이지 (템플릿 상속)
795
+ export const metadata: Metadata = {
796
+ title: 'About', // 결과: "About | My Site"
797
+ }
798
+
799
+ // 템플릿 무시
800
+ export const metadata: Metadata = {
801
+ title: {
802
+ absolute: 'Custom Title', // 템플릿 무시
803
+ },
804
+ }
805
+ \`\`\`
806
+
807
+ ## Open Graph
808
+
809
+ \`\`\`typescript
810
+ export const metadata: Metadata = {
811
+ openGraph: {
812
+ title: 'Page Title',
813
+ description: 'Page description',
814
+ url: 'https://example.com/page',
815
+ siteName: 'My Site',
816
+ images: [
817
+ {
818
+ url: 'https://example.com/og-image.jpg',
819
+ width: 1200,
820
+ height: 630,
821
+ alt: 'OG Image',
822
+ },
823
+ ],
824
+ locale: 'ko_KR',
825
+ type: 'website',
826
+ },
827
+ }
828
+ \`\`\`
829
+
830
+ ### Article Open Graph
831
+
832
+ \`\`\`typescript
833
+ export const metadata: Metadata = {
834
+ openGraph: {
835
+ type: 'article',
836
+ publishedTime: '2024-01-15T00:00:00Z',
837
+ modifiedTime: '2024-01-16T00:00:00Z',
838
+ authors: ['https://example.com/author'],
839
+ section: 'Technology',
840
+ tags: ['React', 'Next.js'],
841
+ },
842
+ }
843
+ \`\`\`
844
+
845
+ ## Twitter Cards
846
+
847
+ \`\`\`typescript
848
+ export const metadata: Metadata = {
849
+ twitter: {
850
+ card: 'summary_large_image',
851
+ title: 'Page Title',
852
+ description: 'Page description',
853
+ site: '@mysite',
854
+ creator: '@author',
855
+ images: ['https://example.com/twitter-image.jpg'],
856
+ },
857
+ }
858
+ \`\`\`
859
+
860
+ ## JSON-LD 구조화 데이터
861
+
862
+ ### 헬퍼 함수 사용
863
+
864
+ \`\`\`typescript
865
+ import {
866
+ createArticleJsonLd,
867
+ createBreadcrumbJsonLd,
868
+ createOrganizationJsonLd,
869
+ createFAQJsonLd,
870
+ createProductJsonLd,
871
+ createLocalBusinessJsonLd,
872
+ createVideoJsonLd,
873
+ createEventJsonLd,
874
+ } from '@mandujs/core'
875
+
876
+ export const metadata: Metadata = {
877
+ jsonLd: [
878
+ createArticleJsonLd({
879
+ headline: 'Article Title',
880
+ author: 'John Doe',
881
+ datePublished: new Date('2024-01-15'),
882
+ publisher: {
883
+ name: 'My Blog',
884
+ logo: 'https://example.com/logo.png',
885
+ },
886
+ }),
887
+ createBreadcrumbJsonLd([
888
+ { name: 'Home', url: 'https://example.com' },
889
+ { name: 'Blog', url: 'https://example.com/blog' },
890
+ ]),
891
+ ],
892
+ }
893
+ \`\`\`
894
+
895
+ ### 직접 작성
896
+
897
+ \`\`\`typescript
898
+ export const metadata: Metadata = {
899
+ jsonLd: {
900
+ '@context': 'https://schema.org',
901
+ '@type': 'Article',
902
+ headline: 'Article Title',
903
+ author: {
904
+ '@type': 'Person',
905
+ name: 'John Doe',
906
+ },
907
+ },
908
+ }
909
+ \`\`\`
910
+
911
+ ## Sitemap
912
+
913
+ \`\`\`typescript
914
+ // app/sitemap.ts
915
+ import type { Sitemap } from '@mandujs/core'
916
+
917
+ export default function sitemap(): Sitemap {
918
+ return [
919
+ {
920
+ url: 'https://example.com',
921
+ lastModified: new Date(),
922
+ changeFrequency: 'daily',
923
+ priority: 1.0,
924
+ },
925
+ {
926
+ url: 'https://example.com/about',
927
+ lastModified: new Date(),
928
+ changeFrequency: 'monthly',
929
+ priority: 0.8,
930
+ },
931
+ {
932
+ url: 'https://example.com/blog',
933
+ images: ['https://example.com/blog-cover.jpg'],
934
+ alternates: {
935
+ languages: {
936
+ en: 'https://example.com/en/blog',
937
+ ko: 'https://example.com/ko/blog',
938
+ },
939
+ },
940
+ },
941
+ ]
942
+ }
943
+ \`\`\`
944
+
945
+ ## Robots.txt
946
+
947
+ \`\`\`typescript
948
+ // app/robots.ts
949
+ import type { RobotsFile } from '@mandujs/core'
950
+
951
+ export default function robots(): RobotsFile {
952
+ return {
953
+ rules: [
954
+ {
955
+ userAgent: '*',
956
+ allow: '/',
957
+ disallow: ['/admin', '/private'],
958
+ },
959
+ {
960
+ userAgent: 'Googlebot',
961
+ allow: '/',
962
+ crawlDelay: 2,
963
+ },
964
+ ],
965
+ sitemap: 'https://example.com/sitemap.xml',
966
+ }
967
+ }
968
+ \`\`\`
969
+
970
+ ## Google SEO 최적화
971
+
972
+ ### Viewport
973
+
974
+ \`\`\`typescript
975
+ export const metadata: Metadata = {
976
+ viewport: {
977
+ width: 'device-width',
978
+ initialScale: 1,
979
+ maximumScale: 5,
980
+ userScalable: true,
981
+ },
982
+ }
983
+ \`\`\`
984
+
985
+ ### Theme Color (다크모드 대응)
986
+
987
+ \`\`\`typescript
988
+ export const metadata: Metadata = {
989
+ themeColor: [
990
+ { color: '#ffffff', media: '(prefers-color-scheme: light)' },
991
+ { color: '#000000', media: '(prefers-color-scheme: dark)' },
992
+ ],
993
+ }
994
+ \`\`\`
995
+
996
+ ### Resource Hints (성능 최적화)
997
+
998
+ \`\`\`typescript
999
+ export const metadata: Metadata = {
1000
+ resourceHints: {
1001
+ preconnect: ['https://fonts.googleapis.com'],
1002
+ dnsPrefetch: ['https://cdn.example.com'],
1003
+ preload: [
1004
+ { href: '/fonts/main.woff2', as: 'font', type: 'font/woff2' },
1005
+ ],
1006
+ prefetch: ['/next-page.js'],
1007
+ },
1008
+ }
1009
+ \`\`\`
1010
+
1011
+ ### Format Detection (iOS Safari)
1012
+
1013
+ \`\`\`typescript
1014
+ export const metadata: Metadata = {
1015
+ formatDetection: {
1016
+ telephone: false,
1017
+ date: false,
1018
+ address: false,
1019
+ email: false,
1020
+ },
1021
+ }
1022
+ \`\`\`
1023
+
1024
+ ### App Links
1025
+
1026
+ \`\`\`typescript
1027
+ export const metadata: Metadata = {
1028
+ appLinks: {
1029
+ iosAppStoreId: '123456789',
1030
+ iosAppName: 'My App',
1031
+ androidPackage: 'com.example.app',
1032
+ androidAppName: 'My App',
1033
+ },
1034
+ }
1035
+ \`\`\`
1036
+
1037
+ ## MCP 도구
1038
+
1039
+ \`\`\`typescript
1040
+ // SEO 메타데이터 미리보기
1041
+ mandu_preview_seo({ metadata: { title: 'Test', description: 'Test desc' } })
1042
+
1043
+ // Sitemap 미리보기
1044
+ mandu_generate_sitemap_preview({
1045
+ entries: [{ url: 'https://example.com', priority: 1.0 }]
1046
+ })
1047
+
1048
+ // Robots.txt 미리보기
1049
+ mandu_generate_robots_preview({
1050
+ rules: { userAgent: '*', allow: '/' },
1051
+ sitemap: 'https://example.com/sitemap.xml'
1052
+ })
1053
+
1054
+ // JSON-LD 생성
1055
+ mandu_create_jsonld({
1056
+ type: 'Article',
1057
+ data: { headline: 'Title', author: 'Name', datePublished: '2024-01-15' }
1058
+ })
1059
+
1060
+ // SEO 파일 생성
1061
+ mandu_write_seo_file({ fileType: 'sitemap' })
1062
+ mandu_write_seo_file({ fileType: 'robots' })
1063
+
1064
+ // SEO 분석
1065
+ mandu_seo_analyze({
1066
+ metadata: { title: 'Test', description: 'Test' },
1067
+ url: 'https://example.com/page'
1068
+ })
1069
+ \`\`\`
1070
+
1071
+ ## SEO 체크리스트
1072
+
1073
+ ### 필수 항목
1074
+ - [ ] title (30-60자)
1075
+ - [ ] description (50-160자)
1076
+ - [ ] viewport 설정
1077
+ - [ ] canonical URL
1078
+
1079
+ ### 권장 항목
1080
+ - [ ] Open Graph (title, description, image)
1081
+ - [ ] Twitter Card
1082
+ - [ ] JSON-LD 구조화 데이터
1083
+ - [ ] sitemap.xml
1084
+ - [ ] robots.txt
1085
+ - [ ] hreflang (다국어 사이트)
1086
+
1087
+ ### 성능 최적화
1088
+ - [ ] preconnect (외부 도메인)
1089
+ - [ ] dns-prefetch
1090
+ - [ ] preload (중요 리소스)
1091
+ `;
1092
+
1093
+ // 모든 가이드 목록
1094
+ export const GUIDES = {
1095
+ slot: GUIDE_SLOT,
1096
+ "fs-routes": GUIDE_FS_ROUTES,
1097
+ hydration: GUIDE_HYDRATION,
1098
+ guard: GUIDE_GUARD,
1099
+ seo: GUIDE_SEO,
1100
+ } as const;
1101
+
1102
+ export type GuideId = keyof typeof GUIDES;
1103
+
1104
+ export function getGuide(id: string): string | null {
1105
+ return GUIDES[id as GuideId] || null;
1106
+ }
1107
+
1108
+ export function listGuides(): { id: string; title: string; description: string }[] {
1109
+ return [
1110
+ {
1111
+ id: "slot",
1112
+ title: "Slot 작성 가이드",
1113
+ description: "Mandu.filling() API를 사용한 비즈니스 로직 작성법",
1114
+ },
1115
+ {
1116
+ id: "fs-routes",
1117
+ title: "FS Routes 가이드",
1118
+ description: "파일 시스템 기반 라우팅 규칙과 패턴",
1119
+ },
1120
+ {
1121
+ id: "hydration",
1122
+ title: "Island Hydration 가이드",
1123
+ description: "부분 hydration과 Island 컴포넌트 작성법",
1124
+ },
1125
+ {
1126
+ id: "guard",
1127
+ title: "Guard 가이드",
1128
+ description: "아키텍처 규칙 강제와 레이어 의존성 관리",
1129
+ },
1130
+ {
1131
+ id: "seo",
1132
+ title: "SEO 가이드",
1133
+ description: "메타데이터, Open Graph, JSON-LD, Sitemap 설정법",
1134
+ },
1135
+ ];
1136
+ }