@kood/claude-code 0.1.0

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 (78) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +297 -0
  3. package/package.json +47 -0
  4. package/templates/hono/CLAUDE.md +376 -0
  5. package/templates/hono/docs/deployment/cloudflare.md +328 -0
  6. package/templates/hono/docs/deployment/index.md +291 -0
  7. package/templates/hono/docs/git/index.md +180 -0
  8. package/templates/hono/docs/library/hono/error-handling.md +400 -0
  9. package/templates/hono/docs/library/hono/index.md +241 -0
  10. package/templates/hono/docs/library/hono/middleware.md +334 -0
  11. package/templates/hono/docs/library/hono/rpc.md +454 -0
  12. package/templates/hono/docs/library/hono/validation.md +328 -0
  13. package/templates/hono/docs/library/prisma/index.md +427 -0
  14. package/templates/hono/docs/library/zod/index.md +413 -0
  15. package/templates/hono/docs/mcp/context7.md +106 -0
  16. package/templates/hono/docs/mcp/index.md +94 -0
  17. package/templates/hono/docs/mcp/sequential-thinking.md +101 -0
  18. package/templates/hono/docs/mcp/sgrep.md +105 -0
  19. package/templates/hono/docs/skills/gemini-review/SKILL.md +220 -0
  20. package/templates/hono/docs/skills/gemini-review/references/checklists.md +136 -0
  21. package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +303 -0
  22. package/templates/tanstack-start/CLAUDE.md +279 -0
  23. package/templates/tanstack-start/docs/architecture/architecture.md +547 -0
  24. package/templates/tanstack-start/docs/deployment/cloudflare.md +346 -0
  25. package/templates/tanstack-start/docs/deployment/index.md +102 -0
  26. package/templates/tanstack-start/docs/deployment/nitro.md +211 -0
  27. package/templates/tanstack-start/docs/deployment/railway.md +364 -0
  28. package/templates/tanstack-start/docs/deployment/vercel.md +287 -0
  29. package/templates/tanstack-start/docs/design/accessibility.md +433 -0
  30. package/templates/tanstack-start/docs/design/color.md +235 -0
  31. package/templates/tanstack-start/docs/design/components.md +409 -0
  32. package/templates/tanstack-start/docs/design/index.md +107 -0
  33. package/templates/tanstack-start/docs/design/safe-area.md +317 -0
  34. package/templates/tanstack-start/docs/design/spacing.md +341 -0
  35. package/templates/tanstack-start/docs/design/tailwind-setup.md +470 -0
  36. package/templates/tanstack-start/docs/design/typography.md +324 -0
  37. package/templates/tanstack-start/docs/git/index.md +203 -0
  38. package/templates/tanstack-start/docs/guides/best-practices.md +753 -0
  39. package/templates/tanstack-start/docs/guides/getting-started.md +304 -0
  40. package/templates/tanstack-start/docs/guides/husky-lint-staged.md +303 -0
  41. package/templates/tanstack-start/docs/guides/prettier.md +189 -0
  42. package/templates/tanstack-start/docs/guides/project-templates.md +710 -0
  43. package/templates/tanstack-start/docs/library/better-auth/2fa.md +136 -0
  44. package/templates/tanstack-start/docs/library/better-auth/advanced.md +138 -0
  45. package/templates/tanstack-start/docs/library/better-auth/index.md +83 -0
  46. package/templates/tanstack-start/docs/library/better-auth/plugins.md +111 -0
  47. package/templates/tanstack-start/docs/library/better-auth/session.md +127 -0
  48. package/templates/tanstack-start/docs/library/better-auth/setup.md +123 -0
  49. package/templates/tanstack-start/docs/library/prisma/crud.md +218 -0
  50. package/templates/tanstack-start/docs/library/prisma/index.md +165 -0
  51. package/templates/tanstack-start/docs/library/prisma/relations.md +191 -0
  52. package/templates/tanstack-start/docs/library/prisma/schema.md +177 -0
  53. package/templates/tanstack-start/docs/library/prisma/setup.md +156 -0
  54. package/templates/tanstack-start/docs/library/prisma/transactions.md +140 -0
  55. package/templates/tanstack-start/docs/library/tanstack-query/index.md +146 -0
  56. package/templates/tanstack-start/docs/library/tanstack-query/invalidation.md +146 -0
  57. package/templates/tanstack-start/docs/library/tanstack-query/optimistic-updates.md +196 -0
  58. package/templates/tanstack-start/docs/library/tanstack-query/setup.md +110 -0
  59. package/templates/tanstack-start/docs/library/tanstack-query/use-mutation.md +170 -0
  60. package/templates/tanstack-start/docs/library/tanstack-query/use-query.md +173 -0
  61. package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +171 -0
  62. package/templates/tanstack-start/docs/library/tanstack-start/index.md +114 -0
  63. package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +142 -0
  64. package/templates/tanstack-start/docs/library/tanstack-start/routing.md +163 -0
  65. package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +128 -0
  66. package/templates/tanstack-start/docs/library/tanstack-start/setup.md +85 -0
  67. package/templates/tanstack-start/docs/library/zod/basic-types.md +186 -0
  68. package/templates/tanstack-start/docs/library/zod/complex-types.md +204 -0
  69. package/templates/tanstack-start/docs/library/zod/index.md +186 -0
  70. package/templates/tanstack-start/docs/library/zod/transforms.md +174 -0
  71. package/templates/tanstack-start/docs/library/zod/validation.md +208 -0
  72. package/templates/tanstack-start/docs/mcp/context7.md +204 -0
  73. package/templates/tanstack-start/docs/mcp/index.md +116 -0
  74. package/templates/tanstack-start/docs/mcp/sequential-thinking.md +180 -0
  75. package/templates/tanstack-start/docs/mcp/sgrep.md +174 -0
  76. package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +220 -0
  77. package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +150 -0
  78. package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +293 -0
