test(ci): add exporter heading levels and a11y typography smoke; update MAIN_PLAN
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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) }),
|
||||
})
|
||||
|
||||
// 확장: hero, faq, cta, features, testimonials 허용
|
||||
const sectionSchema = z.union([
|
||||
sectionHeroSchema,
|
||||
sectionFaqSchema,
|
||||
sectionCtaSchema,
|
||||
sectionFeaturesSchema,
|
||||
sectionTestimonialsSchema,
|
||||
])
|
||||
|
||||
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(),
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user