@kood/claude-code 0.1.6 → 0.1.9
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 +109 -216
- package/package.json +8 -2
- package/templates/hono/CLAUDE.md +59 -328
- package/templates/hono/docs/architecture/architecture.md +93 -747
- package/templates/hono/docs/deployment/cloudflare.md +59 -513
- package/templates/hono/docs/deployment/docker.md +41 -356
- package/templates/hono/docs/deployment/index.md +54 -190
- package/templates/hono/docs/deployment/railway.md +36 -306
- package/templates/hono/docs/deployment/vercel.md +49 -434
- package/templates/hono/docs/library/ai-sdk/index.md +53 -290
- package/templates/hono/docs/library/ai-sdk/openrouter.md +19 -387
- package/templates/hono/docs/library/ai-sdk/providers.md +28 -394
- package/templates/hono/docs/library/ai-sdk/streaming.md +52 -353
- package/templates/hono/docs/library/ai-sdk/structured-output.md +63 -395
- package/templates/hono/docs/library/ai-sdk/tools.md +62 -431
- package/templates/hono/docs/library/hono/env-setup.md +24 -313
- package/templates/hono/docs/library/hono/error-handling.md +34 -295
- package/templates/hono/docs/library/hono/index.md +29 -121
- package/templates/hono/docs/library/hono/middleware.md +21 -188
- package/templates/hono/docs/library/hono/rpc.md +40 -341
- package/templates/hono/docs/library/hono/validation.md +35 -195
- package/templates/hono/docs/library/pino/index.md +42 -333
- package/templates/hono/docs/library/prisma/cloudflare-d1.md +64 -367
- package/templates/hono/docs/library/prisma/config.md +19 -260
- package/templates/hono/docs/library/prisma/index.md +67 -320
- package/templates/hono/docs/library/zod/index.md +53 -257
- package/templates/npx/CLAUDE.md +62 -274
- package/templates/npx/docs/references/patterns.md +160 -0
- package/templates/tanstack-start/CLAUDE.md +100 -256
- package/templates/tanstack-start/docs/architecture/architecture.md +44 -589
- package/templates/tanstack-start/docs/deployment/cloudflare.md +37 -424
- package/templates/tanstack-start/docs/deployment/index.md +57 -286
- package/templates/tanstack-start/docs/deployment/nitro.md +36 -318
- package/templates/tanstack-start/docs/deployment/railway.md +40 -409
- package/templates/tanstack-start/docs/deployment/vercel.md +43 -465
- package/templates/tanstack-start/docs/design/components.md +77 -311
- package/templates/tanstack-start/docs/design/index.md +113 -69
- package/templates/tanstack-start/docs/design/safe-area.md +51 -250
- package/templates/tanstack-start/docs/design/tailwind-setup.md +45 -359
- package/templates/tanstack-start/docs/guides/conventions.md +103 -0
- package/templates/tanstack-start/docs/guides/env-setup.md +34 -340
- package/templates/tanstack-start/docs/guides/getting-started.md +22 -209
- package/templates/tanstack-start/docs/guides/hooks.md +166 -0
- package/templates/tanstack-start/docs/guides/routes.md +166 -0
- package/templates/tanstack-start/docs/guides/services.md +143 -0
- package/templates/tanstack-start/docs/library/better-auth/2fa.md +27 -115
- package/templates/tanstack-start/docs/library/better-auth/advanced.md +22 -105
- package/templates/tanstack-start/docs/library/better-auth/index.md +17 -66
- package/templates/tanstack-start/docs/library/better-auth/plugins.md +11 -88
- package/templates/tanstack-start/docs/library/better-auth/session.md +12 -92
- package/templates/tanstack-start/docs/library/better-auth/setup.md +9 -91
- package/templates/tanstack-start/docs/library/prisma/cloudflare-d1.md +30 -358
- package/templates/tanstack-start/docs/library/prisma/config.md +27 -327
- package/templates/tanstack-start/docs/library/prisma/crud.md +46 -174
- package/templates/tanstack-start/docs/library/prisma/index.md +23 -113
- package/templates/tanstack-start/docs/library/prisma/relations.md +31 -153
- package/templates/tanstack-start/docs/library/prisma/schema.md +40 -217
- package/templates/tanstack-start/docs/library/prisma/setup.md +12 -112
- package/templates/tanstack-start/docs/library/prisma/transactions.md +20 -110
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +26 -97
- package/templates/tanstack-start/docs/library/tanstack-query/invalidation.md +28 -107
- package/templates/tanstack-start/docs/library/tanstack-query/optimistic-updates.md +44 -146
- package/templates/tanstack-start/docs/library/tanstack-query/use-mutation.md +33 -127
- package/templates/tanstack-start/docs/library/tanstack-query/use-query.md +49 -149
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +19 -112
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +33 -80
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +28 -106
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +21 -118
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +34 -246
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +6 -39
- package/templates/tanstack-start/docs/library/zod/complex-types.md +32 -156
- package/templates/tanstack-start/docs/library/zod/index.md +31 -144
- package/templates/tanstack-start/docs/library/zod/transforms.md +20 -129
- package/templates/tanstack-start/docs/library/zod/validation.md +39 -155
- package/templates/hono/docs/commands/git.md +0 -145
- package/templates/hono/docs/mcp/context7.md +0 -106
- package/templates/hono/docs/mcp/index.md +0 -176
- package/templates/hono/docs/mcp/sequential-thinking.md +0 -101
- package/templates/hono/docs/mcp/serena.md +0 -269
- package/templates/hono/docs/mcp/sgrep.md +0 -105
- package/templates/hono/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +0 -136
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +0 -303
- package/templates/npx/docs/commands/git.md +0 -145
- package/templates/npx/docs/mcp/index.md +0 -60
- package/templates/npx/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/npx/docs/skills/gemini-review/references/checklists.md +0 -134
- package/templates/npx/docs/skills/gemini-review/references/prompt-templates.md +0 -301
- package/templates/tanstack-start/docs/commands/git.md +0 -145
- package/templates/tanstack-start/docs/design/accessibility.md +0 -433
- package/templates/tanstack-start/docs/design/color.md +0 -235
- package/templates/tanstack-start/docs/design/spacing.md +0 -341
- package/templates/tanstack-start/docs/design/typography.md +0 -324
- package/templates/tanstack-start/docs/guides/best-practices.md +0 -950
- package/templates/tanstack-start/docs/guides/husky-lint-staged.md +0 -303
- package/templates/tanstack-start/docs/guides/prettier.md +0 -189
- package/templates/tanstack-start/docs/guides/project-templates.md +0 -710
- package/templates/tanstack-start/docs/library/tanstack-query/setup.md +0 -107
- package/templates/tanstack-start/docs/library/zod/basic-types.md +0 -186
- package/templates/tanstack-start/docs/mcp/context7.md +0 -204
- package/templates/tanstack-start/docs/mcp/index.md +0 -177
- package/templates/tanstack-start/docs/mcp/sequential-thinking.md +0 -180
- package/templates/tanstack-start/docs/mcp/serena.md +0 -269
- package/templates/tanstack-start/docs/mcp/sgrep.md +0 -174
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +0 -220
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +0 -144
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +0 -292
|
@@ -1,29 +1,21 @@
|
|
|
1
1
|
# Getting Started
|
|
2
2
|
|
|
3
|
-
TanStack Start 프로젝트 시작
|
|
3
|
+
TanStack Start 프로젝트 시작 가이드.
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
7
|
- Node.js 18+
|
|
8
8
|
- Yarn
|
|
9
|
-
- Claude Code CLI
|
|
10
9
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
### 1. Create TanStack Start Project
|
|
10
|
+
## 프로젝트 생성
|
|
14
11
|
|
|
15
12
|
```bash
|
|
16
13
|
npx create-tsrouter-app@latest my-app --template start
|
|
17
14
|
cd my-app
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### 2. Install Dependencies
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
15
|
yarn install
|
|
24
16
|
```
|
|
25
17
|
|
|
26
|
-
|
|
18
|
+
## 필수 패키지 설치
|
|
27
19
|
|
|
28
20
|
```bash
|
|
29
21
|
# Database (Prisma 7.x)
|
|
@@ -35,85 +27,9 @@ yarn add zod
|
|
|
35
27
|
|
|
36
28
|
# TanStack Query
|
|
37
29
|
yarn add @tanstack/react-query
|
|
38
|
-
|
|
39
|
-
# UI (optional)
|
|
40
|
-
yarn add tailwindcss postcss autoprefixer
|
|
41
|
-
npx tailwindcss init -p
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### 4. Initialize Prisma
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
npx prisma init
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Project Structure
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
my-app/
|
|
54
|
-
├── src/
|
|
55
|
-
│ ├── routes/
|
|
56
|
-
│ │ ├── __root.tsx
|
|
57
|
-
│ │ ├── index.tsx
|
|
58
|
-
│ │ └── users/
|
|
59
|
-
│ │ ├── index.tsx
|
|
60
|
-
│ │ ├── route.tsx # 필요시 route 설정
|
|
61
|
-
│ │ ├── -components/ # 페이지 전용 컴포넌트
|
|
62
|
-
│ │ │ └── user-card.tsx
|
|
63
|
-
│ │ ├── -sections/ # 섹션 분리 (복잡한 경우)
|
|
64
|
-
│ │ │ ├── user-list-section.tsx
|
|
65
|
-
│ │ │ └── user-filter-section.tsx
|
|
66
|
-
│ │ └── -hooks/ # 페이지 전용 훅
|
|
67
|
-
│ │ ├── use-users.ts
|
|
68
|
-
│ │ └── use-user-filter.ts
|
|
69
|
-
│ ├── components/ # 공통 컴포넌트
|
|
70
|
-
│ │ └── ui/
|
|
71
|
-
│ │ ├── button.tsx
|
|
72
|
-
│ │ └── input.tsx
|
|
73
|
-
│ ├── database/ # 데이터베이스 관련
|
|
74
|
-
│ │ └── prisma.ts # Prisma Client 인스턴스
|
|
75
|
-
│ ├── services/ # 도메인별 SDK/서비스 레이어
|
|
76
|
-
│ │ ├── user/
|
|
77
|
-
│ │ │ ├── index.ts # 진입점 (re-export)
|
|
78
|
-
│ │ │ ├── schemas.ts # Zod 스키마
|
|
79
|
-
│ │ │ ├── queries.ts # GET 요청
|
|
80
|
-
│ │ │ └── mutations.ts # POST 요청
|
|
81
|
-
│ │ └── auth/
|
|
82
|
-
│ │ ├── index.ts
|
|
83
|
-
│ │ ├── schemas.ts
|
|
84
|
-
│ │ ├── queries.ts
|
|
85
|
-
│ │ └── mutations.ts
|
|
86
|
-
│ ├── lib/ # 공통 유틸리티
|
|
87
|
-
│ │ ├── query-client.ts
|
|
88
|
-
│ │ └── utils.ts
|
|
89
|
-
│ └── styles/
|
|
90
|
-
│ └── app.css
|
|
91
|
-
├── generated/
|
|
92
|
-
│ └── prisma/ # Prisma Client 출력
|
|
93
|
-
├── prisma/
|
|
94
|
-
│ └── schema.prisma
|
|
95
|
-
├── app.config.ts
|
|
96
|
-
├── package.json
|
|
97
|
-
└── tsconfig.json
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## Route Folder Convention
|
|
101
|
-
|
|
102
|
-
TanStack Start에서 `-` 접두사는 라우트에서 제외됩니다:
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
routes/users/
|
|
106
|
-
├── index.tsx # /users 페이지
|
|
107
|
-
├── route.tsx # route 설정 (loader, beforeLoad 등)
|
|
108
|
-
├── -components/ # ❌ 라우트 아님, 컴포넌트 폴더
|
|
109
|
-
│ └── user-card.tsx
|
|
110
|
-
├── -sections/ # ❌ 라우트 아님, 섹션 폴더
|
|
111
|
-
│ └── user-list-section.tsx
|
|
112
|
-
└── -hooks/ # ❌ 라우트 아님, 훅 폴더
|
|
113
|
-
└── use-users.ts
|
|
114
30
|
```
|
|
115
31
|
|
|
116
|
-
##
|
|
32
|
+
## 초기 설정
|
|
117
33
|
|
|
118
34
|
### app.config.ts
|
|
119
35
|
|
|
@@ -132,7 +48,6 @@ export default defineConfig({
|
|
|
132
48
|
```tsx
|
|
133
49
|
// src/routes/__root.tsx
|
|
134
50
|
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
|
135
|
-
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
|
|
136
51
|
|
|
137
52
|
export const Route = createRootRoute({
|
|
138
53
|
component: RootComponent,
|
|
@@ -140,17 +55,14 @@ export const Route = createRootRoute({
|
|
|
140
55
|
|
|
141
56
|
const RootComponent = (): JSX.Element => {
|
|
142
57
|
return (
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
</div>
|
|
152
|
-
<TanStackRouterDevtools />
|
|
153
|
-
</>
|
|
58
|
+
<div className="min-h-screen">
|
|
59
|
+
<nav className="border-b p-4">
|
|
60
|
+
<a href="/" className="font-bold">My App</a>
|
|
61
|
+
</nav>
|
|
62
|
+
<main className="container mx-auto p-4">
|
|
63
|
+
<Outlet />
|
|
64
|
+
</main>
|
|
65
|
+
</div>
|
|
154
66
|
)
|
|
155
67
|
}
|
|
156
68
|
```
|
|
@@ -174,29 +86,6 @@ const HomePage = (): JSX.Element => {
|
|
|
174
86
|
}
|
|
175
87
|
```
|
|
176
88
|
|
|
177
|
-
## Services Setup
|
|
178
|
-
|
|
179
|
-
### Database Setup
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
// src/database/prisma.ts
|
|
183
|
-
import { PrismaClient } from '../../generated/prisma'
|
|
184
|
-
|
|
185
|
-
const globalForPrisma = globalThis as unknown as {
|
|
186
|
-
prisma: PrismaClient | undefined
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export const prisma =
|
|
190
|
-
globalForPrisma.prisma ??
|
|
191
|
-
new PrismaClient({
|
|
192
|
-
log: process.env.NODE_ENV === 'development' ? ['query'] : [],
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
196
|
-
globalForPrisma.prisma = prisma
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
89
|
### Query Client
|
|
201
90
|
|
|
202
91
|
```typescript
|
|
@@ -206,99 +95,23 @@ import { QueryClient } from '@tanstack/react-query'
|
|
|
206
95
|
export const createQueryClient = (): QueryClient => {
|
|
207
96
|
return new QueryClient({
|
|
208
97
|
defaultOptions: {
|
|
209
|
-
queries: {
|
|
210
|
-
staleTime: 60 * 1000,
|
|
211
|
-
retry: 1,
|
|
212
|
-
},
|
|
98
|
+
queries: { staleTime: 60 * 1000, retry: 1 },
|
|
213
99
|
},
|
|
214
100
|
})
|
|
215
101
|
}
|
|
216
102
|
```
|
|
217
103
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
// src/services/user/schemas.ts
|
|
222
|
-
import { z } from 'zod'
|
|
223
|
-
|
|
224
|
-
export const createUserSchema = z.object({
|
|
225
|
-
email: z.email(),
|
|
226
|
-
name: z.string().min(1).max(100).trim(),
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
export const updateUserSchema = z.object({
|
|
230
|
-
id: z.string(),
|
|
231
|
-
email: z.email().optional(),
|
|
232
|
-
name: z.string().min(1).max(100).trim().optional(),
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
export type CreateUserInput = z.infer<typeof createUserSchema>
|
|
236
|
-
export type UpdateUserInput = z.infer<typeof updateUserSchema>
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
// src/services/user/queries.ts
|
|
241
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
242
|
-
import { prisma } from '@/database/prisma'
|
|
243
|
-
|
|
244
|
-
export const getUsers = createServerFn({ method: 'GET' })
|
|
245
|
-
.handler(async () => {
|
|
246
|
-
return prisma.user.findMany({
|
|
247
|
-
orderBy: { createdAt: 'desc' },
|
|
248
|
-
})
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
export const getUserById = createServerFn({ method: 'GET' })
|
|
252
|
-
.handler(async ({ data: id }: { data: string }) => {
|
|
253
|
-
const user = await prisma.user.findUnique({ where: { id } })
|
|
254
|
-
if (!user) throw new Error('User not found')
|
|
255
|
-
return user
|
|
256
|
-
})
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
// src/services/user/mutations.ts
|
|
261
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
262
|
-
import { prisma } from '@/database/prisma'
|
|
263
|
-
import { createUserSchema, updateUserSchema } from './schemas'
|
|
264
|
-
|
|
265
|
-
export const createUser = createServerFn({ method: 'POST' })
|
|
266
|
-
.inputValidator(createUserSchema)
|
|
267
|
-
.handler(async ({ data }) => {
|
|
268
|
-
return prisma.user.create({ data })
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
export const updateUser = createServerFn({ method: 'POST' })
|
|
272
|
-
.inputValidator(updateUserSchema)
|
|
273
|
-
.handler(async ({ data }) => {
|
|
274
|
-
const { id, ...updateData } = data
|
|
275
|
-
return prisma.user.update({ where: { id }, data: updateData })
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
export const deleteUser = createServerFn({ method: 'POST' })
|
|
279
|
-
.handler(async ({ data: id }: { data: string }) => {
|
|
280
|
-
return prisma.user.delete({ where: { id } })
|
|
281
|
-
})
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
// src/services/user/index.ts
|
|
286
|
-
export * from './schemas'
|
|
287
|
-
export * from './queries'
|
|
288
|
-
export * from './mutations'
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
## Development Commands
|
|
104
|
+
## 개발 명령어
|
|
292
105
|
|
|
293
106
|
| Command | Description |
|
|
294
107
|
|---------|-------------|
|
|
295
|
-
| `yarn dev` |
|
|
296
|
-
| `yarn build` |
|
|
297
|
-
| `yarn start` |
|
|
298
|
-
| `yarn test` | Run tests |
|
|
299
|
-
| `yarn lint` | Check code quality |
|
|
108
|
+
| `yarn dev` | 개발 서버 시작 |
|
|
109
|
+
| `yarn build` | 프로덕션 빌드 |
|
|
110
|
+
| `yarn start` | 프로덕션 서버 |
|
|
300
111
|
|
|
301
|
-
##
|
|
112
|
+
## 다음 단계
|
|
302
113
|
|
|
303
|
-
- [
|
|
304
|
-
- [
|
|
114
|
+
- [conventions.md](./conventions.md) - 코드 컨벤션
|
|
115
|
+
- [routes.md](./routes.md) - 라우트 구조
|
|
116
|
+
- [hooks.md](./hooks.md) - Custom Hook 패턴
|
|
117
|
+
- [services.md](./services.md) - Service Layer
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Custom Hook 패턴
|
|
2
|
+
|
|
3
|
+
페이지/섹션의 모든 로직, 상태, 라이프사이클을 중앙화.
|
|
4
|
+
|
|
5
|
+
## 필수: Hook 내부 순서
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
1. State (useState, zustand store)
|
|
9
|
+
2. Global Hooks (useParams, useNavigate, useQueryClient)
|
|
10
|
+
3. React Query (useQuery → useMutation)
|
|
11
|
+
4. Event Handlers & Functions
|
|
12
|
+
5. useMemo
|
|
13
|
+
6. useEffect
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Page Hook 예시
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// routes/users/-hooks/use-users.ts
|
|
20
|
+
import { useState, useMemo, useEffect, useCallback } from 'react'
|
|
21
|
+
import { useParams, useNavigate } from '@tanstack/react-router'
|
|
22
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
23
|
+
import { useAuthStore } from '@/stores/auth'
|
|
24
|
+
import { getUsers, createUser, deleteUser } from '@/services/user'
|
|
25
|
+
import type { User } from '@/types'
|
|
26
|
+
|
|
27
|
+
interface UseUsersReturn {
|
|
28
|
+
users: User[] | undefined
|
|
29
|
+
filteredUsers: User[]
|
|
30
|
+
isLoading: boolean
|
|
31
|
+
error: Error | null
|
|
32
|
+
search: string
|
|
33
|
+
setSearch: (value: string) => void
|
|
34
|
+
handleCreate: (data: { email: string; name: string }) => void
|
|
35
|
+
handleDelete: (id: string) => void
|
|
36
|
+
isCreating: boolean
|
|
37
|
+
isDeleting: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const useUsers = (): UseUsersReturn => {
|
|
41
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
42
|
+
// 1. State
|
|
43
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
44
|
+
const [search, setSearch] = useState('')
|
|
45
|
+
const { user: currentUser } = useAuthStore()
|
|
46
|
+
|
|
47
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
48
|
+
// 2. Global Hooks
|
|
49
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
50
|
+
const params = useParams({ from: '/users/$id' })
|
|
51
|
+
const navigate = useNavigate()
|
|
52
|
+
const queryClient = useQueryClient()
|
|
53
|
+
|
|
54
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
55
|
+
// 3. React Query
|
|
56
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
57
|
+
const { data: users, isLoading, error } = useQuery({
|
|
58
|
+
queryKey: ['users'],
|
|
59
|
+
queryFn: () => getUsers(),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const createMutation = useMutation({
|
|
63
|
+
mutationFn: createUser,
|
|
64
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const deleteMutation = useMutation({
|
|
68
|
+
mutationFn: deleteUser,
|
|
69
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
73
|
+
// 4. Event Handlers
|
|
74
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
75
|
+
const handleCreate = useCallback(
|
|
76
|
+
(data: { email: string; name: string }) => {
|
|
77
|
+
createMutation.mutate({ data })
|
|
78
|
+
},
|
|
79
|
+
[createMutation]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const handleDelete = useCallback(
|
|
83
|
+
(id: string) => {
|
|
84
|
+
deleteMutation.mutate({ data: id })
|
|
85
|
+
},
|
|
86
|
+
[deleteMutation]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
90
|
+
// 5. useMemo
|
|
91
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
92
|
+
const filteredUsers = useMemo(() => {
|
|
93
|
+
if (!users) return []
|
|
94
|
+
if (!search) return users
|
|
95
|
+
return users.filter((user) =>
|
|
96
|
+
user.name.toLowerCase().includes(search.toLowerCase())
|
|
97
|
+
)
|
|
98
|
+
}, [users, search])
|
|
99
|
+
|
|
100
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
101
|
+
// 6. useEffect
|
|
102
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!currentUser) {
|
|
105
|
+
navigate({ to: '/login' })
|
|
106
|
+
}
|
|
107
|
+
}, [currentUser, navigate])
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
users,
|
|
111
|
+
filteredUsers,
|
|
112
|
+
isLoading,
|
|
113
|
+
error,
|
|
114
|
+
search,
|
|
115
|
+
setSearch,
|
|
116
|
+
handleCreate,
|
|
117
|
+
handleDelete,
|
|
118
|
+
isCreating: createMutation.isPending,
|
|
119
|
+
isDeleting: deleteMutation.isPending,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 잘못된 순서 (금지)
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// ❌ 순서가 뒤섞인 잘못된 예시
|
|
128
|
+
export const useBadHook = () => {
|
|
129
|
+
const queryClient = useQueryClient() // ❌ Global Hook이 먼저
|
|
130
|
+
|
|
131
|
+
useEffect(() => { /* ... */ }, []) // ❌ useEffect가 중간에
|
|
132
|
+
|
|
133
|
+
const [state, setState] = useState() // ❌ State가 나중에
|
|
134
|
+
|
|
135
|
+
const { data } = useQuery({ /* ... */ }) // ❌ Query가 Effect 다음에
|
|
136
|
+
|
|
137
|
+
const computed = useMemo(() => {}, []) // ❌ useMemo 위치 잘못됨
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Filter Hook 예시
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// routes/users/-hooks/use-user-filter.ts
|
|
145
|
+
import { useState, useCallback } from 'react'
|
|
146
|
+
|
|
147
|
+
interface UseUserFilterReturn {
|
|
148
|
+
search: string
|
|
149
|
+
setSearch: (value: string) => void
|
|
150
|
+
role: string
|
|
151
|
+
setRole: (value: string) => void
|
|
152
|
+
clearFilters: () => void
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const useUserFilter = (): UseUserFilterReturn => {
|
|
156
|
+
const [search, setSearch] = useState('')
|
|
157
|
+
const [role, setRole] = useState('')
|
|
158
|
+
|
|
159
|
+
const clearFilters = useCallback(() => {
|
|
160
|
+
setSearch('')
|
|
161
|
+
setRole('')
|
|
162
|
+
}, [])
|
|
163
|
+
|
|
164
|
+
return { search, setSearch, role, setRole, clearFilters }
|
|
165
|
+
}
|
|
166
|
+
```
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# 라우트 구조
|
|
2
|
+
|
|
3
|
+
TanStack Start 파일 기반 라우팅 패턴.
|
|
4
|
+
|
|
5
|
+
## Route 폴더 구조
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
routes/<route-name>/
|
|
9
|
+
├── index.tsx # 페이지 컴포넌트
|
|
10
|
+
├── route.tsx # route 설정 (필요시)
|
|
11
|
+
├── -components/ # 페이지 전용 컴포넌트
|
|
12
|
+
│ ├── user-card.tsx
|
|
13
|
+
│ └── user-form.tsx
|
|
14
|
+
├── -sections/ # 섹션 분리 (복잡한 경우)
|
|
15
|
+
│ ├── user-list-section.tsx
|
|
16
|
+
│ └── user-filter-section.tsx
|
|
17
|
+
└── -hooks/ # 페이지 전용 훅
|
|
18
|
+
├── use-users.ts
|
|
19
|
+
└── use-user-filter.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## `-` 접두사
|
|
23
|
+
|
|
24
|
+
`-` 접두사가 있는 폴더는 라우트에서 제외:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
routes/users/
|
|
28
|
+
├── index.tsx # /users ✅ 라우트
|
|
29
|
+
├── $id.tsx # /users/:id ✅ 라우트
|
|
30
|
+
├── -components/ # ❌ 라우트 아님
|
|
31
|
+
├── -sections/ # ❌ 라우트 아님
|
|
32
|
+
└── -hooks/ # ❌ 라우트 아님
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 기본 Route 패턴
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// routes/users/index.tsx
|
|
39
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
40
|
+
import { UserListSection } from './-sections/user-list-section'
|
|
41
|
+
import { UserFilterSection } from './-sections/user-filter-section'
|
|
42
|
+
|
|
43
|
+
export const Route = createFileRoute('/users/')({
|
|
44
|
+
component: UsersPage,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const UsersPage = (): JSX.Element => {
|
|
48
|
+
return (
|
|
49
|
+
<div className="container mx-auto p-4">
|
|
50
|
+
<h1 className="text-2xl font-bold mb-4">Users</h1>
|
|
51
|
+
<UserFilterSection />
|
|
52
|
+
<UserListSection />
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Section 패턴
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// routes/users/-sections/user-list-section.tsx
|
|
62
|
+
import { useUsers } from '../-hooks/use-users'
|
|
63
|
+
import { UserCard } from '../-components/user-card'
|
|
64
|
+
|
|
65
|
+
export const UserListSection = (): JSX.Element => {
|
|
66
|
+
const { users, isLoading, error, deleteUser, isDeleting } = useUsers()
|
|
67
|
+
|
|
68
|
+
if (isLoading) return <div className="text-center py-8">Loading...</div>
|
|
69
|
+
if (error) return <div className="text-red-600 py-8">Error: {error.message}</div>
|
|
70
|
+
if (!users?.length) return <div className="text-gray-500 py-8">No users found</div>
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
74
|
+
{users.map((user) => (
|
|
75
|
+
<UserCard
|
|
76
|
+
key={user.id}
|
|
77
|
+
user={user}
|
|
78
|
+
onDelete={deleteUser}
|
|
79
|
+
isDeleting={isDeleting}
|
|
80
|
+
/>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Filter Section 패턴
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// routes/users/-sections/user-filter-section.tsx
|
|
91
|
+
import { useUserFilter } from '../-hooks/use-user-filter'
|
|
92
|
+
import { Input } from '@/components/ui/input'
|
|
93
|
+
import { Button } from '@/components/ui/button'
|
|
94
|
+
|
|
95
|
+
export const UserFilterSection = (): JSX.Element => {
|
|
96
|
+
const { search, setSearch, role, setRole, clearFilters } = useUserFilter()
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="flex gap-4 mb-6">
|
|
100
|
+
<Input
|
|
101
|
+
placeholder="Search users..."
|
|
102
|
+
value={search}
|
|
103
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
104
|
+
className="max-w-xs"
|
|
105
|
+
/>
|
|
106
|
+
<select
|
|
107
|
+
value={role}
|
|
108
|
+
onChange={(e) => setRole(e.target.value)}
|
|
109
|
+
className="border rounded px-3 py-2"
|
|
110
|
+
>
|
|
111
|
+
<option value="">All Roles</option>
|
|
112
|
+
<option value="USER">User</option>
|
|
113
|
+
<option value="ADMIN">Admin</option>
|
|
114
|
+
</select>
|
|
115
|
+
<Button variant="outline" onClick={clearFilters}>
|
|
116
|
+
Clear
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 컴포넌트 패턴
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
// routes/users/-components/user-card.tsx
|
|
127
|
+
import type { User } from '@/types'
|
|
128
|
+
import { Button } from '@/components/ui/button'
|
|
129
|
+
|
|
130
|
+
interface UserCardProps {
|
|
131
|
+
user: User
|
|
132
|
+
onDelete?: (id: string) => void
|
|
133
|
+
isDeleting?: boolean
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const UserCard = ({
|
|
137
|
+
user,
|
|
138
|
+
onDelete,
|
|
139
|
+
isDeleting,
|
|
140
|
+
}: UserCardProps): JSX.Element => {
|
|
141
|
+
return (
|
|
142
|
+
<div className="rounded-lg border p-4 shadow-sm">
|
|
143
|
+
<div className="flex items-center gap-4">
|
|
144
|
+
<div className="h-12 w-12 rounded-full bg-gray-200" />
|
|
145
|
+
<div>
|
|
146
|
+
<h3 className="font-semibold">{user.name}</h3>
|
|
147
|
+
<p className="text-sm text-gray-600">{user.email}</p>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{onDelete && (
|
|
152
|
+
<div className="mt-4">
|
|
153
|
+
<Button
|
|
154
|
+
variant="outline"
|
|
155
|
+
size="sm"
|
|
156
|
+
onClick={() => onDelete(user.id)}
|
|
157
|
+
disabled={isDeleting}
|
|
158
|
+
>
|
|
159
|
+
{isDeleting ? 'Deleting...' : 'Delete'}
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
```
|