@su-record/vibe 2.3.0 → 2.3.2
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/.claude/settings.json +35 -35
- package/.claude/settings.local.json +24 -25
- package/.claude/vibe/constitution.md +184 -184
- package/.claude/vibe/rules/core/communication-guide.md +104 -104
- package/.claude/vibe/rules/core/development-philosophy.md +52 -52
- package/.claude/vibe/rules/core/quick-start.md +120 -120
- package/.claude/vibe/rules/languages/dart-flutter.md +509 -509
- package/.claude/vibe/rules/languages/go.md +396 -396
- package/.claude/vibe/rules/languages/java-spring.md +586 -586
- package/.claude/vibe/rules/languages/kotlin-android.md +491 -491
- package/.claude/vibe/rules/languages/python-django.md +371 -371
- package/.claude/vibe/rules/languages/python-fastapi.md +386 -386
- package/.claude/vibe/rules/languages/rust.md +425 -425
- package/.claude/vibe/rules/languages/swift-ios.md +516 -516
- package/.claude/vibe/rules/languages/typescript-nextjs.md +441 -441
- package/.claude/vibe/rules/languages/typescript-node.md +375 -375
- package/.claude/vibe/rules/languages/typescript-nuxt.md +521 -521
- package/.claude/vibe/rules/languages/typescript-react-native.md +446 -446
- package/.claude/vibe/rules/languages/typescript-react.md +525 -525
- package/.claude/vibe/rules/languages/typescript-vue.md +353 -353
- package/.claude/vibe/rules/quality/bdd-contract-testing.md +388 -388
- package/.claude/vibe/rules/quality/checklist.md +276 -276
- package/.claude/vibe/rules/quality/testing-strategy.md +437 -437
- package/.claude/vibe/rules/standards/anti-patterns.md +369 -369
- package/.claude/vibe/rules/standards/code-structure.md +291 -291
- package/.claude/vibe/rules/standards/complexity-metrics.md +312 -312
- package/.claude/vibe/rules/standards/naming-conventions.md +198 -198
- package/.claude/vibe/setup.sh +31 -31
- package/.claude/vibe/templates/constitution-template.md +184 -184
- package/.claude/vibe/templates/contract-backend-template.md +517 -517
- package/.claude/vibe/templates/contract-frontend-template.md +594 -594
- package/.claude/vibe/templates/feature-template.md +96 -96
- package/.claude/vibe/templates/spec-template.md +199 -199
- package/CLAUDE.md +345 -323
- package/LICENSE +21 -21
- package/README.md +744 -724
- package/agents/compounder.md +261 -261
- package/agents/diagrammer.md +178 -178
- package/agents/e2e-tester.md +266 -266
- package/agents/explorer.md +48 -48
- package/agents/implementer.md +53 -53
- package/agents/research/best-practices-agent.md +139 -139
- package/agents/research/codebase-patterns-agent.md +147 -147
- package/agents/research/framework-docs-agent.md +181 -181
- package/agents/research/security-advisory-agent.md +167 -167
- package/agents/review/architecture-reviewer.md +107 -107
- package/agents/review/complexity-reviewer.md +116 -116
- package/agents/review/data-integrity-reviewer.md +88 -88
- package/agents/review/git-history-reviewer.md +103 -103
- package/agents/review/performance-reviewer.md +86 -86
- package/agents/review/python-reviewer.md +152 -152
- package/agents/review/rails-reviewer.md +139 -139
- package/agents/review/react-reviewer.md +144 -144
- package/agents/review/security-reviewer.md +80 -80
- package/agents/review/simplicity-reviewer.md +140 -140
- package/agents/review/test-coverage-reviewer.md +116 -116
- package/agents/review/typescript-reviewer.md +127 -127
- package/agents/searcher.md +54 -54
- package/agents/simplifier.md +119 -119
- package/agents/tester.md +49 -49
- package/agents/ui-previewer.md +137 -137
- package/commands/vibe.analyze.md +245 -180
- package/commands/vibe.reason.md +223 -183
- package/commands/vibe.review.md +200 -136
- package/commands/vibe.run.md +838 -836
- package/commands/vibe.spec.md +419 -383
- package/commands/vibe.utils.md +101 -101
- package/commands/vibe.verify.md +282 -241
- package/dist/cli/index.js +385 -385
- package/dist/lib/MemoryManager.d.ts.map +1 -1
- package/dist/lib/MemoryManager.js +119 -114
- package/dist/lib/MemoryManager.js.map +1 -1
- package/dist/lib/PythonParser.js +108 -108
- package/dist/lib/gemini-mcp.js +15 -15
- package/dist/lib/gemini-oauth.js +35 -35
- package/dist/lib/gpt-mcp.js +17 -17
- package/dist/lib/gpt-oauth.js +44 -44
- package/dist/tools/analytics/getUsageAnalytics.js +12 -12
- package/dist/tools/index.d.ts +50 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +61 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory/createMemoryTimeline.js +10 -10
- package/dist/tools/memory/getMemoryGraph.js +12 -12
- package/dist/tools/memory/getSessionContext.js +9 -9
- package/dist/tools/memory/linkMemories.js +14 -14
- package/dist/tools/memory/listMemories.js +4 -4
- package/dist/tools/memory/recallMemory.js +4 -4
- package/dist/tools/memory/saveMemory.js +4 -4
- package/dist/tools/memory/searchMemoriesAdvanced.js +22 -22
- package/dist/tools/planning/generatePrd.js +46 -46
- package/dist/tools/prompt/enhancePromptGemini.js +160 -160
- package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
- package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
- package/hooks/hooks.json +121 -103
- package/package.json +73 -69
- package/skills/git-worktree.md +178 -178
- package/skills/priority-todos.md +236 -236
|
@@ -1,521 +1,521 @@
|
|
|
1
|
-
# 🟢 TypeScript + Nuxt 3 품질 규칙
|
|
2
|
-
|
|
3
|
-
## 핵심 원칙 (Vue에서 상속)
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
✅ 단일 책임 (SRP)
|
|
7
|
-
✅ 중복 제거 (DRY)
|
|
8
|
-
✅ 재사용성
|
|
9
|
-
✅ 낮은 복잡도
|
|
10
|
-
✅ 함수 ≤ 30줄, Template ≤ 100줄
|
|
11
|
-
✅ 중첩 ≤ 3단계
|
|
12
|
-
✅ Composition API + script setup
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Nuxt 3 특화 규칙
|
|
16
|
-
|
|
17
|
-
### 1. Auto-imports 활용
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
// ✅ Nuxt 3는 자동 import (명시적 import 불필요)
|
|
21
|
-
<script setup lang="ts">
|
|
22
|
-
// ref, computed, watch 등 Vue API 자동 import
|
|
23
|
-
const count = ref(0);
|
|
24
|
-
const doubled = computed(() => count.value * 2);
|
|
25
|
-
|
|
26
|
-
// useFetch, useAsyncData 등 Nuxt composables 자동 import
|
|
27
|
-
const { data } = await useFetch('/api/users');
|
|
28
|
-
|
|
29
|
-
// components/ 폴더의 컴포넌트 자동 import
|
|
30
|
-
// <UserCard /> 바로 사용 가능
|
|
31
|
-
</script>
|
|
32
|
-
|
|
33
|
-
// ❌ 불필요한 import
|
|
34
|
-
import { ref, computed } from 'vue';
|
|
35
|
-
import { useFetch } from '#app';
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### 2. Server API Routes
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
// ✅ server/api/users/index.get.ts (GET /api/users)
|
|
42
|
-
export default defineEventHandler(async (event) => {
|
|
43
|
-
const users = await prisma.user.findMany();
|
|
44
|
-
return users;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// ✅ server/api/users/index.post.ts (POST /api/users)
|
|
48
|
-
export default defineEventHandler(async (event) => {
|
|
49
|
-
const body = await readBody(event);
|
|
50
|
-
|
|
51
|
-
// 유효성 검사
|
|
52
|
-
if (!body.email || !body.name) {
|
|
53
|
-
throw createError({
|
|
54
|
-
statusCode: 400,
|
|
55
|
-
message: '이메일과 이름은 필수입니다',
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const user = await prisma.user.create({ data: body });
|
|
60
|
-
return user;
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// ✅ server/api/users/[id].get.ts (GET /api/users/:id)
|
|
64
|
-
export default defineEventHandler(async (event) => {
|
|
65
|
-
const id = getRouterParam(event, 'id');
|
|
66
|
-
|
|
67
|
-
const user = await prisma.user.findUnique({ where: { id } });
|
|
68
|
-
|
|
69
|
-
if (!user) {
|
|
70
|
-
throw createError({
|
|
71
|
-
statusCode: 404,
|
|
72
|
-
message: '사용자를 찾을 수 없습니다',
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return user;
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ✅ server/api/users/[id].put.ts (PUT /api/users/:id)
|
|
80
|
-
export default defineEventHandler(async (event) => {
|
|
81
|
-
const id = getRouterParam(event, 'id');
|
|
82
|
-
const body = await readBody(event);
|
|
83
|
-
|
|
84
|
-
const user = await prisma.user.update({
|
|
85
|
-
where: { id },
|
|
86
|
-
data: body,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
return user;
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// ✅ server/api/users/[id].delete.ts (DELETE /api/users/:id)
|
|
93
|
-
export default defineEventHandler(async (event) => {
|
|
94
|
-
const id = getRouterParam(event, 'id');
|
|
95
|
-
await prisma.user.delete({ where: { id } });
|
|
96
|
-
return { success: true };
|
|
97
|
-
});
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### 3. Data Fetching (SSR 지원)
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
// ✅ useFetch - 기본 데이터 페칭
|
|
104
|
-
<script setup lang="ts">
|
|
105
|
-
const { data: user, pending, error, refresh } = await useFetch<User>(
|
|
106
|
-
`/api/users/${props.userId}`
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// 옵션 사용
|
|
110
|
-
const { data: posts } = await useFetch('/api/posts', {
|
|
111
|
-
query: { limit: 10, offset: 0 },
|
|
112
|
-
headers: { 'X-Custom': 'value' },
|
|
113
|
-
pick: ['id', 'title'], // 필요한 필드만 선택
|
|
114
|
-
transform: (data) => data.items, // 응답 변환
|
|
115
|
-
});
|
|
116
|
-
</script>
|
|
117
|
-
|
|
118
|
-
// ✅ useAsyncData - 커스텀 페칭 로직
|
|
119
|
-
<script setup lang="ts">
|
|
120
|
-
const { data, pending } = await useAsyncData(
|
|
121
|
-
'user-posts', // 캐시 키
|
|
122
|
-
() => $fetch(`/api/users/${props.userId}/posts`),
|
|
123
|
-
{
|
|
124
|
-
default: () => [], // 기본값
|
|
125
|
-
lazy: true, // 클라이언트에서만 실행
|
|
126
|
-
server: false, // SSR 비활성화
|
|
127
|
-
}
|
|
128
|
-
);
|
|
129
|
-
</script>
|
|
130
|
-
|
|
131
|
-
// ✅ useLazyFetch - 지연 로딩 (Suspense 없이)
|
|
132
|
-
<script setup lang="ts">
|
|
133
|
-
const { data, pending } = useLazyFetch('/api/heavy-data');
|
|
134
|
-
|
|
135
|
-
// pending 상태 처리
|
|
136
|
-
</script>
|
|
137
|
-
<template>
|
|
138
|
-
<div v-if="pending">로딩 중...</div>
|
|
139
|
-
<div v-else>{{ data }}</div>
|
|
140
|
-
</template>
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### 4. State Management
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
// ✅ useState - 서버/클라이언트 공유 상태
|
|
147
|
-
<script setup lang="ts">
|
|
148
|
-
// 모든 컴포넌트에서 공유되는 상태
|
|
149
|
-
const counter = useState('counter', () => 0);
|
|
150
|
-
|
|
151
|
-
function increment() {
|
|
152
|
-
counter.value++;
|
|
153
|
-
}
|
|
154
|
-
</script>
|
|
155
|
-
|
|
156
|
-
// ✅ Pinia Store (복잡한 상태)
|
|
157
|
-
// stores/user.ts
|
|
158
|
-
export const useUserStore = defineStore('user', () => {
|
|
159
|
-
const user = ref<User | null>(null);
|
|
160
|
-
const isLoggedIn = computed(() => !!user.value);
|
|
161
|
-
|
|
162
|
-
async function login(credentials: LoginCredentials) {
|
|
163
|
-
const data = await $fetch('/api/auth/login', {
|
|
164
|
-
method: 'POST',
|
|
165
|
-
body: credentials,
|
|
166
|
-
});
|
|
167
|
-
user.value = data.user;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function logout() {
|
|
171
|
-
user.value = null;
|
|
172
|
-
navigateTo('/login');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return { user, isLoggedIn, login, logout };
|
|
176
|
-
});
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### 5. Middleware
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
// ✅ middleware/auth.ts (Named middleware)
|
|
183
|
-
export default defineNuxtRouteMiddleware((to, from) => {
|
|
184
|
-
const { isLoggedIn } = useUserStore();
|
|
185
|
-
|
|
186
|
-
// 로그인 필요한 페이지 보호
|
|
187
|
-
if (!isLoggedIn && to.meta.requiresAuth) {
|
|
188
|
-
return navigateTo('/login');
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// 페이지에서 사용
|
|
193
|
-
<script setup lang="ts">
|
|
194
|
-
definePageMeta({
|
|
195
|
-
middleware: 'auth',
|
|
196
|
-
requiresAuth: true,
|
|
197
|
-
});
|
|
198
|
-
</script>
|
|
199
|
-
|
|
200
|
-
// ✅ middleware/auth.global.ts (Global middleware)
|
|
201
|
-
export default defineNuxtRouteMiddleware((to, from) => {
|
|
202
|
-
// 모든 라우트에 적용
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// ✅ Server middleware
|
|
206
|
-
// server/middleware/auth.ts
|
|
207
|
-
export default defineEventHandler((event) => {
|
|
208
|
-
const token = getCookie(event, 'auth-token');
|
|
209
|
-
|
|
210
|
-
if (!token && event.path.startsWith('/api/protected')) {
|
|
211
|
-
throw createError({
|
|
212
|
-
statusCode: 401,
|
|
213
|
-
message: '인증이 필요합니다',
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### 6. Layouts & Pages
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
// ✅ layouts/default.vue
|
|
223
|
-
<template>
|
|
224
|
-
<div class="layout">
|
|
225
|
-
<AppHeader />
|
|
226
|
-
<main>
|
|
227
|
-
<slot />
|
|
228
|
-
</main>
|
|
229
|
-
<AppFooter />
|
|
230
|
-
</div>
|
|
231
|
-
</template>
|
|
232
|
-
|
|
233
|
-
// ✅ layouts/admin.vue
|
|
234
|
-
<template>
|
|
235
|
-
<div class="admin-layout">
|
|
236
|
-
<AdminSidebar />
|
|
237
|
-
<main>
|
|
238
|
-
<slot />
|
|
239
|
-
</main>
|
|
240
|
-
</div>
|
|
241
|
-
</template>
|
|
242
|
-
|
|
243
|
-
// ✅ pages/admin/index.vue
|
|
244
|
-
<script setup lang="ts">
|
|
245
|
-
definePageMeta({
|
|
246
|
-
layout: 'admin',
|
|
247
|
-
middleware: ['auth', 'admin-only'],
|
|
248
|
-
});
|
|
249
|
-
</script>
|
|
250
|
-
|
|
251
|
-
// ✅ pages/users/[id].vue (동적 라우트)
|
|
252
|
-
<script setup lang="ts">
|
|
253
|
-
const route = useRoute();
|
|
254
|
-
const userId = route.params.id;
|
|
255
|
-
|
|
256
|
-
const { data: user } = await useFetch(`/api/users/${userId}`);
|
|
257
|
-
</script>
|
|
258
|
-
|
|
259
|
-
// ✅ pages/posts/[...slug].vue (Catch-all 라우트)
|
|
260
|
-
<script setup lang="ts">
|
|
261
|
-
const route = useRoute();
|
|
262
|
-
const slugParts = route.params.slug; // ['a', 'b', 'c']
|
|
263
|
-
</script>
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
### 7. SEO & Meta
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
// ✅ 페이지별 메타 설정
|
|
270
|
-
<script setup lang="ts">
|
|
271
|
-
const { data: post } = await useFetch(`/api/posts/${route.params.id}`);
|
|
272
|
-
|
|
273
|
-
useHead({
|
|
274
|
-
title: post.value?.title,
|
|
275
|
-
meta: [
|
|
276
|
-
{ name: 'description', content: post.value?.summary },
|
|
277
|
-
{ property: 'og:title', content: post.value?.title },
|
|
278
|
-
{ property: 'og:image', content: post.value?.thumbnail },
|
|
279
|
-
],
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
// 또는 useSeoMeta
|
|
283
|
-
useSeoMeta({
|
|
284
|
-
title: post.value?.title,
|
|
285
|
-
ogTitle: post.value?.title,
|
|
286
|
-
description: post.value?.summary,
|
|
287
|
-
ogDescription: post.value?.summary,
|
|
288
|
-
ogImage: post.value?.thumbnail,
|
|
289
|
-
});
|
|
290
|
-
</script>
|
|
291
|
-
|
|
292
|
-
// ✅ nuxt.config.ts 전역 설정
|
|
293
|
-
export default defineNuxtConfig({
|
|
294
|
-
app: {
|
|
295
|
-
head: {
|
|
296
|
-
title: 'My App',
|
|
297
|
-
meta: [
|
|
298
|
-
{ name: 'description', content: 'My awesome app' },
|
|
299
|
-
],
|
|
300
|
-
link: [
|
|
301
|
-
{ rel: 'icon', href: '/favicon.ico' },
|
|
302
|
-
],
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
});
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
### 8. Plugins & Modules
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
// ✅ plugins/api.ts
|
|
312
|
-
export default defineNuxtPlugin(() => {
|
|
313
|
-
const api = $fetch.create({
|
|
314
|
-
baseURL: '/api',
|
|
315
|
-
onRequest({ options }) {
|
|
316
|
-
const token = useCookie('auth-token');
|
|
317
|
-
if (token.value) {
|
|
318
|
-
options.headers = {
|
|
319
|
-
...options.headers,
|
|
320
|
-
Authorization: `Bearer ${token.value}`,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
},
|
|
324
|
-
onResponseError({ response }) {
|
|
325
|
-
if (response.status === 401) {
|
|
326
|
-
navigateTo('/login');
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
provide: { api },
|
|
333
|
-
};
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// 사용
|
|
337
|
-
const { $api } = useNuxtApp();
|
|
338
|
-
const users = await $api('/users');
|
|
339
|
-
|
|
340
|
-
// ✅ plugins/dayjs.client.ts (클라이언트 전용)
|
|
341
|
-
import dayjs from 'dayjs';
|
|
342
|
-
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
343
|
-
|
|
344
|
-
export default defineNuxtPlugin(() => {
|
|
345
|
-
dayjs.extend(relativeTime);
|
|
346
|
-
return { provide: { dayjs } };
|
|
347
|
-
});
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### 9. Composables
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
// ✅ composables/useAuth.ts
|
|
354
|
-
export function useAuth() {
|
|
355
|
-
const user = useState<User | null>('auth-user', () => null);
|
|
356
|
-
const isLoggedIn = computed(() => !!user.value);
|
|
357
|
-
|
|
358
|
-
async function login(email: string, password: string) {
|
|
359
|
-
const data = await $fetch('/api/auth/login', {
|
|
360
|
-
method: 'POST',
|
|
361
|
-
body: { email, password },
|
|
362
|
-
});
|
|
363
|
-
user.value = data.user;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
async function logout() {
|
|
367
|
-
await $fetch('/api/auth/logout', { method: 'POST' });
|
|
368
|
-
user.value = null;
|
|
369
|
-
await navigateTo('/login');
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return { user, isLoggedIn, login, logout };
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// ✅ composables/usePagination.ts
|
|
376
|
-
export function usePagination<T>(
|
|
377
|
-
fetchFn: (page: number) => Promise<{ items: T[]; total: number }>
|
|
378
|
-
) {
|
|
379
|
-
const items = ref<T[]>([]);
|
|
380
|
-
const page = ref(1);
|
|
381
|
-
const total = ref(0);
|
|
382
|
-
const isLoading = ref(false);
|
|
383
|
-
|
|
384
|
-
const hasMore = computed(() => items.value.length < total.value);
|
|
385
|
-
|
|
386
|
-
async function loadMore() {
|
|
387
|
-
if (isLoading.value || !hasMore.value) return;
|
|
388
|
-
|
|
389
|
-
isLoading.value = true;
|
|
390
|
-
const data = await fetchFn(page.value);
|
|
391
|
-
items.value.push(...data.items);
|
|
392
|
-
total.value = data.total;
|
|
393
|
-
page.value++;
|
|
394
|
-
isLoading.value = false;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return { items, isLoading, hasMore, loadMore };
|
|
398
|
-
}
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
### 10. Error Handling
|
|
402
|
-
|
|
403
|
-
```typescript
|
|
404
|
-
// ✅ error.vue (전역 에러 페이지)
|
|
405
|
-
<script setup lang="ts">
|
|
406
|
-
const props = defineProps<{
|
|
407
|
-
error: {
|
|
408
|
-
statusCode: number;
|
|
409
|
-
message: string;
|
|
410
|
-
};
|
|
411
|
-
}>();
|
|
412
|
-
|
|
413
|
-
const handleError = () => clearError({ redirect: '/' });
|
|
414
|
-
</script>
|
|
415
|
-
|
|
416
|
-
<template>
|
|
417
|
-
<div class="error-page">
|
|
418
|
-
<h1>{{ error.statusCode }}</h1>
|
|
419
|
-
<p>{{ error.message }}</p>
|
|
420
|
-
<button @click="handleError">홈으로</button>
|
|
421
|
-
</div>
|
|
422
|
-
</template>
|
|
423
|
-
|
|
424
|
-
// ✅ 컴포넌트 레벨 에러 처리
|
|
425
|
-
<script setup lang="ts">
|
|
426
|
-
const { data, error } = await useFetch('/api/data');
|
|
427
|
-
|
|
428
|
-
if (error.value) {
|
|
429
|
-
throw createError({
|
|
430
|
-
statusCode: error.value.statusCode,
|
|
431
|
-
message: error.value.message,
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
</script>
|
|
435
|
-
|
|
436
|
-
// ✅ NuxtErrorBoundary 사용
|
|
437
|
-
<template>
|
|
438
|
-
<NuxtErrorBoundary @error="logError">
|
|
439
|
-
<SomeComponent />
|
|
440
|
-
<template #error="{ error, clearError }">
|
|
441
|
-
<p>오류 발생: {{ error.message }}</p>
|
|
442
|
-
<button @click="clearError">다시 시도</button>
|
|
443
|
-
</template>
|
|
444
|
-
</NuxtErrorBoundary>
|
|
445
|
-
</template>
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
## 파일 구조 (Nuxt 3)
|
|
449
|
-
|
|
450
|
-
```
|
|
451
|
-
project/
|
|
452
|
-
├── .nuxt/ # 빌드 산출물 (git 제외)
|
|
453
|
-
├── assets/ # 빌드에 포함되는 에셋
|
|
454
|
-
├── components/ # 자동 import 컴포넌트
|
|
455
|
-
│ ├── ui/ # 기본 UI 컴포넌트
|
|
456
|
-
│ ├── features/ # 기능별 컴포넌트
|
|
457
|
-
│ └── App*.vue # 앱 공통 컴포넌트
|
|
458
|
-
├── composables/ # 자동 import composables
|
|
459
|
-
├── layouts/ # 레이아웃
|
|
460
|
-
├── middleware/ # 라우트 미들웨어
|
|
461
|
-
├── pages/ # 파일 기반 라우팅
|
|
462
|
-
├── plugins/ # Nuxt 플러그인
|
|
463
|
-
├── public/ # 정적 파일
|
|
464
|
-
├── server/
|
|
465
|
-
│ ├── api/ # API 라우트
|
|
466
|
-
│ ├── middleware/ # 서버 미들웨어
|
|
467
|
-
│ └── utils/ # 서버 유틸리티
|
|
468
|
-
├── stores/ # Pinia 스토어
|
|
469
|
-
├── types/ # TypeScript 타입
|
|
470
|
-
├── utils/ # 유틸리티 함수
|
|
471
|
-
├── app.vue # 앱 루트
|
|
472
|
-
├── nuxt.config.ts # Nuxt 설정
|
|
473
|
-
└── tsconfig.json # TypeScript 설정
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
## 안티패턴
|
|
477
|
-
|
|
478
|
-
```typescript
|
|
479
|
-
// ❌ 클라이언트에서 직접 DB 접근
|
|
480
|
-
<script setup>
|
|
481
|
-
import { PrismaClient } from '@prisma/client';
|
|
482
|
-
const prisma = new PrismaClient(); // 클라이언트에서 실행 불가
|
|
483
|
-
</script>
|
|
484
|
-
|
|
485
|
-
// ✅ Server API 통해 접근
|
|
486
|
-
const { data } = await useFetch('/api/users');
|
|
487
|
-
|
|
488
|
-
// ❌ useFetch를 조건부로 사용
|
|
489
|
-
if (someCondition) {
|
|
490
|
-
const { data } = await useFetch('/api/data'); // 에러 발생
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// ✅ enabled 옵션 사용
|
|
494
|
-
const { data } = await useFetch('/api/data', {
|
|
495
|
-
immediate: someCondition,
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
// ❌ navigateTo를 setup 밖에서 사용
|
|
499
|
-
function handleClick() {
|
|
500
|
-
navigateTo('/page'); // 가능하지만 비권장
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// ✅ useRouter 사용
|
|
504
|
-
const router = useRouter();
|
|
505
|
-
function handleClick() {
|
|
506
|
-
router.push('/page');
|
|
507
|
-
}
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
## 체크리스트
|
|
511
|
-
|
|
512
|
-
- [ ] Auto-imports 활용 (불필요한 import 제거)
|
|
513
|
-
- [ ] Server API 파일 네이밍 규칙 준수 (*.get.ts, *.post.ts)
|
|
514
|
-
- [ ] useFetch/useAsyncData로 SSR 지원 데이터 페칭
|
|
515
|
-
- [ ] useState로 서버/클라이언트 상태 공유
|
|
516
|
-
- [ ] definePageMeta로 페이지별 메타 설정
|
|
517
|
-
- [ ] 미들웨어로 라우트 보호
|
|
518
|
-
- [ ] NuxtErrorBoundary로 에러 처리
|
|
519
|
-
- [ ] useHead/useSeoMeta로 SEO 최적화
|
|
520
|
-
- [ ] Composables로 로직 재사용
|
|
521
|
-
- [ ] TypeScript 엄격 모드 사용
|
|
1
|
+
# 🟢 TypeScript + Nuxt 3 품질 규칙
|
|
2
|
+
|
|
3
|
+
## 핵심 원칙 (Vue에서 상속)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
✅ 단일 책임 (SRP)
|
|
7
|
+
✅ 중복 제거 (DRY)
|
|
8
|
+
✅ 재사용성
|
|
9
|
+
✅ 낮은 복잡도
|
|
10
|
+
✅ 함수 ≤ 30줄, Template ≤ 100줄
|
|
11
|
+
✅ 중첩 ≤ 3단계
|
|
12
|
+
✅ Composition API + script setup
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Nuxt 3 특화 규칙
|
|
16
|
+
|
|
17
|
+
### 1. Auto-imports 활용
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// ✅ Nuxt 3는 자동 import (명시적 import 불필요)
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
// ref, computed, watch 등 Vue API 자동 import
|
|
23
|
+
const count = ref(0);
|
|
24
|
+
const doubled = computed(() => count.value * 2);
|
|
25
|
+
|
|
26
|
+
// useFetch, useAsyncData 등 Nuxt composables 자동 import
|
|
27
|
+
const { data } = await useFetch('/api/users');
|
|
28
|
+
|
|
29
|
+
// components/ 폴더의 컴포넌트 자동 import
|
|
30
|
+
// <UserCard /> 바로 사용 가능
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
// ❌ 불필요한 import
|
|
34
|
+
import { ref, computed } from 'vue';
|
|
35
|
+
import { useFetch } from '#app';
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Server API Routes
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// ✅ server/api/users/index.get.ts (GET /api/users)
|
|
42
|
+
export default defineEventHandler(async (event) => {
|
|
43
|
+
const users = await prisma.user.findMany();
|
|
44
|
+
return users;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ✅ server/api/users/index.post.ts (POST /api/users)
|
|
48
|
+
export default defineEventHandler(async (event) => {
|
|
49
|
+
const body = await readBody(event);
|
|
50
|
+
|
|
51
|
+
// 유효성 검사
|
|
52
|
+
if (!body.email || !body.name) {
|
|
53
|
+
throw createError({
|
|
54
|
+
statusCode: 400,
|
|
55
|
+
message: '이메일과 이름은 필수입니다',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const user = await prisma.user.create({ data: body });
|
|
60
|
+
return user;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ✅ server/api/users/[id].get.ts (GET /api/users/:id)
|
|
64
|
+
export default defineEventHandler(async (event) => {
|
|
65
|
+
const id = getRouterParam(event, 'id');
|
|
66
|
+
|
|
67
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
68
|
+
|
|
69
|
+
if (!user) {
|
|
70
|
+
throw createError({
|
|
71
|
+
statusCode: 404,
|
|
72
|
+
message: '사용자를 찾을 수 없습니다',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return user;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ✅ server/api/users/[id].put.ts (PUT /api/users/:id)
|
|
80
|
+
export default defineEventHandler(async (event) => {
|
|
81
|
+
const id = getRouterParam(event, 'id');
|
|
82
|
+
const body = await readBody(event);
|
|
83
|
+
|
|
84
|
+
const user = await prisma.user.update({
|
|
85
|
+
where: { id },
|
|
86
|
+
data: body,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return user;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ✅ server/api/users/[id].delete.ts (DELETE /api/users/:id)
|
|
93
|
+
export default defineEventHandler(async (event) => {
|
|
94
|
+
const id = getRouterParam(event, 'id');
|
|
95
|
+
await prisma.user.delete({ where: { id } });
|
|
96
|
+
return { success: true };
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 3. Data Fetching (SSR 지원)
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// ✅ useFetch - 기본 데이터 페칭
|
|
104
|
+
<script setup lang="ts">
|
|
105
|
+
const { data: user, pending, error, refresh } = await useFetch<User>(
|
|
106
|
+
`/api/users/${props.userId}`
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// 옵션 사용
|
|
110
|
+
const { data: posts } = await useFetch('/api/posts', {
|
|
111
|
+
query: { limit: 10, offset: 0 },
|
|
112
|
+
headers: { 'X-Custom': 'value' },
|
|
113
|
+
pick: ['id', 'title'], // 필요한 필드만 선택
|
|
114
|
+
transform: (data) => data.items, // 응답 변환
|
|
115
|
+
});
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
// ✅ useAsyncData - 커스텀 페칭 로직
|
|
119
|
+
<script setup lang="ts">
|
|
120
|
+
const { data, pending } = await useAsyncData(
|
|
121
|
+
'user-posts', // 캐시 키
|
|
122
|
+
() => $fetch(`/api/users/${props.userId}/posts`),
|
|
123
|
+
{
|
|
124
|
+
default: () => [], // 기본값
|
|
125
|
+
lazy: true, // 클라이언트에서만 실행
|
|
126
|
+
server: false, // SSR 비활성화
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
// ✅ useLazyFetch - 지연 로딩 (Suspense 없이)
|
|
132
|
+
<script setup lang="ts">
|
|
133
|
+
const { data, pending } = useLazyFetch('/api/heavy-data');
|
|
134
|
+
|
|
135
|
+
// pending 상태 처리
|
|
136
|
+
</script>
|
|
137
|
+
<template>
|
|
138
|
+
<div v-if="pending">로딩 중...</div>
|
|
139
|
+
<div v-else>{{ data }}</div>
|
|
140
|
+
</template>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 4. State Management
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// ✅ useState - 서버/클라이언트 공유 상태
|
|
147
|
+
<script setup lang="ts">
|
|
148
|
+
// 모든 컴포넌트에서 공유되는 상태
|
|
149
|
+
const counter = useState('counter', () => 0);
|
|
150
|
+
|
|
151
|
+
function increment() {
|
|
152
|
+
counter.value++;
|
|
153
|
+
}
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
// ✅ Pinia Store (복잡한 상태)
|
|
157
|
+
// stores/user.ts
|
|
158
|
+
export const useUserStore = defineStore('user', () => {
|
|
159
|
+
const user = ref<User | null>(null);
|
|
160
|
+
const isLoggedIn = computed(() => !!user.value);
|
|
161
|
+
|
|
162
|
+
async function login(credentials: LoginCredentials) {
|
|
163
|
+
const data = await $fetch('/api/auth/login', {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
body: credentials,
|
|
166
|
+
});
|
|
167
|
+
user.value = data.user;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function logout() {
|
|
171
|
+
user.value = null;
|
|
172
|
+
navigateTo('/login');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { user, isLoggedIn, login, logout };
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 5. Middleware
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// ✅ middleware/auth.ts (Named middleware)
|
|
183
|
+
export default defineNuxtRouteMiddleware((to, from) => {
|
|
184
|
+
const { isLoggedIn } = useUserStore();
|
|
185
|
+
|
|
186
|
+
// 로그인 필요한 페이지 보호
|
|
187
|
+
if (!isLoggedIn && to.meta.requiresAuth) {
|
|
188
|
+
return navigateTo('/login');
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// 페이지에서 사용
|
|
193
|
+
<script setup lang="ts">
|
|
194
|
+
definePageMeta({
|
|
195
|
+
middleware: 'auth',
|
|
196
|
+
requiresAuth: true,
|
|
197
|
+
});
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
// ✅ middleware/auth.global.ts (Global middleware)
|
|
201
|
+
export default defineNuxtRouteMiddleware((to, from) => {
|
|
202
|
+
// 모든 라우트에 적용
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ✅ Server middleware
|
|
206
|
+
// server/middleware/auth.ts
|
|
207
|
+
export default defineEventHandler((event) => {
|
|
208
|
+
const token = getCookie(event, 'auth-token');
|
|
209
|
+
|
|
210
|
+
if (!token && event.path.startsWith('/api/protected')) {
|
|
211
|
+
throw createError({
|
|
212
|
+
statusCode: 401,
|
|
213
|
+
message: '인증이 필요합니다',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 6. Layouts & Pages
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// ✅ layouts/default.vue
|
|
223
|
+
<template>
|
|
224
|
+
<div class="layout">
|
|
225
|
+
<AppHeader />
|
|
226
|
+
<main>
|
|
227
|
+
<slot />
|
|
228
|
+
</main>
|
|
229
|
+
<AppFooter />
|
|
230
|
+
</div>
|
|
231
|
+
</template>
|
|
232
|
+
|
|
233
|
+
// ✅ layouts/admin.vue
|
|
234
|
+
<template>
|
|
235
|
+
<div class="admin-layout">
|
|
236
|
+
<AdminSidebar />
|
|
237
|
+
<main>
|
|
238
|
+
<slot />
|
|
239
|
+
</main>
|
|
240
|
+
</div>
|
|
241
|
+
</template>
|
|
242
|
+
|
|
243
|
+
// ✅ pages/admin/index.vue
|
|
244
|
+
<script setup lang="ts">
|
|
245
|
+
definePageMeta({
|
|
246
|
+
layout: 'admin',
|
|
247
|
+
middleware: ['auth', 'admin-only'],
|
|
248
|
+
});
|
|
249
|
+
</script>
|
|
250
|
+
|
|
251
|
+
// ✅ pages/users/[id].vue (동적 라우트)
|
|
252
|
+
<script setup lang="ts">
|
|
253
|
+
const route = useRoute();
|
|
254
|
+
const userId = route.params.id;
|
|
255
|
+
|
|
256
|
+
const { data: user } = await useFetch(`/api/users/${userId}`);
|
|
257
|
+
</script>
|
|
258
|
+
|
|
259
|
+
// ✅ pages/posts/[...slug].vue (Catch-all 라우트)
|
|
260
|
+
<script setup lang="ts">
|
|
261
|
+
const route = useRoute();
|
|
262
|
+
const slugParts = route.params.slug; // ['a', 'b', 'c']
|
|
263
|
+
</script>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 7. SEO & Meta
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// ✅ 페이지별 메타 설정
|
|
270
|
+
<script setup lang="ts">
|
|
271
|
+
const { data: post } = await useFetch(`/api/posts/${route.params.id}`);
|
|
272
|
+
|
|
273
|
+
useHead({
|
|
274
|
+
title: post.value?.title,
|
|
275
|
+
meta: [
|
|
276
|
+
{ name: 'description', content: post.value?.summary },
|
|
277
|
+
{ property: 'og:title', content: post.value?.title },
|
|
278
|
+
{ property: 'og:image', content: post.value?.thumbnail },
|
|
279
|
+
],
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// 또는 useSeoMeta
|
|
283
|
+
useSeoMeta({
|
|
284
|
+
title: post.value?.title,
|
|
285
|
+
ogTitle: post.value?.title,
|
|
286
|
+
description: post.value?.summary,
|
|
287
|
+
ogDescription: post.value?.summary,
|
|
288
|
+
ogImage: post.value?.thumbnail,
|
|
289
|
+
});
|
|
290
|
+
</script>
|
|
291
|
+
|
|
292
|
+
// ✅ nuxt.config.ts 전역 설정
|
|
293
|
+
export default defineNuxtConfig({
|
|
294
|
+
app: {
|
|
295
|
+
head: {
|
|
296
|
+
title: 'My App',
|
|
297
|
+
meta: [
|
|
298
|
+
{ name: 'description', content: 'My awesome app' },
|
|
299
|
+
],
|
|
300
|
+
link: [
|
|
301
|
+
{ rel: 'icon', href: '/favicon.ico' },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### 8. Plugins & Modules
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// ✅ plugins/api.ts
|
|
312
|
+
export default defineNuxtPlugin(() => {
|
|
313
|
+
const api = $fetch.create({
|
|
314
|
+
baseURL: '/api',
|
|
315
|
+
onRequest({ options }) {
|
|
316
|
+
const token = useCookie('auth-token');
|
|
317
|
+
if (token.value) {
|
|
318
|
+
options.headers = {
|
|
319
|
+
...options.headers,
|
|
320
|
+
Authorization: `Bearer ${token.value}`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
onResponseError({ response }) {
|
|
325
|
+
if (response.status === 401) {
|
|
326
|
+
navigateTo('/login');
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
provide: { api },
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// 사용
|
|
337
|
+
const { $api } = useNuxtApp();
|
|
338
|
+
const users = await $api('/users');
|
|
339
|
+
|
|
340
|
+
// ✅ plugins/dayjs.client.ts (클라이언트 전용)
|
|
341
|
+
import dayjs from 'dayjs';
|
|
342
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
343
|
+
|
|
344
|
+
export default defineNuxtPlugin(() => {
|
|
345
|
+
dayjs.extend(relativeTime);
|
|
346
|
+
return { provide: { dayjs } };
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### 9. Composables
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// ✅ composables/useAuth.ts
|
|
354
|
+
export function useAuth() {
|
|
355
|
+
const user = useState<User | null>('auth-user', () => null);
|
|
356
|
+
const isLoggedIn = computed(() => !!user.value);
|
|
357
|
+
|
|
358
|
+
async function login(email: string, password: string) {
|
|
359
|
+
const data = await $fetch('/api/auth/login', {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
body: { email, password },
|
|
362
|
+
});
|
|
363
|
+
user.value = data.user;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function logout() {
|
|
367
|
+
await $fetch('/api/auth/logout', { method: 'POST' });
|
|
368
|
+
user.value = null;
|
|
369
|
+
await navigateTo('/login');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { user, isLoggedIn, login, logout };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ✅ composables/usePagination.ts
|
|
376
|
+
export function usePagination<T>(
|
|
377
|
+
fetchFn: (page: number) => Promise<{ items: T[]; total: number }>
|
|
378
|
+
) {
|
|
379
|
+
const items = ref<T[]>([]);
|
|
380
|
+
const page = ref(1);
|
|
381
|
+
const total = ref(0);
|
|
382
|
+
const isLoading = ref(false);
|
|
383
|
+
|
|
384
|
+
const hasMore = computed(() => items.value.length < total.value);
|
|
385
|
+
|
|
386
|
+
async function loadMore() {
|
|
387
|
+
if (isLoading.value || !hasMore.value) return;
|
|
388
|
+
|
|
389
|
+
isLoading.value = true;
|
|
390
|
+
const data = await fetchFn(page.value);
|
|
391
|
+
items.value.push(...data.items);
|
|
392
|
+
total.value = data.total;
|
|
393
|
+
page.value++;
|
|
394
|
+
isLoading.value = false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { items, isLoading, hasMore, loadMore };
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### 10. Error Handling
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// ✅ error.vue (전역 에러 페이지)
|
|
405
|
+
<script setup lang="ts">
|
|
406
|
+
const props = defineProps<{
|
|
407
|
+
error: {
|
|
408
|
+
statusCode: number;
|
|
409
|
+
message: string;
|
|
410
|
+
};
|
|
411
|
+
}>();
|
|
412
|
+
|
|
413
|
+
const handleError = () => clearError({ redirect: '/' });
|
|
414
|
+
</script>
|
|
415
|
+
|
|
416
|
+
<template>
|
|
417
|
+
<div class="error-page">
|
|
418
|
+
<h1>{{ error.statusCode }}</h1>
|
|
419
|
+
<p>{{ error.message }}</p>
|
|
420
|
+
<button @click="handleError">홈으로</button>
|
|
421
|
+
</div>
|
|
422
|
+
</template>
|
|
423
|
+
|
|
424
|
+
// ✅ 컴포넌트 레벨 에러 처리
|
|
425
|
+
<script setup lang="ts">
|
|
426
|
+
const { data, error } = await useFetch('/api/data');
|
|
427
|
+
|
|
428
|
+
if (error.value) {
|
|
429
|
+
throw createError({
|
|
430
|
+
statusCode: error.value.statusCode,
|
|
431
|
+
message: error.value.message,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
</script>
|
|
435
|
+
|
|
436
|
+
// ✅ NuxtErrorBoundary 사용
|
|
437
|
+
<template>
|
|
438
|
+
<NuxtErrorBoundary @error="logError">
|
|
439
|
+
<SomeComponent />
|
|
440
|
+
<template #error="{ error, clearError }">
|
|
441
|
+
<p>오류 발생: {{ error.message }}</p>
|
|
442
|
+
<button @click="clearError">다시 시도</button>
|
|
443
|
+
</template>
|
|
444
|
+
</NuxtErrorBoundary>
|
|
445
|
+
</template>
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## 파일 구조 (Nuxt 3)
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
project/
|
|
452
|
+
├── .nuxt/ # 빌드 산출물 (git 제외)
|
|
453
|
+
├── assets/ # 빌드에 포함되는 에셋
|
|
454
|
+
├── components/ # 자동 import 컴포넌트
|
|
455
|
+
│ ├── ui/ # 기본 UI 컴포넌트
|
|
456
|
+
│ ├── features/ # 기능별 컴포넌트
|
|
457
|
+
│ └── App*.vue # 앱 공통 컴포넌트
|
|
458
|
+
├── composables/ # 자동 import composables
|
|
459
|
+
├── layouts/ # 레이아웃
|
|
460
|
+
├── middleware/ # 라우트 미들웨어
|
|
461
|
+
├── pages/ # 파일 기반 라우팅
|
|
462
|
+
├── plugins/ # Nuxt 플러그인
|
|
463
|
+
├── public/ # 정적 파일
|
|
464
|
+
├── server/
|
|
465
|
+
│ ├── api/ # API 라우트
|
|
466
|
+
│ ├── middleware/ # 서버 미들웨어
|
|
467
|
+
│ └── utils/ # 서버 유틸리티
|
|
468
|
+
├── stores/ # Pinia 스토어
|
|
469
|
+
├── types/ # TypeScript 타입
|
|
470
|
+
├── utils/ # 유틸리티 함수
|
|
471
|
+
├── app.vue # 앱 루트
|
|
472
|
+
├── nuxt.config.ts # Nuxt 설정
|
|
473
|
+
└── tsconfig.json # TypeScript 설정
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## 안티패턴
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
// ❌ 클라이언트에서 직접 DB 접근
|
|
480
|
+
<script setup>
|
|
481
|
+
import { PrismaClient } from '@prisma/client';
|
|
482
|
+
const prisma = new PrismaClient(); // 클라이언트에서 실행 불가
|
|
483
|
+
</script>
|
|
484
|
+
|
|
485
|
+
// ✅ Server API 통해 접근
|
|
486
|
+
const { data } = await useFetch('/api/users');
|
|
487
|
+
|
|
488
|
+
// ❌ useFetch를 조건부로 사용
|
|
489
|
+
if (someCondition) {
|
|
490
|
+
const { data } = await useFetch('/api/data'); // 에러 발생
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ✅ enabled 옵션 사용
|
|
494
|
+
const { data } = await useFetch('/api/data', {
|
|
495
|
+
immediate: someCondition,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// ❌ navigateTo를 setup 밖에서 사용
|
|
499
|
+
function handleClick() {
|
|
500
|
+
navigateTo('/page'); // 가능하지만 비권장
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ✅ useRouter 사용
|
|
504
|
+
const router = useRouter();
|
|
505
|
+
function handleClick() {
|
|
506
|
+
router.push('/page');
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## 체크리스트
|
|
511
|
+
|
|
512
|
+
- [ ] Auto-imports 활용 (불필요한 import 제거)
|
|
513
|
+
- [ ] Server API 파일 네이밍 규칙 준수 (*.get.ts, *.post.ts)
|
|
514
|
+
- [ ] useFetch/useAsyncData로 SSR 지원 데이터 페칭
|
|
515
|
+
- [ ] useState로 서버/클라이언트 상태 공유
|
|
516
|
+
- [ ] definePageMeta로 페이지별 메타 설정
|
|
517
|
+
- [ ] 미들웨어로 라우트 보호
|
|
518
|
+
- [ ] NuxtErrorBoundary로 에러 처리
|
|
519
|
+
- [ ] useHead/useSeoMeta로 SEO 최적화
|
|
520
|
+
- [ ] Composables로 로직 재사용
|
|
521
|
+
- [ ] TypeScript 엄격 모드 사용
|