@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.
Files changed (98) hide show
  1. package/.claude/settings.json +35 -35
  2. package/.claude/settings.local.json +24 -25
  3. package/.claude/vibe/constitution.md +184 -184
  4. package/.claude/vibe/rules/core/communication-guide.md +104 -104
  5. package/.claude/vibe/rules/core/development-philosophy.md +52 -52
  6. package/.claude/vibe/rules/core/quick-start.md +120 -120
  7. package/.claude/vibe/rules/languages/dart-flutter.md +509 -509
  8. package/.claude/vibe/rules/languages/go.md +396 -396
  9. package/.claude/vibe/rules/languages/java-spring.md +586 -586
  10. package/.claude/vibe/rules/languages/kotlin-android.md +491 -491
  11. package/.claude/vibe/rules/languages/python-django.md +371 -371
  12. package/.claude/vibe/rules/languages/python-fastapi.md +386 -386
  13. package/.claude/vibe/rules/languages/rust.md +425 -425
  14. package/.claude/vibe/rules/languages/swift-ios.md +516 -516
  15. package/.claude/vibe/rules/languages/typescript-nextjs.md +441 -441
  16. package/.claude/vibe/rules/languages/typescript-node.md +375 -375
  17. package/.claude/vibe/rules/languages/typescript-nuxt.md +521 -521
  18. package/.claude/vibe/rules/languages/typescript-react-native.md +446 -446
  19. package/.claude/vibe/rules/languages/typescript-react.md +525 -525
  20. package/.claude/vibe/rules/languages/typescript-vue.md +353 -353
  21. package/.claude/vibe/rules/quality/bdd-contract-testing.md +388 -388
  22. package/.claude/vibe/rules/quality/checklist.md +276 -276
  23. package/.claude/vibe/rules/quality/testing-strategy.md +437 -437
  24. package/.claude/vibe/rules/standards/anti-patterns.md +369 -369
  25. package/.claude/vibe/rules/standards/code-structure.md +291 -291
  26. package/.claude/vibe/rules/standards/complexity-metrics.md +312 -312
  27. package/.claude/vibe/rules/standards/naming-conventions.md +198 -198
  28. package/.claude/vibe/setup.sh +31 -31
  29. package/.claude/vibe/templates/constitution-template.md +184 -184
  30. package/.claude/vibe/templates/contract-backend-template.md +517 -517
  31. package/.claude/vibe/templates/contract-frontend-template.md +594 -594
  32. package/.claude/vibe/templates/feature-template.md +96 -96
  33. package/.claude/vibe/templates/spec-template.md +199 -199
  34. package/CLAUDE.md +345 -323
  35. package/LICENSE +21 -21
  36. package/README.md +744 -724
  37. package/agents/compounder.md +261 -261
  38. package/agents/diagrammer.md +178 -178
  39. package/agents/e2e-tester.md +266 -266
  40. package/agents/explorer.md +48 -48
  41. package/agents/implementer.md +53 -53
  42. package/agents/research/best-practices-agent.md +139 -139
  43. package/agents/research/codebase-patterns-agent.md +147 -147
  44. package/agents/research/framework-docs-agent.md +181 -181
  45. package/agents/research/security-advisory-agent.md +167 -167
  46. package/agents/review/architecture-reviewer.md +107 -107
  47. package/agents/review/complexity-reviewer.md +116 -116
  48. package/agents/review/data-integrity-reviewer.md +88 -88
  49. package/agents/review/git-history-reviewer.md +103 -103
  50. package/agents/review/performance-reviewer.md +86 -86
  51. package/agents/review/python-reviewer.md +152 -152
  52. package/agents/review/rails-reviewer.md +139 -139
  53. package/agents/review/react-reviewer.md +144 -144
  54. package/agents/review/security-reviewer.md +80 -80
  55. package/agents/review/simplicity-reviewer.md +140 -140
  56. package/agents/review/test-coverage-reviewer.md +116 -116
  57. package/agents/review/typescript-reviewer.md +127 -127
  58. package/agents/searcher.md +54 -54
  59. package/agents/simplifier.md +119 -119
  60. package/agents/tester.md +49 -49
  61. package/agents/ui-previewer.md +137 -137
  62. package/commands/vibe.analyze.md +245 -180
  63. package/commands/vibe.reason.md +223 -183
  64. package/commands/vibe.review.md +200 -136
  65. package/commands/vibe.run.md +838 -836
  66. package/commands/vibe.spec.md +419 -383
  67. package/commands/vibe.utils.md +101 -101
  68. package/commands/vibe.verify.md +282 -241
  69. package/dist/cli/index.js +385 -385
  70. package/dist/lib/MemoryManager.d.ts.map +1 -1
  71. package/dist/lib/MemoryManager.js +119 -114
  72. package/dist/lib/MemoryManager.js.map +1 -1
  73. package/dist/lib/PythonParser.js +108 -108
  74. package/dist/lib/gemini-mcp.js +15 -15
  75. package/dist/lib/gemini-oauth.js +35 -35
  76. package/dist/lib/gpt-mcp.js +17 -17
  77. package/dist/lib/gpt-oauth.js +44 -44
  78. package/dist/tools/analytics/getUsageAnalytics.js +12 -12
  79. package/dist/tools/index.d.ts +50 -0
  80. package/dist/tools/index.d.ts.map +1 -0
  81. package/dist/tools/index.js +61 -0
  82. package/dist/tools/index.js.map +1 -0
  83. package/dist/tools/memory/createMemoryTimeline.js +10 -10
  84. package/dist/tools/memory/getMemoryGraph.js +12 -12
  85. package/dist/tools/memory/getSessionContext.js +9 -9
  86. package/dist/tools/memory/linkMemories.js +14 -14
  87. package/dist/tools/memory/listMemories.js +4 -4
  88. package/dist/tools/memory/recallMemory.js +4 -4
  89. package/dist/tools/memory/saveMemory.js +4 -4
  90. package/dist/tools/memory/searchMemoriesAdvanced.js +22 -22
  91. package/dist/tools/planning/generatePrd.js +46 -46
  92. package/dist/tools/prompt/enhancePromptGemini.js +160 -160
  93. package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
  94. package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
  95. package/hooks/hooks.json +121 -103
  96. package/package.json +73 -69
  97. package/skills/git-worktree.md +178 -178
  98. 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 엄격 모드 사용