126 lines
3.4 KiB
TypeScript
126 lines
3.4 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
const hexColor = z
|
|
.string()
|
|
.regex(/^#([0-9a-fA-F]{3}){1,2}$/i, 'invalid hex color')
|
|
|
|
const url = z.string().url()
|
|
|
|
const sectionHeroSchema = z.object({
|
|
type: z.literal('hero'),
|
|
props: z.object({
|
|
heading: z.string().min(1),
|
|
subheading: z.string().optional().default(''),
|
|
imageUrl: url,
|
|
}),
|
|
})
|
|
|
|
const sectionFaqSchema = z.object({
|
|
type: z.literal('faq'),
|
|
props: z.object({
|
|
items: z.array(z.object({ q: z.string().min(1), a: z.string().min(1) })).min(1),
|
|
}),
|
|
})
|
|
|
|
const sectionCtaSchema = z.object({
|
|
type: z.literal('cta'),
|
|
props: z.object({ text: z.string().min(1), href: z.string().optional().default('#') }),
|
|
})
|
|
|
|
const sectionFeaturesSchema = z.object({
|
|
type: z.literal('features'),
|
|
props: z.object({ items: z.array(z.string().min(1)).min(1) }),
|
|
})
|
|
|
|
const sectionTestimonialsSchema = z.object({
|
|
type: z.literal('testimonials'),
|
|
props: z.object({ items: z.array(z.object({ author: z.string().min(1), quote: z.string().min(1) })).min(1) }),
|
|
})
|
|
|
|
const sectionBenefitsSchema = z.object({
|
|
type: z.literal('benefits'),
|
|
props: z.object({ items: z.array(z.string().min(1)).min(1) }),
|
|
})
|
|
|
|
// 확장: hero, faq, cta, features, testimonials 허용
|
|
const sectionSchema = z.union([
|
|
sectionHeroSchema,
|
|
sectionFaqSchema,
|
|
sectionCtaSchema,
|
|
sectionFeaturesSchema,
|
|
sectionTestimonialsSchema,
|
|
sectionBenefitsSchema,
|
|
])
|
|
|
|
const baseField = {
|
|
name: z.string().min(1),
|
|
label: z.string().min(1),
|
|
required: z.boolean().optional().default(false),
|
|
pattern: z.string().optional(),
|
|
maxLength: z.number().int().positive().optional(),
|
|
help: z.string().optional(),
|
|
}
|
|
|
|
const textField = z.object({ type: z.literal('text'), ...baseField })
|
|
const emailField = z.object({ type: z.literal('email'), ...baseField })
|
|
const telField = z.object({ type: z.literal('tel'), ...baseField })
|
|
const textareaField = z.object({ type: z.literal('textarea'), ...baseField })
|
|
const dateField = z.object({ type: z.literal('date'), ...baseField })
|
|
const checkboxField = z.object({ type: z.literal('checkbox'), ...baseField })
|
|
const radioField = z.object({
|
|
type: z.literal('radio'),
|
|
...baseField,
|
|
options: z.array(z.string().min(1)).min(1),
|
|
})
|
|
const selectField = z.object({
|
|
type: z.literal('select'),
|
|
...baseField,
|
|
options: z.array(z.string().min(1)).min(1),
|
|
})
|
|
|
|
// 파일 업로드는 제외
|
|
export const formFieldSchema = z.union([
|
|
textField,
|
|
emailField,
|
|
telField,
|
|
textareaField,
|
|
dateField,
|
|
checkboxField,
|
|
radioField,
|
|
selectField,
|
|
])
|
|
|
|
export const pageSchema = z.object({
|
|
title: z.string().min(1),
|
|
locale: z.string().min(2),
|
|
theme: z.object({
|
|
primaryColor: hexColor,
|
|
fontFamily: z.string().min(1),
|
|
}),
|
|
sections: z.array(sectionSchema),
|
|
form: z.object({
|
|
actionUrl: z.string().optional().default(''),
|
|
fields: z.array(formFieldSchema).min(0),
|
|
spamProtection: z.object({
|
|
honeypotFieldName: z.string().min(1).default('_hp'),
|
|
minSubmitSeconds: z.number().min(0).default(2),
|
|
}),
|
|
}),
|
|
analytics: z
|
|
.object({
|
|
ga4MeasurementId: z.string().optional().default(''),
|
|
metaPixelId: z.string().optional().default(''),
|
|
customHeadHtml: z.string().optional().default(''),
|
|
})
|
|
.optional()
|
|
.default({ ga4MeasurementId: '', metaPixelId: '', customHeadHtml: '' }),
|
|
seo: z.object({
|
|
title: z.string().min(1),
|
|
description: z.string().min(1),
|
|
ogImage: url.optional(),
|
|
}),
|
|
})
|
|
|
|
export type Page = z.infer<typeof pageSchema>
|
|
export type FormField = z.infer<typeof formFieldSchema>
|