@kood/claude-code 0.3.5 → 0.3.7
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/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/nextjs/CLAUDE.md +228 -0
- package/templates/nextjs/docs/design.md +558 -0
- package/templates/nextjs/docs/guides/conventions.md +343 -0
- package/templates/nextjs/docs/guides/getting-started.md +367 -0
- package/templates/nextjs/docs/guides/routes.md +342 -0
- package/templates/nextjs/docs/library/better-auth/index.md +541 -0
- package/templates/nextjs/docs/library/nextjs/app-router.md +269 -0
- package/templates/nextjs/docs/library/nextjs/caching.md +351 -0
- package/templates/nextjs/docs/library/nextjs/index.md +291 -0
- package/templates/nextjs/docs/library/nextjs/middleware.md +391 -0
- package/templates/nextjs/docs/library/nextjs/route-handlers.md +382 -0
- package/templates/nextjs/docs/library/nextjs/server-actions.md +366 -0
- package/templates/nextjs/docs/library/prisma/cloudflare-d1.md +76 -0
- package/templates/nextjs/docs/library/prisma/config.md +77 -0
- package/templates/nextjs/docs/library/prisma/crud.md +90 -0
- package/templates/nextjs/docs/library/prisma/index.md +73 -0
- package/templates/nextjs/docs/library/prisma/relations.md +69 -0
- package/templates/nextjs/docs/library/prisma/schema.md +98 -0
- package/templates/nextjs/docs/library/prisma/setup.md +49 -0
- package/templates/nextjs/docs/library/prisma/transactions.md +50 -0
- package/templates/nextjs/docs/library/tanstack-query/index.md +66 -0
- package/templates/nextjs/docs/library/tanstack-query/invalidation.md +54 -0
- package/templates/nextjs/docs/library/tanstack-query/optimistic-updates.md +77 -0
- package/templates/nextjs/docs/library/tanstack-query/use-mutation.md +63 -0
- package/templates/nextjs/docs/library/tanstack-query/use-query.md +70 -0
- package/templates/nextjs/docs/library/zod/complex-types.md +61 -0
- package/templates/nextjs/docs/library/zod/index.md +56 -0
- package/templates/nextjs/docs/library/zod/transforms.md +51 -0
- package/templates/nextjs/docs/library/zod/validation.md +70 -0
- package/templates/tanstack-start/CLAUDE.md +7 -3
- package/templates/tanstack-start/docs/architecture.md +38 -7
- package/templates/tanstack-start/docs/guides/hooks.md +28 -0
- package/templates/tanstack-start/docs/guides/routes.md +29 -10
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# Conventions
|
|
2
|
+
|
|
3
|
+
> 코드 작성 규칙
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 파일명
|
|
8
|
+
|
|
9
|
+
| 유형 | 규칙 | 예시 |
|
|
10
|
+
|------|------|------|
|
|
11
|
+
| 컴포넌트 | kebab-case | `user-profile.tsx` |
|
|
12
|
+
| 라우트 | Next.js 규칙 | `page.tsx`, `layout.tsx`, `[id]/page.tsx` |
|
|
13
|
+
| Server Actions | kebab-case | `create-post.ts`, `posts.ts` |
|
|
14
|
+
| 유틸리티 | kebab-case | `format-date.ts` |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## TypeScript
|
|
19
|
+
|
|
20
|
+
### 변수 선언
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// ✅ const 우선
|
|
24
|
+
const user = { name: "Alice" }
|
|
25
|
+
const posts = await prisma.post.findMany()
|
|
26
|
+
|
|
27
|
+
// ❌ let 최소화
|
|
28
|
+
let count = 0 // 변경 필요시만
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 함수 선언
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// ✅ const 화살표 함수 + 명시적 return type
|
|
35
|
+
const getUser = async (id: string): Promise<User> => {
|
|
36
|
+
return prisma.user.findUnique({ where: { id } })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ❌ function 키워드 (export default 제외)
|
|
40
|
+
function getUser(id: string) {
|
|
41
|
+
return prisma.user.findUnique({ where: { id } })
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 타입 정의
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// ✅ interface (객체)
|
|
49
|
+
interface User {
|
|
50
|
+
id: string
|
|
51
|
+
name: string
|
|
52
|
+
email: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ✅ type (유니온, 기타)
|
|
56
|
+
type Status = "active" | "inactive"
|
|
57
|
+
type UserOrNull = User | null
|
|
58
|
+
|
|
59
|
+
// ❌ any 금지 → unknown 사용
|
|
60
|
+
const data: unknown = JSON.parse(jsonString)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Import 순서
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// 1. 외부 라이브러리
|
|
69
|
+
import { useState } from "react"
|
|
70
|
+
import { useQuery } from "@tanstack/react-query"
|
|
71
|
+
|
|
72
|
+
// 2. @/ alias
|
|
73
|
+
import { Button } from "@/components/ui/button"
|
|
74
|
+
import { prisma } from "@/database/prisma"
|
|
75
|
+
|
|
76
|
+
// 3. 상대경로
|
|
77
|
+
import { UserProfile } from "./-components/user-profile"
|
|
78
|
+
|
|
79
|
+
// 4. 타입 (분리)
|
|
80
|
+
import type { User } from "@/types"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 컴포넌트
|
|
86
|
+
|
|
87
|
+
### Server Component (기본)
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// ✅ async 함수 + 직접 데이터 페칭
|
|
91
|
+
export default async function PostsPage() {
|
|
92
|
+
const posts = await prisma.post.findMany()
|
|
93
|
+
return <PostsList posts={posts} />
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Client Component
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// ✅ "use client" + 상호작용
|
|
101
|
+
"use client"
|
|
102
|
+
|
|
103
|
+
import { useState } from "react"
|
|
104
|
+
|
|
105
|
+
export function Counter() {
|
|
106
|
+
const [count, setCount] = useState(0)
|
|
107
|
+
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Server Actions
|
|
114
|
+
|
|
115
|
+
### 파일 상단
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// ✅ "use server" + 여러 함수
|
|
119
|
+
"use server"
|
|
120
|
+
|
|
121
|
+
export async function createPost(formData: FormData) {
|
|
122
|
+
// ...
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function deletePost(id: string) {
|
|
126
|
+
// ...
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Zod 검증
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
"use server"
|
|
134
|
+
|
|
135
|
+
import { z } from "zod"
|
|
136
|
+
|
|
137
|
+
const schema = z.object({
|
|
138
|
+
title: z.string().min(1),
|
|
139
|
+
content: z.string(),
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
export async function createPost(formData: FormData) {
|
|
143
|
+
const parsed = schema.parse({
|
|
144
|
+
title: formData.get("title"),
|
|
145
|
+
content: formData.get("content"),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// ...
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 주석
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// ✅ 코드 묶음 단위로 한글 주석
|
|
158
|
+
// 사용자 인증 체크
|
|
159
|
+
const session = await auth.api.getSession({ headers: headers() })
|
|
160
|
+
if (!session?.user) redirect("/login")
|
|
161
|
+
|
|
162
|
+
// 게시글 조회
|
|
163
|
+
const posts = await prisma.post.findMany({
|
|
164
|
+
where: { userId: session.user.id },
|
|
165
|
+
orderBy: { createdAt: "desc" },
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// ❌ 모든 줄마다 주석
|
|
169
|
+
const session = await auth.api.getSession({ headers: headers() }) // 세션 조회
|
|
170
|
+
if (!session?.user) redirect("/login") // 로그인 리다이렉트
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Prisma
|
|
176
|
+
|
|
177
|
+
### Multi-File 구조
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
prisma/schema/
|
|
181
|
+
├── +base.prisma # datasource, generator
|
|
182
|
+
├── +enum.prisma # 모든 enum
|
|
183
|
+
└── user.prisma # User 모델
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 한글 주석 필수
|
|
187
|
+
|
|
188
|
+
```prisma
|
|
189
|
+
/// 사용자
|
|
190
|
+
model User {
|
|
191
|
+
id String @id @default(cuid()) /// 고유 ID
|
|
192
|
+
email String @unique /// 이메일 (고유)
|
|
193
|
+
name String? /// 이름 (옵션)
|
|
194
|
+
role Role @default(USER) /// 역할
|
|
195
|
+
createdAt DateTime @default(now()) /// 생성일
|
|
196
|
+
updatedAt DateTime @updatedAt /// 수정일
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// 역할
|
|
200
|
+
enum Role {
|
|
201
|
+
USER /// 일반 사용자
|
|
202
|
+
ADMIN /// 관리자
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 폴더 구조
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
app/
|
|
212
|
+
├── (auth)/ # Route Group
|
|
213
|
+
│ ├── login/
|
|
214
|
+
│ │ └── page.tsx
|
|
215
|
+
│ └── signup/
|
|
216
|
+
│ └── page.tsx
|
|
217
|
+
├── dashboard/
|
|
218
|
+
│ ├── page.tsx
|
|
219
|
+
│ ├── -components/ # 페이지 전용 (필수)
|
|
220
|
+
│ │ ├── stats.tsx
|
|
221
|
+
│ │ └── chart.tsx
|
|
222
|
+
│ └── settings/
|
|
223
|
+
│ └── page.tsx
|
|
224
|
+
└── _components/ # 공통 Client Components
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**규칙:**
|
|
228
|
+
- `-components/` - 페이지 전용 (밖에서 import 불가)
|
|
229
|
+
- `_components/` - 공통 (라우트에 포함 안됨)
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Custom Hook 순서
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
"use client"
|
|
237
|
+
|
|
238
|
+
export function useExample() {
|
|
239
|
+
// 1. State (useState, zustand)
|
|
240
|
+
const [count, setCount] = useState(0)
|
|
241
|
+
|
|
242
|
+
// 2. Global Hooks
|
|
243
|
+
const router = useRouter()
|
|
244
|
+
const params = useParams()
|
|
245
|
+
const searchParams = useSearchParams()
|
|
246
|
+
|
|
247
|
+
// 3. React Query
|
|
248
|
+
const { data: posts } = useQuery({ queryKey: ["posts"], queryFn: getPosts })
|
|
249
|
+
const mutation = useMutation({ mutationFn: createPost })
|
|
250
|
+
|
|
251
|
+
// 4. Event Handlers
|
|
252
|
+
const handleClick = () => {
|
|
253
|
+
setCount(count + 1)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 5. useMemo
|
|
257
|
+
const total = useMemo(() => posts?.length || 0, [posts])
|
|
258
|
+
|
|
259
|
+
// 6. useEffect
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
console.log(count)
|
|
262
|
+
}, [count])
|
|
263
|
+
|
|
264
|
+
return { count, handleClick, total }
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 베스트 프랙티스
|
|
271
|
+
|
|
272
|
+
### ✅ DO
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// 1. 명시적 타입
|
|
276
|
+
const getUser = async (id: string): Promise<User> => { /* ... */ }
|
|
277
|
+
|
|
278
|
+
// 2. Zod 검증
|
|
279
|
+
const schema = z.object({ email: z.email() })
|
|
280
|
+
const parsed = schema.parse(data)
|
|
281
|
+
|
|
282
|
+
// 3. 에러 처리
|
|
283
|
+
try {
|
|
284
|
+
await prisma.post.create({ data })
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error("Error creating post:", error)
|
|
287
|
+
throw error
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 4. revalidatePath
|
|
291
|
+
await prisma.post.create({ data })
|
|
292
|
+
revalidatePath("/posts")
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### ❌ DON'T
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// 1. any 사용
|
|
299
|
+
const data: any = await fetchData() // ❌
|
|
300
|
+
|
|
301
|
+
// 2. 검증 누락
|
|
302
|
+
const email = formData.get("email") // ❌ Zod 검증 필요
|
|
303
|
+
await createUser({ email })
|
|
304
|
+
|
|
305
|
+
// 3. 에러 무시
|
|
306
|
+
await prisma.post.create({ data }) // ❌ try-catch 필요
|
|
307
|
+
|
|
308
|
+
// 4. 캐시 무효화 누락
|
|
309
|
+
await prisma.post.create({ data }) // ❌ revalidatePath 필요
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Git 커밋
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# ✅ 한 줄, prefix 사용
|
|
318
|
+
git commit -m "feat: 게시글 생성 기능 추가"
|
|
319
|
+
git commit -m "fix: 로그인 버그 수정"
|
|
320
|
+
|
|
321
|
+
# ❌ 여러 줄, 이모지, AI 표시
|
|
322
|
+
git commit -m "feat: 게시글 생성 기능 추가
|
|
323
|
+
|
|
324
|
+
상세 설명...
|
|
325
|
+
|
|
326
|
+
Co-Authored-By: Claude Code <noreply@anthropic.com>" # ❌
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Prefix:**
|
|
330
|
+
- `feat` - 새 기능
|
|
331
|
+
- `fix` - 버그 수정
|
|
332
|
+
- `refactor` - 리팩토링
|
|
333
|
+
- `style` - 코드 스타일
|
|
334
|
+
- `docs` - 문서
|
|
335
|
+
- `test` - 테스트
|
|
336
|
+
- `chore` - 기타
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## 참조
|
|
341
|
+
|
|
342
|
+
- [Next.js 공식 문서](https://nextjs.org/docs)
|
|
343
|
+
- [TypeScript 공식 문서](https://www.typescriptlang.org/)
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
> Next.js 15 프로젝트 시작하기
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 프로젝트 생성
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx create-next-app@latest my-app \
|
|
11
|
+
--typescript \
|
|
12
|
+
--tailwind \
|
|
13
|
+
--app \
|
|
14
|
+
--src-dir \
|
|
15
|
+
--import-alias "@/*"
|
|
16
|
+
|
|
17
|
+
cd my-app
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 필수 의존성
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Database
|
|
27
|
+
npm install prisma @prisma/client
|
|
28
|
+
npm install -D prisma
|
|
29
|
+
|
|
30
|
+
# Validation
|
|
31
|
+
npm install zod
|
|
32
|
+
|
|
33
|
+
# Auth
|
|
34
|
+
npm install better-auth
|
|
35
|
+
|
|
36
|
+
# Data Fetching
|
|
37
|
+
npm install @tanstack/react-query
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 폴더 구조
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
src/
|
|
46
|
+
├── app/
|
|
47
|
+
│ ├── layout.tsx # Root layout
|
|
48
|
+
│ ├── page.tsx # Home
|
|
49
|
+
│ ├── (auth)/
|
|
50
|
+
│ │ ├── login/
|
|
51
|
+
│ │ │ └── page.tsx
|
|
52
|
+
│ │ └── signup/
|
|
53
|
+
│ │ └── page.tsx
|
|
54
|
+
│ ├── dashboard/
|
|
55
|
+
│ │ ├── page.tsx
|
|
56
|
+
│ │ └── -components/ # 페이지 전용
|
|
57
|
+
│ └── api/
|
|
58
|
+
│ └── auth/
|
|
59
|
+
│ └── [...all]/
|
|
60
|
+
│ └── route.ts
|
|
61
|
+
├── actions/ # Server Actions (공통)
|
|
62
|
+
│ ├── posts.ts
|
|
63
|
+
│ └── users.ts
|
|
64
|
+
├── components/
|
|
65
|
+
│ └── ui/ # UI 컴포넌트
|
|
66
|
+
├── lib/
|
|
67
|
+
│ ├── auth.ts # Better Auth 설정
|
|
68
|
+
│ ├── auth-client.ts # Auth Client
|
|
69
|
+
│ ├── prisma.ts # Prisma Client
|
|
70
|
+
│ └── query-client.ts # React Query Client
|
|
71
|
+
├── database/
|
|
72
|
+
│ └── prisma.ts
|
|
73
|
+
└── middleware.ts
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 환경 변수
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# .env.local
|
|
82
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
|
|
83
|
+
BETTER_AUTH_SECRET="your-secret-key"
|
|
84
|
+
BETTER_AUTH_URL="http://localhost:3000"
|
|
85
|
+
|
|
86
|
+
# 소셜 로그인 (옵션)
|
|
87
|
+
GOOGLE_CLIENT_ID="..."
|
|
88
|
+
GOOGLE_CLIENT_SECRET="..."
|
|
89
|
+
GITHUB_CLIENT_ID="..."
|
|
90
|
+
GITHUB_CLIENT_SECRET="..."
|
|
91
|
+
|
|
92
|
+
# 클라이언트 공개 변수
|
|
93
|
+
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Prisma 설정
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# 초기화
|
|
102
|
+
npx prisma init
|
|
103
|
+
|
|
104
|
+
# 스키마 생성
|
|
105
|
+
mkdir -p prisma/schema
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```prisma
|
|
109
|
+
// prisma/schema/+base.prisma
|
|
110
|
+
datasource db {
|
|
111
|
+
provider = "postgresql"
|
|
112
|
+
url = env("DATABASE_URL")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
generator client {
|
|
116
|
+
provider = "prisma-client-js"
|
|
117
|
+
output = "../node_modules/.prisma/client"
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```prisma
|
|
122
|
+
// prisma/schema/+enum.prisma
|
|
123
|
+
enum Role {
|
|
124
|
+
USER
|
|
125
|
+
ADMIN
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```prisma
|
|
130
|
+
// prisma/schema/user.prisma
|
|
131
|
+
model User {
|
|
132
|
+
id String @id @default(cuid())
|
|
133
|
+
email String @unique
|
|
134
|
+
name String?
|
|
135
|
+
role Role @default(USER)
|
|
136
|
+
createdAt DateTime @default(now())
|
|
137
|
+
updatedAt DateTime @updatedAt
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# DB 동기화
|
|
143
|
+
npx prisma db push
|
|
144
|
+
|
|
145
|
+
# Client 생성
|
|
146
|
+
npx prisma generate
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Prisma Client
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// src/database/prisma.ts
|
|
155
|
+
import { PrismaClient } from "@prisma/client"
|
|
156
|
+
|
|
157
|
+
const globalForPrisma = globalThis as unknown as {
|
|
158
|
+
prisma: PrismaClient | undefined
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
|
162
|
+
|
|
163
|
+
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Better Auth 설정
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// src/lib/auth.ts
|
|
172
|
+
import { betterAuth } from "better-auth"
|
|
173
|
+
import { prismaAdapter } from "better-auth/adapters/prisma"
|
|
174
|
+
import { prisma } from "@/database/prisma"
|
|
175
|
+
|
|
176
|
+
export const auth = betterAuth({
|
|
177
|
+
database: prismaAdapter(prisma, { provider: "postgresql" }),
|
|
178
|
+
emailAndPassword: { enabled: true },
|
|
179
|
+
socialProviders: {
|
|
180
|
+
google: {
|
|
181
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
182
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// src/lib/auth-client.ts
|
|
190
|
+
import { createAuthClient } from "better-auth/react"
|
|
191
|
+
|
|
192
|
+
export const authClient = createAuthClient({
|
|
193
|
+
baseURL: process.env.NEXT_PUBLIC_APP_URL!,
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// app/api/auth/[...all]/route.ts
|
|
199
|
+
import { auth } from "@/lib/auth"
|
|
200
|
+
|
|
201
|
+
export const GET = (request: Request) => auth.handler(request)
|
|
202
|
+
export const POST = (request: Request) => auth.handler(request)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## React Query 설정
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// src/lib/query-client.ts
|
|
211
|
+
import { QueryClient } from "@tanstack/react-query"
|
|
212
|
+
|
|
213
|
+
export function makeQueryClient() {
|
|
214
|
+
return new QueryClient({
|
|
215
|
+
defaultOptions: {
|
|
216
|
+
queries: {
|
|
217
|
+
staleTime: 60 * 1000, // 1분
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let browserQueryClient: QueryClient | undefined = undefined
|
|
224
|
+
|
|
225
|
+
export function getQueryClient() {
|
|
226
|
+
if (typeof window === "undefined") {
|
|
227
|
+
return makeQueryClient()
|
|
228
|
+
} else {
|
|
229
|
+
if (!browserQueryClient) browserQueryClient = makeQueryClient()
|
|
230
|
+
return browserQueryClient
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// app/providers.tsx
|
|
237
|
+
"use client"
|
|
238
|
+
|
|
239
|
+
import { QueryClientProvider } from "@tanstack/react-query"
|
|
240
|
+
import { getQueryClient } from "@/lib/query-client"
|
|
241
|
+
|
|
242
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
243
|
+
const queryClient = getQueryClient()
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<QueryClientProvider client={queryClient}>
|
|
247
|
+
{children}
|
|
248
|
+
</QueryClientProvider>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// app/layout.tsx
|
|
255
|
+
import { Providers } from "./providers"
|
|
256
|
+
|
|
257
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
258
|
+
return (
|
|
259
|
+
<html lang="ko">
|
|
260
|
+
<body>
|
|
261
|
+
<Providers>{children}</Providers>
|
|
262
|
+
</body>
|
|
263
|
+
</html>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 첫 Server Action
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// actions/posts.ts
|
|
274
|
+
"use server"
|
|
275
|
+
|
|
276
|
+
import { z } from "zod"
|
|
277
|
+
import { prisma } from "@/database/prisma"
|
|
278
|
+
import { revalidatePath } from "next/cache"
|
|
279
|
+
|
|
280
|
+
const createPostSchema = z.object({
|
|
281
|
+
title: z.string().min(1),
|
|
282
|
+
content: z.string(),
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
export async function createPost(formData: FormData) {
|
|
286
|
+
const parsed = createPostSchema.parse({
|
|
287
|
+
title: formData.get("title"),
|
|
288
|
+
content: formData.get("content"),
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const post = await prisma.post.create({ data: parsed })
|
|
292
|
+
revalidatePath("/posts")
|
|
293
|
+
return post
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 첫 페이지 (Server Component)
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// app/posts/page.tsx
|
|
303
|
+
import { prisma } from "@/database/prisma"
|
|
304
|
+
|
|
305
|
+
export default async function PostsPage() {
|
|
306
|
+
const posts = await prisma.post.findMany()
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<div>
|
|
310
|
+
<h1>Posts</h1>
|
|
311
|
+
<ul>
|
|
312
|
+
{posts.map(post => (
|
|
313
|
+
<li key={post.id}>{post.title}</li>
|
|
314
|
+
))}
|
|
315
|
+
</ul>
|
|
316
|
+
</div>
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 첫 폼 (Client Component)
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// app/posts/_components/create-post-form.tsx
|
|
327
|
+
"use client"
|
|
328
|
+
|
|
329
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
|
330
|
+
import { createPost } from "@/actions/posts"
|
|
331
|
+
|
|
332
|
+
export function CreatePostForm() {
|
|
333
|
+
const queryClient = useQueryClient()
|
|
334
|
+
|
|
335
|
+
const mutation = useMutation({
|
|
336
|
+
mutationFn: createPost,
|
|
337
|
+
onSuccess: () => {
|
|
338
|
+
queryClient.invalidateQueries({ queryKey: ["posts"] })
|
|
339
|
+
},
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<form
|
|
344
|
+
onSubmit={(e) => {
|
|
345
|
+
e.preventDefault()
|
|
346
|
+
const formData = new FormData(e.currentTarget)
|
|
347
|
+
mutation.mutate(formData)
|
|
348
|
+
}}
|
|
349
|
+
>
|
|
350
|
+
<input name="title" placeholder="Title" required />
|
|
351
|
+
<textarea name="content" placeholder="Content" required />
|
|
352
|
+
<button type="submit" disabled={mutation.isPending}>
|
|
353
|
+
{mutation.isPending ? "Creating..." : "Create Post"}
|
|
354
|
+
</button>
|
|
355
|
+
</form>
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 다음 단계
|
|
363
|
+
|
|
364
|
+
- [Conventions](conventions.md) - 코드 컨벤션
|
|
365
|
+
- [Routes](routes.md) - 라우팅 패턴
|
|
366
|
+
- [Server Actions](server-actions.md) - Server Actions 패턴
|
|
367
|
+
- [Client Components](client-components.md) - Client Components 패턴
|