@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,353 +1,353 @@
|
|
|
1
|
-
# 🟢 TypeScript + Vue/Nuxt 품질 규칙
|
|
2
|
-
|
|
3
|
-
## 핵심 원칙 (core에서 상속)
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
✅ 단일 책임 (SRP)
|
|
7
|
-
✅ 중복 제거 (DRY)
|
|
8
|
-
✅ 재사용성
|
|
9
|
-
✅ 낮은 복잡도
|
|
10
|
-
✅ 함수 ≤ 30줄, Template ≤ 100줄
|
|
11
|
-
✅ 중첩 ≤ 3단계
|
|
12
|
-
✅ Cyclomatic complexity ≤ 10
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Vue 3 + TypeScript 특화 규칙
|
|
16
|
-
|
|
17
|
-
### 1. Composition API 사용 (Options API 지양)
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
// ❌ Options API (레거시)
|
|
21
|
-
export default {
|
|
22
|
-
data() {
|
|
23
|
-
return { count: 0 };
|
|
24
|
-
},
|
|
25
|
-
methods: {
|
|
26
|
-
increment() {
|
|
27
|
-
this.count++;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// ✅ Composition API + script setup
|
|
33
|
-
<script setup lang="ts">
|
|
34
|
-
import { ref, computed, onMounted } from 'vue';
|
|
35
|
-
|
|
36
|
-
const count = ref(0);
|
|
37
|
-
const doubled = computed(() => count.value * 2);
|
|
38
|
-
|
|
39
|
-
function increment() {
|
|
40
|
-
count.value++;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
onMounted(() => {
|
|
44
|
-
console.log('컴포넌트 마운트됨');
|
|
45
|
-
});
|
|
46
|
-
</script>
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### 2. 타입 안전한 Props/Emits
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
// ✅ Props 타입 정의
|
|
53
|
-
interface Props {
|
|
54
|
-
userId: string;
|
|
55
|
-
title?: string;
|
|
56
|
-
items: Item[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
60
|
-
title: '기본 제목',
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// ✅ Emits 타입 정의
|
|
64
|
-
interface Emits {
|
|
65
|
-
(e: 'update', value: string): void;
|
|
66
|
-
(e: 'delete', id: number): void;
|
|
67
|
-
(e: 'select', item: Item): void;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const emit = defineEmits<Emits>();
|
|
71
|
-
|
|
72
|
-
// 사용
|
|
73
|
-
emit('update', '새 값');
|
|
74
|
-
emit('delete', 123);
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### 3. Composables로 로직 분리
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
// ✅ composables/useUser.ts
|
|
81
|
-
import { ref, computed } from 'vue';
|
|
82
|
-
import type { User } from '@/types';
|
|
83
|
-
|
|
84
|
-
export function useUser(userId: string) {
|
|
85
|
-
const user = ref<User | null>(null);
|
|
86
|
-
const isLoading = ref(false);
|
|
87
|
-
const error = ref<string | null>(null);
|
|
88
|
-
|
|
89
|
-
const fullName = computed(() =>
|
|
90
|
-
user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
async function fetchUser() {
|
|
94
|
-
isLoading.value = true;
|
|
95
|
-
error.value = null;
|
|
96
|
-
try {
|
|
97
|
-
const response = await api.getUser(userId);
|
|
98
|
-
user.value = response.data;
|
|
99
|
-
} catch (e) {
|
|
100
|
-
error.value = '사용자를 불러오지 못했습니다';
|
|
101
|
-
} finally {
|
|
102
|
-
isLoading.value = false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
user,
|
|
108
|
-
isLoading,
|
|
109
|
-
error,
|
|
110
|
-
fullName,
|
|
111
|
-
fetchUser,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 컴포넌트에서 사용
|
|
116
|
-
<script setup lang="ts">
|
|
117
|
-
const { user, isLoading, fetchUser } = useUser(props.userId);
|
|
118
|
-
|
|
119
|
-
onMounted(fetchUser);
|
|
120
|
-
</script>
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### 4. Pinia 상태 관리
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
// ✅ stores/user.ts
|
|
127
|
-
import { defineStore } from 'pinia';
|
|
128
|
-
import type { User } from '@/types';
|
|
129
|
-
|
|
130
|
-
interface UserState {
|
|
131
|
-
currentUser: User | null;
|
|
132
|
-
users: User[];
|
|
133
|
-
isLoading: boolean;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export const useUserStore = defineStore('user', {
|
|
137
|
-
state: (): UserState => ({
|
|
138
|
-
currentUser: null,
|
|
139
|
-
users: [],
|
|
140
|
-
isLoading: false,
|
|
141
|
-
}),
|
|
142
|
-
|
|
143
|
-
getters: {
|
|
144
|
-
isLoggedIn: (state) => !!state.currentUser,
|
|
145
|
-
userCount: (state) => state.users.length,
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
actions: {
|
|
149
|
-
async login(email: string, password: string) {
|
|
150
|
-
this.isLoading = true;
|
|
151
|
-
try {
|
|
152
|
-
const user = await authApi.login(email, password);
|
|
153
|
-
this.currentUser = user;
|
|
154
|
-
} finally {
|
|
155
|
-
this.isLoading = false;
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
logout() {
|
|
160
|
-
this.currentUser = null;
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Setup Store 스타일 (권장)
|
|
166
|
-
export const useUserStore = defineStore('user', () => {
|
|
167
|
-
const currentUser = ref<User | null>(null);
|
|
168
|
-
const isLoggedIn = computed(() => !!currentUser.value);
|
|
169
|
-
|
|
170
|
-
async function login(email: string, password: string) {
|
|
171
|
-
currentUser.value = await authApi.login(email, password);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return { currentUser, isLoggedIn, login };
|
|
175
|
-
});
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### 5. Nuxt 3 특화 규칙
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
// ✅ Server API Routes (server/api/)
|
|
182
|
-
// server/api/users/[id].get.ts
|
|
183
|
-
export default defineEventHandler(async (event) => {
|
|
184
|
-
const id = getRouterParam(event, 'id');
|
|
185
|
-
|
|
186
|
-
if (!id) {
|
|
187
|
-
throw createError({
|
|
188
|
-
statusCode: 400,
|
|
189
|
-
message: 'ID가 필요합니다',
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const user = await prisma.user.findUnique({ where: { id } });
|
|
194
|
-
|
|
195
|
-
if (!user) {
|
|
196
|
-
throw createError({
|
|
197
|
-
statusCode: 404,
|
|
198
|
-
message: '사용자를 찾을 수 없습니다',
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return user;
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// ✅ useFetch / useAsyncData
|
|
206
|
-
<script setup lang="ts">
|
|
207
|
-
// SSR 지원 데이터 페칭
|
|
208
|
-
const { data: user, pending, error } = await useFetch<User>(
|
|
209
|
-
`/api/users/${props.userId}`
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
// 캐싱 키 지정
|
|
213
|
-
const { data: posts } = await useAsyncData(
|
|
214
|
-
`user-${props.userId}-posts`,
|
|
215
|
-
() => $fetch(`/api/users/${props.userId}/posts`)
|
|
216
|
-
);
|
|
217
|
-
</script>
|
|
218
|
-
|
|
219
|
-
// ✅ Middleware
|
|
220
|
-
// middleware/auth.ts
|
|
221
|
-
export default defineNuxtRouteMiddleware((to, from) => {
|
|
222
|
-
const { isLoggedIn } = useUserStore();
|
|
223
|
-
|
|
224
|
-
if (!isLoggedIn && to.path !== '/login') {
|
|
225
|
-
return navigateTo('/login');
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### 6. 컴포넌트 구조
|
|
231
|
-
|
|
232
|
-
```vue
|
|
233
|
-
<!-- ✅ 권장 컴포넌트 구조 -->
|
|
234
|
-
<script setup lang="ts">
|
|
235
|
-
// 1. 타입 import
|
|
236
|
-
import type { User, Item } from '@/types';
|
|
237
|
-
|
|
238
|
-
// 2. 컴포넌트 import
|
|
239
|
-
import UserAvatar from '@/components/UserAvatar.vue';
|
|
240
|
-
|
|
241
|
-
// 3. Props/Emits
|
|
242
|
-
interface Props {
|
|
243
|
-
user: User;
|
|
244
|
-
editable?: boolean;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
248
|
-
editable: false,
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const emit = defineEmits<{
|
|
252
|
-
(e: 'update', user: User): void;
|
|
253
|
-
}>();
|
|
254
|
-
|
|
255
|
-
// 4. Composables
|
|
256
|
-
const { isLoading, save } = useUserForm();
|
|
257
|
-
|
|
258
|
-
// 5. Reactive state
|
|
259
|
-
const formData = ref({ ...props.user });
|
|
260
|
-
const isEditing = ref(false);
|
|
261
|
-
|
|
262
|
-
// 6. Computed
|
|
263
|
-
const canSave = computed(() =>
|
|
264
|
-
formData.value.name.length > 0 && !isLoading.value
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
// 7. Methods
|
|
268
|
-
async function handleSave() {
|
|
269
|
-
await save(formData.value);
|
|
270
|
-
emit('update', formData.value);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// 8. Lifecycle
|
|
274
|
-
onMounted(() => {
|
|
275
|
-
console.log('컴포넌트 준비됨');
|
|
276
|
-
});
|
|
277
|
-
</script>
|
|
278
|
-
|
|
279
|
-
<template>
|
|
280
|
-
<div class="user-card">
|
|
281
|
-
<UserAvatar :src="user.avatar" />
|
|
282
|
-
<h2>{{ user.name }}</h2>
|
|
283
|
-
<button
|
|
284
|
-
v-if="editable"
|
|
285
|
-
:disabled="!canSave"
|
|
286
|
-
@click="handleSave"
|
|
287
|
-
>
|
|
288
|
-
저장
|
|
289
|
-
</button>
|
|
290
|
-
</div>
|
|
291
|
-
</template>
|
|
292
|
-
|
|
293
|
-
<style scoped>
|
|
294
|
-
.user-card {
|
|
295
|
-
padding: 1rem;
|
|
296
|
-
border-radius: 8px;
|
|
297
|
-
}
|
|
298
|
-
</style>
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
## 안티패턴
|
|
302
|
-
|
|
303
|
-
```typescript
|
|
304
|
-
// ❌ v-if와 v-for 함께 사용
|
|
305
|
-
<li v-for="user in users" v-if="user.isActive">
|
|
306
|
-
|
|
307
|
-
// ✅ computed로 필터링
|
|
308
|
-
const activeUsers = computed(() => users.value.filter(u => u.isActive));
|
|
309
|
-
<li v-for="user in activeUsers">
|
|
310
|
-
|
|
311
|
-
// ❌ Props 직접 수정
|
|
312
|
-
props.user.name = '새 이름';
|
|
313
|
-
|
|
314
|
-
// ✅ emit으로 부모에게 알림
|
|
315
|
-
emit('update', { ...props.user, name: '새 이름' });
|
|
316
|
-
|
|
317
|
-
// ❌ $refs 남용
|
|
318
|
-
this.$refs.input.focus();
|
|
319
|
-
|
|
320
|
-
// ✅ template ref + expose
|
|
321
|
-
const inputRef = ref<HTMLInputElement>();
|
|
322
|
-
defineExpose({ focus: () => inputRef.value?.focus() });
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
## 파일 구조 (Nuxt 3)
|
|
326
|
-
|
|
327
|
-
```
|
|
328
|
-
project/
|
|
329
|
-
├── components/
|
|
330
|
-
│ ├── ui/ # 기본 UI 컴포넌트
|
|
331
|
-
│ ├── features/ # 기능별 컴포넌트
|
|
332
|
-
│ └── layouts/ # 레이아웃 컴포넌트
|
|
333
|
-
├── composables/ # Composition 함수
|
|
334
|
-
├── stores/ # Pinia 스토어
|
|
335
|
-
├── server/
|
|
336
|
-
│ ├── api/ # API 라우트
|
|
337
|
-
│ ├── middleware/ # 서버 미들웨어
|
|
338
|
-
│ └── utils/ # 서버 유틸리티
|
|
339
|
-
├── pages/ # 파일 기반 라우팅
|
|
340
|
-
├── middleware/ # 클라이언트 미들웨어
|
|
341
|
-
├── types/ # TypeScript 타입
|
|
342
|
-
└── utils/ # 유틸리티 함수
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
## 체크리스트
|
|
346
|
-
|
|
347
|
-
- [ ] Composition API + `<script setup>` 사용
|
|
348
|
-
- [ ] Props/Emits 타입 정의
|
|
349
|
-
- [ ] Composables로 로직 분리
|
|
350
|
-
- [ ] Pinia Setup Store 스타일 사용
|
|
351
|
-
- [ ] `any` 타입 사용 금지
|
|
352
|
-
- [ ] v-if/v-for 분리
|
|
353
|
-
- [ ] scoped 스타일 사용
|
|
1
|
+
# 🟢 TypeScript + Vue/Nuxt 품질 규칙
|
|
2
|
+
|
|
3
|
+
## 핵심 원칙 (core에서 상속)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
✅ 단일 책임 (SRP)
|
|
7
|
+
✅ 중복 제거 (DRY)
|
|
8
|
+
✅ 재사용성
|
|
9
|
+
✅ 낮은 복잡도
|
|
10
|
+
✅ 함수 ≤ 30줄, Template ≤ 100줄
|
|
11
|
+
✅ 중첩 ≤ 3단계
|
|
12
|
+
✅ Cyclomatic complexity ≤ 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Vue 3 + TypeScript 특화 규칙
|
|
16
|
+
|
|
17
|
+
### 1. Composition API 사용 (Options API 지양)
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// ❌ Options API (레거시)
|
|
21
|
+
export default {
|
|
22
|
+
data() {
|
|
23
|
+
return { count: 0 };
|
|
24
|
+
},
|
|
25
|
+
methods: {
|
|
26
|
+
increment() {
|
|
27
|
+
this.count++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ✅ Composition API + script setup
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { ref, computed, onMounted } from 'vue';
|
|
35
|
+
|
|
36
|
+
const count = ref(0);
|
|
37
|
+
const doubled = computed(() => count.value * 2);
|
|
38
|
+
|
|
39
|
+
function increment() {
|
|
40
|
+
count.value++;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onMounted(() => {
|
|
44
|
+
console.log('컴포넌트 마운트됨');
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. 타입 안전한 Props/Emits
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// ✅ Props 타입 정의
|
|
53
|
+
interface Props {
|
|
54
|
+
userId: string;
|
|
55
|
+
title?: string;
|
|
56
|
+
items: Item[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
60
|
+
title: '기본 제목',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ✅ Emits 타입 정의
|
|
64
|
+
interface Emits {
|
|
65
|
+
(e: 'update', value: string): void;
|
|
66
|
+
(e: 'delete', id: number): void;
|
|
67
|
+
(e: 'select', item: Item): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const emit = defineEmits<Emits>();
|
|
71
|
+
|
|
72
|
+
// 사용
|
|
73
|
+
emit('update', '새 값');
|
|
74
|
+
emit('delete', 123);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Composables로 로직 분리
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// ✅ composables/useUser.ts
|
|
81
|
+
import { ref, computed } from 'vue';
|
|
82
|
+
import type { User } from '@/types';
|
|
83
|
+
|
|
84
|
+
export function useUser(userId: string) {
|
|
85
|
+
const user = ref<User | null>(null);
|
|
86
|
+
const isLoading = ref(false);
|
|
87
|
+
const error = ref<string | null>(null);
|
|
88
|
+
|
|
89
|
+
const fullName = computed(() =>
|
|
90
|
+
user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
async function fetchUser() {
|
|
94
|
+
isLoading.value = true;
|
|
95
|
+
error.value = null;
|
|
96
|
+
try {
|
|
97
|
+
const response = await api.getUser(userId);
|
|
98
|
+
user.value = response.data;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
error.value = '사용자를 불러오지 못했습니다';
|
|
101
|
+
} finally {
|
|
102
|
+
isLoading.value = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
user,
|
|
108
|
+
isLoading,
|
|
109
|
+
error,
|
|
110
|
+
fullName,
|
|
111
|
+
fetchUser,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 컴포넌트에서 사용
|
|
116
|
+
<script setup lang="ts">
|
|
117
|
+
const { user, isLoading, fetchUser } = useUser(props.userId);
|
|
118
|
+
|
|
119
|
+
onMounted(fetchUser);
|
|
120
|
+
</script>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. Pinia 상태 관리
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// ✅ stores/user.ts
|
|
127
|
+
import { defineStore } from 'pinia';
|
|
128
|
+
import type { User } from '@/types';
|
|
129
|
+
|
|
130
|
+
interface UserState {
|
|
131
|
+
currentUser: User | null;
|
|
132
|
+
users: User[];
|
|
133
|
+
isLoading: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const useUserStore = defineStore('user', {
|
|
137
|
+
state: (): UserState => ({
|
|
138
|
+
currentUser: null,
|
|
139
|
+
users: [],
|
|
140
|
+
isLoading: false,
|
|
141
|
+
}),
|
|
142
|
+
|
|
143
|
+
getters: {
|
|
144
|
+
isLoggedIn: (state) => !!state.currentUser,
|
|
145
|
+
userCount: (state) => state.users.length,
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
actions: {
|
|
149
|
+
async login(email: string, password: string) {
|
|
150
|
+
this.isLoading = true;
|
|
151
|
+
try {
|
|
152
|
+
const user = await authApi.login(email, password);
|
|
153
|
+
this.currentUser = user;
|
|
154
|
+
} finally {
|
|
155
|
+
this.isLoading = false;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
logout() {
|
|
160
|
+
this.currentUser = null;
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Setup Store 스타일 (권장)
|
|
166
|
+
export const useUserStore = defineStore('user', () => {
|
|
167
|
+
const currentUser = ref<User | null>(null);
|
|
168
|
+
const isLoggedIn = computed(() => !!currentUser.value);
|
|
169
|
+
|
|
170
|
+
async function login(email: string, password: string) {
|
|
171
|
+
currentUser.value = await authApi.login(email, password);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { currentUser, isLoggedIn, login };
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 5. Nuxt 3 특화 규칙
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// ✅ Server API Routes (server/api/)
|
|
182
|
+
// server/api/users/[id].get.ts
|
|
183
|
+
export default defineEventHandler(async (event) => {
|
|
184
|
+
const id = getRouterParam(event, 'id');
|
|
185
|
+
|
|
186
|
+
if (!id) {
|
|
187
|
+
throw createError({
|
|
188
|
+
statusCode: 400,
|
|
189
|
+
message: 'ID가 필요합니다',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
194
|
+
|
|
195
|
+
if (!user) {
|
|
196
|
+
throw createError({
|
|
197
|
+
statusCode: 404,
|
|
198
|
+
message: '사용자를 찾을 수 없습니다',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return user;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ✅ useFetch / useAsyncData
|
|
206
|
+
<script setup lang="ts">
|
|
207
|
+
// SSR 지원 데이터 페칭
|
|
208
|
+
const { data: user, pending, error } = await useFetch<User>(
|
|
209
|
+
`/api/users/${props.userId}`
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// 캐싱 키 지정
|
|
213
|
+
const { data: posts } = await useAsyncData(
|
|
214
|
+
`user-${props.userId}-posts`,
|
|
215
|
+
() => $fetch(`/api/users/${props.userId}/posts`)
|
|
216
|
+
);
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
// ✅ Middleware
|
|
220
|
+
// middleware/auth.ts
|
|
221
|
+
export default defineNuxtRouteMiddleware((to, from) => {
|
|
222
|
+
const { isLoggedIn } = useUserStore();
|
|
223
|
+
|
|
224
|
+
if (!isLoggedIn && to.path !== '/login') {
|
|
225
|
+
return navigateTo('/login');
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 6. 컴포넌트 구조
|
|
231
|
+
|
|
232
|
+
```vue
|
|
233
|
+
<!-- ✅ 권장 컴포넌트 구조 -->
|
|
234
|
+
<script setup lang="ts">
|
|
235
|
+
// 1. 타입 import
|
|
236
|
+
import type { User, Item } from '@/types';
|
|
237
|
+
|
|
238
|
+
// 2. 컴포넌트 import
|
|
239
|
+
import UserAvatar from '@/components/UserAvatar.vue';
|
|
240
|
+
|
|
241
|
+
// 3. Props/Emits
|
|
242
|
+
interface Props {
|
|
243
|
+
user: User;
|
|
244
|
+
editable?: boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
248
|
+
editable: false,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const emit = defineEmits<{
|
|
252
|
+
(e: 'update', user: User): void;
|
|
253
|
+
}>();
|
|
254
|
+
|
|
255
|
+
// 4. Composables
|
|
256
|
+
const { isLoading, save } = useUserForm();
|
|
257
|
+
|
|
258
|
+
// 5. Reactive state
|
|
259
|
+
const formData = ref({ ...props.user });
|
|
260
|
+
const isEditing = ref(false);
|
|
261
|
+
|
|
262
|
+
// 6. Computed
|
|
263
|
+
const canSave = computed(() =>
|
|
264
|
+
formData.value.name.length > 0 && !isLoading.value
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// 7. Methods
|
|
268
|
+
async function handleSave() {
|
|
269
|
+
await save(formData.value);
|
|
270
|
+
emit('update', formData.value);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 8. Lifecycle
|
|
274
|
+
onMounted(() => {
|
|
275
|
+
console.log('컴포넌트 준비됨');
|
|
276
|
+
});
|
|
277
|
+
</script>
|
|
278
|
+
|
|
279
|
+
<template>
|
|
280
|
+
<div class="user-card">
|
|
281
|
+
<UserAvatar :src="user.avatar" />
|
|
282
|
+
<h2>{{ user.name }}</h2>
|
|
283
|
+
<button
|
|
284
|
+
v-if="editable"
|
|
285
|
+
:disabled="!canSave"
|
|
286
|
+
@click="handleSave"
|
|
287
|
+
>
|
|
288
|
+
저장
|
|
289
|
+
</button>
|
|
290
|
+
</div>
|
|
291
|
+
</template>
|
|
292
|
+
|
|
293
|
+
<style scoped>
|
|
294
|
+
.user-card {
|
|
295
|
+
padding: 1rem;
|
|
296
|
+
border-radius: 8px;
|
|
297
|
+
}
|
|
298
|
+
</style>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## 안티패턴
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// ❌ v-if와 v-for 함께 사용
|
|
305
|
+
<li v-for="user in users" v-if="user.isActive">
|
|
306
|
+
|
|
307
|
+
// ✅ computed로 필터링
|
|
308
|
+
const activeUsers = computed(() => users.value.filter(u => u.isActive));
|
|
309
|
+
<li v-for="user in activeUsers">
|
|
310
|
+
|
|
311
|
+
// ❌ Props 직접 수정
|
|
312
|
+
props.user.name = '새 이름';
|
|
313
|
+
|
|
314
|
+
// ✅ emit으로 부모에게 알림
|
|
315
|
+
emit('update', { ...props.user, name: '새 이름' });
|
|
316
|
+
|
|
317
|
+
// ❌ $refs 남용
|
|
318
|
+
this.$refs.input.focus();
|
|
319
|
+
|
|
320
|
+
// ✅ template ref + expose
|
|
321
|
+
const inputRef = ref<HTMLInputElement>();
|
|
322
|
+
defineExpose({ focus: () => inputRef.value?.focus() });
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## 파일 구조 (Nuxt 3)
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
project/
|
|
329
|
+
├── components/
|
|
330
|
+
│ ├── ui/ # 기본 UI 컴포넌트
|
|
331
|
+
│ ├── features/ # 기능별 컴포넌트
|
|
332
|
+
│ └── layouts/ # 레이아웃 컴포넌트
|
|
333
|
+
├── composables/ # Composition 함수
|
|
334
|
+
├── stores/ # Pinia 스토어
|
|
335
|
+
├── server/
|
|
336
|
+
│ ├── api/ # API 라우트
|
|
337
|
+
│ ├── middleware/ # 서버 미들웨어
|
|
338
|
+
│ └── utils/ # 서버 유틸리티
|
|
339
|
+
├── pages/ # 파일 기반 라우팅
|
|
340
|
+
├── middleware/ # 클라이언트 미들웨어
|
|
341
|
+
├── types/ # TypeScript 타입
|
|
342
|
+
└── utils/ # 유틸리티 함수
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## 체크리스트
|
|
346
|
+
|
|
347
|
+
- [ ] Composition API + `<script setup>` 사용
|
|
348
|
+
- [ ] Props/Emits 타입 정의
|
|
349
|
+
- [ ] Composables로 로직 분리
|
|
350
|
+
- [ ] Pinia Setup Store 스타일 사용
|
|
351
|
+
- [ ] `any` 타입 사용 금지
|
|
352
|
+
- [ ] v-if/v-for 분리
|
|
353
|
+
- [ ] scoped 스타일 사용
|