@su-record/vibe 0.4.5 → 0.4.6
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/agents/simplifier.md +1 -1
- package/.claude/commands/vibe.analyze.md +1 -1
- package/.claude/commands/vibe.run.md +1 -1
- package/.claude/commands/vibe.spec.md +2 -2
- package/.claude/commands/vibe.verify.md +1 -1
- package/.claude/settings.local.json +3 -1
- package/README.md +4 -4
- package/bin/vibe +41 -13
- package/package.json +1 -1
- package/templates/hooks-template.json +1 -1
- package/.agent/rules/core/communication-guide.md +0 -104
- package/.agent/rules/core/development-philosophy.md +0 -53
- package/.agent/rules/core/quick-start.md +0 -121
- package/.agent/rules/languages/dart-flutter.md +0 -509
- package/.agent/rules/languages/go.md +0 -396
- package/.agent/rules/languages/java-spring.md +0 -586
- package/.agent/rules/languages/kotlin-android.md +0 -491
- package/.agent/rules/languages/python-django.md +0 -371
- package/.agent/rules/languages/python-fastapi.md +0 -386
- package/.agent/rules/languages/rust.md +0 -425
- package/.agent/rules/languages/swift-ios.md +0 -516
- package/.agent/rules/languages/typescript-nextjs.md +0 -441
- package/.agent/rules/languages/typescript-node.md +0 -375
- package/.agent/rules/languages/typescript-react-native.md +0 -446
- package/.agent/rules/languages/typescript-react.md +0 -525
- package/.agent/rules/languages/typescript-vue.md +0 -353
- package/.agent/rules/quality/bdd-contract-testing.md +0 -388
- package/.agent/rules/quality/checklist.md +0 -276
- package/.agent/rules/quality/testing-strategy.md +0 -437
- package/.agent/rules/standards/anti-patterns.md +0 -369
- package/.agent/rules/standards/code-structure.md +0 -291
- package/.agent/rules/standards/complexity-metrics.md +0 -312
- package/.agent/rules/standards/naming-conventions.md +0 -198
- package/.agent/rules/tools/mcp-hi-ai-guide.md +0 -665
- package/.agent/rules/tools/mcp-workflow.md +0 -51
|
@@ -1,441 +0,0 @@
|
|
|
1
|
-
# ⚡ TypeScript + Next.js 품질 규칙
|
|
2
|
-
|
|
3
|
-
## 핵심 원칙 (core + React에서 상속)
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
✅ 단일 책임 (SRP)
|
|
7
|
-
✅ 중복 제거 (DRY)
|
|
8
|
-
✅ 재사용성
|
|
9
|
-
✅ 낮은 복잡도
|
|
10
|
-
✅ 함수 ≤ 30줄, JSX ≤ 50줄
|
|
11
|
-
✅ React 규칙 모두 적용
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Next.js 특화 규칙
|
|
15
|
-
|
|
16
|
-
### 1. App Router (Next.js 13+) 우선
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
// ✅ App Router 구조
|
|
20
|
-
app/
|
|
21
|
-
├── layout.tsx # 루트 레이아웃
|
|
22
|
-
├── page.tsx # 홈 페이지
|
|
23
|
-
├── loading.tsx # 로딩 UI
|
|
24
|
-
├── error.tsx # 에러 UI
|
|
25
|
-
├── not-found.tsx # 404 페이지
|
|
26
|
-
├── users/
|
|
27
|
-
│ ├── page.tsx # /users
|
|
28
|
-
│ ├── [id]/
|
|
29
|
-
│ │ └── page.tsx # /users/:id
|
|
30
|
-
│ └── loading.tsx # /users 로딩
|
|
31
|
-
└── api/
|
|
32
|
-
└── users/
|
|
33
|
-
└── route.ts # API Route
|
|
34
|
-
|
|
35
|
-
// ✅ 서버 컴포넌트 (기본)
|
|
36
|
-
export default async function UsersPage() {
|
|
37
|
-
// 서버에서 데이터 페칭
|
|
38
|
-
const users = await getUsers();
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<div>
|
|
42
|
-
<h1>Users</h1>
|
|
43
|
-
<UserList users={users} />
|
|
44
|
-
</div>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ✅ 클라이언트 컴포넌트 (필요 시에만)
|
|
49
|
-
'use client';
|
|
50
|
-
|
|
51
|
-
import { useState } from 'react';
|
|
52
|
-
|
|
53
|
-
export function InteractiveButton() {
|
|
54
|
-
const [count, setCount] = useState(0);
|
|
55
|
-
|
|
56
|
-
return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### 2. 서버 컴포넌트 vs 클라이언트 컴포넌트
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
// ✅ 서버 컴포넌트 (권장)
|
|
64
|
-
// - 데이터 페칭
|
|
65
|
-
// - 환경 변수 접근
|
|
66
|
-
// - DB 직접 접근
|
|
67
|
-
// - 민감한 정보 처리
|
|
68
|
-
|
|
69
|
-
async function UserProfile({ userId }: { userId: string }) {
|
|
70
|
-
// 서버에서만 실행 (API 키 노출 안 됨)
|
|
71
|
-
const user = await db.user.findUnique({
|
|
72
|
-
where: { id: userId },
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
return <div>{user.name}</div>;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ✅ 클라이언트 컴포넌트 (필요 시만)
|
|
79
|
-
// - useState, useEffect 사용
|
|
80
|
-
// - 이벤트 핸들러
|
|
81
|
-
// - 브라우저 API
|
|
82
|
-
// - 서드파티 라이브러리 (대부분)
|
|
83
|
-
|
|
84
|
-
'use client';
|
|
85
|
-
|
|
86
|
-
function SearchBar() {
|
|
87
|
-
const [query, setQuery] = useState('');
|
|
88
|
-
|
|
89
|
-
return <input value={query} onChange={e => setQuery(e.target.value)} />;
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### 3. Data Fetching 패턴
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
// ✅ 서버 컴포넌트에서 직접 fetch
|
|
97
|
-
async function PostsPage() {
|
|
98
|
-
// 자동 캐싱, 재검증
|
|
99
|
-
const posts = await fetch('https://api.example.com/posts', {
|
|
100
|
-
next: { revalidate: 60 }, // 60초 캐싱
|
|
101
|
-
}).then(res => res.json());
|
|
102
|
-
|
|
103
|
-
return <PostList posts={posts} />;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ✅ 병렬 데이터 페칭
|
|
107
|
-
async function UserDashboard({ userId }: { userId: string }) {
|
|
108
|
-
const [user, posts, comments] = await Promise.all([
|
|
109
|
-
getUser(userId),
|
|
110
|
-
getUserPosts(userId),
|
|
111
|
-
getUserComments(userId),
|
|
112
|
-
]);
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<div>
|
|
116
|
-
<UserCard user={user} />
|
|
117
|
-
<PostList posts={posts} />
|
|
118
|
-
<CommentList comments={comments} />
|
|
119
|
-
</div>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ✅ 순차적 데이터 페칭 (의존 관계)
|
|
124
|
-
async function UserWithPosts({ username }: { username: string }) {
|
|
125
|
-
// 1. 사용자 조회
|
|
126
|
-
const user = await getUserByUsername(username);
|
|
127
|
-
|
|
128
|
-
// 2. 사용자 ID로 게시물 조회
|
|
129
|
-
const posts = await getUserPosts(user.id);
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<div>
|
|
133
|
-
<UserCard user={user} />
|
|
134
|
-
<PostList posts={posts} />
|
|
135
|
-
</div>
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### 4. API Routes (Route Handlers)
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
// app/api/users/route.ts
|
|
144
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
145
|
-
import { z } from 'zod';
|
|
146
|
-
|
|
147
|
-
const createUserSchema = z.object({
|
|
148
|
-
email: z.string().email(),
|
|
149
|
-
name: z.string().min(1),
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// ✅ GET /api/users
|
|
153
|
-
export async function GET(request: NextRequest) {
|
|
154
|
-
try {
|
|
155
|
-
const users = await db.user.findMany();
|
|
156
|
-
return NextResponse.json(users);
|
|
157
|
-
} catch (error) {
|
|
158
|
-
return NextResponse.json(
|
|
159
|
-
{ error: 'Failed to fetch users' },
|
|
160
|
-
{ status: 500 }
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ✅ POST /api/users
|
|
166
|
-
export async function POST(request: NextRequest) {
|
|
167
|
-
try {
|
|
168
|
-
const body = await request.json();
|
|
169
|
-
const data = createUserSchema.parse(body);
|
|
170
|
-
|
|
171
|
-
const user = await db.user.create({ data });
|
|
172
|
-
|
|
173
|
-
return NextResponse.json(user, { status: 201 });
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (error instanceof z.ZodError) {
|
|
176
|
-
return NextResponse.json({ error: error.errors }, { status: 400 });
|
|
177
|
-
}
|
|
178
|
-
return NextResponse.json(
|
|
179
|
-
{ error: 'Failed to create user' },
|
|
180
|
-
{ status: 500 }
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// app/api/users/[id]/route.ts
|
|
186
|
-
// ✅ GET /api/users/:id
|
|
187
|
-
export async function GET(
|
|
188
|
-
request: NextRequest,
|
|
189
|
-
{ params }: { params: { id: string } }
|
|
190
|
-
) {
|
|
191
|
-
const user = await db.user.findUnique({
|
|
192
|
-
where: { id: params.id },
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
if (!user) {
|
|
196
|
-
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return NextResponse.json(user);
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### 5. Metadata & SEO
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
// ✅ 정적 메타데이터
|
|
207
|
-
import { Metadata } from 'next';
|
|
208
|
-
|
|
209
|
-
export const metadata: Metadata = {
|
|
210
|
-
title: 'My App',
|
|
211
|
-
description: 'My awesome app',
|
|
212
|
-
openGraph: {
|
|
213
|
-
title: 'My App',
|
|
214
|
-
description: 'My awesome app',
|
|
215
|
-
images: ['/og-image.png'],
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
// ✅ 동적 메타데이터
|
|
220
|
-
export async function generateMetadata({
|
|
221
|
-
params,
|
|
222
|
-
}: {
|
|
223
|
-
params: { id: string };
|
|
224
|
-
}): Promise<Metadata> {
|
|
225
|
-
const user = await getUser(params.id);
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
title: user.name,
|
|
229
|
-
description: user.bio,
|
|
230
|
-
openGraph: {
|
|
231
|
-
title: user.name,
|
|
232
|
-
images: [user.avatar],
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### 6. Streaming & Suspense
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
// ✅ Streaming으로 빠른 초기 렌더링
|
|
242
|
-
import { Suspense } from 'react';
|
|
243
|
-
|
|
244
|
-
export default function Dashboard() {
|
|
245
|
-
return (
|
|
246
|
-
<div>
|
|
247
|
-
<h1>Dashboard</h1>
|
|
248
|
-
|
|
249
|
-
{/* 빠른 컴포넌트 먼저 렌더 */}
|
|
250
|
-
<QuickStats />
|
|
251
|
-
|
|
252
|
-
{/* 느린 컴포넌트 나중에 스트리밍 */}
|
|
253
|
-
<Suspense fallback={<ChartSkeleton />}>
|
|
254
|
-
<SlowChart />
|
|
255
|
-
</Suspense>
|
|
256
|
-
|
|
257
|
-
<Suspense fallback={<TableSkeleton />}>
|
|
258
|
-
<SlowTable />
|
|
259
|
-
</Suspense>
|
|
260
|
-
</div>
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async function SlowChart() {
|
|
265
|
-
const data = await fetchSlowData();
|
|
266
|
-
return <Chart data={data} />;
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
### 7. Server Actions
|
|
271
|
-
|
|
272
|
-
```typescript
|
|
273
|
-
// ✅ Server Action (서버에서만 실행)
|
|
274
|
-
'use server';
|
|
275
|
-
|
|
276
|
-
import { revalidatePath } from 'next/cache';
|
|
277
|
-
|
|
278
|
-
export async function createUser(formData: FormData) {
|
|
279
|
-
const email = formData.get('email') as string;
|
|
280
|
-
const name = formData.get('name') as string;
|
|
281
|
-
|
|
282
|
-
// 서버에서 직접 DB 접근
|
|
283
|
-
const user = await db.user.create({
|
|
284
|
-
data: { email, name },
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// 캐시 재검증
|
|
288
|
-
revalidatePath('/users');
|
|
289
|
-
|
|
290
|
-
return user;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// ✅ 클라이언트에서 사용
|
|
294
|
-
'use client';
|
|
295
|
-
|
|
296
|
-
import { createUser } from './actions';
|
|
297
|
-
|
|
298
|
-
export function CreateUserForm() {
|
|
299
|
-
return (
|
|
300
|
-
<form action={createUser}>
|
|
301
|
-
<input name="email" type="email" required />
|
|
302
|
-
<input name="name" required />
|
|
303
|
-
<button type="submit">Create</button>
|
|
304
|
-
</form>
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### 8. Middleware
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
// middleware.ts (루트)
|
|
313
|
-
import { NextResponse } from 'next/server';
|
|
314
|
-
import type { NextRequest } from 'next/server';
|
|
315
|
-
|
|
316
|
-
export function middleware(request: NextRequest) {
|
|
317
|
-
// 인증 체크
|
|
318
|
-
const token = request.cookies.get('token')?.value;
|
|
319
|
-
|
|
320
|
-
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
321
|
-
return NextResponse.redirect(new URL('/login', request.url));
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// 헤더 추가
|
|
325
|
-
const response = NextResponse.next();
|
|
326
|
-
response.headers.set('X-Custom-Header', 'value');
|
|
327
|
-
|
|
328
|
-
return response;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export const config = {
|
|
332
|
-
matcher: ['/dashboard/:path*', '/api/:path*'],
|
|
333
|
-
};
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
### 9. 환경 변수
|
|
337
|
-
|
|
338
|
-
```typescript
|
|
339
|
-
// ✅ 서버 전용 환경 변수
|
|
340
|
-
const dbUrl = process.env.DATABASE_URL; // 서버 컴포넌트에서만
|
|
341
|
-
const apiKey = process.env.API_SECRET_KEY;
|
|
342
|
-
|
|
343
|
-
// ✅ 클라이언트 노출 환경 변수 (NEXT_PUBLIC_ 접두사)
|
|
344
|
-
const publicUrl = process.env.NEXT_PUBLIC_API_URL;
|
|
345
|
-
|
|
346
|
-
// .env.local
|
|
347
|
-
DATABASE_URL=postgresql://...
|
|
348
|
-
API_SECRET_KEY=secret123
|
|
349
|
-
NEXT_PUBLIC_API_URL=https://api.example.com
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### 10. 이미지 최적화
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
import Image from 'next/image';
|
|
356
|
-
|
|
357
|
-
// ✅ Next.js Image 컴포넌트 (자동 최적화)
|
|
358
|
-
export function UserAvatar({ user }: { user: User }) {
|
|
359
|
-
return (
|
|
360
|
-
<Image
|
|
361
|
-
src={user.avatar}
|
|
362
|
-
alt={user.name}
|
|
363
|
-
width={100}
|
|
364
|
-
height={100}
|
|
365
|
-
priority // LCP 이미지는 priority
|
|
366
|
-
/>
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// ✅ 외부 이미지 (next.config.js 설정 필요)
|
|
371
|
-
// next.config.js
|
|
372
|
-
module.exports = {
|
|
373
|
-
images: {
|
|
374
|
-
domains: ['example.com', 'cdn.example.com'],
|
|
375
|
-
},
|
|
376
|
-
};
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
## 안티패턴
|
|
380
|
-
|
|
381
|
-
```typescript
|
|
382
|
-
// ❌ 클라이언트 컴포넌트에서 서버 전용 코드
|
|
383
|
-
'use client';
|
|
384
|
-
|
|
385
|
-
function BadComponent() {
|
|
386
|
-
const data = await db.user.findMany(); // ❌ 클라이언트에서 DB 접근 불가
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ❌ 서버 컴포넌트에서 브라우저 API
|
|
390
|
-
async function BadServerComponent() {
|
|
391
|
-
const width = window.innerWidth; // ❌ window는 브라우저에만 존재
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ❌ API Route에서 다른 API Route 호출
|
|
395
|
-
export async function GET() {
|
|
396
|
-
const response = await fetch('http://localhost:3000/api/users'); // ❌
|
|
397
|
-
// 대신 직접 DB 함수 호출
|
|
398
|
-
const users = await getUsers(); // ✅
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// ❌ 환경 변수 노출
|
|
402
|
-
'use client';
|
|
403
|
-
|
|
404
|
-
function BadClient() {
|
|
405
|
-
const apiKey = process.env.API_SECRET_KEY; // ❌ undefined (클라이언트에서)
|
|
406
|
-
}
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
## 성능 최적화
|
|
410
|
-
|
|
411
|
-
```typescript
|
|
412
|
-
// ✅ Static Generation (SSG)
|
|
413
|
-
export async function generateStaticParams() {
|
|
414
|
-
const posts = await getPosts();
|
|
415
|
-
return posts.map(post => ({ id: post.id }));
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ✅ Incremental Static Regeneration (ISR)
|
|
419
|
-
export const revalidate = 60; // 60초마다 재생성
|
|
420
|
-
|
|
421
|
-
// ✅ Dynamic Rendering (SSR)
|
|
422
|
-
export const dynamic = 'force-dynamic';
|
|
423
|
-
|
|
424
|
-
// ✅ Partial Prerendering (실험적)
|
|
425
|
-
export const experimental_ppr = true;
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
## 체크리스트
|
|
429
|
-
|
|
430
|
-
Next.js 코드 작성 시:
|
|
431
|
-
|
|
432
|
-
- [ ] App Router 사용 (Pages Router 지양)
|
|
433
|
-
- [ ] 서버 컴포넌트 우선 (클라이언트 최소화)
|
|
434
|
-
- [ ] API Route 대신 Server Action 고려
|
|
435
|
-
- [ ] 메타데이터 정의 (SEO)
|
|
436
|
-
- [ ] Suspense로 Streaming
|
|
437
|
-
- [ ] Next.js Image 컴포넌트 사용
|
|
438
|
-
- [ ] 환경 변수 올바르게 사용
|
|
439
|
-
- [ ] Middleware로 인증/권한 체크
|
|
440
|
-
- [ ] 캐싱 전략 설정 (revalidate)
|
|
441
|
-
- [ ] TypeScript 엄격 모드
|