@kood/claude-code 0.2.1 → 0.2.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/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/hono/docs/library/drizzle/cloudflare-d1.md +247 -0
- package/templates/hono/docs/library/drizzle/config.md +167 -0
- package/templates/hono/docs/library/drizzle/index.md +259 -0
- package/templates/tanstack-start/docs/library/drizzle/cloudflare-d1.md +146 -0
- package/templates/tanstack-start/docs/library/drizzle/config.md +118 -0
- package/templates/tanstack-start/docs/library/drizzle/crud.md +205 -0
- package/templates/tanstack-start/docs/library/drizzle/index.md +79 -0
- package/templates/tanstack-start/docs/library/drizzle/relations.md +202 -0
- package/templates/tanstack-start/docs/library/drizzle/schema.md +154 -0
- package/templates/tanstack-start/docs/library/drizzle/setup.md +95 -0
- package/templates/tanstack-start/docs/library/drizzle/transactions.md +127 -0
- package/templates/tanstack-start/docs/library/tanstack-router/error-handling.md +204 -0
- package/templates/tanstack-start/docs/library/tanstack-router/hooks.md +195 -0
- package/templates/tanstack-start/docs/library/tanstack-router/index.md +150 -0
- package/templates/tanstack-start/docs/library/tanstack-router/navigation.md +150 -0
- package/templates/tanstack-start/docs/library/tanstack-router/route-context.md +203 -0
- package/templates/tanstack-start/docs/library/tanstack-router/search-params.md +213 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# TanStack Router - Route Context
|
|
2
|
+
|
|
3
|
+
beforeLoad, context, protected routes 구현.
|
|
4
|
+
|
|
5
|
+
## beforeLoad
|
|
6
|
+
|
|
7
|
+
라우트 로드 전 실행. 인증 체크, 리다이렉트, context 추가.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
11
|
+
|
|
12
|
+
export const Route = createFileRoute('/dashboard')({
|
|
13
|
+
beforeLoad: async ({ context, location }) => {
|
|
14
|
+
// context에서 인증 상태 확인
|
|
15
|
+
if (!context.auth.isAuthenticated) {
|
|
16
|
+
throw redirect({
|
|
17
|
+
to: '/login',
|
|
18
|
+
search: { redirect: location.href },
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 추가 context 반환 (loader에서 사용 가능)
|
|
23
|
+
return {
|
|
24
|
+
userPermissions: await fetchPermissions(context.auth.user.id),
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
loader: async ({ context }) => {
|
|
28
|
+
// beforeLoad에서 반환한 context 사용
|
|
29
|
+
return fetchDashboardData(context.userPermissions)
|
|
30
|
+
},
|
|
31
|
+
component: DashboardPage,
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Protected Routes (Layout)
|
|
36
|
+
|
|
37
|
+
pathless layout으로 보호된 라우트 그룹 생성.
|
|
38
|
+
|
|
39
|
+
### _authed.tsx (Layout)
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// routes/_authed.tsx
|
|
43
|
+
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
|
|
44
|
+
import { getCurrentUser } from '@/functions/auth'
|
|
45
|
+
|
|
46
|
+
export const Route = createFileRoute('/_authed')({
|
|
47
|
+
beforeLoad: async ({ location }) => {
|
|
48
|
+
const user = await getCurrentUser()
|
|
49
|
+
|
|
50
|
+
if (!user) {
|
|
51
|
+
throw redirect({
|
|
52
|
+
to: '/login',
|
|
53
|
+
search: { redirect: location.href },
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 하위 라우트에서 사용할 context
|
|
58
|
+
return { user }
|
|
59
|
+
},
|
|
60
|
+
component: () => <Outlet />,
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### _authed/dashboard.tsx
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// routes/_authed/dashboard.tsx
|
|
68
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
69
|
+
|
|
70
|
+
export const Route = createFileRoute('/_authed/dashboard')({
|
|
71
|
+
component: DashboardPage,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
function DashboardPage() {
|
|
75
|
+
// _authed에서 전달된 context
|
|
76
|
+
const { user } = Route.useRouteContext()
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<h1>Welcome, {user.name}!</h1>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 구조
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
routes/
|
|
90
|
+
├── _authed.tsx # Protected layout (beforeLoad에서 인증 체크)
|
|
91
|
+
├── _authed/
|
|
92
|
+
│ ├── dashboard.tsx # /dashboard (protected)
|
|
93
|
+
│ ├── settings.tsx # /settings (protected)
|
|
94
|
+
│ └── profile.tsx # /profile (protected)
|
|
95
|
+
├── login.tsx # /login (public)
|
|
96
|
+
└── index.tsx # / (public)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Root Context
|
|
100
|
+
|
|
101
|
+
전역 context 설정 (QueryClient, Auth 등).
|
|
102
|
+
|
|
103
|
+
### __root.tsx
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// routes/__root.tsx
|
|
107
|
+
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
|
|
108
|
+
import type { QueryClient } from '@tanstack/react-query'
|
|
109
|
+
|
|
110
|
+
interface RouterContext {
|
|
111
|
+
queryClient: QueryClient
|
|
112
|
+
auth: {
|
|
113
|
+
isAuthenticated: boolean
|
|
114
|
+
user: { id: string; name: string } | null
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
119
|
+
component: RootLayout,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
function RootLayout() {
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
<Outlet />
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Router 생성
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
// app/router.tsx
|
|
135
|
+
import { createRouter } from '@tanstack/react-router'
|
|
136
|
+
import { routeTree } from './routeTree.gen'
|
|
137
|
+
import { queryClient } from './query-client'
|
|
138
|
+
|
|
139
|
+
export const router = createRouter({
|
|
140
|
+
routeTree,
|
|
141
|
+
context: {
|
|
142
|
+
queryClient,
|
|
143
|
+
auth: {
|
|
144
|
+
isAuthenticated: false,
|
|
145
|
+
user: null,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## redirect
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { redirect } from '@tanstack/react-router'
|
|
155
|
+
|
|
156
|
+
// 기본 리다이렉트
|
|
157
|
+
throw redirect({ to: '/login' })
|
|
158
|
+
|
|
159
|
+
// Search params 포함
|
|
160
|
+
throw redirect({
|
|
161
|
+
to: '/login',
|
|
162
|
+
search: { redirect: '/dashboard' },
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// 동적 파라미터
|
|
166
|
+
throw redirect({
|
|
167
|
+
to: '/posts/$postId',
|
|
168
|
+
params: { postId: '123' },
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// replace (뒤로가기 X)
|
|
172
|
+
throw redirect({
|
|
173
|
+
to: '/home',
|
|
174
|
+
replace: true,
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Context 사용 위치
|
|
179
|
+
|
|
180
|
+
| 위치 | 접근 방법 |
|
|
181
|
+
|------|----------|
|
|
182
|
+
| beforeLoad | `{ context }` 파라미터 |
|
|
183
|
+
| loader | `{ context }` 파라미터 |
|
|
184
|
+
| component | `Route.useRouteContext()` |
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
export const Route = createFileRoute('/example')({
|
|
188
|
+
beforeLoad: ({ context }) => {
|
|
189
|
+
console.log(context.auth)
|
|
190
|
+
return { extra: 'data' }
|
|
191
|
+
},
|
|
192
|
+
loader: ({ context }) => {
|
|
193
|
+
// beforeLoad 반환값도 포함됨
|
|
194
|
+
console.log(context.extra)
|
|
195
|
+
},
|
|
196
|
+
component: ExamplePage,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
function ExamplePage() {
|
|
200
|
+
const context = Route.useRouteContext()
|
|
201
|
+
// context.auth, context.extra 모두 접근 가능
|
|
202
|
+
}
|
|
203
|
+
```
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# TanStack Router - Search Params
|
|
2
|
+
|
|
3
|
+
Type-safe URL search params with Zod validation.
|
|
4
|
+
|
|
5
|
+
## 기본 사용
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
|
|
11
|
+
// 스키마 정의
|
|
12
|
+
const productSearchSchema = z.object({
|
|
13
|
+
page: z.number().catch(1),
|
|
14
|
+
filter: z.string().catch(''),
|
|
15
|
+
sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
type ProductSearch = z.infer<typeof productSearchSchema>
|
|
19
|
+
|
|
20
|
+
// 라우트에 적용
|
|
21
|
+
export const Route = createFileRoute('/products')({
|
|
22
|
+
validateSearch: productSearchSchema,
|
|
23
|
+
component: ProductsPage,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// 컴포넌트에서 사용
|
|
27
|
+
function ProductsPage() {
|
|
28
|
+
const { page, filter, sort } = Route.useSearch()
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div>
|
|
32
|
+
<p>Page: {page}</p>
|
|
33
|
+
<p>Filter: {filter}</p>
|
|
34
|
+
<p>Sort: {sort}</p>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Zod 스키마 패턴
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { z } from 'zod'
|
|
44
|
+
|
|
45
|
+
// 기본값 (.catch)
|
|
46
|
+
const schema = z.object({
|
|
47
|
+
page: z.number().catch(1), // 파싱 실패 시 1
|
|
48
|
+
search: z.string().optional(), // undefined 허용
|
|
49
|
+
tab: z.enum(['all', 'active']).catch('all'),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// 복잡한 타입
|
|
53
|
+
const advancedSchema = z.object({
|
|
54
|
+
// 배열
|
|
55
|
+
tags: z.array(z.string()).catch([]),
|
|
56
|
+
|
|
57
|
+
// 날짜
|
|
58
|
+
from: z.string().date().optional(),
|
|
59
|
+
to: z.string().date().optional(),
|
|
60
|
+
|
|
61
|
+
// 숫자 범위
|
|
62
|
+
minPrice: z.number().min(0).catch(0),
|
|
63
|
+
maxPrice: z.number().max(10000).catch(10000),
|
|
64
|
+
|
|
65
|
+
// Boolean
|
|
66
|
+
inStock: z.boolean().catch(true),
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Search Params 업데이트
|
|
71
|
+
|
|
72
|
+
### Link로 업데이트
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { Link } from '@tanstack/react-router'
|
|
76
|
+
|
|
77
|
+
// 전체 교체
|
|
78
|
+
<Link to="/products" search={{ page: 1, sort: 'newest' }}>
|
|
79
|
+
Reset
|
|
80
|
+
</Link>
|
|
81
|
+
|
|
82
|
+
// 병합
|
|
83
|
+
<Link to="/products" search={prev => ({ ...prev, page: 2 })}>
|
|
84
|
+
Next Page
|
|
85
|
+
</Link>
|
|
86
|
+
|
|
87
|
+
// 특정 값만 변경
|
|
88
|
+
<Link
|
|
89
|
+
to="/products"
|
|
90
|
+
search={prev => ({ ...prev, sort: 'price' })}
|
|
91
|
+
>
|
|
92
|
+
Sort by Price
|
|
93
|
+
</Link>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### useNavigate로 업데이트
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
100
|
+
|
|
101
|
+
function Pagination() {
|
|
102
|
+
const navigate = useNavigate()
|
|
103
|
+
const { page } = Route.useSearch()
|
|
104
|
+
|
|
105
|
+
const goToPage = (newPage: number) => {
|
|
106
|
+
navigate({
|
|
107
|
+
to: '/products',
|
|
108
|
+
search: prev => ({ ...prev, page: newPage }),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div>
|
|
114
|
+
<button onClick={() => goToPage(page - 1)} disabled={page <= 1}>
|
|
115
|
+
Prev
|
|
116
|
+
</button>
|
|
117
|
+
<span>Page {page}</span>
|
|
118
|
+
<button onClick={() => goToPage(page + 1)}>
|
|
119
|
+
Next
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 실전 예시
|
|
127
|
+
|
|
128
|
+
### 필터 + 정렬 + 페이지네이션
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
|
|
132
|
+
import { z } from 'zod'
|
|
133
|
+
|
|
134
|
+
const searchSchema = z.object({
|
|
135
|
+
page: z.number().min(1).catch(1),
|
|
136
|
+
pageSize: z.number().catch(10),
|
|
137
|
+
search: z.string().catch(''),
|
|
138
|
+
category: z.enum(['all', 'tech', 'lifestyle']).catch('all'),
|
|
139
|
+
sort: z.enum(['newest', 'oldest', 'popular']).catch('newest'),
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
export const Route = createFileRoute('/posts')({
|
|
143
|
+
validateSearch: searchSchema,
|
|
144
|
+
loaderDeps: ({ search }) => ({ search }), // search 변경 시 loader 재실행
|
|
145
|
+
loader: async ({ deps: { search } }) => {
|
|
146
|
+
return fetchPosts(search)
|
|
147
|
+
},
|
|
148
|
+
component: PostsPage,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
function PostsPage() {
|
|
152
|
+
const { page, search, category, sort } = Route.useSearch()
|
|
153
|
+
const posts = Route.useLoaderData()
|
|
154
|
+
const navigate = useNavigate()
|
|
155
|
+
|
|
156
|
+
const updateSearch = (updates: Partial<typeof searchSchema._type>) => {
|
|
157
|
+
navigate({
|
|
158
|
+
to: '/posts',
|
|
159
|
+
search: prev => ({ ...prev, ...updates, page: 1 }), // 필터 변경 시 1페이지로
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div>
|
|
165
|
+
{/* 검색 */}
|
|
166
|
+
<input
|
|
167
|
+
value={search}
|
|
168
|
+
onChange={e => updateSearch({ search: e.target.value })}
|
|
169
|
+
placeholder="Search..."
|
|
170
|
+
/>
|
|
171
|
+
|
|
172
|
+
{/* 카테고리 필터 */}
|
|
173
|
+
<select
|
|
174
|
+
value={category}
|
|
175
|
+
onChange={e => updateSearch({ category: e.target.value as any })}
|
|
176
|
+
>
|
|
177
|
+
<option value="all">All</option>
|
|
178
|
+
<option value="tech">Tech</option>
|
|
179
|
+
<option value="lifestyle">Lifestyle</option>
|
|
180
|
+
</select>
|
|
181
|
+
|
|
182
|
+
{/* 정렬 */}
|
|
183
|
+
<select
|
|
184
|
+
value={sort}
|
|
185
|
+
onChange={e => updateSearch({ sort: e.target.value as any })}
|
|
186
|
+
>
|
|
187
|
+
<option value="newest">Newest</option>
|
|
188
|
+
<option value="oldest">Oldest</option>
|
|
189
|
+
<option value="popular">Popular</option>
|
|
190
|
+
</select>
|
|
191
|
+
|
|
192
|
+
{/* 목록 */}
|
|
193
|
+
{posts.map(post => (
|
|
194
|
+
<div key={post.id}>{post.title}</div>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## loaderDeps
|
|
202
|
+
|
|
203
|
+
Search params 변경 시 loader 재실행하려면 `loaderDeps` 필요.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
export const Route = createFileRoute('/products')({
|
|
207
|
+
validateSearch: searchSchema,
|
|
208
|
+
loaderDeps: ({ search }) => ({ search }), // 의존성 선언
|
|
209
|
+
loader: async ({ deps: { search } }) => {
|
|
210
|
+
return fetchProducts(search)
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
```
|