@kood/claude-code 0.1.7 → 0.1.10

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.
Files changed (49) hide show
  1. package/dist/index.js +137 -3
  2. package/package.json +8 -2
  3. package/templates/hono/CLAUDE.md +53 -326
  4. package/templates/hono/docs/architecture/architecture.md +93 -747
  5. package/templates/hono/docs/deployment/cloudflare.md +59 -513
  6. package/templates/hono/docs/deployment/docker.md +41 -356
  7. package/templates/hono/docs/deployment/index.md +49 -190
  8. package/templates/hono/docs/deployment/railway.md +36 -306
  9. package/templates/hono/docs/deployment/vercel.md +49 -434
  10. package/templates/hono/docs/library/ai-sdk/index.md +53 -290
  11. package/templates/hono/docs/library/ai-sdk/openrouter.md +19 -387
  12. package/templates/hono/docs/library/ai-sdk/providers.md +28 -394
  13. package/templates/hono/docs/library/ai-sdk/streaming.md +52 -353
  14. package/templates/hono/docs/library/ai-sdk/structured-output.md +63 -395
  15. package/templates/hono/docs/library/ai-sdk/tools.md +62 -431
  16. package/templates/hono/docs/library/hono/env-setup.md +24 -313
  17. package/templates/hono/docs/library/hono/error-handling.md +34 -295
  18. package/templates/hono/docs/library/hono/index.md +24 -122
  19. package/templates/hono/docs/library/hono/middleware.md +21 -188
  20. package/templates/hono/docs/library/hono/rpc.md +40 -341
  21. package/templates/hono/docs/library/hono/validation.md +35 -195
  22. package/templates/hono/docs/library/pino/index.md +42 -333
  23. package/templates/hono/docs/library/prisma/cloudflare-d1.md +64 -367
  24. package/templates/hono/docs/library/prisma/config.md +19 -260
  25. package/templates/hono/docs/library/prisma/index.md +64 -320
  26. package/templates/hono/docs/library/zod/index.md +53 -257
  27. package/templates/npx/CLAUDE.md +58 -276
  28. package/templates/npx/docs/references/patterns.md +160 -0
  29. package/templates/tanstack-start/CLAUDE.md +0 -4
  30. package/templates/tanstack-start/docs/architecture/architecture.md +44 -589
  31. package/templates/tanstack-start/docs/design/index.md +119 -12
  32. package/templates/tanstack-start/docs/guides/conventions.md +103 -0
  33. package/templates/tanstack-start/docs/guides/env-setup.md +34 -340
  34. package/templates/tanstack-start/docs/guides/getting-started.md +22 -209
  35. package/templates/tanstack-start/docs/guides/hooks.md +166 -0
  36. package/templates/tanstack-start/docs/guides/routes.md +166 -0
  37. package/templates/tanstack-start/docs/guides/services.md +143 -0
  38. package/templates/tanstack-start/docs/library/tanstack-query/index.md +18 -2
  39. package/templates/tanstack-start/docs/library/zod/index.md +16 -1
  40. package/templates/tanstack-start/docs/design/accessibility.md +0 -163
  41. package/templates/tanstack-start/docs/design/color.md +0 -93
  42. package/templates/tanstack-start/docs/design/spacing.md +0 -122
  43. package/templates/tanstack-start/docs/design/typography.md +0 -80
  44. package/templates/tanstack-start/docs/guides/best-practices.md +0 -950
  45. package/templates/tanstack-start/docs/guides/husky-lint-staged.md +0 -303
  46. package/templates/tanstack-start/docs/guides/prettier.md +0 -189
  47. package/templates/tanstack-start/docs/guides/project-templates.md +0 -710
  48. package/templates/tanstack-start/docs/library/tanstack-query/setup.md +0 -48
  49. package/templates/tanstack-start/docs/library/zod/basic-types.md +0 -74