@@ -0,0 +1,753 @@
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
+ ## TypeScript Standards
150
+
151
+ ### Use `const` for Functions
152
+
153
+ ```typescript
154
+ // ✅ Preferred
155
+ const getUserById = async (id: string): Promise<User> => {
156
+ return prisma.user.findUnique({ where: { id } })
157
+ }
158
+
159
+ // ❌ Avoid
160
+ function getUserById(id: string): Promise<User> {
161
+ return prisma.user.findUnique({ where: { id } })
162
+ }
163
+ ```
164
+
165
+ ### Explicit Return Types
166
+
167
+ ```typescript
168
+ // ✅ Always specify return types
169
+ const formatDate = (date: Date): string => {
170
+ return date.toISOString()
171
+ }
172
+
173
+ // ✅ Component return types
174
+ const UserCard = ({ user }: UserCardProps): JSX.Element => {
175
+ return <div>{user.name}</div>
176
+ }
177
+ ```
178
+
179
+ ### No `any` Types
180
+
181
+ ```typescript
182
+ // ✅ Use unknown
183
+ const parseJSON = (data: string): unknown => {
184
+ return JSON.parse(data)
185
+ }
186
+
187
+ // ❌ Never use any
188
+ const parseJSON = (data: string): any => {
189
+ return JSON.parse(data)
190
+ }
191
+ ```
192
+
193
+ ### Import Order
194
+
195
+ ```typescript
196
+ // 1. External libraries
197
+ import { createFileRoute } from '@tanstack/react-router'
198
+ import { useQuery } from '@tanstack/react-query'
199
+
200
+ // 2. Internal packages
201
+ import { Button } from '@/components/ui/button'
202
+ import { prisma } from '@/lib/prisma'
203
+
204
+ // 3. Relative imports (route-specific)
205
+ import { UserCard } from './-components/user-card'
206
+ import { useUsers } from './-hooks/use-users'
207
+
208
+ // 4. Type imports
209
+ import type { User } from '@/types'
210
+ ```
211
+
212
+ ## Route Patterns
213
+
214
+ ### Basic Route with Hook
215
+
216
+ ```tsx
217
+ // routes/users/index.tsx
218
+ import { createFileRoute } from '@tanstack/react-router'
219
+ import { UserListSection } from './-sections/user-list-section'
220
+ import { UserFilterSection } from './-sections/user-filter-section'
221
+
222
+ export const Route = createFileRoute('/users/')({
223
+ component: UsersPage,
224
+ })
225
+
226
+ const UsersPage = (): JSX.Element => {
227
+ return (
228
+ <div className="container mx-auto p-4">
229
+ <h1 className="text-2xl font-bold mb-4">Users</h1>
230
+ <UserFilterSection />
231
+ <UserListSection />
232
+ </div>
233
+ )
234
+ }
235
+ ```
236
+
237
+ ### Page Hook
238
+
239
+ ```typescript
240
+ // routes/users/-hooks/use-users.ts
241
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
242
+ import { getUsers, createUser, deleteUser } from '@/services/user'
243
+ import type { User } from '@/types'
244
+
245
+ interface UseUsersReturn {
246
+ users: User[] | undefined
247
+ isLoading: boolean
248
+ error: Error | null
249
+ createUser: (data: { email: string; name: string }) => void
250
+ deleteUser: (id: string) => void
251
+ isCreating: boolean
252
+ isDeleting: boolean
253
+ }
254
+
255
+ export const useUsers = (): UseUsersReturn => {
256
+ const queryClient = useQueryClient()
257
+
258
+ const { data: users, isLoading, error } = useQuery({
259
+ queryKey: ['users'],
260
+ queryFn: () => getUsers(),
261
+ })
262
+
263
+ const createMutation = useMutation({
264
+ mutationFn: createUser,
265
+ onSuccess: () => {
266
+ queryClient.invalidateQueries({ queryKey: ['users'] })
267
+ },
268
+ })
269
+
270
+ const deleteMutation = useMutation({
271
+ mutationFn: deleteUser,
272
+ onSuccess: () => {
273
+ queryClient.invalidateQueries({ queryKey: ['users'] })
274
+ },
275
+ })
276
+
277
+ return {
278
+ users,
279
+ isLoading,
280
+ error,
281
+ createUser: createMutation.mutate,
282
+ deleteUser: deleteMutation.mutate,
283
+ isCreating: createMutation.isPending,
284
+ isDeleting: deleteMutation.isPending,
285
+ }
286
+ }
287
+ ```
288
+
289
+ ### Section with Hook
290
+
291
+ ```tsx
292
+ // routes/users/-sections/user-list-section.tsx
293
+ import { useUsers } from '../-hooks/use-users'
294
+ import { UserCard } from '../-components/user-card'
295
+
296
+ export const UserListSection = (): JSX.Element => {
297
+ const { users, isLoading, error, deleteUser, isDeleting } = useUsers()
298
+
299
+ if (isLoading) {
300
+ return <div className="text-center py-8">Loading...</div>
301
+ }
302
+
303
+ if (error) {
304
+ return <div className="text-red-600 py-8">Error: {error.message}</div>
305
+ }
306
+
307
+ if (!users?.length) {
308
+ return <div className="text-gray-500 py-8">No users found</div>
309
+ }
310
+
311
+ return (
312
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
313
+ {users.map((user) => (
314
+ <UserCard
315
+ key={user.id}
316
+ user={user}
317
+ onDelete={deleteUser}
318
+ isDeleting={isDeleting}
319
+ />
320
+ ))}
321
+ </div>
322
+ )
323
+ }
324
+ ```
325
+
326
+ ### Filter Section with Hook
327
+
328
+ ```tsx
329
+ // routes/users/-sections/user-filter-section.tsx
330
+ import { useUserFilter } from '../-hooks/use-user-filter'
331
+ import { Input } from '@/components/ui/input'
332
+ import { Button } from '@/components/ui/button'
333
+
334
+ export const UserFilterSection = (): JSX.Element => {
335
+ const { search, setSearch, role, setRole, clearFilters } = useUserFilter()
336
+
337
+ return (
338
+ <div className="flex gap-4 mb-6">
339
+ <Input
340
+ placeholder="Search users..."
341
+ value={search}
342
+ onChange={(e) => setSearch(e.target.value)}
343
+ className="max-w-xs"
344
+ />
345
+ <select
346
+ value={role}
347
+ onChange={(e) => setRole(e.target.value)}
348
+ className="border rounded px-3 py-2"
349
+ >
350
+ <option value="">All Roles</option>
351
+ <option value="USER">User</option>
352
+ <option value="ADMIN">Admin</option>
353
+ </select>
354
+ <Button variant="outline" onClick={clearFilters}>
355
+ Clear
356
+ </Button>
357
+ </div>
358
+ )
359
+ }
360
+ ```
361
+
362
+ ### Filter Hook
363
+
364
+ ```typescript
365
+ // routes/users/-hooks/use-user-filter.ts
366
+ import { useState, useCallback } from 'react'
367
+
368
+ interface UseUserFilterReturn {
369
+ search: string
370
+ setSearch: (value: string) => void
371
+ role: string
372
+ setRole: (value: string) => void
373
+ clearFilters: () => void
374
+ }
375
+
376
+ export const useUserFilter = (): UseUserFilterReturn => {
377
+ const [search, setSearch] = useState('')
378
+ const [role, setRole] = useState('')
379
+
380
+ const clearFilters = useCallback(() => {
381
+ setSearch('')
382
+ setRole('')
383
+ }, [])
384
+
385
+ return {
386
+ search,
387
+ setSearch,
388
+ role,
389
+ setRole,
390
+ clearFilters,
391
+ }
392
+ }
393
+ ```
394
+
395
+ ### Page Component
396
+
397
+ ```tsx
398
+ // routes/users/-components/user-card.tsx
399
+ import type { User } from '@/types'
400
+ import { Button } from '@/components/ui/button'
401
+
402
+ interface UserCardProps {
403
+ user: User
404
+ onDelete?: (id: string) => void
405
+ isDeleting?: boolean
406
+ }
407
+
408
+ export const UserCard = ({
409
+ user,
410
+ onDelete,
411
+ isDeleting,
412
+ }: UserCardProps): JSX.Element => {
413
+ return (
414
+ <div className="rounded-lg border p-4 shadow-sm">
415
+ <div className="flex items-center gap-4">
416
+ <div className="h-12 w-12 rounded-full bg-gray-200" />
417
+ <div>
418
+ <h3 className="font-semibold">{user.name}</h3>
419
+ <p className="text-sm text-gray-600">{user.email}</p>
420
+ </div>
421
+ </div>
422
+
423
+ {onDelete && (
424
+ <div className="mt-4">
425
+ <Button
426
+ variant="outline"
427
+ size="sm"
428
+ onClick={() => onDelete(user.id)}
429
+ disabled={isDeleting}
430
+ >
431
+ {isDeleting ? 'Deleting...' : 'Delete'}
432
+ </Button>
433
+ </div>
434
+ )}
435
+ </div>
436
+ )
437
+ }
438
+ ```
439
+
440
+ ## Service Layer
441
+
442
+ ### Service 폴더 구조
443
+
444
+ 도메인별로 폴더를 분리하고, 파일을 용도에 따라 구분합니다:
445
+
446
+ ```
447
+ services/
448
+ ├── user/
449
+ │ ├── index.ts # 진입점 (re-export)
450
+ │ ├── schemas.ts # Zod 스키마
451
+ │ ├── queries.ts # GET 요청 (읽기)
452
+ │ └── mutations.ts # POST 요청 (쓰기)
453
+ ├── auth/
454
+ │ ├── index.ts
455
+ │ ├── schemas.ts
456
+ │ ├── queries.ts
457
+ │ └── mutations.ts
458
+ └── post/
459
+ ├── index.ts
460
+ ├── schemas.ts
461
+ ├── queries.ts
462
+ └── mutations.ts
463
+ ```
464
+
465
+ ### Schemas 파일
466
+
467
+ ```typescript
468
+ // services/user/schemas.ts
469
+ import { z } from 'zod'
470
+
471
+ export const createUserSchema = z.object({
472
+ email: z.email(),
473
+ name: z.string().min(1).max(100).trim(),
474
+ })
475
+
476
+ export const updateUserSchema = z.object({
477
+ id: z.string(),
478
+ email: z.email().optional(),
479
+ name: z.string().min(1).max(100).trim().optional(),
480
+ })
481
+
482
+ export type CreateUserInput = z.infer<typeof createUserSchema>
483
+ export type UpdateUserInput = z.infer<typeof updateUserSchema>
484
+ ```
485
+
486
+ ### Queries 파일
487
+
488
+ ```typescript
489
+ // services/user/queries.ts
490
+ import { createServerFn } from '@tanstack/react-start'
491
+ import { prisma } from '@/database/prisma'
492
+
493
+ export const getUsers = createServerFn({ method: 'GET' })
494
+ .handler(async () => {
495
+ return prisma.user.findMany({
496
+ orderBy: { createdAt: 'desc' },
497
+ })
498
+ })
499
+
500
+ export const getUserById = createServerFn({ method: 'GET' })
501
+ .handler(async ({ data: id }: { data: string }) => {
502
+ const user = await prisma.user.findUnique({ where: { id } })
503
+ if (!user) throw new Error('User not found')
504
+ return user
505
+ })
506
+
507
+ export const getUserByEmail = createServerFn({ method: 'GET' })
508
+ .handler(async ({ data: email }: { data: string }) => {
509
+ return prisma.user.findUnique({ where: { email } })
510
+ })
511
+ ```
512
+
513
+ ### Mutations 파일
514
+
515
+ ```typescript
516
+ // services/user/mutations.ts
517
+ import { createServerFn } from '@tanstack/react-start'
518
+ import { prisma } from '@/database/prisma'
519
+ import { createUserSchema, updateUserSchema } from './schemas'
520
+
521
+ export const createUser = createServerFn({ method: 'POST' })
522
+ .inputValidator(createUserSchema)
523
+ .handler(async ({ data }) => {
524
+ return prisma.user.create({ data })
525
+ })
526
+
527
+ export const updateUser = createServerFn({ method: 'POST' })
528
+ .inputValidator(updateUserSchema)
529
+ .handler(async ({ data }) => {
530
+ const { id, ...updateData } = data
531
+ return prisma.user.update({ where: { id }, data: updateData })
532
+ })
533
+
534
+ export const deleteUser = createServerFn({ method: 'POST' })
535
+ .handler(async ({ data: id }: { data: string }) => {
536
+ return prisma.user.delete({ where: { id } })
537
+ })
538
+ ```
539
+
540
+ ### Service 진입점 파일
541
+
542
+ ```typescript
543
+ // services/user/index.ts
544
+ export * from './schemas'
545
+ export * from './queries'
546
+ export * from './mutations'
547
+ ```
548
+
549
+ ### 사용 예시
550
+
551
+ ```typescript
552
+ // routes/users/-hooks/use-users.ts
553
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
554
+ import { getUsers, createUser, deleteUser } from '@/services/user'
555
+ ```
556
+
557
+ ## Common UI Components
558
+
559
+ ### Button Component
560
+
561
+ ```tsx
562
+ // components/ui/button.tsx
563
+ interface ButtonProps {
564
+ children: React.ReactNode
565
+ variant?: 'primary' | 'secondary' | 'outline'
566
+ size?: 'sm' | 'md' | 'lg'
567
+ onClick?: () => void
568
+ disabled?: boolean
569
+ type?: 'button' | 'submit' | 'reset'
570
+ }
571
+
572
+ export const Button = ({
573
+ children,
574
+ variant = 'primary',
575
+ size = 'md',
576
+ onClick,
577
+ disabled,
578
+ type = 'button',
579
+ }: ButtonProps): JSX.Element => {
580
+ const baseStyles = 'rounded font-medium transition-colors disabled:opacity-50'
581
+
582
+ const variants = {
583
+ primary: 'bg-blue-600 text-white hover:bg-blue-700',
584
+ secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
585
+ outline: 'border border-gray-300 hover:bg-gray-50',
586
+ }
587
+
588
+ const sizes = {
589
+ sm: 'px-3 py-1.5 text-sm',
590
+ md: 'px-4 py-2',
591
+ lg: 'px-6 py-3 text-lg',
592
+ }
593
+
594
+ return (
595
+ <button
596
+ type={type}
597
+ onClick={onClick}
598
+ disabled={disabled}
599
+ className={`${baseStyles} ${variants[variant]} ${sizes[size]}`}
600
+ >
601
+ {children}
602
+ </button>
603
+ )
604
+ }
605
+ ```
606
+
607
+ ### Input Component
608
+
609
+ ```tsx
610
+ // components/ui/input.tsx
611
+ interface InputProps {
612
+ placeholder?: string
613
+ value: string
614
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
615
+ type?: 'text' | 'email' | 'password'
616
+ className?: string
617
+ disabled?: boolean
618
+ }
619
+
620
+ export const Input = ({
621
+ placeholder,
622
+ value,
623
+ onChange,
624
+ type = 'text',
625
+ className = '',
626
+ disabled,
627
+ }: InputProps): JSX.Element => {
628
+ return (
629
+ <input
630
+ type={type}
631
+ placeholder={placeholder}
632
+ value={value}
633
+ onChange={onChange}
634
+ disabled={disabled}
635
+ className={`border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 ${className}`}
636
+ />
637
+ )
638
+ }
639
+ ```
640
+
641
+ ## Error Handling
642
+
643
+ ### Custom Error Classes
644
+
645
+ ```typescript
646
+ // lib/errors.ts
647
+ export class AppError extends Error {
648
+ constructor(
649
+ message: string,
650
+ public statusCode: number = 500,
651
+ public code: string = 'INTERNAL_ERROR'
652
+ ) {
653
+ super(message)
654
+ this.name = 'AppError'
655
+ }
656
+ }
657
+
658
+ export class NotFoundError extends AppError {
659
+ constructor(resource: string) {
660
+ super(`${resource} not found`, 404, 'NOT_FOUND')
661
+ }
662
+ }
663
+
664
+ export class ValidationError extends AppError {
665
+ constructor(message: string) {
666
+ super(message, 400, 'VALIDATION_ERROR')
667
+ }
668
+ }
669
+ ```
670
+
671
+ ## Testing
672
+
673
+ ### Test Structure
674
+
675
+ ```typescript
676
+ // __tests__/services/user-service.test.ts
677
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
678
+
679
+ describe('UserService', () => {
680
+ beforeEach(() => {
681
+ vi.clearAllMocks()
682
+ })
683
+
684
+ describe('createUser', () => {
685
+ it('creates a user with valid input', async () => {
686
+ const input = { email: 'test@example.com', name: 'Test' }
687
+ const result = await createUser(input)
688
+
689
+ expect(result).toMatchObject({
690
+ email: input.email,
691
+ name: input.name,
692
+ })
693
+ })
694
+
695
+ it('throws on invalid email', async () => {
696
+ const input = { email: 'invalid', name: 'Test' }
697
+
698
+ await expect(createUser(input)).rejects.toThrow()
699
+ })
700
+ })
701
+ })
702
+ ```
703
+
704
+ ## Performance
705
+
706
+ ### React Optimization
707
+
708
+ ```tsx
709
+ import { useMemo, useCallback, memo } from 'react'
710
+
711
+ // Memoize expensive computations
712
+ const sortedUsers = useMemo(
713
+ () => users.sort((a, b) => a.name.localeCompare(b.name)),
714
+ [users]
715
+ )
716
+
717
+ // Memoize callbacks
718
+ const handleClick = useCallback(() => {
719
+ setIsOpen(true)
720
+ }, [])
721
+
722
+ // Memoize components
723
+ export const UserCard = memo(({ user }: { user: User }): JSX.Element => {
724
+ return <div>{user.name}</div>
725
+ })
726
+ ```
727
+
728
+ ## Security
729
+
730
+ ### Environment Variables
731
+
732
+ - `.env` 파일은 절대 커밋하지 않음
733
+ - `.env.example` 제공
734
+ - 시작 시 환경변수 검증
735
+
736
+ ### Input Validation
737
+
738
+ ```typescript
739
+ import { z } from 'zod'
740
+
741
+ // Zod v4 API
742
+ const userInputSchema = z.object({
743
+ name: z.string().min(1).max(100).trim(),
744
+ email: z.email().toLowerCase(), // v4: z.email()
745
+ })
746
+
747
+ const envSchema = z.object({
748
+ NODE_ENV: z.enum(['development', 'production', 'test']),
749
+ DATABASE_URL: z.url(), // v4: z.url()
750
+ })
751
+
752
+ export const env = envSchema.parse(process.env)
753
+ ```