@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.
- package/README.md +320 -0
- package/package.json +1 -1
- package/src/activity-monitor.ts +847 -231
- package/src/resources/handlers.ts +244 -0
- package/src/resources/skills/guides.ts +1136 -0
- package/src/resources/skills/index.ts +12 -0
- package/src/resources/skills/loader.ts +218 -0
- package/src/resources/skills/mandu-composition/SKILL.md +91 -0
- package/src/resources/skills/mandu-composition/metadata.json +13 -0
- package/src/resources/skills/mandu-composition/rules/_sections.md +26 -0
- package/src/resources/skills/mandu-composition/rules/_template.md +77 -0
- package/src/resources/skills/mandu-composition/rules/comp-arch-avoid-boolean-props.md +146 -0
- package/src/resources/skills/mandu-composition/rules/comp-arch-compound-components.md +164 -0
- package/src/resources/skills/mandu-composition/rules/comp-island-event.md +161 -0
- package/src/resources/skills/mandu-composition/rules/comp-island-slot-split.md +167 -0
- package/src/resources/skills/mandu-composition/rules/comp-pattern-children.md +149 -0
- package/src/resources/skills/mandu-composition/rules/comp-state-context-interface.md +148 -0
- package/src/resources/skills/mandu-composition/rules/comp-state-lift-state.md +150 -0
- package/src/resources/skills/mandu-deployment/SKILL.md +92 -0
- package/src/resources/skills/mandu-deployment/_sections.md +41 -0
- package/src/resources/skills/mandu-deployment/_template.md +38 -0
- package/src/resources/skills/mandu-deployment/metadata.json +13 -0
- package/src/resources/skills/mandu-deployment/rules/deploy-build-bun.md +109 -0
- package/src/resources/skills/mandu-deployment/rules/deploy-build-output.md +115 -0
- package/src/resources/skills/mandu-deployment/rules/deploy-cicd-github.md +219 -0
- package/src/resources/skills/mandu-deployment/rules/deploy-docker-bun.md +150 -0
- package/src/resources/skills/mandu-deployment/rules/deploy-docker-compose.md +223 -0
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-fly.md +152 -0
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-render.md +179 -0
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +323 -0
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-vercel.md +140 -0
- package/src/resources/skills/mandu-fs-routes/SKILL.md +82 -0
- package/src/resources/skills/mandu-fs-routes/metadata.json +12 -0
- package/src/resources/skills/mandu-fs-routes/rules/_sections.md +36 -0
- package/src/resources/skills/mandu-fs-routes/rules/_template.md +69 -0
- package/src/resources/skills/mandu-fs-routes/rules/routes-api-methods.md +65 -0
- package/src/resources/skills/mandu-fs-routes/rules/routes-dynamic-param.md +93 -0
- package/src/resources/skills/mandu-fs-routes/rules/routes-naming-page.md +55 -0
- package/src/resources/skills/mandu-guard/SKILL.md +129 -0
- package/src/resources/skills/mandu-guard/metadata.json +12 -0
- package/src/resources/skills/mandu-guard/rules/_sections.md +36 -0
- package/src/resources/skills/mandu-guard/rules/_template.md +82 -0
- package/src/resources/skills/mandu-guard/rules/guard-config-rules.md +100 -0
- package/src/resources/skills/mandu-guard/rules/guard-layer-direction.md +76 -0
- package/src/resources/skills/mandu-guard/rules/guard-preset-mandu.md +81 -0
- package/src/resources/skills/mandu-guard/rules/guard-validate-import.md +80 -0
- package/src/resources/skills/mandu-hydration/SKILL.md +91 -0
- package/src/resources/skills/mandu-hydration/metadata.json +12 -0
- package/src/resources/skills/mandu-hydration/rules/_sections.md +31 -0
- package/src/resources/skills/mandu-hydration/rules/_template.md +72 -0
- package/src/resources/skills/mandu-hydration/rules/hydration-data-event.md +109 -0
- package/src/resources/skills/mandu-hydration/rules/hydration-directive-use-client.md +55 -0
- package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +113 -0
- package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +68 -0
- package/src/resources/skills/mandu-performance/SKILL.md +85 -0
- package/src/resources/skills/mandu-performance/metadata.json +14 -0
- package/src/resources/skills/mandu-performance/rules/_sections.md +31 -0
- package/src/resources/skills/mandu-performance/rules/_template.md +64 -0
- package/src/resources/skills/mandu-performance/rules/perf-async-defer-await.md +103 -0
- package/src/resources/skills/mandu-performance/rules/perf-async-parallel.md +95 -0
- package/src/resources/skills/mandu-performance/rules/perf-bun-file.md +124 -0
- package/src/resources/skills/mandu-performance/rules/perf-bun-serve.md +125 -0
- package/src/resources/skills/mandu-performance/rules/perf-bundle-imports.md +80 -0
- package/src/resources/skills/mandu-performance/rules/perf-bundle-island-lazy.md +145 -0
- package/src/resources/skills/mandu-performance/rules/perf-cache-react.md +98 -0
- package/src/resources/skills/mandu-performance/rules/perf-render-transitions.md +154 -0
- package/src/resources/skills/mandu-security/SKILL.md +87 -0
- package/src/resources/skills/mandu-security/metadata.json +13 -0
- package/src/resources/skills/mandu-security/rules/_sections.md +31 -0
- package/src/resources/skills/mandu-security/rules/_template.md +74 -0
- package/src/resources/skills/mandu-security/rules/sec-auth-guard.md +127 -0
- package/src/resources/skills/mandu-security/rules/sec-env-management.md +133 -0
- package/src/resources/skills/mandu-security/rules/sec-input-validate.md +148 -0
- package/src/resources/skills/mandu-security/rules/sec-protect-csrf.md +146 -0
- package/src/resources/skills/mandu-security/rules/sec-protect-headers.md +138 -0
- package/src/resources/skills/mandu-slot/SKILL.md +85 -0
- package/src/resources/skills/mandu-slot/metadata.json +12 -0
- package/src/resources/skills/mandu-slot/rules/_sections.md +36 -0
- package/src/resources/skills/mandu-slot/rules/_template.md +63 -0
- package/src/resources/skills/mandu-slot/rules/slot-basic-structure.md +38 -0
- package/src/resources/skills/mandu-slot/rules/slot-ctx-response.md +56 -0
- package/src/resources/skills/mandu-slot/rules/slot-guard-auth.md +59 -0
- package/src/resources/skills/mandu-slot/rules/slot-http-methods.md +64 -0
- package/src/resources/skills/mandu-styling/SKILL.md +118 -0
- package/src/resources/skills/mandu-styling/_sections.md +36 -0
- package/src/resources/skills/mandu-styling/_template.md +32 -0
- package/src/resources/skills/mandu-styling/metadata.json +13 -0
- package/src/resources/skills/mandu-styling/rules/style-component-compound.md +235 -0
- package/src/resources/skills/mandu-styling/rules/style-component-slots.md +255 -0
- package/src/resources/skills/mandu-styling/rules/style-component-tokens.md +205 -0
- package/src/resources/skills/mandu-styling/rules/style-island-animations.md +272 -0
- package/src/resources/skills/mandu-styling/rules/style-island-scoping.md +167 -0
- package/src/resources/skills/mandu-styling/rules/style-island-variants.md +221 -0
- package/src/resources/skills/mandu-styling/rules/style-perf-critical.md +209 -0
- package/src/resources/skills/mandu-styling/rules/style-perf-purge.md +192 -0
- package/src/resources/skills/mandu-styling/rules/style-setup-modules.md +162 -0
- package/src/resources/skills/mandu-styling/rules/style-setup-panda.md +164 -0
- package/src/resources/skills/mandu-styling/rules/style-setup-tailwind.md +161 -0
- package/src/resources/skills/mandu-styling/rules/style-theme-darkmode.md +229 -0
- package/src/resources/skills/mandu-testing/SKILL.md +99 -0
- package/src/resources/skills/mandu-testing/metadata.json +13 -0
- package/src/resources/skills/mandu-testing/rules/_sections.md +26 -0
- package/src/resources/skills/mandu-testing/rules/_template.md +65 -0
- package/src/resources/skills/mandu-testing/rules/test-component-island.md +195 -0
- package/src/resources/skills/mandu-testing/rules/test-e2e-playwright.md +196 -0
- package/src/resources/skills/mandu-testing/rules/test-mock-fetch.md +219 -0
- package/src/resources/skills/mandu-testing/rules/test-slot-unit.md +192 -0
- package/src/resources/skills/mandu-ui/SKILL.md +117 -0
- package/src/resources/skills/mandu-ui/_sections.md +23 -0
- package/src/resources/skills/mandu-ui/_template.md +32 -0
- package/src/resources/skills/mandu-ui/metadata.json +13 -0
- package/src/resources/skills/mandu-ui/rules/ui-accessibility-aria.md +232 -0
- package/src/resources/skills/mandu-ui/rules/ui-accessibility-focus.md +238 -0
- package/src/resources/skills/mandu-ui/rules/ui-composition-patterns.md +259 -0
- package/src/resources/skills/mandu-ui/rules/ui-island-integration.md +258 -0
- package/src/resources/skills/mandu-ui/rules/ui-radix-patterns.md +213 -0
- package/src/resources/skills/mandu-ui/rules/ui-shadcn-setup.md +209 -0
- package/src/resources/skills/recipes.ts +932 -0
- package/src/server.ts +3 -0
- package/src/tools/hydration.ts +8 -8
- package/src/tools/index.ts +1 -0
- 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
|
+
}
|