Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | import { z } from 'zod'; import { toTypedSchema } from '@vee-validate/zod'; import { optionalEmptyToUndef, stringMax, stringMaxOptional, jpPostalRe } from '@/libs/vee/rules'; import { makeMsg, type TFunc } from '@/libs/vee/messages'; import { emptyToNull} from '@/utils' export type AddressForm = { purposeId: number | null; // ← 必須キー + null許可 countryCode: string | null; // ← 必須キー + null許可 administrativeArea?: string; locality?: string; dependentLocality?: string; postalCode?: string; addressLine1: string; addressLine2?: string; addressLine3?: string; latitude?: number | null; // ← スキーマに合わせて null も許可 longitude?: number | null; // ← 同上 } export function makeAddressSchema(t: TFunc, L: Record<string, string>) { const m = makeMsg(t, L); const base = z.object({ purposeId: z.preprocess( emptyToNull, z.number() // z.number({ // required_error: m.req('purposeId'), // invalid_type_error: m.req('purposeId') // }) .int() .positive(m.positive('purposeId')) .nullable() .refine(v => v !== null, { message: m.req('purposeId') }) ), countryCode: z.preprocess( emptyToNull, z.string() .length(2, t('validation.rules.len.string', { field: L.countryCode, len: 2 })) .transform(s => s.toUpperCase()) .nullable() .refine(v => v !== null, { message: m.req('countryCode'), }) ), administrativeArea: optionalEmptyToUndef() .or(stringMax(128, m.max('administrativeArea', 128))), locality: stringMaxOptional(128, m.max('locality', 128)), dependentLocality: stringMaxOptional(128, m.max('dependentLocality', 128)), postalCode: optionalEmptyToUndef() .or(stringMax(16, m.max('postalCode', 16))), addressLine1: z .string() .trim() .nonempty(m.req('addressLine1')) .min(1, m.min('addressLine1', 1)) .max(160, m.max('addressLine1', 160)), addressLine2: stringMaxOptional(160, m.max('addressLine2', 160)), addressLine3: stringMaxOptional(160, m.max('addressLine3', 160)), latitude: z .coerce .number() // ← オプション渡さない .nullable() .optional() // 文字列→数値化されるが、"abc" などは NaN になる。NaN を弾いてメッセージを出す .refine(v => v == null || !Number.isNaN(v), { message: m.typeNum('latitude'), }) // 範囲チェックも必要なら追記 .refine(v => v == null || (v >= -90 && v <= 90), { message: m.range('latitude', -90, 90), }), longitude: z .coerce .number() .nullable() .optional() // 文字列→数値化されるが、"abc" などは NaN になる。NaN を弾いてメッセージを出す .refine(v => v == null || !Number.isNaN(v), { message: m.typeNum('longitude') }) // 範囲チェックも必要なら追記 .refine(v => v == null || (v >= -180 && v <= 180), { message: m.range('longitude', -180, 180), }), }) // 緯度経度はセットで .refine((data) => { const latOk = typeof data.latitude === 'number'; const lngOk = typeof data.longitude === 'number'; return (latOk && lngOk) || (!latOk && !lngOk); }, { path: ['latitude'], message: m.coordPair() }) // JP の時の追加ルール .superRefine((data, ctx) => { if (data.countryCode?.toUpperCase() === 'JP') { if (!data.administrativeArea) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['administrativeArea'], message: m.req('administrativeArea') }); } if (data.postalCode && !jpPostalRe.test(data.postalCode)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['postalCode'], message: m.jpPostal('postalCode') }); } } }); return toTypedSchema<AddressForm>(base); } |