@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,69 @@
|
|
|
1
|
+
# Prisma - 관계 쿼리
|
|
2
|
+
|
|
3
|
+
## 중첩 생성
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
const user = await prisma.user.create({
|
|
7
|
+
data: {
|
|
8
|
+
email: 'user@prisma.io',
|
|
9
|
+
posts: { create: [{ title: 'Post 1' }, { title: 'Post 2' }] },
|
|
10
|
+
},
|
|
11
|
+
include: { posts: true },
|
|
12
|
+
})
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 관계 연결
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// connect - 기존 연결
|
|
19
|
+
author: { connect: { id: 1 } }
|
|
20
|
+
|
|
21
|
+
// connectOrCreate - 있으면 연결, 없으면 생성
|
|
22
|
+
categories: { connectOrCreate: { where: { name: 'Tech' }, create: { name: 'Tech' } } }
|
|
23
|
+
|
|
24
|
+
// disconnect - 관계 해제
|
|
25
|
+
author: { disconnect: true }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 관계 포함 조회
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// include
|
|
32
|
+
const users = await prisma.user.findMany({ include: { posts: true, profile: true } })
|
|
33
|
+
|
|
34
|
+
// 중첩
|
|
35
|
+
include: { posts: { include: { categories: true } } }
|
|
36
|
+
|
|
37
|
+
// 필터 + 정렬
|
|
38
|
+
include: { posts: { where: { published: true }, orderBy: { createdAt: 'desc' }, take: 5 } }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 관계로 필터링
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// some - 하나라도 만족
|
|
45
|
+
where: { posts: { some: { published: true } } }
|
|
46
|
+
|
|
47
|
+
// every - 모두 만족
|
|
48
|
+
where: { posts: { every: { published: true } } }
|
|
49
|
+
|
|
50
|
+
// none - 만족 없음
|
|
51
|
+
where: { posts: { none: { published: false } } }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 카운트
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
include: { _count: { select: { posts: true } } }
|
|
58
|
+
// 결과: { _count: { posts: 5 } }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 중첩 수정/삭제
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// updateMany
|
|
65
|
+
posts: { updateMany: { where: { published: false }, data: { published: true } } }
|
|
66
|
+
|
|
67
|
+
// deleteMany
|
|
68
|
+
posts: { deleteMany: { where: { published: false } } }
|
|
69
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Prisma - 스키마 정의 (Multi-File)
|
|
2
|
+
|
|
3
|
+
## ⚠️ 필수 규칙
|
|
4
|
+
|
|
5
|
+
1. **Multi-File 구조** 사용
|
|
6
|
+
2. **모든 요소에 한글 주석** (파일, 모델, 필드, enum)
|
|
7
|
+
|
|
8
|
+
## 구조
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
prisma/schema/
|
|
12
|
+
├── +base.prisma # datasource, generator
|
|
13
|
+
├── +enum.prisma # 모든 enum
|
|
14
|
+
├── user.prisma # User 모델
|
|
15
|
+
└── post.prisma # Post 모델
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 예시
|
|
19
|
+
|
|
20
|
+
```prisma
|
|
21
|
+
// +base.prisma
|
|
22
|
+
datasource db {
|
|
23
|
+
provider = "postgresql"
|
|
24
|
+
url = env("DATABASE_URL")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
generator client {
|
|
28
|
+
provider = "prisma-client"
|
|
29
|
+
output = "../generated/prisma"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// +enum.prisma
|
|
33
|
+
enum Role {
|
|
34
|
+
USER // 일반 사용자
|
|
35
|
+
ADMIN // 관리자
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// user.prisma
|
|
39
|
+
model User {
|
|
40
|
+
id Int @id @default(autoincrement())
|
|
41
|
+
email String @unique // 로그인 이메일
|
|
42
|
+
name String? // 표시 이름
|
|
43
|
+
role Role @default(USER)
|
|
44
|
+
posts Post[] // 작성 게시글 (1:N)
|
|
45
|
+
profile Profile? // 프로필 (1:1)
|
|
46
|
+
createdAt DateTime @default(now())
|
|
47
|
+
updatedAt DateTime @updatedAt
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 관계 유형
|
|
52
|
+
|
|
53
|
+
```prisma
|
|
54
|
+
// 1:1
|
|
55
|
+
model Profile {
|
|
56
|
+
id Int @id @default(autoincrement())
|
|
57
|
+
user User @relation(fields: [userId], references: [id])
|
|
58
|
+
userId Int @unique
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 1:N
|
|
62
|
+
model Post {
|
|
63
|
+
author User @relation(fields: [authorId], references: [id])
|
|
64
|
+
authorId Int
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// M:N (암묵적)
|
|
68
|
+
model Post { categories Category[] }
|
|
69
|
+
model Category { posts Post[] }
|
|
70
|
+
|
|
71
|
+
// M:N (명시적)
|
|
72
|
+
model CategoriesOnPosts {
|
|
73
|
+
post Post @relation(fields: [postId], references: [id])
|
|
74
|
+
postId Int
|
|
75
|
+
category Category @relation(fields: [categoryId], references: [id])
|
|
76
|
+
categoryId Int
|
|
77
|
+
@@id([postId, categoryId])
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 기타
|
|
82
|
+
|
|
83
|
+
```prisma
|
|
84
|
+
// 선택적 관계
|
|
85
|
+
author User? @relation(fields: [authorId], references: [id])
|
|
86
|
+
authorId Int?
|
|
87
|
+
|
|
88
|
+
// 인덱스
|
|
89
|
+
@@index([authorId])
|
|
90
|
+
@@index([createdAt])
|
|
91
|
+
|
|
92
|
+
// 복합 키
|
|
93
|
+
@@id([postId, tagId])
|
|
94
|
+
|
|
95
|
+
// 매핑
|
|
96
|
+
@map("user_id")
|
|
97
|
+
@@map("users")
|
|
98
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Prisma - 설치 및 설정
|
|
2
|
+
|
|
3
|
+
## 설치
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
yarn add @prisma/client@7
|
|
7
|
+
yarn add -D prisma@7
|
|
8
|
+
npx prisma init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## v6 → v7 업그레이드
|
|
12
|
+
|
|
13
|
+
```prisma
|
|
14
|
+
// v6 (이전)
|
|
15
|
+
generator client {
|
|
16
|
+
provider = "prisma-client-js"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// v7 (필수)
|
|
20
|
+
generator client {
|
|
21
|
+
provider = "prisma-client"
|
|
22
|
+
output = "../generated/prisma"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Prisma Client
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// lib/prisma.ts
|
|
30
|
+
import { PrismaClient } from './generated/prisma'
|
|
31
|
+
|
|
32
|
+
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
|
|
33
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: ['query'] })
|
|
34
|
+
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## TanStack Start 연동
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
41
|
+
import { prisma } from '@/lib/prisma'
|
|
42
|
+
|
|
43
|
+
export const getUsers = createServerFn({ method: 'GET' })
|
|
44
|
+
.handler(async () => prisma.user.findMany({ include: { posts: true } }))
|
|
45
|
+
|
|
46
|
+
export const createUser = createServerFn({ method: 'POST' })
|
|
47
|
+
.inputValidator((data: { email: string; name: string }) => data)
|
|
48
|
+
.handler(async ({ data }) => prisma.user.create({ data }))
|
|
49
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Prisma - 트랜잭션
|
|
2
|
+
|
|
3
|
+
## 배열 기반 트랜잭션
|
|
4
|
+
|
|
5
|
+
하나라도 실패하면 모두 롤백.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
const deletePosts = prisma.post.deleteMany({ where: { authorId: 7 } })
|
|
9
|
+
const deleteUser = prisma.user.delete({ where: { id: 7 } })
|
|
10
|
+
await prisma.$transaction([deletePosts, deleteUser])
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 인터랙티브 트랜잭션
|
|
14
|
+
|
|
15
|
+
복잡한 로직, 조건부 처리.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
19
|
+
const sender = await tx.account.findUnique({ where: { id: senderId } })
|
|
20
|
+
if (!sender || sender.balance < amount) throw new Error('Insufficient balance')
|
|
21
|
+
|
|
22
|
+
await tx.account.update({ where: { id: senderId }, data: { balance: { decrement: amount } } })
|
|
23
|
+
await tx.account.update({ where: { id: recipientId }, data: { balance: { increment: amount } } })
|
|
24
|
+
|
|
25
|
+
return { success: true }
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 옵션
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
await prisma.$transaction(async (tx) => { ... }, {
|
|
33
|
+
maxWait: 5000, // 최대 대기 (ms)
|
|
34
|
+
timeout: 10000, // 타임아웃 (ms)
|
|
35
|
+
isolationLevel: 'Serializable', // ReadUncommitted | ReadCommitted | RepeatableRead | Serializable
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 에러 처리
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
try {
|
|
43
|
+
await prisma.$transaction(async (tx) => {
|
|
44
|
+
await tx.user.create({ data: { email } })
|
|
45
|
+
if (someCondition) throw new Error('Rollback')
|
|
46
|
+
})
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// 전체 롤백됨
|
|
49
|
+
}
|
|
50
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# TanStack Query
|
|
2
|
+
|
|
3
|
+
> 5.x | React Data Fetching
|
|
4
|
+
|
|
5
|
+
@use-query.md
|
|
6
|
+
@use-mutation.md
|
|
7
|
+
@invalidation.md
|
|
8
|
+
@optimistic-updates.md
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<quick_reference>
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// useQuery
|
|
16
|
+
const { data, isLoading, error } = useQuery({
|
|
17
|
+
queryKey: ['users'],
|
|
18
|
+
queryFn: () => getUsers(),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// useMutation + invalidation
|
|
22
|
+
const queryClient = useQueryClient()
|
|
23
|
+
const mutation = useMutation({
|
|
24
|
+
mutationFn: createUser,
|
|
25
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
|
26
|
+
})
|
|
27
|
+
mutation.mutate({ email, name })
|
|
28
|
+
|
|
29
|
+
// Optimistic Update
|
|
30
|
+
const mutation = useMutation({
|
|
31
|
+
mutationFn: updateUser,
|
|
32
|
+
onMutate: async (newUser) => {
|
|
33
|
+
await queryClient.cancelQueries({ queryKey: ['users'] })
|
|
34
|
+
const previous = queryClient.getQueryData(['users'])
|
|
35
|
+
queryClient.setQueryData(['users'], (old) => [...old, newUser])
|
|
36
|
+
return { previous }
|
|
37
|
+
},
|
|
38
|
+
onError: (err, newUser, context) => {
|
|
39
|
+
queryClient.setQueryData(['users'], context.previous)
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// 설정
|
|
44
|
+
const queryClient = new QueryClient({
|
|
45
|
+
defaultOptions: {
|
|
46
|
+
queries: {
|
|
47
|
+
staleTime: 1000 * 60 * 5, // 5분
|
|
48
|
+
gcTime: 1000 * 60 * 30, // 30분
|
|
49
|
+
retry: 3,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const App = () => (
|
|
55
|
+
<QueryClientProvider client={queryClient}>
|
|
56
|
+
<YourApp />
|
|
57
|
+
</QueryClientProvider>
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Query Keys
|
|
61
|
+
['todos'] // 단순
|
|
62
|
+
['todo', { id: 5 }] // 파라미터
|
|
63
|
+
['todos', 'list', { filters }] // 계층적
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
</quick_reference>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# TanStack Query - Query 무효화
|
|
2
|
+
|
|
3
|
+
<patterns>
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
const queryClient = useQueryClient()
|
|
7
|
+
|
|
8
|
+
// 단일
|
|
9
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
10
|
+
|
|
11
|
+
// 다중
|
|
12
|
+
await Promise.all([
|
|
13
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
|
14
|
+
queryClient.invalidateQueries({ queryKey: ['reminders'] }),
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
// 전체
|
|
18
|
+
queryClient.invalidateQueries()
|
|
19
|
+
|
|
20
|
+
// 옵션
|
|
21
|
+
queryClient.invalidateQueries({
|
|
22
|
+
queryKey: ['posts'],
|
|
23
|
+
exact: true, // 정확한 키 매칭만
|
|
24
|
+
refetchType: 'active', // 'active' | 'inactive' | 'all' | 'none'
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Query Key 매칭
|
|
28
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] }) // prefix 매칭
|
|
29
|
+
queryClient.invalidateQueries({ queryKey: ['todos', 'list'], exact: true }) // 정확한 매칭
|
|
30
|
+
|
|
31
|
+
// Mutation과 함께
|
|
32
|
+
useMutation({
|
|
33
|
+
mutationFn: addTodo,
|
|
34
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
|
35
|
+
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), // 성공/실패 무관
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// 직접 업데이트 vs 무효화
|
|
39
|
+
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) // 더 빠름
|
|
40
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] }) // 서버 데이터 보장
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
</patterns>
|
|
44
|
+
|
|
45
|
+
<options>
|
|
46
|
+
|
|
47
|
+
| refetchType | 설명 |
|
|
48
|
+
|-------------|------|
|
|
49
|
+
| active | 렌더링 중인 쿼리만 재조회 (기본) |
|
|
50
|
+
| inactive | 비활성 쿼리만 |
|
|
51
|
+
| all | 모든 매칭 쿼리 |
|
|
52
|
+
| none | 무효화만, 재조회 안함 |
|
|
53
|
+
|
|
54
|
+
</options>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# TanStack Query - Optimistic Updates
|
|
2
|
+
|
|
3
|
+
<patterns>
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// 추가
|
|
7
|
+
useMutation({
|
|
8
|
+
mutationFn: addTodo,
|
|
9
|
+
onMutate: async (newTodo) => {
|
|
10
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
11
|
+
const previousTodos = queryClient.getQueryData(['todos'])
|
|
12
|
+
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
|
|
13
|
+
return { previousTodos }
|
|
14
|
+
},
|
|
15
|
+
onError: (err, newTodo, context) => {
|
|
16
|
+
queryClient.setQueryData(['todos'], context.previousTodos)
|
|
17
|
+
},
|
|
18
|
+
onSettled: () => {
|
|
19
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// 삭제
|
|
24
|
+
useMutation({
|
|
25
|
+
mutationFn: deleteTodo,
|
|
26
|
+
onMutate: async (todoId) => {
|
|
27
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
28
|
+
const previousTodos = queryClient.getQueryData(['todos'])
|
|
29
|
+
queryClient.setQueryData(['todos'], (old) =>
|
|
30
|
+
old.filter((todo) => todo.id !== todoId)
|
|
31
|
+
)
|
|
32
|
+
return { previousTodos }
|
|
33
|
+
},
|
|
34
|
+
onError: (err, todoId, context) => {
|
|
35
|
+
queryClient.setQueryData(['todos'], context.previousTodos)
|
|
36
|
+
},
|
|
37
|
+
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// 토글
|
|
41
|
+
useMutation({
|
|
42
|
+
mutationFn: toggleTodo,
|
|
43
|
+
onMutate: async (todoId) => {
|
|
44
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
45
|
+
const previousTodos = queryClient.getQueryData(['todos'])
|
|
46
|
+
queryClient.setQueryData(['todos'], (old) =>
|
|
47
|
+
old.map((todo) =>
|
|
48
|
+
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
return { previousTodos }
|
|
52
|
+
},
|
|
53
|
+
onError: (err, todoId, context) => {
|
|
54
|
+
queryClient.setQueryData(['todos'], context.previousTodos)
|
|
55
|
+
},
|
|
56
|
+
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// 단일 항목
|
|
60
|
+
useMutation({
|
|
61
|
+
mutationFn: updateTodo,
|
|
62
|
+
onMutate: async (newTodo) => {
|
|
63
|
+
await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })
|
|
64
|
+
const previousTodo = queryClient.getQueryData(['todos', newTodo.id])
|
|
65
|
+
queryClient.setQueryData(['todos', newTodo.id], newTodo)
|
|
66
|
+
return { previousTodo, newTodo }
|
|
67
|
+
},
|
|
68
|
+
onError: (err, newTodo, context) => {
|
|
69
|
+
queryClient.setQueryData(['todos', context.newTodo.id], context.previousTodo)
|
|
70
|
+
},
|
|
71
|
+
onSettled: (newTodo) => {
|
|
72
|
+
queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
</patterns>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# TanStack Query - useMutation
|
|
2
|
+
|
|
3
|
+
<patterns>
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// 기본
|
|
7
|
+
const queryClient = useQueryClient()
|
|
8
|
+
const mutation = useMutation({
|
|
9
|
+
mutationFn: postTodo,
|
|
10
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
|
11
|
+
})
|
|
12
|
+
mutation.mutate({ title: 'New Todo' })
|
|
13
|
+
|
|
14
|
+
// 콜백
|
|
15
|
+
useMutation({
|
|
16
|
+
mutationFn: updateTodo,
|
|
17
|
+
onMutate: async (newTodo) => {
|
|
18
|
+
// mutation 시작 전 (optimistic update용)
|
|
19
|
+
return { previousData } // context로 전달
|
|
20
|
+
},
|
|
21
|
+
onSuccess: (data, variables, context) => {},
|
|
22
|
+
onError: (error, variables, context) => {},
|
|
23
|
+
onSettled: (data, error, variables, context) => {
|
|
24
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// mutate vs mutateAsync
|
|
29
|
+
mutation.mutate(data, {
|
|
30
|
+
onSuccess: (result) => console.log(result),
|
|
31
|
+
onError: (error) => console.log(error),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await mutation.mutateAsync(data)
|
|
36
|
+
} catch (error) { ... }
|
|
37
|
+
|
|
38
|
+
// 캐시 업데이트
|
|
39
|
+
useMutation({
|
|
40
|
+
mutationFn: patchTodo,
|
|
41
|
+
onSuccess: (data) => {
|
|
42
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
43
|
+
queryClient.setQueryData(['todo', { id: data.id }], data)
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
</patterns>
|
|
49
|
+
|
|
50
|
+
<returns>
|
|
51
|
+
|
|
52
|
+
| 속성 | 설명 |
|
|
53
|
+
|------|------|
|
|
54
|
+
| data | mutation 결과 |
|
|
55
|
+
| error | 에러 객체 |
|
|
56
|
+
| isPending | 실행 중 |
|
|
57
|
+
| isSuccess/isError | 상태 |
|
|
58
|
+
| mutate | 실행 (비동기) |
|
|
59
|
+
| mutateAsync | 실행 (Promise) |
|
|
60
|
+
| reset | 상태 초기화 |
|
|
61
|
+
| variables | 전달된 변수 |
|
|
62
|
+
|
|
63
|
+
</returns>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# TanStack Query - useQuery
|
|
2
|
+
|
|
3
|
+
<patterns>
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// 기본
|
|
7
|
+
const { data, isLoading, error } = useQuery({
|
|
8
|
+
queryKey: ['todos'],
|
|
9
|
+
queryFn: getTodos,
|
|
10
|
+
})
|
|
11
|
+
if (isLoading) return <div>Loading...</div>
|
|
12
|
+
if (error) return <div>Error: {error.message}</div>
|
|
13
|
+
|
|
14
|
+
// 옵션
|
|
15
|
+
useQuery({
|
|
16
|
+
queryKey: ['todos'],
|
|
17
|
+
queryFn: fetchTodos,
|
|
18
|
+
staleTime: 1000 * 60 * 5, // fresh 유지 시간
|
|
19
|
+
gcTime: 1000 * 60 * 30, // 가비지 컬렉션 시간
|
|
20
|
+
refetchOnWindowFocus: true, // 포커스 시 리페치
|
|
21
|
+
refetchInterval: 1000 * 60, // 자동 리페치 간격
|
|
22
|
+
retry: 3, // 재시도 횟수
|
|
23
|
+
enabled: !!userId, // 조건부 실행
|
|
24
|
+
initialData: [], // 초기 데이터
|
|
25
|
+
select: (data) => data.filter(t => t.done), // 데이터 변환
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// 파라미터
|
|
29
|
+
useQuery({
|
|
30
|
+
queryKey: ['todo', todoId],
|
|
31
|
+
queryFn: () => fetchTodoById(todoId),
|
|
32
|
+
enabled: !!todoId,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// 의존적 쿼리
|
|
36
|
+
const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: ... })
|
|
37
|
+
const { data: posts } = useQuery({
|
|
38
|
+
queryKey: ['posts', user?.id],
|
|
39
|
+
queryFn: () => fetchPostsByUserId(user!.id),
|
|
40
|
+
enabled: !!user?.id,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// 병렬
|
|
44
|
+
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
|
|
45
|
+
const postsQuery = useQuery({ queryKey: ['posts'], queryFn: fetchPosts })
|
|
46
|
+
|
|
47
|
+
// 동적 병렬
|
|
48
|
+
const userQueries = useQueries({
|
|
49
|
+
queries: userIds.map((id) => ({
|
|
50
|
+
queryKey: ['user', id],
|
|
51
|
+
queryFn: () => fetchUserById(id),
|
|
52
|
+
})),
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
</patterns>
|
|
57
|
+
|
|
58
|
+
<returns>
|
|
59
|
+
|
|
60
|
+
| 속성 | 설명 |
|
|
61
|
+
|------|------|
|
|
62
|
+
| data | 쿼리 결과 |
|
|
63
|
+
| error | 에러 객체 |
|
|
64
|
+
| isLoading | 첫 로딩 중 |
|
|
65
|
+
| isFetching | 백그라운드 페칭 중 |
|
|
66
|
+
| isError/isSuccess | 상태 |
|
|
67
|
+
| refetch | 수동 리페치 |
|
|
68
|
+
| status | 'pending' \| 'error' \| 'success' |
|
|
69
|
+
|
|
70
|
+
</returns>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Zod - 복합 타입
|
|
2
|
+
|
|
3
|
+
<patterns>
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// 객체
|
|
7
|
+
const UserSchema = z.object({
|
|
8
|
+
name: z.string(),
|
|
9
|
+
email: z.email(),
|
|
10
|
+
age: z.number().optional(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
UserSchema.partial() // 모든 필드 optional
|
|
14
|
+
UserSchema.required() // 모든 필드 required
|
|
15
|
+
UserSchema.pick({ name: true }) // 특정 필드만
|
|
16
|
+
UserSchema.omit({ email: true }) // 특정 필드 제외
|
|
17
|
+
UserSchema.extend({ role: z.enum(['admin', 'user']) })
|
|
18
|
+
UserSchema.merge(AnotherSchema)
|
|
19
|
+
|
|
20
|
+
z.strictObject({ name: z.string() }) // v4: 추가 키 에러
|
|
21
|
+
z.looseObject({ name: z.string() }) // v4: 추가 키 통과
|
|
22
|
+
|
|
23
|
+
// 배열/튜플
|
|
24
|
+
z.array(z.string())
|
|
25
|
+
z.array(z.number()).min(1).max(10).length(5).nonempty()
|
|
26
|
+
z.tuple([z.string(), z.number()]) // [string, number]
|
|
27
|
+
|
|
28
|
+
// 유니온
|
|
29
|
+
z.union([z.string(), z.number()])
|
|
30
|
+
z.string().or(z.number())
|
|
31
|
+
|
|
32
|
+
z.discriminatedUnion('type', [
|
|
33
|
+
z.object({ type: z.literal('circle'), radius: z.number() }),
|
|
34
|
+
z.object({ type: z.literal('rectangle'), width: z.number(), height: z.number() }),
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
// Enum
|
|
38
|
+
const Status = z.enum(['pending', 'done', 'cancelled'])
|
|
39
|
+
type Status = z.infer<typeof Status> // 'pending' | 'done' | 'cancelled'
|
|
40
|
+
|
|
41
|
+
enum Fruits { Apple, Banana }
|
|
42
|
+
z.nativeEnum(Fruits)
|
|
43
|
+
|
|
44
|
+
// Record/Map/Set
|
|
45
|
+
z.record(z.string(), z.object({ name: z.string() })) // { [key: string]: { name: string } }
|
|
46
|
+
z.map(z.string(), z.number()) // Map<string, number>
|
|
47
|
+
z.set(z.number()) // Set<number>
|
|
48
|
+
|
|
49
|
+
// 재귀
|
|
50
|
+
type Json = string | number | boolean | null | { [key: string]: Json } | Json[]
|
|
51
|
+
|
|
52
|
+
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
|
|
53
|
+
z.union([
|
|
54
|
+
z.string(), z.number(), z.boolean(), z.null(),
|
|
55
|
+
z.array(jsonSchema),
|
|
56
|
+
z.record(jsonSchema)
|
|
57
|
+
])
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
</patterns>
|