@mandujs/core 0.12.2 → 0.13.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 (173) hide show
  1. package/README.ko.md +304 -304
  2. package/README.md +653 -653
  3. package/package.json +1 -1
  4. package/src/brain/architecture/analyzer.ts +28 -26
  5. package/src/brain/doctor/analyzer.ts +1 -1
  6. package/src/bundler/build.ts +91 -91
  7. package/src/bundler/css.ts +302 -302
  8. package/src/bundler/dev.ts +0 -1
  9. package/src/change/history.ts +3 -3
  10. package/src/change/snapshot.ts +10 -9
  11. package/src/change/transaction.ts +2 -2
  12. package/src/client/Link.tsx +227 -227
  13. package/src/client/globals.ts +44 -44
  14. package/src/client/hooks.ts +267 -267
  15. package/src/client/index.ts +5 -5
  16. package/src/client/island.ts +8 -8
  17. package/src/client/router.ts +435 -435
  18. package/src/client/runtime.ts +23 -23
  19. package/src/client/serialize.ts +404 -404
  20. package/src/client/window-state.ts +101 -101
  21. package/src/config/mandu.ts +94 -96
  22. package/src/config/validate.ts +213 -215
  23. package/src/config/watcher.ts +311 -311
  24. package/src/constants.ts +40 -40
  25. package/src/content/content-layer.ts +314 -314
  26. package/src/content/content.test.ts +433 -433
  27. package/src/content/data-store.ts +245 -245
  28. package/src/content/digest.ts +133 -133
  29. package/src/content/index.ts +164 -164
  30. package/src/content/loader-context.ts +172 -172
  31. package/src/content/loaders/api.ts +216 -216
  32. package/src/content/loaders/file.ts +169 -169
  33. package/src/content/loaders/glob.ts +252 -252
  34. package/src/content/loaders/index.ts +34 -34
  35. package/src/content/loaders/types.ts +137 -137
  36. package/src/content/meta-store.ts +209 -209
  37. package/src/content/types.ts +282 -282
  38. package/src/content/watcher.ts +135 -135
  39. package/src/contract/client-safe.test.ts +42 -42
  40. package/src/contract/client-safe.ts +114 -114
  41. package/src/contract/client.ts +16 -16
  42. package/src/contract/define.ts +459 -459
  43. package/src/contract/handler.ts +10 -10
  44. package/src/contract/normalize.test.ts +276 -276
  45. package/src/contract/normalize.ts +404 -404
  46. package/src/contract/registry.test.ts +206 -206
  47. package/src/contract/registry.ts +568 -568
  48. package/src/contract/schema.ts +48 -48
  49. package/src/contract/types.ts +58 -58
  50. package/src/contract/validator.ts +32 -32
  51. package/src/devtools/ai/context-builder.ts +375 -375
  52. package/src/devtools/ai/index.ts +25 -25
  53. package/src/devtools/ai/mcp-connector.ts +465 -465
  54. package/src/devtools/client/catchers/error-catcher.ts +327 -327
  55. package/src/devtools/client/catchers/index.ts +18 -18
  56. package/src/devtools/client/catchers/network-proxy.ts +363 -363
  57. package/src/devtools/client/components/index.ts +39 -39
  58. package/src/devtools/client/components/kitchen-root.tsx +362 -362
  59. package/src/devtools/client/components/mandu-character.tsx +241 -241
  60. package/src/devtools/client/components/overlay.tsx +368 -368
  61. package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
  62. package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
  63. package/src/devtools/client/components/panel/index.ts +32 -32
  64. package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
  65. package/src/devtools/client/components/panel/network-panel.tsx +292 -292
  66. package/src/devtools/client/components/panel/panel-container.tsx +259 -259
  67. package/src/devtools/client/filters/context-filters.ts +282 -282
  68. package/src/devtools/client/filters/index.ts +16 -16
  69. package/src/devtools/client/index.ts +63 -63
  70. package/src/devtools/client/persistence.ts +335 -335
  71. package/src/devtools/client/state-manager.ts +478 -478
  72. package/src/devtools/design-tokens.ts +263 -263
  73. package/src/devtools/hook/create-hook.ts +207 -207
  74. package/src/devtools/hook/index.ts +13 -13
  75. package/src/devtools/index.ts +439 -439
  76. package/src/devtools/init.ts +266 -266
  77. package/src/devtools/protocol.ts +237 -237
  78. package/src/devtools/server/index.ts +17 -17
  79. package/src/devtools/server/source-context.ts +444 -444
  80. package/src/devtools/types.ts +319 -319
  81. package/src/devtools/worker/index.ts +25 -25
  82. package/src/devtools/worker/redaction-worker.ts +222 -222
  83. package/src/devtools/worker/worker-manager.ts +409 -409
  84. package/src/error/classifier.ts +2 -2
  85. package/src/error/domains.ts +265 -265
  86. package/src/error/formatter.ts +32 -32
  87. package/src/error/result.ts +46 -46
  88. package/src/error/stack-analyzer.ts +5 -0
  89. package/src/error/types.ts +6 -6
  90. package/src/errors/extractor.ts +409 -409
  91. package/src/errors/index.ts +19 -19
  92. package/src/filling/auth.ts +308 -308
  93. package/src/filling/context.ts +569 -569
  94. package/src/filling/deps.ts +238 -238
  95. package/src/generator/contract-glue.ts +2 -1
  96. package/src/generator/generate.ts +12 -10
  97. package/src/generator/index.ts +3 -3
  98. package/src/generator/templates.ts +80 -79
  99. package/src/guard/analyzer.ts +360 -360
  100. package/src/guard/ast-analyzer.ts +806 -806
  101. package/src/guard/auto-correct.ts +1 -1
  102. package/src/guard/check.ts +128 -128
  103. package/src/guard/contract-guard.ts +9 -9
  104. package/src/guard/file-type.test.ts +24 -24
  105. package/src/guard/presets/atomic.ts +70 -70
  106. package/src/guard/presets/clean.ts +77 -77
  107. package/src/guard/presets/cqrs.test.ts +35 -14
  108. package/src/guard/presets/fsd.ts +79 -79
  109. package/src/guard/presets/hexagonal.ts +68 -68
  110. package/src/guard/presets/index.ts +291 -291
  111. package/src/guard/reporter.ts +445 -445
  112. package/src/guard/rules.ts +12 -12
  113. package/src/guard/statistics.ts +578 -578
  114. package/src/guard/suggestions.ts +358 -358
  115. package/src/guard/types.ts +348 -348
  116. package/src/guard/validator.ts +834 -834
  117. package/src/guard/watcher.ts +404 -404
  118. package/src/index.ts +1 -0
  119. package/src/intent/index.ts +310 -310
  120. package/src/island/index.ts +304 -304
  121. package/src/logging/index.ts +22 -22
  122. package/src/logging/transports.ts +365 -365
  123. package/src/paths.test.ts +47 -0
  124. package/src/paths.ts +47 -0
  125. package/src/plugins/index.ts +38 -38
  126. package/src/plugins/registry.ts +377 -377
  127. package/src/plugins/types.ts +363 -363
  128. package/src/report/build.ts +1 -1
  129. package/src/report/index.ts +1 -1
  130. package/src/router/fs-patterns.ts +387 -387
  131. package/src/router/fs-routes.ts +344 -401
  132. package/src/router/fs-scanner.ts +497 -497
  133. package/src/router/fs-types.ts +270 -278
  134. package/src/router/index.ts +81 -81
  135. package/src/runtime/boundary.tsx +232 -232
  136. package/src/runtime/compose.ts +222 -222
  137. package/src/runtime/lifecycle.ts +381 -381
  138. package/src/runtime/logger.test.ts +345 -345
  139. package/src/runtime/logger.ts +677 -677
  140. package/src/runtime/router.test.ts +476 -476
  141. package/src/runtime/router.ts +105 -105
  142. package/src/runtime/security.ts +155 -155
  143. package/src/runtime/server.ts +24 -24
  144. package/src/runtime/session-key.ts +328 -328
  145. package/src/runtime/ssr.ts +367 -367
  146. package/src/runtime/streaming-ssr.ts +1245 -1245
  147. package/src/runtime/trace.ts +144 -144
  148. package/src/seo/index.ts +214 -214
  149. package/src/seo/integration/ssr.ts +307 -307
  150. package/src/seo/render/basic.ts +427 -427
  151. package/src/seo/render/index.ts +143 -143
  152. package/src/seo/render/jsonld.ts +539 -539
  153. package/src/seo/render/opengraph.ts +191 -191
  154. package/src/seo/render/robots.ts +116 -116
  155. package/src/seo/render/sitemap.ts +137 -137
  156. package/src/seo/render/twitter.ts +126 -126
  157. package/src/seo/resolve/index.ts +353 -353
  158. package/src/seo/resolve/opengraph.ts +143 -143
  159. package/src/seo/resolve/robots.ts +73 -73
  160. package/src/seo/resolve/title.ts +94 -94
  161. package/src/seo/resolve/twitter.ts +73 -73
  162. package/src/seo/resolve/url.ts +97 -97
  163. package/src/seo/routes/index.ts +290 -290
  164. package/src/seo/types.ts +575 -575
  165. package/src/slot/validator.ts +39 -39
  166. package/src/spec/index.ts +3 -3
  167. package/src/spec/load.ts +76 -76
  168. package/src/spec/lock.ts +56 -56
  169. package/src/utils/bun.ts +8 -8
  170. package/src/utils/lru-cache.ts +75 -75
  171. package/src/utils/safe-io.ts +188 -188
  172. package/src/utils/string-safe.ts +298 -298
  173. package/src/watcher/rules.ts +5 -5
