@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,710 +0,0 @@
|
|
|
1
|
-
# Project Templates
|
|
2
|
-
|
|
3
|
-
TanStack Start 프로젝트 템플릿 및 구조 가이드입니다.
|
|
4
|
-
|
|
5
|
-
## TanStack Start Project Structure
|
|
6
|
-
|
|
7
|
-
### Base Structure
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
my-app/
|
|
11
|
-
├── src/
|
|
12
|
-
│ ├── routes/ # 파일 기반 라우팅
|
|
13
|
-
│ │ ├── __root.tsx # Root layout
|
|
14
|
-
│ │ ├── index.tsx # Home (/)
|
|
15
|
-
│ │ ├── about.tsx # /about
|
|
16
|
-
│ │ ├── users/
|
|
17
|
-
│ │ │ ├── index.tsx # /users
|
|
18
|
-
│ │ │ ├── $id.tsx # /users/:id
|
|
19
|
-
│ │ │ ├── -components/ # 페이지 전용 컴포넌트
|
|
20
|
-
│ │ │ │ └── user-card.tsx
|
|
21
|
-
│ │ │ ├── -sections/ # 섹션 분리
|
|
22
|
-
│ │ │ │ ├── user-list-section.tsx
|
|
23
|
-
│ │ │ │ └── user-form-section.tsx
|
|
24
|
-
│ │ │ └── -hooks/ # 페이지 전용 훅
|
|
25
|
-
│ │ │ └── use-users.ts
|
|
26
|
-
│ │ └── api/
|
|
27
|
-
│ │ └── health.ts # API route
|
|
28
|
-
│ ├── components/ # 공통 컴포넌트
|
|
29
|
-
│ │ └── ui/
|
|
30
|
-
│ │ ├── button.tsx
|
|
31
|
-
│ │ ├── input.tsx
|
|
32
|
-
│ │ └── modal.tsx
|
|
33
|
-
│ ├── database/ # 데이터베이스 관련
|
|
34
|
-
│ │ ├── prisma.ts # Prisma Client 인스턴스
|
|
35
|
-
│ │ └── seed.ts # 시드 데이터 (선택)
|
|
36
|
-
│ ├── services/ # 도메인별 SDK/서비스 레이어
|
|
37
|
-
│ │ ├── user/
|
|
38
|
-
│ │ │ ├── index.ts # 진입점 (re-export)
|
|
39
|
-
│ │ │ ├── schemas.ts # Zod 스키마
|
|
40
|
-
│ │ │ ├── queries.ts # GET 요청
|
|
41
|
-
│ │ │ └── mutations.ts # POST 요청
|
|
42
|
-
│ │ ├── auth/
|
|
43
|
-
│ │ │ ├── index.ts
|
|
44
|
-
│ │ │ ├── schemas.ts
|
|
45
|
-
│ │ │ ├── queries.ts
|
|
46
|
-
│ │ │ └── mutations.ts
|
|
47
|
-
│ │ └── post/
|
|
48
|
-
│ │ ├── index.ts
|
|
49
|
-
│ │ ├── schemas.ts
|
|
50
|
-
│ │ ├── queries.ts
|
|
51
|
-
│ │ └── mutations.ts
|
|
52
|
-
│ ├── lib/ # 공통 유틸리티
|
|
53
|
-
│ │ ├── query-client.ts
|
|
54
|
-
│ │ └── utils.ts
|
|
55
|
-
│ ├── hooks/ # 공통 훅
|
|
56
|
-
│ │ ├── use-auth.ts
|
|
57
|
-
│ │ └── use-media-query.ts
|
|
58
|
-
│ ├── types/ # 타입 정의
|
|
59
|
-
│ │ └── index.ts
|
|
60
|
-
│ └── styles/
|
|
61
|
-
│ └── app.css
|
|
62
|
-
├── generated/
|
|
63
|
-
│ └── prisma/ # Prisma Client 출력
|
|
64
|
-
├── prisma/
|
|
65
|
-
│ └── schema.prisma
|
|
66
|
-
├── public/
|
|
67
|
-
├── app.config.ts
|
|
68
|
-
├── package.json
|
|
69
|
-
├── tsconfig.json
|
|
70
|
-
└── tailwind.config.ts
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Route Folder Convention
|
|
74
|
-
|
|
75
|
-
### `-` 접두사 규칙
|
|
76
|
-
|
|
77
|
-
TanStack Start에서 `-` 접두사가 있는 폴더는 라우트에서 제외됩니다:
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
routes/users/
|
|
81
|
-
├── index.tsx # /users ✅ 라우트
|
|
82
|
-
├── $id.tsx # /users/:id ✅ 라우트
|
|
83
|
-
├── route.tsx # route 설정 (선택)
|
|
84
|
-
├── -components/ # ❌ 라우트 아님, 컴포넌트
|
|
85
|
-
├── -sections/ # ❌ 라우트 아님, 섹션
|
|
86
|
-
└── -hooks/ # ❌ 라우트 아님, 훅
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Route with Sections
|
|
90
|
-
|
|
91
|
-
복잡한 페이지는 섹션으로 분리:
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
routes/dashboard/
|
|
95
|
-
├── index.tsx
|
|
96
|
-
├── -sections/
|
|
97
|
-
│ ├── stats-section.tsx
|
|
98
|
-
│ ├── recent-activity-section.tsx
|
|
99
|
-
│ └── quick-actions-section.tsx
|
|
100
|
-
├── -components/
|
|
101
|
-
│ ├── stat-card.tsx
|
|
102
|
-
│ └── activity-item.tsx
|
|
103
|
-
└── -hooks/
|
|
104
|
-
├── use-dashboard-stats.ts
|
|
105
|
-
└── use-recent-activity.ts
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Route Patterns
|
|
109
|
-
|
|
110
|
-
### Basic Route
|
|
111
|
-
|
|
112
|
-
```tsx
|
|
113
|
-
// routes/about.tsx
|
|
114
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
115
|
-
|
|
116
|
-
export const Route = createFileRoute('/about')({
|
|
117
|
-
component: AboutPage,
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
const AboutPage = (): JSX.Element => {
|
|
121
|
-
return <h1>About</h1>
|
|
122
|
-
}
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Route with Sections and Hooks
|
|
126
|
-
|
|
127
|
-
```tsx
|
|
128
|
-
// routes/users/index.tsx
|
|
129
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
130
|
-
import { UserListSection } from './-sections/user-list-section'
|
|
131
|
-
import { UserFilterSection } from './-sections/user-filter-section'
|
|
132
|
-
|
|
133
|
-
export const Route = createFileRoute('/users/')({
|
|
134
|
-
component: UsersPage,
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
const UsersPage = (): JSX.Element => {
|
|
138
|
-
return (
|
|
139
|
-
<div className="container mx-auto p-4">
|
|
140
|
-
<h1 className="text-2xl font-bold mb-4">Users</h1>
|
|
141
|
-
<UserFilterSection />
|
|
142
|
-
<UserListSection />
|
|
143
|
-
</div>
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Page Hook
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
// routes/users/-hooks/use-users.ts
|
|
152
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
153
|
-
import { getUsers, createUser, deleteUser } from '@/services/user'
|
|
154
|
-
import type { User } from '@/types'
|
|
155
|
-
|
|
156
|
-
interface UseUsersReturn {
|
|
157
|
-
users: User[] | undefined
|
|
158
|
-
isLoading: boolean
|
|
159
|
-
error: Error | null
|
|
160
|
-
createUser: (data: { email: string; name: string }) => void
|
|
161
|
-
deleteUser: (id: string) => void
|
|
162
|
-
isCreating: boolean
|
|
163
|
-
isDeleting: boolean
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export const useUsers = (): UseUsersReturn => {
|
|
167
|
-
const queryClient = useQueryClient()
|
|
168
|
-
|
|
169
|
-
const { data: users, isLoading, error } = useQuery({
|
|
170
|
-
queryKey: ['users'],
|
|
171
|
-
queryFn: () => getUsers(),
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
const createMutation = useMutation({
|
|
175
|
-
mutationFn: createUser,
|
|
176
|
-
onSuccess: () => {
|
|
177
|
-
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
178
|
-
},
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
const deleteMutation = useMutation({
|
|
182
|
-
mutationFn: deleteUser,
|
|
183
|
-
onSuccess: () => {
|
|
184
|
-
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
185
|
-
},
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
users,
|
|
190
|
-
isLoading,
|
|
191
|
-
error,
|
|
192
|
-
createUser: createMutation.mutate,
|
|
193
|
-
deleteUser: deleteMutation.mutate,
|
|
194
|
-
isCreating: createMutation.isPending,
|
|
195
|
-
isDeleting: deleteMutation.isPending,
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Section Component
|
|
201
|
-
|
|
202
|
-
```tsx
|
|
203
|
-
// routes/users/-sections/user-list-section.tsx
|
|
204
|
-
import { useUsers } from '../-hooks/use-users'
|
|
205
|
-
import { UserCard } from '../-components/user-card'
|
|
206
|
-
|
|
207
|
-
export const UserListSection = (): JSX.Element => {
|
|
208
|
-
const { users, isLoading, error, deleteUser, isDeleting } = useUsers()
|
|
209
|
-
|
|
210
|
-
if (isLoading) {
|
|
211
|
-
return <div className="text-center py-8">Loading...</div>
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (error) {
|
|
215
|
-
return <div className="text-red-600 py-8">Error: {error.message}</div>
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (!users?.length) {
|
|
219
|
-
return <div className="text-gray-500 py-8">No users found</div>
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
224
|
-
{users.map((user) => (
|
|
225
|
-
<UserCard
|
|
226
|
-
key={user.id}
|
|
227
|
-
user={user}
|
|
228
|
-
onDelete={deleteUser}
|
|
229
|
-
isDeleting={isDeleting}
|
|
230
|
-
/>
|
|
231
|
-
))}
|
|
232
|
-
</div>
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### Page Component
|
|
238
|
-
|
|
239
|
-
```tsx
|
|
240
|
-
// routes/users/-components/user-card.tsx
|
|
241
|
-
import type { User } from '@/types'
|
|
242
|
-
import { Button } from '@/components/ui/button'
|
|
243
|
-
|
|
244
|
-
interface UserCardProps {
|
|
245
|
-
user: User
|
|
246
|
-
onDelete?: (id: string) => void
|
|
247
|
-
isDeleting?: boolean
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export const UserCard = ({
|
|
251
|
-
user,
|
|
252
|
-
onDelete,
|
|
253
|
-
isDeleting,
|
|
254
|
-
}: UserCardProps): JSX.Element => {
|
|
255
|
-
return (
|
|
256
|
-
<div className="rounded-lg border p-4 shadow-sm">
|
|
257
|
-
<div className="flex items-center gap-4">
|
|
258
|
-
<div className="h-12 w-12 rounded-full bg-gray-200" />
|
|
259
|
-
<div>
|
|
260
|
-
<h3 className="font-semibold">{user.name}</h3>
|
|
261
|
-
<p className="text-sm text-gray-600">{user.email}</p>
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
|
|
265
|
-
{onDelete && (
|
|
266
|
-
<div className="mt-4">
|
|
267
|
-
<Button
|
|
268
|
-
variant="outline"
|
|
269
|
-
size="sm"
|
|
270
|
-
onClick={() => onDelete(user.id)}
|
|
271
|
-
disabled={isDeleting}
|
|
272
|
-
>
|
|
273
|
-
{isDeleting ? 'Deleting...' : 'Delete'}
|
|
274
|
-
</Button>
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### Dynamic Route
|
|
283
|
-
|
|
284
|
-
```tsx
|
|
285
|
-
// routes/users/$id.tsx
|
|
286
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
287
|
-
import { UserDetailSection } from './-sections/user-detail-section'
|
|
288
|
-
import { UserPostsSection } from './-sections/user-posts-section'
|
|
289
|
-
|
|
290
|
-
export const Route = createFileRoute('/users/$id')({
|
|
291
|
-
component: UserDetailPage,
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
const UserDetailPage = (): JSX.Element => {
|
|
295
|
-
const { id } = Route.useParams()
|
|
296
|
-
|
|
297
|
-
return (
|
|
298
|
-
<div className="container mx-auto p-4">
|
|
299
|
-
<UserDetailSection userId={id} />
|
|
300
|
-
<UserPostsSection userId={id} />
|
|
301
|
-
</div>
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### Layout Route
|
|
307
|
-
|
|
308
|
-
```tsx
|
|
309
|
-
// routes/dashboard/_layout.tsx
|
|
310
|
-
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
|
311
|
-
import { DashboardSidebar } from './-components/dashboard-sidebar'
|
|
312
|
-
|
|
313
|
-
export const Route = createFileRoute('/dashboard/_layout')({
|
|
314
|
-
component: DashboardLayout,
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
const DashboardLayout = (): JSX.Element => {
|
|
318
|
-
return (
|
|
319
|
-
<div className="flex">
|
|
320
|
-
<DashboardSidebar />
|
|
321
|
-
<main className="flex-1 p-4">
|
|
322
|
-
<Outlet />
|
|
323
|
-
</main>
|
|
324
|
-
</div>
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
## Component Templates
|
|
330
|
-
|
|
331
|
-
### Common UI Component
|
|
332
|
-
|
|
333
|
-
```tsx
|
|
334
|
-
// components/ui/button.tsx
|
|
335
|
-
interface ButtonProps {
|
|
336
|
-
children: React.ReactNode
|
|
337
|
-
variant?: 'primary' | 'secondary' | 'outline'
|
|
338
|
-
size?: 'sm' | 'md' | 'lg'
|
|
339
|
-
onClick?: () => void
|
|
340
|
-
disabled?: boolean
|
|
341
|
-
type?: 'button' | 'submit' | 'reset'
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export const Button = ({
|
|
345
|
-
children,
|
|
346
|
-
variant = 'primary',
|
|
347
|
-
size = 'md',
|
|
348
|
-
onClick,
|
|
349
|
-
disabled,
|
|
350
|
-
type = 'button',
|
|
351
|
-
}: ButtonProps): JSX.Element => {
|
|
352
|
-
const baseStyles = 'rounded font-medium transition-colors disabled:opacity-50'
|
|
353
|
-
|
|
354
|
-
const variants = {
|
|
355
|
-
primary: 'bg-blue-600 text-white hover:bg-blue-700',
|
|
356
|
-
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
|
|
357
|
-
outline: 'border border-gray-300 hover:bg-gray-50',
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const sizes = {
|
|
361
|
-
sm: 'px-3 py-1.5 text-sm',
|
|
362
|
-
md: 'px-4 py-2',
|
|
363
|
-
lg: 'px-6 py-3 text-lg',
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return (
|
|
367
|
-
<button
|
|
368
|
-
type={type}
|
|
369
|
-
onClick={onClick}
|
|
370
|
-
disabled={disabled}
|
|
371
|
-
className={`${baseStyles} ${variants[variant]} ${sizes[size]}`}
|
|
372
|
-
>
|
|
373
|
-
{children}
|
|
374
|
-
</button>
|
|
375
|
-
)
|
|
376
|
-
}
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### Modal Component
|
|
380
|
-
|
|
381
|
-
```tsx
|
|
382
|
-
// components/ui/modal.tsx
|
|
383
|
-
import { useCallback, useEffect } from 'react'
|
|
384
|
-
|
|
385
|
-
interface ModalProps {
|
|
386
|
-
isOpen: boolean
|
|
387
|
-
onClose: () => void
|
|
388
|
-
title: string
|
|
389
|
-
children: React.ReactNode
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export const Modal = ({
|
|
393
|
-
isOpen,
|
|
394
|
-
onClose,
|
|
395
|
-
title,
|
|
396
|
-
children,
|
|
397
|
-
}: ModalProps): JSX.Element | null => {
|
|
398
|
-
const handleEscape = useCallback(
|
|
399
|
-
(e: KeyboardEvent) => {
|
|
400
|
-
if (e.key === 'Escape') onClose()
|
|
401
|
-
},
|
|
402
|
-
[onClose]
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
useEffect(() => {
|
|
406
|
-
document.addEventListener('keydown', handleEscape)
|
|
407
|
-
return () => document.removeEventListener('keydown', handleEscape)
|
|
408
|
-
}, [handleEscape])
|
|
409
|
-
|
|
410
|
-
if (!isOpen) return null
|
|
411
|
-
|
|
412
|
-
return (
|
|
413
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
414
|
-
<div
|
|
415
|
-
className="absolute inset-0 bg-black/50"
|
|
416
|
-
onClick={onClose}
|
|
417
|
-
/>
|
|
418
|
-
<div className="relative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-lg">
|
|
419
|
-
<h2 className="text-xl font-semibold mb-4">{title}</h2>
|
|
420
|
-
{children}
|
|
421
|
-
</div>
|
|
422
|
-
</div>
|
|
423
|
-
)
|
|
424
|
-
}
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
## Service Templates
|
|
428
|
-
|
|
429
|
-
### Database Setup
|
|
430
|
-
|
|
431
|
-
```typescript
|
|
432
|
-
// database/prisma.ts
|
|
433
|
-
import { PrismaClient } from '../../generated/prisma'
|
|
434
|
-
|
|
435
|
-
const globalForPrisma = globalThis as unknown as {
|
|
436
|
-
prisma: PrismaClient | undefined
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
export const prisma =
|
|
440
|
-
globalForPrisma.prisma ??
|
|
441
|
-
new PrismaClient({
|
|
442
|
-
log: process.env.NODE_ENV === 'development' ? ['query'] : [],
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
446
|
-
globalForPrisma.prisma = prisma
|
|
447
|
-
}
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
### Service 폴더 구조
|
|
451
|
-
|
|
452
|
-
```
|
|
453
|
-
services/
|
|
454
|
-
├── user/
|
|
455
|
-
│ ├── index.ts # 진입점 (re-export)
|
|
456
|
-
│ ├── schemas.ts # Zod 스키마
|
|
457
|
-
│ ├── queries.ts # GET 요청 (읽기)
|
|
458
|
-
│ └── mutations.ts # POST 요청 (쓰기)
|
|
459
|
-
├── auth/
|
|
460
|
-
│ ├── index.ts
|
|
461
|
-
│ ├── schemas.ts
|
|
462
|
-
│ ├── queries.ts
|
|
463
|
-
│ └── mutations.ts
|
|
464
|
-
└── post/
|
|
465
|
-
├── index.ts
|
|
466
|
-
├── schemas.ts
|
|
467
|
-
├── queries.ts
|
|
468
|
-
└── mutations.ts
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
### Schemas 파일
|
|
472
|
-
|
|
473
|
-
```typescript
|
|
474
|
-
// services/user/schemas.ts
|
|
475
|
-
import { z } from 'zod'
|
|
476
|
-
|
|
477
|
-
export const createUserSchema = z.object({
|
|
478
|
-
email: z.email(),
|
|
479
|
-
name: z.string().min(1).max(100).trim(),
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
export const updateUserSchema = z.object({
|
|
483
|
-
id: z.string(),
|
|
484
|
-
email: z.email().optional(),
|
|
485
|
-
name: z.string().min(1).max(100).trim().optional(),
|
|
486
|
-
})
|
|
487
|
-
|
|
488
|
-
export type CreateUserInput = z.infer<typeof createUserSchema>
|
|
489
|
-
export type UpdateUserInput = z.infer<typeof updateUserSchema>
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
### Queries 파일
|
|
493
|
-
|
|
494
|
-
```typescript
|
|
495
|
-
// services/user/queries.ts
|
|
496
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
497
|
-
import { prisma } from '@/database/prisma'
|
|
498
|
-
|
|
499
|
-
export const getUsers = createServerFn({ method: 'GET' })
|
|
500
|
-
.handler(async () => {
|
|
501
|
-
return prisma.user.findMany({
|
|
502
|
-
orderBy: { createdAt: 'desc' },
|
|
503
|
-
})
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
export const getUserById = createServerFn({ method: 'GET' })
|
|
507
|
-
.handler(async ({ data: id }: { data: string }) => {
|
|
508
|
-
const user = await prisma.user.findUnique({ where: { id } })
|
|
509
|
-
if (!user) throw new Error('User not found')
|
|
510
|
-
return user
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
export const getUserByEmail = createServerFn({ method: 'GET' })
|
|
514
|
-
.handler(async ({ data: email }: { data: string }) => {
|
|
515
|
-
return prisma.user.findUnique({ where: { email } })
|
|
516
|
-
})
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### Mutations 파일
|
|
520
|
-
|
|
521
|
-
```typescript
|
|
522
|
-
// services/user/mutations.ts
|
|
523
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
524
|
-
import { prisma } from '@/database/prisma'
|
|
525
|
-
import { createUserSchema, updateUserSchema } from './schemas'
|
|
526
|
-
|
|
527
|
-
export const createUser = createServerFn({ method: 'POST' })
|
|
528
|
-
.inputValidator(createUserSchema)
|
|
529
|
-
.handler(async ({ data }) => {
|
|
530
|
-
return prisma.user.create({ data })
|
|
531
|
-
})
|
|
532
|
-
|
|
533
|
-
export const updateUser = createServerFn({ method: 'POST' })
|
|
534
|
-
.inputValidator(updateUserSchema)
|
|
535
|
-
.handler(async ({ data }) => {
|
|
536
|
-
const { id, ...updateData } = data
|
|
537
|
-
return prisma.user.update({ where: { id }, data: updateData })
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
export const deleteUser = createServerFn({ method: 'POST' })
|
|
541
|
-
.handler(async ({ data: id }: { data: string }) => {
|
|
542
|
-
return prisma.user.delete({ where: { id } })
|
|
543
|
-
})
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
### Service 진입점
|
|
547
|
-
|
|
548
|
-
```typescript
|
|
549
|
-
// services/user/index.ts
|
|
550
|
-
export * from './schemas'
|
|
551
|
-
export * from './queries'
|
|
552
|
-
export * from './mutations'
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
## Common Hook Templates
|
|
556
|
-
|
|
557
|
-
### Auth Hook
|
|
558
|
-
|
|
559
|
-
```typescript
|
|
560
|
-
// hooks/use-auth.ts
|
|
561
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
562
|
-
import { getCurrentUser, login, logout } from '@/services/auth'
|
|
563
|
-
import type { User, LoginInput } from '@/types'
|
|
564
|
-
|
|
565
|
-
interface UseAuthReturn {
|
|
566
|
-
user: User | null | undefined
|
|
567
|
-
isLoading: boolean
|
|
568
|
-
isAuthenticated: boolean
|
|
569
|
-
login: (data: LoginInput) => void
|
|
570
|
-
logout: () => void
|
|
571
|
-
isLoggingIn: boolean
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
export const useAuth = (): UseAuthReturn => {
|
|
575
|
-
const queryClient = useQueryClient()
|
|
576
|
-
|
|
577
|
-
const { data: user, isLoading } = useQuery({
|
|
578
|
-
queryKey: ['auth', 'user'],
|
|
579
|
-
queryFn: () => getCurrentUser(),
|
|
580
|
-
retry: false,
|
|
581
|
-
})
|
|
582
|
-
|
|
583
|
-
const loginMutation = useMutation({
|
|
584
|
-
mutationFn: login,
|
|
585
|
-
onSuccess: () => {
|
|
586
|
-
queryClient.invalidateQueries({ queryKey: ['auth'] })
|
|
587
|
-
},
|
|
588
|
-
})
|
|
589
|
-
|
|
590
|
-
const logoutMutation = useMutation({
|
|
591
|
-
mutationFn: logout,
|
|
592
|
-
onSuccess: () => {
|
|
593
|
-
queryClient.setQueryData(['auth', 'user'], null)
|
|
594
|
-
queryClient.invalidateQueries({ queryKey: ['auth'] })
|
|
595
|
-
},
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
return {
|
|
599
|
-
user,
|
|
600
|
-
isLoading,
|
|
601
|
-
isAuthenticated: !!user,
|
|
602
|
-
login: loginMutation.mutate,
|
|
603
|
-
logout: logoutMutation.mutate,
|
|
604
|
-
isLoggingIn: loginMutation.isPending,
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
## Database Schema Template
|
|
610
|
-
|
|
611
|
-
```prisma
|
|
612
|
-
// prisma/schema.prisma
|
|
613
|
-
datasource db {
|
|
614
|
-
provider = "postgresql"
|
|
615
|
-
url = env("DATABASE_URL")
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
generator client {
|
|
619
|
-
provider = "prisma-client"
|
|
620
|
-
output = "../generated/prisma"
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
model User {
|
|
624
|
-
id String @id @default(cuid())
|
|
625
|
-
email String @unique
|
|
626
|
-
name String?
|
|
627
|
-
role Role @default(USER)
|
|
628
|
-
posts Post[]
|
|
629
|
-
createdAt DateTime @default(now())
|
|
630
|
-
updatedAt DateTime @updatedAt
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
model Post {
|
|
634
|
-
id String @id @default(cuid())
|
|
635
|
-
title String
|
|
636
|
-
content String?
|
|
637
|
-
published Boolean @default(false)
|
|
638
|
-
author User @relation(fields: [authorId], references: [id])
|
|
639
|
-
authorId String
|
|
640
|
-
createdAt DateTime @default(now())
|
|
641
|
-
updatedAt DateTime @updatedAt
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
enum Role {
|
|
645
|
-
USER
|
|
646
|
-
ADMIN
|
|
647
|
-
}
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
## Configuration Templates
|
|
651
|
-
|
|
652
|
-
### Tailwind Config
|
|
653
|
-
|
|
654
|
-
```typescript
|
|
655
|
-
// tailwind.config.ts
|
|
656
|
-
import type { Config } from 'tailwindcss'
|
|
657
|
-
|
|
658
|
-
export default {
|
|
659
|
-
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
|
660
|
-
theme: {
|
|
661
|
-
extend: {
|
|
662
|
-
colors: {
|
|
663
|
-
primary: {
|
|
664
|
-
50: '#eff6ff',
|
|
665
|
-
500: '#3b82f6',
|
|
666
|
-
600: '#2563eb',
|
|
667
|
-
700: '#1d4ed8',
|
|
668
|
-
},
|
|
669
|
-
},
|
|
670
|
-
},
|
|
671
|
-
},
|
|
672
|
-
plugins: [],
|
|
673
|
-
} satisfies Config
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
### TypeScript Config
|
|
677
|
-
|
|
678
|
-
```json
|
|
679
|
-
{
|
|
680
|
-
"compilerOptions": {
|
|
681
|
-
"target": "ES2022",
|
|
682
|
-
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
683
|
-
"module": "ESNext",
|
|
684
|
-
"moduleResolution": "bundler",
|
|
685
|
-
"jsx": "react-jsx",
|
|
686
|
-
"strict": true,
|
|
687
|
-
"noUnusedLocals": true,
|
|
688
|
-
"noUnusedParameters": true,
|
|
689
|
-
"paths": {
|
|
690
|
-
"@/*": ["./src/*"]
|
|
691
|
-
}
|
|
692
|
-
},
|
|
693
|
-
"include": ["src/**/*", "*.config.ts"]
|
|
694
|
-
}
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
### Environment Schema
|
|
698
|
-
|
|
699
|
-
```typescript
|
|
700
|
-
// lib/env.ts
|
|
701
|
-
import { z } from 'zod'
|
|
702
|
-
|
|
703
|
-
const envSchema = z.object({
|
|
704
|
-
NODE_ENV: z.enum(['development', 'production', 'test']),
|
|
705
|
-
DATABASE_URL: z.url(),
|
|
706
|
-
API_SECRET: z.string().min(32),
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
export const env = envSchema.parse(process.env)
|
|
710
|
-
```
|