@@ -1,950 +0,0 @@
1
- # Best Practices
2
-
3
- TanStack Start 애플리케이션 개발을 위한 모범 사례 가이드입니다.
4
-
5
- ## File Naming Convention
6
-
7
- **모든 파일은 kebab-case**:
8
-
9
- ```
10
- ✅ user-profile.tsx
11
- ✅ auth-service.ts
12
- ✅ use-user-filter.ts
13
- ✅ user-list-section.tsx
14
-
15
- ❌ UserProfile.tsx
16
- ❌ authService.ts
17
- ❌ useUserFilter.ts
18
- ```
19
-
20
- ## Route Folder Structure
21
-
22
- ### 기본 구조
23
-
24
- ```
25
- routes/<route-name>/
26
- ├── index.tsx # 페이지 컴포넌트
27
- ├── route.tsx # route 설정 (필요시)
28
- ├── -components/ # 페이지 전용 컴포넌트
29
- │ ├── user-card.tsx
30
- │ └── user-form.tsx
31
- ├── -sections/ # 섹션 분리 (복잡한 경우)
32
- │ ├── user-list-section.tsx
33
- │ └── user-filter-section.tsx
34
- └── -hooks/ # 페이지 전용 훅
35
- ├── use-users.ts
36
- └── use-user-filter.ts
37
- ```
38
-
39
- ### TanStack Start `-` 접두사
40
-
41
- `-` 접두사가 있는 폴더는 라우트에서 제외됩니다:
42
-
43
- ```
44
- routes/users/
45
- ├── index.tsx # /users ✅ 라우트
46
- ├── $id.tsx # /users/:id ✅ 라우트
47
- ├── -components/ # ❌ 라우트 아님
48
- ├── -sections/ # ❌ 라우트 아님
49
- └── -hooks/ # ❌ 라우트 아님
50
- ```
51
-
52
- ## Project Organization
53
-
54
- ### 전체 폴더 구조
55
-
56
- ```
57
- src/
58
- ├── routes/ # 파일 기반 라우팅
59
- │ ├── __root.tsx
60
- │ ├── index.tsx
61
- │ └── users/
62
- │ ├── index.tsx
63
- │ ├── -components/
64
- │ ├── -sections/
65
- │ └── -hooks/
66
- ├── components/ # 공통 컴포넌트
67
- │ └── ui/
68
- │ ├── button.tsx
69
- │ ├── input.tsx
70
- │ └── modal.tsx
71
- ├── database/ # 데이터베이스 관련
72
- │ ├── prisma.ts # Prisma Client 인스턴스
73
- │ └── seed.ts # 시드 데이터 (필요시)
74
- ├── services/ # 도메인별 SDK/서비스 레이어
75
- │ ├── user/
76
- │ │ ├── index.ts # 진입점 (re-export)
77
- │ │ ├── schemas.ts # Zod 스키마
78
- │ │ ├── queries.ts # GET 요청 (읽기)
79
- │ │ └── mutations.ts # POST 요청 (쓰기)
80
- │ ├── auth/
81
- │ │ ├── index.ts
82
- │ │ ├── schemas.ts
83
- │ │ ├── queries.ts
84
- │ │ └── mutations.ts
85
- │ └── post/
86
- │ ├── index.ts
87
- │ ├── schemas.ts
88
- │ ├── queries.ts
89
- │ └── mutations.ts
90
- ├── lib/ # 공통 유틸리티
91
- │ ├── query-client.ts
92
- │ ├── utils.ts
93
- │ └── constants.ts
94
- ├── hooks/ # 공통 훅
95
- │ ├── use-auth.ts
96
- │ └── use-media-query.ts
97
- └── types/ # 타입 정의
98
- └── index.ts
99
- ```
100
-
101
- ### Database 폴더 구조
102
-
103
- ```
104
- database/
105
- ├── prisma.ts # Prisma Client 싱글톤
106
- └── seed.ts # 시드 스크립트 (선택)
107
- ```
108
-
109
- ```typescript
110
- // database/prisma.ts
111
- import { PrismaClient } from '../../generated/prisma'
112
-
113
- const globalForPrisma = globalThis as unknown as {
114
- prisma: PrismaClient | undefined
115
- }
116
-
117
- export const prisma =
118
- globalForPrisma.prisma ??
119
- new PrismaClient({
120
- log: process.env.NODE_ENV === 'development' ? ['query'] : [],
121
- })
122
-
123
- if (process.env.NODE_ENV !== 'production') {
124
- globalForPrisma.prisma = prisma
125
- }
126
- ```
127
-
128
- ### Services 폴더 구조
129
-
130
- ```
131
- services/
132
- ├── user/ # User 도메인
133
- │ ├── index.ts # 진입점 (re-export)
134
- │ ├── schemas.ts # Zod 스키마
135
- │ ├── queries.ts # GET 요청 (읽기)
136
- │ └── mutations.ts # POST 요청 (쓰기)
137
- ├── auth/ # Auth 도메인
138
- │ ├── index.ts
139
- │ ├── schemas.ts
140
- │ ├── queries.ts
141
- │ └── mutations.ts
142
- └── post/ # Post 도메인
143
- ├── index.ts
144
- ├── schemas.ts
145
- ├── queries.ts
146
- └── mutations.ts
147
- ```
148
-
149
- ## 코드 작성 규칙
150
-
151
- ### UTF-8 인코딩 유지
152
-
153
- 모든 한글 텍스트는 UTF-8 인코딩이 깨지지 않도록 작성합니다.
154
-
155
- ### 한글 주석 작성 규칙
156
-
157
- **묶음 단위로 한글 주석을 작성합니다** (너무 세세하게 X)
158
-
159
- ```typescript
160
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
161
- // 사용자 관련 상태
162
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
163
- const [user, setUser] = useState<User | null>(null)
164
- const [isLoading, setIsLoading] = useState(false)
165
-
166
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
167
- // 데이터 조회
168
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
169
- const { data: users } = useQuery({
170
- queryKey: ['users'],
171
- queryFn: () => getUsers(),
172
- })
173
-
174
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
175
- // 이벤트 핸들러
176
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
177
- const handleSubmit = () => { /* ... */ }
178
- const handleDelete = () => { /* ... */ }
179
- ```
180
-
181
- ### ❌ 너무 세세한 주석 (금지)
182
-
183
- ```typescript
184
- // ❌ 이렇게 하지 마세요
185
- const [user, setUser] = useState(null) // 사용자 상태
186
- const [isLoading, setIsLoading] = useState(false) // 로딩 상태
187
- const [error, setError] = useState(null) // 에러 상태
188
- ```
189
-
190
- ---
191
-
192
- ## TypeScript Standards
193
-
194
- ### Use `const` for Functions
195
-
196
- ```typescript
197
- // ✅ Preferred
198
- const getUserById = async (id: string): Promise<User> => {
199
- return prisma.user.findUnique({ where: { id } })
200
- }
201
-
202
- // ❌ Avoid
203
- function getUserById(id: string): Promise<User> {
204
- return prisma.user.findUnique({ where: { id } })
205
- }
206
- ```
207
-
208
- ### Explicit Return Types
209
-
210
- ```typescript
211
- // ✅ Always specify return types
212
- const formatDate = (date: Date): string => {
213
- return date.toISOString()
214
- }
215
-
216
- // ✅ Component return types
217
- const UserCard = ({ user }: UserCardProps): JSX.Element => {
218
- return <div>{user.name}</div>
219
- }
220
- ```
221
-
222
- ### No `any` Types
223
-
224
- ```typescript
225
- // ✅ Use unknown
226
- const parseJSON = (data: string): unknown => {
227
- return JSON.parse(data)
228
- }
229
-
230
- // ❌ Never use any
231
- const parseJSON = (data: string): any => {
232
- return JSON.parse(data)
233
- }
234
- ```
235
-
236
- ### Import Order
237
-
238
- ```typescript
239
- // 1. External libraries
240
- import { createFileRoute } from '@tanstack/react-router'
241
- import { useQuery } from '@tanstack/react-query'
242
-
243
- // 2. Internal packages
244
- import { Button } from '@/components/ui/button'
245
- import { prisma } from '@/lib/prisma'
246
-
247
- // 3. Relative imports (route-specific)
248
- import { UserCard } from './-components/user-card'
249
- import { useUsers } from './-hooks/use-users'
250
-
251
- // 4. Type imports
252
- import type { User } from '@/types'
253
- ```
254
-
255
- ## Route Patterns
256
-
257
- ### Basic Route with Hook
258
-
259
- ```tsx
260
- // routes/users/index.tsx
261
- import { createFileRoute } from '@tanstack/react-router'
262
- import { UserListSection } from './-sections/user-list-section'
263
- import { UserFilterSection } from './-sections/user-filter-section'
264
-
265
- export const Route = createFileRoute('/users/')({
266
- component: UsersPage,
267
- })
268
-
269
- const UsersPage = (): JSX.Element => {
270
- return (
271
- <div className="container mx-auto p-4">
272
- <h1 className="text-2xl font-bold mb-4">Users</h1>
273
- <UserFilterSection />
274
- <UserListSection />
275
- </div>
276
- )
277
- }
278
- ```
279
-
280
- ### Custom Hook 작성 규칙
281
-
282
- **Purpose**: 페이지 또는 섹션의 **모든 로직, 상태, 라이프사이클**을 중앙화합니다.
283
-
284
- - 페이지 훅: 페이지 전체 로직 담당
285
- - 섹션 훅: 해당 섹션의 로직만 담당 (섹션으로 분리한 경우)
286
-
287
- ### ⚠️ 필수: Custom Hook 내부 순서
288
-
289
- 훅 내부 코드는 **반드시 아래 순서**를 따릅니다:
290
-
291
- ```
292
- 1. State (useState, zustand store)
293
- 2. Global Hooks (useParams, useNavigate, useQueryClient 등)
294
- 3. React Query (useQuery → useMutation 순서)
295
- 4. Event Handlers & Functions
296
- 5. useMemo
297
- 6. useEffect
298
- ```
299
-
300
- ### ✅ 올바른 Custom Hook 예시
301
-
302
- ```typescript
303
- // routes/users/-hooks/use-users.ts
304
- import { useState, useMemo, useEffect, useCallback } from 'react'
305
- import { useParams, useNavigate } from '@tanstack/react-router'
306
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
307
- import { useAuthStore } from '@/stores/auth'
308
- import { getUsers, createUser, deleteUser } from '@/services/user'
309
- import type { User } from '@/types'
310
-
311
- interface UseUsersReturn {
312
- users: User[] | undefined
313
- filteredUsers: User[]
314
- isLoading: boolean
315
- error: Error | null
316
- search: string
317
- setSearch: (value: string) => void
318
- handleCreate: (data: { email: string; name: string }) => void
319
- handleDelete: (id: string) => void
320
- isCreating: boolean
321
- isDeleting: boolean
322
- }
323
-
324
- export const useUsers = (): UseUsersReturn => {
325
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
326
- // 1. State (useState, zustand store)
327
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
328
- const [search, setSearch] = useState('')
329
- const { user: currentUser } = useAuthStore()
330
-
331
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
332
- // 2. Global Hooks
333
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
334
- const params = useParams({ from: '/users/$id' })
335
- const navigate = useNavigate()
336
- const queryClient = useQueryClient()
337
-
338
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
339
- // 3. React Query (useQuery → useMutation)
340
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
341
- const { data: users, isLoading, error } = useQuery({
342
- queryKey: ['users'],
343
- queryFn: () => getUsers(),
344
- })
345
-
346
- const createMutation = useMutation({
347
- mutationFn: createUser,
348
- onSuccess: () => {
349
- queryClient.invalidateQueries({ queryKey: ['users'] })
350
- },
351
- })
352
-
353
- const deleteMutation = useMutation({
354
- mutationFn: deleteUser,
355
- onSuccess: () => {
356
- queryClient.invalidateQueries({ queryKey: ['users'] })
357
- },
358
- })
359
-
360
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
361
- // 4. Event Handlers & Functions
362
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
363
- const handleCreate = useCallback(
364
- (data: { email: string; name: string }) => {
365
- createMutation.mutate({ data })
366
- },
367
- [createMutation]
368
- )
369
-
370
- const handleDelete = useCallback(
371
- (id: string) => {
372
- deleteMutation.mutate({ data: id })
373
- },
374
- [deleteMutation]
375
- )
376
-
377
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
378
- // 5. useMemo
379
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
380
- const filteredUsers = useMemo(() => {
381
- if (!users) return []
382
- if (!search) return users
383
- return users.filter((user) =>
384
- user.name.toLowerCase().includes(search.toLowerCase())
385
- )
386
- }, [users, search])
387
-
388
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
389
- // 6. useEffect
390
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
391
- useEffect(() => {
392
- if (!currentUser) {
393
- navigate({ to: '/login' })
394
- }
395
- }, [currentUser, navigate])
396
-
397
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
398
- // Return
399
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
400
- return {
401
- users,
402
- filteredUsers,
403
- isLoading,
404
- error,
405
- search,
406
- setSearch,
407
- handleCreate,
408
- handleDelete,
409
- isCreating: createMutation.isPending,
410
- isDeleting: deleteMutation.isPending,
411
- }
412
- }
413
- ```
414
-
415
- ### ❌ 잘못된 순서 (금지)
416
-
417
- ```typescript
418
- // ❌ 순서가 뒤섞인 잘못된 예시
419
- export const useBadHook = () => {
420
- const queryClient = useQueryClient() // ❌ Global Hook이 먼저
421
-
422
- useEffect(() => { /* ... */ }, []) // ❌ useEffect가 중간에
423
-
424
- const [state, setState] = useState() // ❌ State가 나중에
425
-
426
- const { data } = useQuery({ /* ... */ }) // ❌ Query가 Effect 다음에
427
-
428
- const computed = useMemo(() => {}, []) // ❌ useMemo 위치 잘못됨
429
- }
430
- ```
431
-
432
- ### Page Hook (간단한 예시)
433
-
434
- ```typescript
435
- // routes/users/-hooks/use-users.ts
436
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
437
- import { getUsers, createUser, deleteUser } from '@/services/user'
438
- import type { User } from '@/types'
439
-
440
- interface UseUsersReturn {
441
- users: User[] | undefined
442
- isLoading: boolean
443
- error: Error | null
444
- createUser: (data: { email: string; name: string }) => void
445
- deleteUser: (id: string) => void
446
- isCreating: boolean
447
- isDeleting: boolean
448
- }
449
-
450
- export const useUsers = (): UseUsersReturn => {
451
- // 2. Global Hooks
452
- const queryClient = useQueryClient()
453
-
454
- // 3. React Query (useQuery → useMutation)
455
- const { data: users, isLoading, error } = useQuery({
456
- queryKey: ['users'],
457
- queryFn: () => getUsers(),
458
- })
459
-
460
- const createMutation = useMutation({
461
- mutationFn: createUser,
462
- onSuccess: () => {
463
- queryClient.invalidateQueries({ queryKey: ['users'] })
464
- },
465
- })
466
-
467
- const deleteMutation = useMutation({
468
- mutationFn: deleteUser,
469
- onSuccess: () => {
470
- queryClient.invalidateQueries({ queryKey: ['users'] })
471
- },
472
- })
473
-
474
- return {
475
- users,
476
- isLoading,
477
- error,
478
- createUser: createMutation.mutate,
479
- deleteUser: deleteMutation.mutate,
480
- isCreating: createMutation.isPending,
481
- isDeleting: deleteMutation.isPending,
482
- }
483
- }
484
- ```
485
-
486
- ### Section with Hook
487
-
488
- ```tsx
489
- // routes/users/-sections/user-list-section.tsx
490
- import { useUsers } from '../-hooks/use-users'
491
- import { UserCard } from '../-components/user-card'
492
-
493
- export const UserListSection = (): JSX.Element => {
494
- const { users, isLoading, error, deleteUser, isDeleting } = useUsers()
495
-
496
- if (isLoading) {
497
- return <div className="text-center py-8">Loading...</div>
498
- }
499
-
500
- if (error) {
501
- return <div className="text-red-600 py-8">Error: {error.message}</div>
502
- }
503
-
504
- if (!users?.length) {
505
- return <div className="text-gray-500 py-8">No users found</div>
506
- }
507
-
508
- return (
509
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
510
- {users.map((user) => (
511
- <UserCard
512
- key={user.id}
513
- user={user}
514
- onDelete={deleteUser}
515
- isDeleting={isDeleting}
516
- />
517
- ))}
518
- </div>
519
- )
520
- }
521
- ```
522
-
523
- ### Filter Section with Hook
524
-
525
- ```tsx
526
- // routes/users/-sections/user-filter-section.tsx
527
- import { useUserFilter } from '../-hooks/use-user-filter'
528
- import { Input } from '@/components/ui/input'
529
- import { Button } from '@/components/ui/button'
530
-
531
- export const UserFilterSection = (): JSX.Element => {
532
- const { search, setSearch, role, setRole, clearFilters } = useUserFilter()
533
-
534
- return (
535
- <div className="flex gap-4 mb-6">
536
- <Input
537
- placeholder="Search users..."
538
- value={search}
539
- onChange={(e) => setSearch(e.target.value)}
540
- className="max-w-xs"
541
- />
542
- <select
543
- value={role}
544
- onChange={(e) => setRole(e.target.value)}
545
- className="border rounded px-3 py-2"
546
- >
547
- <option value="">All Roles</option>
548
- <option value="USER">User</option>
549
- <option value="ADMIN">Admin</option>
550
- </select>
551
- <Button variant="outline" onClick={clearFilters}>
552
- Clear
553
- </Button>
554
- </div>
555
- )
556
- }
557
- ```
558
-
559
- ### Filter Hook
560
-
561
- ```typescript
562
- // routes/users/-hooks/use-user-filter.ts
563
- import { useState, useCallback } from 'react'
564
-
565
- interface UseUserFilterReturn {
566
- search: string
567
- setSearch: (value: string) => void
568
- role: string
569
- setRole: (value: string) => void
570
- clearFilters: () => void
571
- }
572
-
573
- export const useUserFilter = (): UseUserFilterReturn => {
574
- const [search, setSearch] = useState('')
575
- const [role, setRole] = useState('')
576
-
577
- const clearFilters = useCallback(() => {
578
- setSearch('')
579
- setRole('')
580
- }, [])
581
-
582
- return {
583
- search,
584
- setSearch,
585
- role,
586
- setRole,
587
- clearFilters,
588
- }
589
- }
590
- ```
591
-
592
- ### Page Component
593
-
594
- ```tsx
595
- // routes/users/-components/user-card.tsx
596
- import type { User } from '@/types'
597
- import { Button } from '@/components/ui/button'
598
-
599
- interface UserCardProps {
600
- user: User
601
- onDelete?: (id: string) => void
602
- isDeleting?: boolean
603
- }
604
-
605
- export const UserCard = ({
606
- user,
607
- onDelete,
608
- isDeleting,
609
- }: UserCardProps): JSX.Element => {
610
- return (
611
- <div className="rounded-lg border p-4 shadow-sm">
612
- <div className="flex items-center gap-4">
613
- <div className="h-12 w-12 rounded-full bg-gray-200" />
614
- <div>
615
- <h3 className="font-semibold">{user.name}</h3>
616
- <p className="text-sm text-gray-600">{user.email}</p>
617
- </div>
618
- </div>
619
-
620
- {onDelete && (
621
- <div className="mt-4">
622
- <Button
623
- variant="outline"
624
- size="sm"
625
- onClick={() => onDelete(user.id)}
626
- disabled={isDeleting}
627
- >
628
- {isDeleting ? 'Deleting...' : 'Delete'}
629
- </Button>
630
- </div>
631
- )}
632
- </div>
633
- )
634
- }
635
- ```
636
-
637
- ## Service Layer
638
-
639
- ### Service 폴더 구조
640
-
641
- 도메인별로 폴더를 분리하고, 파일을 용도에 따라 구분합니다:
642
-
643
- ```
644
- services/
645
- ├── user/
646
- │ ├── index.ts # 진입점 (re-export)
647
- │ ├── schemas.ts # Zod 스키마
648
- │ ├── queries.ts # GET 요청 (읽기)
649
- │ └── mutations.ts # POST 요청 (쓰기)
650
- ├── auth/
651
- │ ├── index.ts
652
- │ ├── schemas.ts
653
- │ ├── queries.ts
654
- │ └── mutations.ts
655
- └── post/
656
- ├── index.ts
657
- ├── schemas.ts
658
- ├── queries.ts
659
- └── mutations.ts
660
- ```
661
-
662
- ### Schemas 파일
663
-
664
- ```typescript
665
- // services/user/schemas.ts
666
- import { z } from 'zod'
667
-
668
- export const createUserSchema = z.object({
669
- email: z.email(),
670
- name: z.string().min(1).max(100).trim(),
671
- })
672
-
673
- export const updateUserSchema = z.object({
674
- id: z.string(),
675
- email: z.email().optional(),
676
- name: z.string().min(1).max(100).trim().optional(),
677
- })
678
-
679
- export type CreateUserInput = z.infer<typeof createUserSchema>
680
- export type UpdateUserInput = z.infer<typeof updateUserSchema>
681
- ```
682
-
683
- ### Queries 파일
684
-
685
- ```typescript
686
- // services/user/queries.ts
687
- import { createServerFn } from '@tanstack/react-start'
688
- import { prisma } from '@/database/prisma'
689
-
690
- export const getUsers = createServerFn({ method: 'GET' })
691
- .handler(async () => {
692
- return prisma.user.findMany({
693
- orderBy: { createdAt: 'desc' },
694
- })
695
- })
696
-
697
- export const getUserById = createServerFn({ method: 'GET' })
698
- .handler(async ({ data: id }: { data: string }) => {
699
- const user = await prisma.user.findUnique({ where: { id } })
700
- if (!user) throw new Error('User not found')
701
- return user
702
- })
703
-
704
- export const getUserByEmail = createServerFn({ method: 'GET' })
705
- .handler(async ({ data: email }: { data: string }) => {
706
- return prisma.user.findUnique({ where: { email } })
707
- })
708
- ```
709
-
710
- ### Mutations 파일
711
-
712
- ```typescript
713
- // services/user/mutations.ts
714
- import { createServerFn } from '@tanstack/react-start'
715
- import { prisma } from '@/database/prisma'
716
- import { createUserSchema, updateUserSchema } from './schemas'
717
-
718
- export const createUser = createServerFn({ method: 'POST' })
719
- .inputValidator(createUserSchema)
720
- .handler(async ({ data }) => {
721
- return prisma.user.create({ data })
722
- })
723
-
724
- export const updateUser = createServerFn({ method: 'POST' })
725
- .inputValidator(updateUserSchema)
726
- .handler(async ({ data }) => {
727
- const { id, ...updateData } = data
728
- return prisma.user.update({ where: { id }, data: updateData })
729
- })
730
-
731
- export const deleteUser = createServerFn({ method: 'POST' })
732
- .handler(async ({ data: id }: { data: string }) => {
733
- return prisma.user.delete({ where: { id } })
734
- })
735
- ```
736
-
737
- ### Service 진입점 파일
738
-
739
- ```typescript
740
- // services/user/index.ts
741
- export * from './schemas'
742
- export * from './queries'
743
- export * from './mutations'
744
- ```
745
-
746
- ### 사용 예시
747
-
748
- ```typescript
749
- // routes/users/-hooks/use-users.ts
750
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
751
- import { getUsers, createUser, deleteUser } from '@/services/user'
752
- ```
753
-
754
- ## Common UI Components
755
-
756
- ### Button Component
757
-
758
- ```tsx
759
- // components/ui/button.tsx
760
- interface ButtonProps {
761
- children: React.ReactNode
762
- variant?: 'primary' | 'secondary' | 'outline'
763
- size?: 'sm' | 'md' | 'lg'
764
- onClick?: () => void
765
- disabled?: boolean
766
- type?: 'button' | 'submit' | 'reset'
767
- }
768
-
769
- export const Button = ({
770
- children,
771
- variant = 'primary',
772
- size = 'md',
773
- onClick,
774
- disabled,
775
- type = 'button',
776
- }: ButtonProps): JSX.Element => {
777
- const baseStyles = 'rounded font-medium transition-colors disabled:opacity-50'
778
-
779
- const variants = {
780
- primary: 'bg-blue-600 text-white hover:bg-blue-700',
781
- secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
782
- outline: 'border border-gray-300 hover:bg-gray-50',
783
- }
784
-
785
- const sizes = {
786
- sm: 'px-3 py-1.5 text-sm',
787
- md: 'px-4 py-2',
788
- lg: 'px-6 py-3 text-lg',
789
- }
790
-
791
- return (
792
- <button
793
- type={type}
794
- onClick={onClick}
795
- disabled={disabled}
796
- className={`${baseStyles} ${variants[variant]} ${sizes[size]}`}
797
- >
798
- {children}
799
- </button>
800
- )
801
- }
802
- ```
803
-
804
- ### Input Component
805
-
806
- ```tsx
807
- // components/ui/input.tsx
808
- interface InputProps {
809
- placeholder?: string
810
- value: string
811
- onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
812
- type?: 'text' | 'email' | 'password'
813
- className?: string
814
- disabled?: boolean
815
- }
816
-
817
- export const Input = ({
818
- placeholder,
819
- value,
820
- onChange,
821
- type = 'text',
822
- className = '',
823
- disabled,
824
- }: InputProps): JSX.Element => {
825
- return (
826
- <input
827
- type={type}
828
- placeholder={placeholder}
829
- value={value}
830
- onChange={onChange}
831
- disabled={disabled}
832
- className={`border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 ${className}`}
833
- />
834
- )
835
- }
836
- ```
837
-
838
- ## Error Handling
839
-
840
- ### Custom Error Classes
841
-
842
- ```typescript
843
- // lib/errors.ts
844
- export class AppError extends Error {
845
- constructor(
846
- message: string,
847
- public statusCode: number = 500,
848
- public code: string = 'INTERNAL_ERROR'
849
- ) {
850
- super(message)
851
- this.name = 'AppError'
852
- }
853
- }
854
-
855
- export class NotFoundError extends AppError {
856
- constructor(resource: string) {
857
- super(`${resource} not found`, 404, 'NOT_FOUND')
858
- }
859
- }
860
-
861
- export class ValidationError extends AppError {
862
- constructor(message: string) {
863
- super(message, 400, 'VALIDATION_ERROR')
864
- }
865
- }
866
- ```
867
-
868
- ## Testing
869
-
870
- ### Test Structure
871
-
872
- ```typescript
873
- // __tests__/services/user-service.test.ts
874
- import { describe, it, expect, beforeEach, vi } from 'vitest'
875
-
876
- describe('UserService', () => {
877
- beforeEach(() => {
878
- vi.clearAllMocks()
879
- })
880
-
881
- describe('createUser', () => {
882
- it('creates a user with valid input', async () => {
883
- const input = { email: 'test@example.com', name: 'Test' }
884
- const result = await createUser(input)
885
-
886
- expect(result).toMatchObject({
887
- email: input.email,
888
- name: input.name,
889
- })
890
- })
891
-
892
- it('throws on invalid email', async () => {
893
- const input = { email: 'invalid', name: 'Test' }
894
-
895
- await expect(createUser(input)).rejects.toThrow()
896
- })
897
- })
898
- })
899
- ```
900
-
901
- ## Performance
902
-
903
- ### React Optimization
904
-
905
- ```tsx
906
- import { useMemo, useCallback, memo } from 'react'
907
-
908
- // Memoize expensive computations
909
- const sortedUsers = useMemo(
910
- () => users.sort((a, b) => a.name.localeCompare(b.name)),
911
- [users]
912
- )
913
-
914
- // Memoize callbacks
915
- const handleClick = useCallback(() => {
916
- setIsOpen(true)
917
- }, [])
918
-
919
- // Memoize components
920
- export const UserCard = memo(({ user }: { user: User }): JSX.Element => {
921
- return <div>{user.name}</div>
922
- })
923
- ```
924
-
925
- ## Security
926
-
927
- ### Environment Variables
928
-
929
- - `.env` 파일은 절대 커밋하지 않음
930
- - `.env.example` 제공
931
- - 시작 시 환경변수 검증
932
-
933
- ### Input Validation
934
-
935
- ```typescript
936
- import { z } from 'zod'
937
-
938
- // Zod v4 API
939
- const userInputSchema = z.object({
940
- name: z.string().min(1).max(100).trim(),
941
- email: z.email().toLowerCase(), // v4: z.email()
942
- })
943
-
944
- const envSchema = z.object({
945
- NODE_ENV: z.enum(['development', 'production', 'test']),
946
- DATABASE_URL: z.url(), // v4: z.url()
947
- })
948
-
949
- export const env = envSchema.parse(process.env)
950
- ```