@@ -1,404 +1,404 @@
1
- /**
2
- * Mandu Schema Normalization
3
- * 스키마 기반 데이터 정규화 (보안 + 타입 안전성)
4
- *
5
- * 기능:
6
- * - Strip: 정의되지 않은 필드 제거 (Mass Assignment 방지)
7
- * - Strict: 정의되지 않은 필드 있으면 에러
8
- * - Coerce: 타입 자동 변환 (문자열 → 숫자 등)
9
- *
10
- * @example
11
- * ```typescript
12
- * import { normalizeData, NormalizeMode } from "@mandujs/core/contract";
13
- *
14
- * const schema = z.object({ name: z.string(), age: z.number() });
15
- * const input = { name: "Kim", age: 25, admin: true };
16
- *
17
- * // Strip 모드: 정의된 필드만 추출
18
- * const result = normalizeData(schema, input, { mode: "strip" });
19
- * // { name: "Kim", age: 25 }
20
- * ```
21
- */
22
-
23
- import { z, type ZodTypeAny, type ZodObject, type ZodRawShape } from "zod";
24
-
25
- /**
26
- * 정규화 모드
27
- * - strip: 정의되지 않은 필드 제거 (기본값, 권장)
28
- * - strict: 정의되지 않은 필드 있으면 에러
29
- * - passthrough: 모든 필드 허용 (정규화 안 함)
30
- */
31
- export type NormalizeMode = "strip" | "strict" | "passthrough";
32
-
33
- /**
34
- * 정규화 옵션
35
- */
36
- export interface NormalizeOptions {
37
- /**
38
- * 정규화 모드
39
- * @default "strip"
40
- */
41
- mode?: NormalizeMode;
42
-
43
- /**
44
- * Query/Params의 타입 자동 변환 (coerce) 활성화
45
- * URL의 query string과 path params는 항상 문자열이므로
46
- * 스키마에 정의된 타입으로 자동 변환
47
- * @default true
48
- */
49
- coerceQueryParams?: boolean;
50
-
51
- /**
52
- * 깊은 정규화 (중첩 객체에도 적용)
53
- * @default true
54
- */
55
- deep?: boolean;
56
- }
57
-
58
- /**
59
- * 전역 기본 정규화 옵션
60
- */
61
- const DEFAULT_OPTIONS: Required<NormalizeOptions> = {
62
- mode: "strip",
63
- coerceQueryParams: true,
64
- deep: true,
65
- };
66
-
67
- /**
68
- * 전역 옵션 설정
69
- */
70
- let globalOptions: Required<NormalizeOptions> = { ...DEFAULT_OPTIONS };
71
-
72
- /**
73
- * 전역 정규화 옵션 설정
74
- *
75
- * @example
76
- * ```typescript
77
- * setNormalizeOptions({
78
- * mode: "strict",
79
- * coerceQueryParams: true,
80
- * });
81
- * ```
82
- */
83
- export function setNormalizeOptions(options: NormalizeOptions): void {
84
- globalOptions = { ...DEFAULT_OPTIONS, ...options };
85
- }
86
-
87
- /**
88
- * 현재 전역 옵션 조회
89
- */
90
- export function getNormalizeOptions(): Required<NormalizeOptions> {
91
- return { ...globalOptions };
92
- }
93
-
94
- /**
95
- * 전역 옵션 초기화
96
- */
97
- export function resetNormalizeOptions(): void {
98
- globalOptions = { ...DEFAULT_OPTIONS };
99
- }
100
-
101
- /**
102
- * ZodObject 스키마에 정규화 모드 적용
103
- *
104
- * @param schema - Zod 객체 스키마
105
- * @param mode - 정규화 모드
106
- * @returns 모드가 적용된 스키마
107
- */
108
- export function applyNormalizeMode<T extends ZodRawShape>(
109
- schema: ZodObject<T>,
110
- mode: NormalizeMode
111
- ): ZodObject<T> {
112
- switch (mode) {
113
- case "strip":
114
- return schema.strip();
115
- case "strict":
116
- return schema.strict();
117
- case "passthrough":
118
- return schema.passthrough();
119
- default:
120
- return schema.strip();
121
- }
122
- }
123
-
124
- /**
125
- * 스키마가 ZodObject인지 확인
126
- */
127
- function isZodObject(schema: ZodTypeAny): schema is ZodObject<ZodRawShape> {
128
- return schema instanceof z.ZodObject;
129
- }
130
-
131
- /**
132
- * 스키마에 정규화 적용
133
- * ZodObject가 아닌 경우 원본 반환
134
- *
135
- * @param schema - Zod 스키마
136
- * @param options - 정규화 옵션
137
- * @returns 정규화된 스키마
138
- */
139
- export function normalizeSchema<T extends ZodTypeAny>(
140
- schema: T,
141
- options?: NormalizeOptions
142
- ): T {
143
- const opts = { ...globalOptions, ...options };
144
-
145
- if (!isZodObject(schema)) {
146
- return schema;
147
- }
148
-
149
- return applyNormalizeMode(schema, opts.mode) as T;
150
- }
151
-
152
- /**
153
- * 데이터 정규화 실행
154
- * 스키마에 정의된 필드만 추출하고 타입 변환
155
- *
156
- * @param schema - Zod 스키마
157
- * @param data - 입력 데이터
158
- * @param options - 정규화 옵션
159
- * @returns 정규화된 데이터
160
- * @throws ZodError - 검증 실패 시
161
- *
162
- * @example
163
- * ```typescript
164
- * const schema = z.object({ name: z.string(), age: z.number() });
165
- *
166
- * // Strip 모드 (기본)
167
- * normalizeData(schema, { name: "Kim", age: 25, admin: true });
168
- * // → { name: "Kim", age: 25 }
169
- *
170
- * // Strict 모드
171
- * normalizeData(schema, { name: "Kim", age: 25, admin: true }, { mode: "strict" });
172
- * // → ZodError: Unrecognized key(s) in object: 'admin'
173
- * ```
174
- */
175
- export function normalizeData<T extends ZodTypeAny>(
176
- schema: T,
177
- data: unknown,
178
- options?: NormalizeOptions
179
- ): z.infer<T> {
180
- const normalizedSchema = normalizeSchema(schema, options);
181
- return normalizedSchema.parse(data);
182
- }
183
-
184
- /**
185
- * 안전한 데이터 정규화 (에러 시 null 반환)
186
- *
187
- * @param schema - Zod 스키마
188
- * @param data - 입력 데이터
189
- * @param options - 정규화 옵션
190
- * @returns 정규화 결과
191
- */
192
- export function safeNormalizeData<T extends ZodTypeAny>(
193
- schema: T,
194
- data: unknown,
195
- options?: NormalizeOptions
196
- ): {
197
- success: true;
198
- data: z.infer<T>;
199
- } | {
200
- success: false;
201
- error: z.ZodError;
202
- } {
203
- const normalizedSchema = normalizeSchema(schema, options);
204
- const result = normalizedSchema.safeParse(data);
205
-
206
- if (result.success) {
207
- return { success: true, data: result.data };
208
- }
209
-
210
- return { success: false, error: result.error };
211
- }
212
-
213
- /**
214
- * Query/Params용 coerce 스키마 생성
215
- * URL에서 오는 값은 항상 문자열이므로 자동 변환 필요
216
- *
217
- * @example
218
- * ```typescript
219
- * // 원본 스키마
220
- * const schema = z.object({
221
- * page: z.number(),
222
- * active: z.boolean(),
223
- * });
224
- *
225
- * // Coerce 적용
226
- * const coercedSchema = createCoerceSchema(schema);
227
- * coercedSchema.parse({ page: "1", active: "true" });
228
- * // → { page: 1, active: true }
229
- * ```
230
- */
231
- export function createCoerceSchema<T extends ZodRawShape>(
232
- schema: ZodObject<T>
233
- ): ZodObject<T> {
234
- const shape = schema.shape;
235
- const coercedShape: Record<string, ZodTypeAny> = {};
236
-
237
- for (const [key, value] of Object.entries(shape)) {
238
- coercedShape[key] = applyCoercion(value as ZodTypeAny);
239
- }
240
-
241
- return z.object(coercedShape as T);
242
- }
243
-
244
- /**
245
- * 단일 스키마에 coercion 적용
246
- */
247
- function applyCoercion(schema: ZodTypeAny): ZodTypeAny {
248
- // ZodNumber → z.coerce.number()
249
- if (schema instanceof z.ZodNumber) {
250
- let coerced = z.coerce.number();
251
- // 기존 체크 유지 (min, max 등)
252
- const checks = (schema as any)._def.checks || [];
253
- for (const check of checks) {
254
- switch (check.kind) {
255
- case "min":
256
- coerced = check.inclusive
257
- ? coerced.gte(check.value)
258
- : coerced.gt(check.value);
259
- break;
260
- case "max":
261
- coerced = check.inclusive
262
- ? coerced.lte(check.value)
263
- : coerced.lt(check.value);
264
- break;
265
- case "int":
266
- coerced = coerced.int();
267
- break;
268
- }
269
- }
270
- return coerced;
271
- }
272
-
273
- // ZodBoolean → z.coerce.boolean() 또는 커스텀 변환
274
- if (schema instanceof z.ZodBoolean) {
275
- // "true", "false", "1", "0" 처리
276
- return z.preprocess((val) => {
277
- if (typeof val === "string") {
278
- if (val === "true" || val === "1") return true;
279
- if (val === "false" || val === "0") return false;
280
- }
281
- return val;
282
- }, z.boolean());
283
- }
284
-
285
- // ZodBigInt → z.coerce.bigint()
286
- if (schema instanceof z.ZodBigInt) {
287
- return z.coerce.bigint();
288
- }
289
-
290
- // ZodDate → z.coerce.date()
291
- if (schema instanceof z.ZodDate) {
292
- return z.coerce.date();
293
- }
294
-
295
- // ZodOptional → 내부 스키마에 coercion 적용
296
- if (schema instanceof z.ZodOptional) {
297
- return applyCoercion((schema as any)._def.innerType).optional();
298
- }
299
-
300
- // ZodDefault → 내부 스키마에 coercion 적용
301
- if (schema instanceof z.ZodDefault) {
302
- const inner = applyCoercion((schema as any)._def.innerType);
303
- return inner.default((schema as any)._def.defaultValue());
304
- }
305
-
306
- // ZodNullable → 내부 스키마에 coercion 적용
307
- if (schema instanceof z.ZodNullable) {
308
- return applyCoercion((schema as any)._def.innerType).nullable();
309
- }
310
-
311
- // ZodArray → 배열 요소에 coercion 적용 (쿼리스트링 배열)
312
- if (schema instanceof z.ZodArray) {
313
- return z.array(applyCoercion((schema as any)._def.type));
314
- }
315
-
316
- // 그 외는 원본 반환
317
- return schema;
318
- }
319
-
320
- /**
321
- * Request 데이터 전체 정규화
322
- * query, body, params, headers 각각에 적절한 정규화 적용
323
- */
324
- export interface NormalizedRequestData {
325
- query?: unknown;
326
- body?: unknown;
327
- params?: unknown;
328
- headers?: unknown;
329
- }
330
-
331
- export interface RequestSchemas {
332
- query?: ZodTypeAny;
333
- body?: ZodTypeAny;
334
- params?: ZodTypeAny;
335
- headers?: ZodTypeAny;
336
- }
337
-
338
- /**
339
- * Request 데이터 정규화
340
- * - query/params: coerce 적용 (문자열 → 숫자 등)
341
- * - body: strip/strict 모드 적용
342
- * - headers: 그대로 검증
343
- *
344
- * @param schemas - 각 필드별 스키마
345
- * @param data - 원본 데이터
346
- * @param options - 정규화 옵션
347
- * @returns 정규화된 데이터
348
- */
349
- export function normalizeRequestData(
350
- schemas: RequestSchemas,
351
- data: NormalizedRequestData,
352
- options?: NormalizeOptions
353
- ): NormalizedRequestData {
354
- const opts = { ...globalOptions, ...options };
355
- const result: NormalizedRequestData = {};
356
-
357
- // Query: coerce + strip
358
- if (schemas.query && data.query !== undefined) {
359
- let querySchema = schemas.query;
360
-
361
- // coerce 적용
362
- if (opts.coerceQueryParams && isZodObject(querySchema)) {
363
- querySchema = createCoerceSchema(querySchema);
364
- }
365
-
366
- // strip/strict 적용
367
- querySchema = normalizeSchema(querySchema, opts);
368
-
369
- result.query = querySchema.parse(data.query);
370
- }
371
-
372
- // Params: coerce + strip
373
- if (schemas.params && data.params !== undefined) {
374
- let paramsSchema = schemas.params;
375
-
376
- // coerce 적용
377
- if (opts.coerceQueryParams && isZodObject(paramsSchema)) {
378
- paramsSchema = createCoerceSchema(paramsSchema);
379
- }
380
-
381
- // strip/strict 적용
382
- paramsSchema = normalizeSchema(paramsSchema, opts);
383
-
384
- result.params = paramsSchema.parse(data.params);
385
- }
386
-
387
- // Body: strip/strict만 적용 (coerce 안 함 - JSON은 타입 보존)
388
- if (schemas.body && data.body !== undefined) {
389
- const bodySchema = normalizeSchema(schemas.body, opts);
390
- result.body = bodySchema.parse(data.body);
391
- }
392
-
393
- // Headers: 검증만 (정규화 안 함)
394
- if (schemas.headers && data.headers !== undefined) {
395
- result.headers = schemas.headers.parse(data.headers);
396
- }
397
-
398
- return result;
399
- }
400
-
401
- /**
402
- * 타입 유틸리티: 정규화된 데이터 타입 추론
403
- */
404
- export type NormalizedData<T extends ZodTypeAny> = z.infer<T>;
1
+ /**
2
+ * Mandu Schema Normalization
3
+ * 스키마 기반 데이터 정규화 (보안 + 타입 안전성)
4
+ *
5
+ * 기능:
6
+ * - Strip: 정의되지 않은 필드 제거 (Mass Assignment 방지)
7
+ * - Strict: 정의되지 않은 필드 있으면 에러
8
+ * - Coerce: 타입 자동 변환 (문자열 → 숫자 등)
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { normalizeData, NormalizeMode } from "@mandujs/core/contract";
13
+ *
14
+ * const schema = z.object({ name: z.string(), age: z.number() });
15
+ * const input = { name: "Kim", age: 25, admin: true };
16
+ *
17
+ * // Strip 모드: 정의된 필드만 추출
18
+ * const result = normalizeData(schema, input, { mode: "strip" });
19
+ * // { name: "Kim", age: 25 }
20
+ * ```
21
+ */
22
+
23
+ import { z, type ZodTypeAny, type ZodObject, type ZodRawShape } from "zod";
24
+
25
+ /**
26
+ * 정규화 모드
27
+ * - strip: 정의되지 않은 필드 제거 (기본값, 권장)
28
+ * - strict: 정의되지 않은 필드 있으면 에러
29
+ * - passthrough: 모든 필드 허용 (정규화 안 함)
30
+ */
31
+ export type NormalizeMode = "strip" | "strict" | "passthrough";
32
+
33
+ /**
34
+ * 정규화 옵션
35
+ */
36
+ export interface NormalizeOptions {
37
+ /**
38
+ * 정규화 모드
39
+ * @default "strip"
40
+ */
41
+ mode?: NormalizeMode;
42
+
43
+ /**
44
+ * Query/Params의 타입 자동 변환 (coerce) 활성화
45
+ * URL의 query string과 path params는 항상 문자열이므로
46
+ * 스키마에 정의된 타입으로 자동 변환
47
+ * @default true
48
+ */
49
+ coerceQueryParams?: boolean;
50
+
51
+ /**
52
+ * 깊은 정규화 (중첩 객체에도 적용)
53
+ * @default true
54
+ */
55
+ deep?: boolean;
56
+ }
57
+
58
+ /**
59
+ * 전역 기본 정규화 옵션
60
+ */
61
+ const DEFAULT_OPTIONS: Required<NormalizeOptions> = {
62
+ mode: "strip",
63
+ coerceQueryParams: true,
64
+ deep: true,
65
+ };
66
+
67
+ /**
68
+ * 전역 옵션 설정
69
+ */
70
+ let globalOptions: Required<NormalizeOptions> = { ...DEFAULT_OPTIONS };
71
+
72
+ /**
73
+ * 전역 정규화 옵션 설정
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * setNormalizeOptions({
78
+ * mode: "strict",
79
+ * coerceQueryParams: true,
80
+ * });
81
+ * ```
82
+ */
83
+ export function setNormalizeOptions(options: NormalizeOptions): void {
84
+ globalOptions = { ...DEFAULT_OPTIONS, ...options };
85
+ }
86
+
87
+ /**
88
+ * 현재 전역 옵션 조회
89
+ */
90
+ export function getNormalizeOptions(): Required<NormalizeOptions> {
91
+ return { ...globalOptions };
92
+ }
93
+
94
+ /**
95
+ * 전역 옵션 초기화
96
+ */
97
+ export function resetNormalizeOptions(): void {
98
+ globalOptions = { ...DEFAULT_OPTIONS };
99
+ }
100
+
101
+ /**
102
+ * ZodObject 스키마에 정규화 모드 적용
103
+ *
104
+ * @param schema - Zod 객체 스키마
105
+ * @param mode - 정규화 모드
106
+ * @returns 모드가 적용된 스키마
107
+ */
108
+ export function applyNormalizeMode<T extends ZodRawShape>(
109
+ schema: ZodObject<T>,
110
+ mode: NormalizeMode
111
+ ): ZodObject<T> {
112
+ switch (mode) {
113
+ case "strip":
114
+ return schema.strip();
115
+ case "strict":
116
+ return schema.strict();
117
+ case "passthrough":
118
+ return schema.passthrough();
119
+ default:
120
+ return schema.strip();
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 스키마가 ZodObject인지 확인
126
+ */
127
+ function isZodObject(schema: ZodTypeAny): schema is ZodObject<ZodRawShape> {
128
+ return schema instanceof z.ZodObject;
129
+ }
130
+
131
+ /**
132
+ * 스키마에 정규화 적용
133
+ * ZodObject가 아닌 경우 원본 반환
134
+ *
135
+ * @param schema - Zod 스키마
136
+ * @param options - 정규화 옵션
137
+ * @returns 정규화된 스키마
138
+ */
139
+ export function normalizeSchema<T extends ZodTypeAny>(
140
+ schema: T,
141
+ options?: NormalizeOptions
142
+ ): T {
143
+ const opts = { ...globalOptions, ...options };
144
+
145
+ if (!isZodObject(schema)) {
146
+ return schema;
147
+ }
148
+
149
+ return applyNormalizeMode(schema, opts.mode) as T;
150
+ }
151
+
152
+ /**
153
+ * 데이터 정규화 실행
154
+ * 스키마에 정의된 필드만 추출하고 타입 변환
155
+ *
156
+ * @param schema - Zod 스키마
157
+ * @param data - 입력 데이터
158
+ * @param options - 정규화 옵션
159
+ * @returns 정규화된 데이터
160
+ * @throws ZodError - 검증 실패 시
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * const schema = z.object({ name: z.string(), age: z.number() });
165
+ *
166
+ * // Strip 모드 (기본)
167
+ * normalizeData(schema, { name: "Kim", age: 25, admin: true });
168
+ * // → { name: "Kim", age: 25 }
169
+ *
170
+ * // Strict 모드
171
+ * normalizeData(schema, { name: "Kim", age: 25, admin: true }, { mode: "strict" });
172
+ * // → ZodError: Unrecognized key(s) in object: 'admin'
173
+ * ```
174
+ */
175
+ export function normalizeData<T extends ZodTypeAny>(
176
+ schema: T,
177
+ data: unknown,
178
+ options?: NormalizeOptions
179
+ ): z.infer<T> {
180
+ const normalizedSchema = normalizeSchema(schema, options);
181
+ return normalizedSchema.parse(data);
182
+ }
183
+
184
+ /**
185
+ * 안전한 데이터 정규화 (에러 시 null 반환)
186
+ *
187
+ * @param schema - Zod 스키마
188
+ * @param data - 입력 데이터
189
+ * @param options - 정규화 옵션
190
+ * @returns 정규화 결과
191
+ */
192
+ export function safeNormalizeData<T extends ZodTypeAny>(
193
+ schema: T,
194
+ data: unknown,
195
+ options?: NormalizeOptions
196
+ ): {
197
+ success: true;
198
+ data: z.infer<T>;
199
+ } | {
200
+ success: false;
201
+ error: z.ZodError;
202
+ } {
203
+ const normalizedSchema = normalizeSchema(schema, options);
204
+ const result = normalizedSchema.safeParse(data);
205
+
206
+ if (result.success) {
207
+ return { success: true, data: result.data };
208
+ }
209
+
210
+ return { success: false, error: result.error };
211
+ }
212
+
213
+ /**
214
+ * Query/Params용 coerce 스키마 생성
215
+ * URL에서 오는 값은 항상 문자열이므로 자동 변환 필요
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * // 원본 스키마
220
+ * const schema = z.object({
221
+ * page: z.number(),
222
+ * active: z.boolean(),
223
+ * });
224
+ *
225
+ * // Coerce 적용
226
+ * const coercedSchema = createCoerceSchema(schema);
227
+ * coercedSchema.parse({ page: "1", active: "true" });
228
+ * // → { page: 1, active: true }
229
+ * ```
230
+ */
231
+ export function createCoerceSchema<T extends ZodRawShape>(
232
+ schema: ZodObject<T>
233
+ ): ZodObject<T> {
234
+ const shape = schema.shape;
235
+ const coercedShape: Record<string, ZodTypeAny> = {};
236
+
237
+ for (const [key, value] of Object.entries(shape)) {
238
+ coercedShape[key] = applyCoercion(value as ZodTypeAny);
239
+ }
240
+
241
+ return z.object(coercedShape as T);
242
+ }
243
+
244
+ /**
245
+ * 단일 스키마에 coercion 적용
246
+ */
247
+ function applyCoercion(schema: ZodTypeAny): ZodTypeAny {
248
+ // ZodNumber → z.coerce.number()
249
+ if (schema instanceof z.ZodNumber) {
250
+ let coerced = z.coerce.number();
251
+ // 기존 체크 유지 (min, max 등)
252
+ const checks = (schema as any)._def.checks || [];
253
+ for (const check of checks) {
254
+ switch (check.kind) {
255
+ case "min":
256
+ coerced = check.inclusive
257
+ ? coerced.gte(check.value)
258
+ : coerced.gt(check.value);
259
+ break;
260
+ case "max":
261
+ coerced = check.inclusive
262
+ ? coerced.lte(check.value)
263
+ : coerced.lt(check.value);
264
+ break;
265
+ case "int":
266
+ coerced = coerced.int();
267
+ break;
268
+ }
269
+ }
270
+ return coerced;
271
+ }
272
+
273
+ // ZodBoolean → z.coerce.boolean() 또는 커스텀 변환
274
+ if (schema instanceof z.ZodBoolean) {
275
+ // "true", "false", "1", "0" 처리
276
+ return z.preprocess((val) => {
277
+ if (typeof val === "string") {
278
+ if (val === "true" || val === "1") return true;
279
+ if (val === "false" || val === "0") return false;
280
+ }
281
+ return val;
282
+ }, z.boolean());
283
+ }
284
+
285
+ // ZodBigInt → z.coerce.bigint()
286
+ if (schema instanceof z.ZodBigInt) {
287
+ return z.coerce.bigint();
288
+ }
289
+
290
+ // ZodDate → z.coerce.date()
291
+ if (schema instanceof z.ZodDate) {
292
+ return z.coerce.date();
293
+ }
294
+
295
+ // ZodOptional → 내부 스키마에 coercion 적용
296
+ if (schema instanceof z.ZodOptional) {
297
+ return applyCoercion((schema as any)._def.innerType).optional();
298
+ }
299
+
300
+ // ZodDefault → 내부 스키마에 coercion 적용
301
+ if (schema instanceof z.ZodDefault) {
302
+ const inner = applyCoercion((schema as any)._def.innerType);
303
+ return inner.default((schema as any)._def.defaultValue());
304
+ }
305
+
306
+ // ZodNullable → 내부 스키마에 coercion 적용
307
+ if (schema instanceof z.ZodNullable) {
308
+ return applyCoercion((schema as any)._def.innerType).nullable();
309
+ }
310
+
311
+ // ZodArray → 배열 요소에 coercion 적용 (쿼리스트링 배열)
312
+ if (schema instanceof z.ZodArray) {
313
+ return z.array(applyCoercion((schema as any)._def.type));
314
+ }
315
+
316
+ // 그 외는 원본 반환
317
+ return schema;
318
+ }
319
+
320
+ /**
321
+ * Request 데이터 전체 정규화
322
+ * query, body, params, headers 각각에 적절한 정규화 적용
323
+ */
324
+ export interface NormalizedRequestData {
325
+ query?: unknown;
326
+ body?: unknown;
327
+ params?: unknown;
328
+ headers?: unknown;
329
+ }
330
+
331
+ export interface RequestSchemas {
332
+ query?: ZodTypeAny;
333
+ body?: ZodTypeAny;
334
+ params?: ZodTypeAny;
335
+ headers?: ZodTypeAny;
336
+ }
337
+
338
+ /**
339
+ * Request 데이터 정규화
340
+ * - query/params: coerce 적용 (문자열 → 숫자 등)
341
+ * - body: strip/strict 모드 적용
342
+ * - headers: 그대로 검증
343
+ *
344
+ * @param schemas - 각 필드별 스키마
345
+ * @param data - 원본 데이터
346
+ * @param options - 정규화 옵션
347
+ * @returns 정규화된 데이터
348
+ */
349
+ export function normalizeRequestData(
350
+ schemas: RequestSchemas,
351
+ data: NormalizedRequestData,
352
+ options?: NormalizeOptions
353
+ ): NormalizedRequestData {
354
+ const opts = { ...globalOptions, ...options };
355
+ const result: NormalizedRequestData = {};
356
+
357
+ // Query: coerce + strip
358
+ if (schemas.query && data.query !== undefined) {
359
+ let querySchema = schemas.query;
360
+
361
+ // coerce 적용
362
+ if (opts.coerceQueryParams && isZodObject(querySchema)) {
363
+ querySchema = createCoerceSchema(querySchema);
364
+ }
365
+
366
+ // strip/strict 적용
367
+ querySchema = normalizeSchema(querySchema, opts);
368
+
369
+ result.query = querySchema.parse(data.query);
370
+ }
371
+
372
+ // Params: coerce + strip
373
+ if (schemas.params && data.params !== undefined) {
374
+ let paramsSchema = schemas.params;
375
+
376
+ // coerce 적용
377
+ if (opts.coerceQueryParams && isZodObject(paramsSchema)) {
378
+ paramsSchema = createCoerceSchema(paramsSchema);
379
+ }
380
+
381
+ // strip/strict 적용
382
+ paramsSchema = normalizeSchema(paramsSchema, opts);
383
+
384
+ result.params = paramsSchema.parse(data.params);
385
+ }
386
+
387
+ // Body: strip/strict만 적용 (coerce 안 함 - JSON은 타입 보존)
388
+ if (schemas.body && data.body !== undefined) {
389
+ const bodySchema = normalizeSchema(schemas.body, opts);
390
+ result.body = bodySchema.parse(data.body);
391
+ }
392
+
393
+ // Headers: 검증만 (정규화 안 함)
394
+ if (schemas.headers && data.headers !== undefined) {
395
+ result.headers = schemas.headers.parse(data.headers);
396
+ }
397
+
398
+ return result;
399
+ }
400
+
401
+ /**
402
+ * 타입 유틸리티: 정규화된 데이터 타입 추론
403
+ */
404
+ export type NormalizedData<T extends ZodTypeAny> = z.infer<T>;