@kood/claude-code 0.1.6 → 0.1.7
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 +21 -243
- package/package.json +1 -1
- package/templates/hono/CLAUDE.md +10 -6
- package/templates/hono/docs/deployment/index.md +5 -0
- package/templates/hono/docs/library/hono/index.md +6 -0
- package/templates/hono/docs/library/prisma/index.md +3 -0
- package/templates/npx/CLAUDE.md +8 -2
- package/templates/tanstack-start/CLAUDE.md +103 -255
- 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/accessibility.md +56 -326
- package/templates/tanstack-start/docs/design/color.md +37 -179
- package/templates/tanstack-start/docs/design/components.md +77 -311
- package/templates/tanstack-start/docs/design/index.md +24 -87
- package/templates/tanstack-start/docs/design/safe-area.md +51 -250
- package/templates/tanstack-start/docs/design/spacing.md +57 -276
- package/templates/tanstack-start/docs/design/tailwind-setup.md +45 -359
- package/templates/tanstack-start/docs/design/typography.md +40 -284
- 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 +12 -99
- 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/setup.md +11 -70
- 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/basic-types.md +33 -145
- package/templates/tanstack-start/docs/library/zod/complex-types.md +32 -156
- package/templates/tanstack-start/docs/library/zod/index.md +22 -150
- 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/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,21 +1,25 @@
|
|
|
1
1
|
# Zod
|
|
2
2
|
|
|
3
|
-
> **
|
|
3
|
+
> **v4** | TypeScript Schema Validation
|
|
4
|
+
|
|
5
|
+
@basic-types.md
|
|
6
|
+
@complex-types.md
|
|
7
|
+
@transforms.md
|
|
8
|
+
@validation.md
|
|
4
9
|
|
|
5
10
|
---
|
|
6
11
|
|
|
7
|
-
##
|
|
12
|
+
## Quick Reference
|
|
8
13
|
|
|
9
14
|
```typescript
|
|
10
15
|
// 기본 스키마
|
|
11
16
|
const schema = z.object({
|
|
12
|
-
email: z.email(), // v4 (string().email()
|
|
17
|
+
email: z.email(), // v4! (string().email() 아님)
|
|
13
18
|
name: z.string().min(1).trim(),
|
|
14
|
-
website: z.url().optional(), // v4 (string().url()
|
|
19
|
+
website: z.url().optional(), // v4! (string().url() 아님)
|
|
15
20
|
age: z.number().int().positive(),
|
|
16
21
|
})
|
|
17
22
|
|
|
18
|
-
// 타입 추출
|
|
19
23
|
type Input = z.infer<typeof schema>
|
|
20
24
|
|
|
21
25
|
// 파싱
|
|
@@ -28,159 +32,27 @@ export const createUser = createServerFn({ method: 'POST' })
|
|
|
28
32
|
.handler(async ({ data }) => prisma.user.create({ data }))
|
|
29
33
|
```
|
|
30
34
|
|
|
31
|
-
### v4 새 API
|
|
35
|
+
### ⚠️ v4 새 API
|
|
32
36
|
|
|
33
37
|
```typescript
|
|
34
|
-
// ✅ v4
|
|
35
|
-
z.email()
|
|
36
|
-
z.
|
|
37
|
-
z.uuid()
|
|
38
|
-
z.iso.date()
|
|
39
|
-
z.iso.datetime()
|
|
38
|
+
// ✅ v4
|
|
39
|
+
z.email() z.url() z.uuid()
|
|
40
|
+
z.iso.date() z.iso.datetime()
|
|
40
41
|
|
|
41
|
-
// ❌ v3
|
|
42
|
-
z.string().email()
|
|
43
|
-
z.string().url()
|
|
42
|
+
// ❌ v3 deprecated
|
|
43
|
+
z.string().email() z.string().url()
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
## 문서 구조
|
|
49
|
-
|
|
50
|
-
- [기본 타입](./basic-types.md) - 문자열, 숫자, 불리언, 날짜
|
|
51
|
-
- [복합 타입](./complex-types.md) - 객체, 배열, 튜플, 유니온, Enum
|
|
52
|
-
- [변환](./transforms.md) - Transform, Coerce, Preprocess
|
|
53
|
-
- [검증](./validation.md) - Refinement, Superrefine, 커스텀 검증
|
|
54
|
-
|
|
55
|
-
## 빠른 시작
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
yarn add zod
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### 기본 사용법
|
|
46
|
+
### v4 주요 변경
|
|
62
47
|
|
|
63
48
|
```typescript
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// 스키마 생성
|
|
67
|
-
const mySchema = z.string()
|
|
68
|
-
|
|
69
|
-
// parsing (실패 시 에러 throw)
|
|
70
|
-
mySchema.parse('tuna') // => "tuna"
|
|
71
|
-
mySchema.parse(12) // => throws ZodError
|
|
72
|
-
|
|
73
|
-
// safe parsing (에러를 던지지 않음)
|
|
74
|
-
mySchema.safeParse('tuna') // => { success: true; data: "tuna" }
|
|
75
|
-
mySchema.safeParse(12) // => { success: false; error: ZodError }
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### 타입 추론
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
import { z } from 'zod'
|
|
82
|
-
|
|
83
|
-
const Player = z.object({
|
|
84
|
-
username: z.string(),
|
|
85
|
-
xp: z.number(),
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
// 스키마에서 타입 추출
|
|
89
|
-
type Player = z.infer<typeof Player>
|
|
90
|
-
|
|
91
|
-
// 타입 안전하게 사용
|
|
92
|
-
const player: Player = { username: 'billie', xp: 100 }
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## v4 주요 변경사항
|
|
96
|
-
|
|
97
|
-
### 에러 커스터마이징 통합
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
// v3 (deprecated)
|
|
101
|
-
z.string().min(5, { message: "Too short." })
|
|
102
|
-
z.string({ invalid_type_error: "Not a string", required_error: "Required" })
|
|
103
|
-
|
|
104
|
-
// v4 - error 파라미터로 통합
|
|
49
|
+
// 에러 커스텀: message → error
|
|
105
50
|
z.string().min(5, { error: "Too short." })
|
|
106
|
-
z.string({
|
|
107
|
-
error: (issue) => issue.input === undefined
|
|
108
|
-
? "This field is required"
|
|
109
|
-
: "Not a string"
|
|
110
|
-
})
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### 새로운 최상위 문자열 포맷 API
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
// v3 (deprecated)
|
|
117
|
-
z.string().email()
|
|
118
|
-
z.string().uuid()
|
|
119
|
-
|
|
120
|
-
// v4 - 최상위 API (더 나은 tree-shaking)
|
|
121
|
-
z.email()
|
|
122
|
-
z.uuid()
|
|
123
|
-
z.url()
|
|
124
|
-
z.base64()
|
|
125
|
-
z.nanoid()
|
|
126
|
-
z.cuid()
|
|
127
|
-
z.cuid2()
|
|
128
|
-
z.ulid()
|
|
129
|
-
z.ipv4()
|
|
130
|
-
z.ipv6()
|
|
131
|
-
z.iso.date()
|
|
132
|
-
z.iso.time()
|
|
133
|
-
z.iso.datetime()
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Strict/Loose 객체
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
// v3
|
|
140
|
-
z.object({ name: z.string() }).strict()
|
|
141
|
-
z.object({ name: z.string() }).passthrough()
|
|
142
|
-
|
|
143
|
-
// v4
|
|
144
|
-
z.strictObject({ name: z.string() })
|
|
145
|
-
z.looseObject({ name: z.string() })
|
|
146
|
-
```
|
|
147
51
|
|
|
148
|
-
|
|
52
|
+
// Strict/Loose 객체
|
|
53
|
+
z.strictObject({ name: z.string() }) // 추가 키 에러
|
|
54
|
+
z.looseObject({ name: z.string() }) // 추가 키 통과
|
|
149
55
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
z.string()
|
|
153
|
-
.refine(val => val.includes("@"))
|
|
154
|
-
.min(5) // ❌ Property 'min' does not exist on type ZodEffects
|
|
155
|
-
|
|
156
|
-
// v4 - 스키마 내부에 저장되어 체이닝 가능
|
|
157
|
-
z.string()
|
|
158
|
-
.refine(val => val.includes("@"))
|
|
159
|
-
.min(5) // ✅
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## TanStack Start와 함께 사용
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
import { createServerFn } from '@tanstack/react-start'
|
|
166
|
-
import { zodValidator } from '@tanstack/react-start/validators'
|
|
167
|
-
import { z } from 'zod'
|
|
168
|
-
|
|
169
|
-
const createUserSchema = z.object({
|
|
170
|
-
email: z.email(), // v4 새 API
|
|
171
|
-
name: z.string().min(1).max(100).trim(),
|
|
172
|
-
bio: z.string().max(500).optional(),
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
export const createUser = createServerFn({ method: 'POST' })
|
|
176
|
-
.inputValidator(zodValidator(createUserSchema))
|
|
177
|
-
.handler(async ({ data }) => {
|
|
178
|
-
// data는 타입 안전함
|
|
179
|
-
return prisma.user.create({ data })
|
|
180
|
-
})
|
|
56
|
+
// Refinement 체이닝 가능
|
|
57
|
+
z.string().refine(val => val.includes("@")).min(5) // ✅ v4
|
|
181
58
|
```
|
|
182
|
-
|
|
183
|
-
## 참고 자료
|
|
184
|
-
|
|
185
|
-
- [Zod 공식 문서](https://zod.dev)
|
|
186
|
-
- [Zod GitHub](https://github.com/colinhacks/zod)
|
|
@@ -1,148 +1,56 @@
|
|
|
1
1
|
# Zod - 변환
|
|
2
2
|
|
|
3
|
-
> **상위 문서**: [Zod](./index.md)
|
|
4
|
-
|
|
5
3
|
## Transform
|
|
6
4
|
|
|
7
|
-
입력값을 다른 형태로 변환합니다.
|
|
8
|
-
|
|
9
5
|
```typescript
|
|
10
6
|
const stringToLength = z.string().transform((val) => val.length)
|
|
7
|
+
stringToLength.parse('hello') // => 5
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type MySchemaIn = z.input<typeof stringToLength> // string
|
|
15
|
-
type MySchemaOut = z.output<typeof stringToLength> // number
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
### 체인으로 변환
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
const emailToLower = z.string()
|
|
22
|
-
.email()
|
|
23
|
-
.transform((email) => email.toLowerCase())
|
|
24
|
-
|
|
25
|
-
emailToLower.parse('User@Example.COM') // => "user@example.com"
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Pipe를 사용한 Transform
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
const stringToLength = z.string().pipe(z.transform(val => val.length))
|
|
32
|
-
|
|
33
|
-
stringToLength.parse('hello') // => 5
|
|
9
|
+
type In = z.input<typeof stringToLength> // string
|
|
10
|
+
type Out = z.output<typeof stringToLength> // number
|
|
34
11
|
```
|
|
35
12
|
|
|
36
|
-
|
|
13
|
+
## Pipe (검증 후 변환)
|
|
37
14
|
|
|
38
15
|
```typescript
|
|
39
16
|
const stringToNumber = z.string()
|
|
40
17
|
.transform((val) => parseInt(val, 10))
|
|
41
18
|
.pipe(z.number().min(0).max(100))
|
|
42
19
|
|
|
43
|
-
stringToNumber.parse('50')
|
|
44
|
-
stringToNumber.parse('150') //
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Transform 내에서 검증
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
const coercedInt = z.transform((val, ctx) => {
|
|
51
|
-
try {
|
|
52
|
-
const parsed = Number.parseInt(String(val))
|
|
53
|
-
return parsed
|
|
54
|
-
} catch (e) {
|
|
55
|
-
ctx.issues.push({
|
|
56
|
-
code: 'custom',
|
|
57
|
-
message: 'Not a number',
|
|
58
|
-
input: val,
|
|
59
|
-
})
|
|
60
|
-
return z.NEVER // 타입에 영향 없이 에러 반환
|
|
61
|
-
}
|
|
62
|
-
})
|
|
20
|
+
stringToNumber.parse('50') // => 50
|
|
21
|
+
stringToNumber.parse('150') // throws
|
|
63
22
|
```
|
|
64
23
|
|
|
65
24
|
## Coerce (강제 변환)
|
|
66
25
|
|
|
67
|
-
입력값을 자동으로 해당 타입으로 변환합니다.
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
z.coerce.string() // 모든 값을 문자열로
|
|
71
|
-
z.coerce.number() // 모든 값을 숫자로
|
|
72
|
-
z.coerce.boolean() // 모든 값을 불리언으로
|
|
73
|
-
z.coerce.date() // 모든 값을 날짜로
|
|
74
|
-
z.coerce.bigint() // 모든 값을 BigInt로
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### v4 Coerce 입력 타입 변경
|
|
78
|
-
|
|
79
26
|
```typescript
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
27
|
+
z.coerce.string() // 모든 값 → 문자열
|
|
28
|
+
z.coerce.number() // 모든 값 → 숫자
|
|
29
|
+
z.coerce.boolean() // 모든 값 → 불리언
|
|
30
|
+
z.coerce.date() // 모든 값 → 날짜
|
|
31
|
+
z.coerce.bigint() // 모든 값 → BigInt
|
|
85
32
|
|
|
86
|
-
|
|
33
|
+
z.coerce.number().parse("42") // => 42
|
|
34
|
+
z.coerce.date().parse("2021-01-01") // => Date
|
|
87
35
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
z.coerce.string().parse(123) // => "123"
|
|
91
|
-
z.coerce.string().parse(true) // => "true"
|
|
92
|
-
z.coerce.string().parse(null) // => "null"
|
|
93
|
-
|
|
94
|
-
// number coercion
|
|
95
|
-
z.coerce.number().parse("42") // => 42
|
|
96
|
-
z.coerce.number().parse(true) // => 1
|
|
97
|
-
z.coerce.number().parse(false) // => 0
|
|
98
|
-
|
|
99
|
-
// boolean coercion
|
|
100
|
-
z.coerce.boolean().parse("true") // => true
|
|
101
|
-
z.coerce.boolean().parse("") // => false
|
|
102
|
-
z.coerce.boolean().parse(1) // => true
|
|
103
|
-
|
|
104
|
-
// date coercion
|
|
105
|
-
z.coerce.date().parse("2021-01-01") // => Date
|
|
106
|
-
z.coerce.date().parse(1609459200000) // => Date
|
|
36
|
+
// ⚠️ v4: 입력 타입이 unknown으로 변경됨
|
|
37
|
+
type In = z.input<typeof z.coerce.string()> // unknown
|
|
107
38
|
```
|
|
108
39
|
|
|
109
40
|
### 환경변수 불리언 (v4 권장)
|
|
110
41
|
|
|
111
|
-
`z.coerce.boolean()` 대신 더 정교한 `z.stringbool()` 사용:
|
|
112
|
-
|
|
113
42
|
```typescript
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
strbool.parse("true") // => true
|
|
117
|
-
strbool.parse("yes") // => true
|
|
118
|
-
strbool.parse("1") // => true
|
|
119
|
-
strbool.parse("false") // => false
|
|
120
|
-
strbool.parse("no") // => false
|
|
121
|
-
strbool.parse("0") // => false
|
|
43
|
+
z.stringbool() // "true"/"yes"/"1" → true
|
|
44
|
+
// "false"/"no"/"0" → false
|
|
122
45
|
```
|
|
123
46
|
|
|
124
47
|
## Preprocess
|
|
125
48
|
|
|
126
|
-
검증 전에 데이터를 전처리합니다.
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
const castToString = z.preprocess((val) => String(val), z.string())
|
|
130
|
-
|
|
131
|
-
castToString.parse(123) // => "123"
|
|
132
|
-
castToString.parse(null) // => "null"
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### Preprocess vs Coerce
|
|
136
|
-
|
|
137
49
|
```typescript
|
|
138
|
-
|
|
139
|
-
const trimmedString = z.preprocess(
|
|
50
|
+
const trimmed = z.preprocess(
|
|
140
51
|
(val) => typeof val === 'string' ? val.trim() : val,
|
|
141
52
|
z.string()
|
|
142
53
|
)
|
|
143
|
-
|
|
144
|
-
// coerce - 내장 변환만
|
|
145
|
-
const coercedString = z.coerce.string()
|
|
146
54
|
```
|
|
147
55
|
|
|
148
56
|
## 입력/출력 타입 분리
|
|
@@ -152,23 +60,6 @@ const Schema = z.object({
|
|
|
152
60
|
createdAt: z.string().transform((str) => new Date(str)),
|
|
153
61
|
})
|
|
154
62
|
|
|
155
|
-
type SchemaInput = z.input<typeof Schema>
|
|
156
|
-
// { createdAt:
|
|
157
|
-
|
|
158
|
-
type SchemaOutput = z.output<typeof Schema>
|
|
159
|
-
// { createdAt: Date }
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## 조건부 변환
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
const conditionalTransform = z.string().transform((val) => {
|
|
166
|
-
if (val === 'null') return null
|
|
167
|
-
if (val === 'undefined') return undefined
|
|
168
|
-
return val
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
conditionalTransform.parse('hello') // => "hello"
|
|
172
|
-
conditionalTransform.parse('null') // => null
|
|
173
|
-
conditionalTransform.parse('undefined') // => undefined
|
|
63
|
+
type SchemaInput = z.input<typeof Schema> // { createdAt: string }
|
|
64
|
+
type SchemaOutput = z.output<typeof Schema> // { createdAt: Date }
|
|
174
65
|
```
|
|
@@ -1,208 +1,92 @@
|
|
|
1
1
|
# Zod - 검증
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Refinement (커스텀 검증)
|
|
3
|
+
## Refinement
|
|
6
4
|
|
|
7
5
|
```typescript
|
|
8
|
-
// v4
|
|
6
|
+
// v4: message → error
|
|
9
7
|
const PasswordSchema = z.string()
|
|
10
8
|
.min(8)
|
|
11
|
-
.refine((val) => /[A-Z]/.test(val), {
|
|
12
|
-
|
|
13
|
-
})
|
|
14
|
-
.refine((val) => /[0-9]/.test(val), {
|
|
15
|
-
error: '숫자가 포함되어야 합니다',
|
|
16
|
-
})
|
|
17
|
-
```
|
|
9
|
+
.refine((val) => /[A-Z]/.test(val), { error: '대문자 필수' })
|
|
10
|
+
.refine((val) => /[0-9]/.test(val), { error: '숫자 필수' })
|
|
18
11
|
|
|
19
|
-
|
|
12
|
+
// v4: refinement 후 체이닝 가능
|
|
13
|
+
z.string().refine(val => val.includes("@")).min(5) // ✅
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
// v3 - ZodEffects로 래핑되어 .min() 호출 불가
|
|
23
|
-
z.string()
|
|
24
|
-
.refine(val => val.includes("@"))
|
|
25
|
-
.min(5) // ❌ 에러
|
|
26
|
-
|
|
27
|
-
// v4 - 스키마 내부에 저장되어 체이닝 가능
|
|
28
|
-
z.string()
|
|
29
|
-
.refine(val => val.includes("@"))
|
|
30
|
-
.min(5) // ✅ 정상 작동
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### Async Refinement
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
15
|
+
// Async
|
|
36
16
|
const schema = z.string().refine(async (val) => val.length <= 8)
|
|
37
|
-
|
|
38
|
-
await schema.parseAsync('hello') // => "hello"
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### 이메일 중복 검사 예시
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
const EmailSchema = z.email() // v4 새 API
|
|
45
|
-
.refine(async (email) => {
|
|
46
|
-
const exists = await checkEmailExists(email)
|
|
47
|
-
return !exists
|
|
48
|
-
}, {
|
|
49
|
-
error: '이미 사용 중인 이메일입니다', // v4: message → error
|
|
50
|
-
})
|
|
17
|
+
await schema.parseAsync('hello')
|
|
51
18
|
```
|
|
52
19
|
|
|
53
|
-
## Superrefine (고급
|
|
54
|
-
|
|
55
|
-
여러 이슈를 추가하거나 경로를 지정할 수 있습니다.
|
|
56
|
-
|
|
57
|
-
> **v4 변경**: `ctx.path`는 더 이상 사용할 수 없습니다 (성능 향상을 위해 제거됨)
|
|
20
|
+
## Superrefine (고급)
|
|
58
21
|
|
|
59
22
|
```typescript
|
|
60
|
-
|
|
23
|
+
z.object({
|
|
61
24
|
password: z.string(),
|
|
62
25
|
confirmPassword: z.string(),
|
|
63
26
|
}).superRefine((data, ctx) => {
|
|
64
27
|
if (data.password !== data.confirmPassword) {
|
|
65
28
|
ctx.addIssue({
|
|
66
29
|
code: z.ZodIssueCode.custom,
|
|
67
|
-
message: '
|
|
30
|
+
message: '비밀번호 불일치',
|
|
68
31
|
path: ['confirmPassword'],
|
|
69
32
|
})
|
|
70
33
|
}
|
|
71
|
-
// ctx.path // ❌ v4에서 더 이상 사용 불가
|
|
72
34
|
})
|
|
35
|
+
// ⚠️ v4: ctx.path 사용 불가
|
|
73
36
|
```
|
|
74
37
|
|
|
75
|
-
|
|
38
|
+
## 커스텀 스키마
|
|
76
39
|
|
|
77
40
|
```typescript
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
age: z.number(),
|
|
82
|
-
}).superRefine((data, ctx) => {
|
|
83
|
-
// 여러 조건 검증
|
|
84
|
-
if (data.age < 13 && data.email.includes('work')) {
|
|
85
|
-
ctx.addIssue({
|
|
86
|
-
code: z.ZodIssueCode.custom,
|
|
87
|
-
message: '13세 미만은 업무용 이메일을 사용할 수 없습니다',
|
|
88
|
-
path: ['email'],
|
|
89
|
-
})
|
|
90
|
-
}
|
|
41
|
+
const px = z.custom<`${number}px`>((val) =>
|
|
42
|
+
typeof val === 'string' && /^\d+px$/.test(val)
|
|
43
|
+
)
|
|
91
44
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
code: z.ZodIssueCode.custom,
|
|
95
|
-
message: 'admin 사용자명은 18세 이상만 사용 가능합니다',
|
|
96
|
-
path: ['username'],
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
})
|
|
45
|
+
px.parse('42px') // ✅
|
|
46
|
+
px.parse('42vw') // throws
|
|
100
47
|
```
|
|
101
48
|
|
|
102
|
-
##
|
|
49
|
+
## 에러 처리
|
|
103
50
|
|
|
104
51
|
```typescript
|
|
105
|
-
const
|
|
106
|
-
return typeof val === 'string' ? /^\d+px$/.test(val) : false
|
|
107
|
-
})
|
|
52
|
+
const result = schema.safeParse(data)
|
|
108
53
|
|
|
109
|
-
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
result.error.errors.forEach((err) => {
|
|
56
|
+
console.log(err.path, err.message, err.code)
|
|
57
|
+
})
|
|
110
58
|
|
|
111
|
-
|
|
112
|
-
|
|
59
|
+
// 평면화
|
|
60
|
+
const flat = result.error.flatten()
|
|
61
|
+
// { fieldErrors: { email: ['Invalid email'] } }
|
|
62
|
+
}
|
|
113
63
|
```
|
|
114
64
|
|
|
115
|
-
## TanStack Start
|
|
116
|
-
|
|
117
|
-
### Server Function 입력 검증
|
|
65
|
+
## TanStack Start 연동
|
|
118
66
|
|
|
119
67
|
```typescript
|
|
120
|
-
|
|
121
|
-
import { zodValidator } from '@tanstack/react-start/validators'
|
|
122
|
-
import { z } from 'zod'
|
|
123
|
-
|
|
124
|
-
const createUserSchema = z.object({
|
|
125
|
-
email: z.email(), // v4 새 API
|
|
126
|
-
name: z.string().min(1).max(100).trim(),
|
|
127
|
-
bio: z.string().max(500).optional(),
|
|
128
|
-
})
|
|
129
|
-
|
|
68
|
+
// Server Function
|
|
130
69
|
export const createUser = createServerFn({ method: 'POST' })
|
|
131
70
|
.inputValidator(zodValidator(createUserSchema))
|
|
132
|
-
.handler(async ({ data }) => {
|
|
133
|
-
// data는 타입 안전함
|
|
134
|
-
return prisma.user.create({ data })
|
|
135
|
-
})
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### 환경 변수 검증
|
|
71
|
+
.handler(async ({ data }) => prisma.user.create({ data }))
|
|
139
72
|
|
|
140
|
-
|
|
141
|
-
const
|
|
73
|
+
// 환경 변수
|
|
74
|
+
const env = z.object({
|
|
142
75
|
NODE_ENV: z.enum(['development', 'production', 'test']),
|
|
143
76
|
DATABASE_URL: z.string().url(),
|
|
144
77
|
API_SECRET: z.string().min(32),
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
export const env = envSchema.parse(process.env)
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Middleware에서 사용
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
import { createMiddleware } from '@tanstack/react-start'
|
|
154
|
-
import { zodValidator } from '@tanstack/react-start/validators'
|
|
155
|
-
import { z } from 'zod'
|
|
156
|
-
|
|
157
|
-
const mySchema = z.object({
|
|
158
|
-
workspaceId: z.string(),
|
|
159
|
-
})
|
|
78
|
+
}).parse(process.env)
|
|
160
79
|
|
|
80
|
+
// Middleware
|
|
161
81
|
const workspaceMiddleware = createMiddleware({ type: 'function' })
|
|
162
|
-
.inputValidator(zodValidator(
|
|
163
|
-
.server(({ next, data }) =>
|
|
164
|
-
console.log('Workspace ID:', data.workspaceId)
|
|
165
|
-
return next()
|
|
166
|
-
})
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
## 에러 처리
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
const result = UserSchema.safeParse(data)
|
|
173
|
-
|
|
174
|
-
if (!result.success) {
|
|
175
|
-
// ZodError 객체
|
|
176
|
-
const errors = result.error.errors
|
|
177
|
-
|
|
178
|
-
errors.forEach((err) => {
|
|
179
|
-
console.log('Path:', err.path)
|
|
180
|
-
console.log('Message:', err.message)
|
|
181
|
-
console.log('Code:', err.code)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
// 평면화된 에러
|
|
185
|
-
const flatErrors = result.error.flatten()
|
|
186
|
-
console.log(flatErrors.fieldErrors)
|
|
187
|
-
// { email: ['Invalid email'], name: ['Required'] }
|
|
188
|
-
}
|
|
82
|
+
.inputValidator(zodValidator(z.object({ workspaceId: z.string() })))
|
|
83
|
+
.server(({ next, data }) => next())
|
|
189
84
|
```
|
|
190
85
|
|
|
191
|
-
## Zod Mini (경량
|
|
192
|
-
|
|
193
|
-
v4에서 새롭게 추가된 경량 API:
|
|
86
|
+
## Zod Mini (v4 경량)
|
|
194
87
|
|
|
195
88
|
```typescript
|
|
196
89
|
import * as z from "zod/mini"
|
|
197
90
|
|
|
198
|
-
|
|
199
|
-
z.string().min(5).max(10).trim()
|
|
200
|
-
|
|
201
|
-
// Zod Mini - check 메서드로 통합
|
|
202
|
-
z.string().check(
|
|
203
|
-
z.minLength(5),
|
|
204
|
-
z.maxLength(10),
|
|
205
|
-
z.toLowerCase(),
|
|
206
|
-
z.trim(),
|
|
207
|
-
)
|
|
91
|
+
z.string().check(z.minLength(5), z.maxLength(10), z.trim())
|
|
208
92
|
```
|