@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.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +297 -0
- package/package.json +47 -0
- package/templates/hono/CLAUDE.md +376 -0
- package/templates/hono/docs/deployment/cloudflare.md +328 -0
- package/templates/hono/docs/deployment/index.md +291 -0
- package/templates/hono/docs/git/index.md +180 -0
- package/templates/hono/docs/library/hono/error-handling.md +400 -0
- package/templates/hono/docs/library/hono/index.md +241 -0
- package/templates/hono/docs/library/hono/middleware.md +334 -0
- package/templates/hono/docs/library/hono/rpc.md +454 -0
- package/templates/hono/docs/library/hono/validation.md +328 -0
- package/templates/hono/docs/library/prisma/index.md +427 -0
- package/templates/hono/docs/library/zod/index.md +413 -0
- package/templates/hono/docs/mcp/context7.md +106 -0
- package/templates/hono/docs/mcp/index.md +94 -0
- package/templates/hono/docs/mcp/sequential-thinking.md +101 -0
- package/templates/hono/docs/mcp/sgrep.md +105 -0
- package/templates/hono/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/hono/docs/skills/gemini-review/references/checklists.md +136 -0
- package/templates/hono/docs/skills/gemini-review/references/prompt-templates.md +303 -0
- package/templates/tanstack-start/CLAUDE.md +279 -0
- package/templates/tanstack-start/docs/architecture/architecture.md +547 -0
- package/templates/tanstack-start/docs/deployment/cloudflare.md +346 -0
- package/templates/tanstack-start/docs/deployment/index.md +102 -0
- package/templates/tanstack-start/docs/deployment/nitro.md +211 -0
- package/templates/tanstack-start/docs/deployment/railway.md +364 -0
- package/templates/tanstack-start/docs/deployment/vercel.md +287 -0
- package/templates/tanstack-start/docs/design/accessibility.md +433 -0
- package/templates/tanstack-start/docs/design/color.md +235 -0
- package/templates/tanstack-start/docs/design/components.md +409 -0
- package/templates/tanstack-start/docs/design/index.md +107 -0
- package/templates/tanstack-start/docs/design/safe-area.md +317 -0
- package/templates/tanstack-start/docs/design/spacing.md +341 -0
- package/templates/tanstack-start/docs/design/tailwind-setup.md +470 -0
- package/templates/tanstack-start/docs/design/typography.md +324 -0
- package/templates/tanstack-start/docs/git/index.md +203 -0
- package/templates/tanstack-start/docs/guides/best-practices.md +753 -0
- package/templates/tanstack-start/docs/guides/getting-started.md +304 -0
- package/templates/tanstack-start/docs/guides/husky-lint-staged.md +303 -0
- package/templates/tanstack-start/docs/guides/prettier.md +189 -0
- package/templates/tanstack-start/docs/guides/project-templates.md +710 -0
- package/templates/tanstack-start/docs/library/better-auth/2fa.md +136 -0
- package/templates/tanstack-start/docs/library/better-auth/advanced.md +138 -0
- package/templates/tanstack-start/docs/library/better-auth/index.md +83 -0
- package/templates/tanstack-start/docs/library/better-auth/plugins.md +111 -0
- package/templates/tanstack-start/docs/library/better-auth/session.md +127 -0
- package/templates/tanstack-start/docs/library/better-auth/setup.md +123 -0
- package/templates/tanstack-start/docs/library/prisma/crud.md +218 -0
- package/templates/tanstack-start/docs/library/prisma/index.md +165 -0
- package/templates/tanstack-start/docs/library/prisma/relations.md +191 -0
- package/templates/tanstack-start/docs/library/prisma/schema.md +177 -0
- package/templates/tanstack-start/docs/library/prisma/setup.md +156 -0
- package/templates/tanstack-start/docs/library/prisma/transactions.md +140 -0
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +146 -0
- package/templates/tanstack-start/docs/library/tanstack-query/invalidation.md +146 -0
- package/templates/tanstack-start/docs/library/tanstack-query/optimistic-updates.md +196 -0
- package/templates/tanstack-start/docs/library/tanstack-query/setup.md +110 -0
- package/templates/tanstack-start/docs/library/tanstack-query/use-mutation.md +170 -0
- package/templates/tanstack-start/docs/library/tanstack-query/use-query.md +173 -0
- package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +171 -0
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +114 -0
- package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +142 -0
- package/templates/tanstack-start/docs/library/tanstack-start/routing.md +163 -0
- package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +128 -0
- package/templates/tanstack-start/docs/library/tanstack-start/setup.md +85 -0
- package/templates/tanstack-start/docs/library/zod/basic-types.md +186 -0
- package/templates/tanstack-start/docs/library/zod/complex-types.md +204 -0
- package/templates/tanstack-start/docs/library/zod/index.md +186 -0
- package/templates/tanstack-start/docs/library/zod/transforms.md +174 -0
- package/templates/tanstack-start/docs/library/zod/validation.md +208 -0
- package/templates/tanstack-start/docs/mcp/context7.md +204 -0
- package/templates/tanstack-start/docs/mcp/index.md +116 -0
- package/templates/tanstack-start/docs/mcp/sequential-thinking.md +180 -0
- package/templates/tanstack-start/docs/mcp/sgrep.md +174 -0
- package/templates/tanstack-start/docs/skills/gemini-review/SKILL.md +220 -0
- package/templates/tanstack-start/docs/skills/gemini-review/references/checklists.md +150 -0
- package/templates/tanstack-start/docs/skills/gemini-review/references/prompt-templates.md +293 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Zod - 복합 타입
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [Zod](./index.md)
|
|
4
|
+
|
|
5
|
+
## 객체
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
const UserSchema = z.object({
|
|
9
|
+
name: z.string(),
|
|
10
|
+
email: z.email(), // v4 새 API
|
|
11
|
+
age: z.number().optional(), // 선택적 필드
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// 검증
|
|
15
|
+
const result = UserSchema.parse({
|
|
16
|
+
name: 'john',
|
|
17
|
+
email: 'john@example.com',
|
|
18
|
+
age: 25,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// 안전한 검증
|
|
22
|
+
const safeResult = UserSchema.safeParse(data)
|
|
23
|
+
if (safeResult.success) {
|
|
24
|
+
console.log(safeResult.data)
|
|
25
|
+
} else {
|
|
26
|
+
console.log(safeResult.error)
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 객체 메서드
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// 부분 객체 (모든 필드가 optional)
|
|
34
|
+
const PartialUser = UserSchema.partial()
|
|
35
|
+
|
|
36
|
+
// 필수 객체 (모든 필드가 required)
|
|
37
|
+
const RequiredUser = UserSchema.required()
|
|
38
|
+
|
|
39
|
+
// 필드 선택
|
|
40
|
+
const UserName = UserSchema.pick({ name: true })
|
|
41
|
+
|
|
42
|
+
// 필드 제외
|
|
43
|
+
const UserWithoutEmail = UserSchema.omit({ email: true })
|
|
44
|
+
|
|
45
|
+
// 확장
|
|
46
|
+
const ExtendedUser = UserSchema.extend({
|
|
47
|
+
role: z.enum(['admin', 'user']),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// 병합
|
|
51
|
+
const MergedSchema = UserSchema.merge(AnotherSchema)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Strict/Loose 객체 (v4)
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// v3
|
|
58
|
+
z.object({ name: z.string() }).strict() // 추가 키 에러
|
|
59
|
+
z.object({ name: z.string() }).passthrough() // 추가 키 통과
|
|
60
|
+
|
|
61
|
+
// v4 - 새로운 API
|
|
62
|
+
z.strictObject({ name: z.string() }) // 추가 키 에러
|
|
63
|
+
z.looseObject({ name: z.string() }) // 추가 키 통과
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 배열
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
z.array(z.string())
|
|
70
|
+
z.array(z.number()).min(1) // 최소 1개
|
|
71
|
+
z.array(z.number()).max(10) // 최대 10개
|
|
72
|
+
z.array(z.number()).length(5) // 정확히 5개
|
|
73
|
+
z.array(z.number()).nonempty() // 비어있지 않음
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 튜플
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const tuple = z.tuple([
|
|
80
|
+
z.string(), // 첫 번째 요소
|
|
81
|
+
z.number(), // 두 번째 요소
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
type Tuple = z.infer<typeof tuple>
|
|
85
|
+
// [string, number]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 유니온
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const StringOrNumber = z.union([z.string(), z.number()])
|
|
92
|
+
// 또는
|
|
93
|
+
const StringOrNumber = z.string().or(z.number())
|
|
94
|
+
|
|
95
|
+
type StringOrNumber = z.infer<typeof StringOrNumber>
|
|
96
|
+
// string | number
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Discriminated Union
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const Shape = z.discriminatedUnion('type', [
|
|
103
|
+
z.object({ type: z.literal('circle'), radius: z.number() }),
|
|
104
|
+
z.object({ type: z.literal('rectangle'), width: z.number(), height: z.number() }),
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
type Shape = z.infer<typeof Shape>
|
|
108
|
+
// { type: 'circle'; radius: number } | { type: 'rectangle'; width: number; height: number }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### v4 향상된 Discriminated Union
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// 유니온 및 파이프 discriminator 지원
|
|
115
|
+
const MyResult = z.discriminatedUnion("status", [
|
|
116
|
+
// 단순 리터럴
|
|
117
|
+
z.object({ status: z.literal("aaa"), data: z.string() }),
|
|
118
|
+
// 유니온 discriminator
|
|
119
|
+
z.object({ status: z.union([z.literal("bbb"), z.literal("ccc")]) }),
|
|
120
|
+
// 파이프 discriminator
|
|
121
|
+
z.object({ status: z.literal("fail").transform(val => val.toUpperCase()) }),
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
// 중첩 discriminated union
|
|
125
|
+
const BaseError = z.object({ status: z.literal("failed"), message: z.string() })
|
|
126
|
+
|
|
127
|
+
const MyResult2 = z.discriminatedUnion("status", [
|
|
128
|
+
z.object({ status: z.literal("success"), data: z.string() }),
|
|
129
|
+
z.discriminatedUnion("code", [
|
|
130
|
+
BaseError.extend({ code: z.literal(400) }),
|
|
131
|
+
BaseError.extend({ code: z.literal(401) }),
|
|
132
|
+
BaseError.extend({ code: z.literal(500) })
|
|
133
|
+
])
|
|
134
|
+
])
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Enum
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// 문자열 enum
|
|
141
|
+
const FishEnum = z.enum(['Salmon', 'Tuna', 'Trout'])
|
|
142
|
+
|
|
143
|
+
FishEnum.parse('Salmon') // => "Salmon"
|
|
144
|
+
FishEnum.parse('Swordfish') // => ❌ throws
|
|
145
|
+
|
|
146
|
+
type Fish = z.infer<typeof FishEnum>
|
|
147
|
+
// 'Salmon' | 'Tuna' | 'Trout'
|
|
148
|
+
|
|
149
|
+
// Native enum
|
|
150
|
+
enum Fruits {
|
|
151
|
+
Apple,
|
|
152
|
+
Banana,
|
|
153
|
+
}
|
|
154
|
+
const FruitSchema = z.nativeEnum(Fruits)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Record
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const UserStore = z.record(z.string(), z.object({
|
|
161
|
+
name: z.string(),
|
|
162
|
+
}))
|
|
163
|
+
|
|
164
|
+
type UserStore = z.infer<typeof UserStore>
|
|
165
|
+
// { [key: string]: { name: string } }
|
|
166
|
+
|
|
167
|
+
// 사용
|
|
168
|
+
const userStore: UserStore = {}
|
|
169
|
+
userStore['77d2586b-9e8e-4ecf-8b21-ea7e0530eadd'] = {
|
|
170
|
+
name: 'Carlotta',
|
|
171
|
+
} // passes
|
|
172
|
+
|
|
173
|
+
userStore['77d2586b-9e8e-4ecf-8b21-ea7e0530eadd'] = {
|
|
174
|
+
whatever: 'Ice cream sundae',
|
|
175
|
+
} // TypeError
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Map & Set
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// Map
|
|
182
|
+
const stringNumberMap = z.map(z.string(), z.number())
|
|
183
|
+
type StringNumberMap = z.infer<typeof stringNumberMap>
|
|
184
|
+
// Map<string, number>
|
|
185
|
+
|
|
186
|
+
// Set
|
|
187
|
+
const numberSet = z.set(z.number())
|
|
188
|
+
type NumberSet = z.infer<typeof numberSet>
|
|
189
|
+
// Set<number>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 재귀 스키마 (JSON)
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()])
|
|
196
|
+
type Literal = z.infer<typeof literalSchema>
|
|
197
|
+
type Json = Literal | { [key: string]: Json } | Json[]
|
|
198
|
+
|
|
199
|
+
const jsonSchema: z.ZodType<Json> = z.lazy(() =>
|
|
200
|
+
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
jsonSchema.parse({ foo: [1, 2, { bar: 'baz' }] })
|
|
204
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Zod
|
|
2
|
+
|
|
3
|
+
> **Version**: 4.x | TypeScript Schema Validation
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Quick Reference (복사용)
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// 기본 스키마
|
|
11
|
+
const schema = z.object({
|
|
12
|
+
email: z.email(), // v4 (string().email() 아님!)
|
|
13
|
+
name: z.string().min(1).trim(),
|
|
14
|
+
website: z.url().optional(), // v4 (string().url() 아님!)
|
|
15
|
+
age: z.number().int().positive(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// 타입 추출
|
|
19
|
+
type Input = z.infer<typeof schema>
|
|
20
|
+
|
|
21
|
+
// 파싱
|
|
22
|
+
schema.parse(data) // 실패 시 throw
|
|
23
|
+
schema.safeParse(data) // { success, data/error }
|
|
24
|
+
|
|
25
|
+
// TanStack Start 연동
|
|
26
|
+
export const createUser = createServerFn({ method: 'POST' })
|
|
27
|
+
.inputValidator(schema)
|
|
28
|
+
.handler(async ({ data }) => prisma.user.create({ data }))
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### v4 새 API (⚠️ 중요)
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// ✅ v4 사용
|
|
35
|
+
z.email()
|
|
36
|
+
z.url()
|
|
37
|
+
z.uuid()
|
|
38
|
+
z.iso.date()
|
|
39
|
+
z.iso.datetime()
|
|
40
|
+
|
|
41
|
+
// ❌ v3 (deprecated)
|
|
42
|
+
z.string().email()
|
|
43
|
+
z.string().url()
|
|
44
|
+
```
|
|
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
|
+
### 기본 사용법
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { z } from 'zod'
|
|
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 파라미터로 통합
|
|
105
|
+
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
|
+
|
|
148
|
+
### Refinements가 스키마 내부에 저장
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// v3 - ZodEffects로 래핑되어 .min() 호출 불가
|
|
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
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## 참고 자료
|
|
184
|
+
|
|
185
|
+
- [Zod 공식 문서](https://zod.dev)
|
|
186
|
+
- [Zod GitHub](https://github.com/colinhacks/zod)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Zod - 변환
|
|
2
|
+
|
|
3
|
+
> **상위 문서**: [Zod](./index.md)
|
|
4
|
+
|
|
5
|
+
## Transform
|
|
6
|
+
|
|
7
|
+
입력값을 다른 형태로 변환합니다.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const stringToLength = z.string().transform((val) => val.length)
|
|
11
|
+
|
|
12
|
+
stringToLength.parse('string') // => 6
|
|
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
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 검증 후 변환
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const stringToNumber = z.string()
|
|
40
|
+
.transform((val) => parseInt(val, 10))
|
|
41
|
+
.pipe(z.number().min(0).max(100))
|
|
42
|
+
|
|
43
|
+
stringToNumber.parse('50') // => 50
|
|
44
|
+
stringToNumber.parse('150') // => throws
|
|
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
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Coerce (강제 변환)
|
|
66
|
+
|
|
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
|
+
```typescript
|
|
80
|
+
const schema = z.coerce.string()
|
|
81
|
+
type schemaInput = z.input<typeof schema>
|
|
82
|
+
// v3: string
|
|
83
|
+
// v4: unknown // ⚠️ 주의: 입력 타입이 unknown으로 변경됨
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Coerce 예시
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// string coercion
|
|
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
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 환경변수 불리언 (v4 권장)
|
|
110
|
+
|
|
111
|
+
`z.coerce.boolean()` 대신 더 정교한 `z.stringbool()` 사용:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const strbool = z.stringbool()
|
|
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
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Preprocess
|
|
125
|
+
|
|
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
|
+
```typescript
|
|
138
|
+
// preprocess - 커스텀 로직 가능
|
|
139
|
+
const trimmedString = z.preprocess(
|
|
140
|
+
(val) => typeof val === 'string' ? val.trim() : val,
|
|
141
|
+
z.string()
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// coerce - 내장 변환만
|
|
145
|
+
const coercedString = z.coerce.string()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## 입력/출력 타입 분리
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const Schema = z.object({
|
|
152
|
+
createdAt: z.string().transform((str) => new Date(str)),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
type SchemaInput = z.input<typeof Schema>
|
|
156
|
+
// { createdAt: string }
|
|
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
|
|
174
|
+
```
|