UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

459 lines (428 loc) 13.8 kB
import { z } from "zod"; import React from "react"; // 기본 validation 스키마들 export const colorSchema = z .string() .regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, { message: "올바른 색상 코드를 입력해주세요 (예: #FF0000)", }); export const urlSchema = z .string() .url({ message: "올바른 URL을 입력해주세요", }) .or(z.literal("")); export const emailSchema = z .string() .email({ message: "올바른 이메일 주소를 입력해주세요", }) .or(z.literal("")); export const numberRangeSchema = (min: number, max: number) => z .number() .min(min, { message: `${min} 이상의 값을 입력해주세요` }) .max(max, { message: `${max} 이하의 값을 입력해주세요` }); export const stringLengthSchema = (min: number, max: number) => z .string() .min(min, { message: `최소 ${min}자 이상 입력해주세요` }) .max(max, { message: `최대 ${max}자까지 입력 가능합니다` }); // Image Content 스키마 export const imageContentSchema = z.object({ url: urlSchema, alt: z.string().min(1, { message: "대체 텍스트를 입력해주세요" }), width: z.number().optional(), height: z.number().optional(), caption: z.string().optional(), }); // Video Content 스키마 export const videoContentSchema = z.object({ url: urlSchema, thumbnail: urlSchema.optional(), autoplay: z.boolean(), loop: z.boolean(), muted: z.boolean(), controls: z.boolean(), }); // Button Content 스키마 export const buttonContentSchema = z.object({ text: stringLengthSchema(1, 50), url: urlSchema.optional(), type: z.enum(["primary", "secondary", "outline", "ghost"]), size: z.enum(["small", "medium", "large"]), icon: z .object({ name: z.string(), library: z.enum(["heroicons", "lucide", "feather", "custom"]), size: numberRangeSchema(12, 64), color: colorSchema.optional(), }) .optional(), onClick: z.string().optional(), }); // Background Style 스키마 export const backgroundStyleSchema = z.object({ type: z.enum(["color", "gradient", "image", "video"]), color: colorSchema.optional(), gradient: z .object({ direction: numberRangeSchema(0, 360), colors: z.array( z.object({ color: colorSchema, position: numberRangeSchema(0, 100), }) ), }) .optional(), image: z .object({ url: urlSchema, position: z.enum(["center", "top", "bottom", "left", "right"]), size: z.enum(["cover", "contain", "auto"]), repeat: z.enum(["no-repeat", "repeat", "repeat-x", "repeat-y"]), overlay: colorSchema.optional(), }) .optional(), video: videoContentSchema.optional(), }); // Spacing Style 스키마 export const spacingStyleSchema = z.object({ padding: z.object({ top: numberRangeSchema(0, 200), right: numberRangeSchema(0, 200), bottom: numberRangeSchema(0, 200), left: numberRangeSchema(0, 200), }), margin: z.object({ top: numberRangeSchema(-100, 100), right: numberRangeSchema(-100, 100), bottom: numberRangeSchema(-100, 100), left: numberRangeSchema(-100, 100), }), }); // Typography Style 스키마 export const typographyStyleSchema = z.object({ fontFamily: z.string().min(1, { message: "폰트 패밀리를 선택해주세요" }), fontSize: numberRangeSchema(8, 128), fontWeight: z.enum([ "100", "200", "300", "400", "500", "600", "700", "800", "900", ]), lineHeight: numberRangeSchema(0.5, 3), letterSpacing: numberRangeSchema(-5, 10), textAlign: z.enum(["left", "center", "right", "justify"]), textDecoration: z.enum(["none", "underline", "line-through"]), textTransform: z.enum(["none", "uppercase", "lowercase", "capitalize"]), }); // Hero Section Content 스키마 export const heroSectionContentSchema = z.object({ title: stringLengthSchema(1, 100), subtitle: stringLengthSchema(0, 150).optional(), description: stringLengthSchema(0, 300).optional(), image: imageContentSchema.optional(), video: videoContentSchema.optional(), buttons: z.array(buttonContentSchema).max(3, { message: "최대 3개의 버튼까지 추가할 수 있습니다", }), overlay: z .object({ color: colorSchema, opacity: numberRangeSchema(0, 1), gradient: z .object({ direction: numberRangeSchema(0, 360), colors: z.array( z.object({ color: colorSchema, position: numberRangeSchema(0, 100), }) ), }) .optional(), }) .optional(), }); // Features Section Content 스키마 export const featuresSectionContentSchema = z.object({ title: stringLengthSchema(1, 100), description: stringLengthSchema(0, 300).optional(), features: z .array( z.object({ id: z.string(), title: stringLengthSchema(1, 50), description: stringLengthSchema(1, 200), icon: z .object({ name: z.string(), library: z.enum(["heroicons", "lucide", "feather", "custom"]), size: numberRangeSchema(16, 64), color: colorSchema.optional(), }) .optional(), image: imageContentSchema.optional(), link: z .object({ url: urlSchema, text: stringLengthSchema(1, 30), target: z.enum(["_self", "_blank", "_parent", "_top"]), rel: z.string().optional(), }) .optional(), }) ) .min(1, { message: "최소 1개의 기능을 추가해주세요" }), layout: z.enum(["grid", "list", "carousel"]), columns: numberRangeSchema(1, 4), }); // CTA Section Content 스키마 export const ctaSectionContentSchema = z.object({ title: stringLengthSchema(1, 100), description: stringLengthSchema(0, 300).optional(), button: buttonContentSchema, background: z .object({ type: z.enum(["color", "gradient", "image", "video"]), value: z.string().min(1, { message: "배경 값을 설정해주세요" }), overlay: z .object({ color: colorSchema, opacity: numberRangeSchema(0, 1), gradient: z .object({ direction: numberRangeSchema(0, 360), colors: z.array( z.object({ color: colorSchema, position: numberRangeSchema(0, 100), }) ), }) .optional(), }) .optional(), }) .optional(), layout: z.enum(["centered", "split", "banner"]), }); // Pricing Section Content 스키마 export const pricingSectionContentSchema = z.object({ title: stringLengthSchema(1, 100), description: stringLengthSchema(0, 300).optional(), plans: z .array( z.object({ id: z.string(), name: stringLengthSchema(1, 30), description: stringLengthSchema(0, 100).optional(), price: z.number().min(0, { message: "가격은 0 이상이어야 합니다" }), currency: z.string().length(3, { message: "통화 코드는 3글자여야 합니다 (예: KRW, USD)", }), billing: z.enum(["monthly", "yearly"]), features: z.array( z.string().min(1, { message: "기능 설명을 입력해주세요" }) ), featured: z.boolean(), button: buttonContentSchema, }) ) .min(1, { message: "최소 1개의 요금제를 추가해주세요" }), layout: z.enum(["cards", "table", "comparison"]), billing: z.enum(["monthly", "yearly", "both"]), }); // Text Section Content 스키마 export const textContentSchema = z.object({ title: stringLengthSchema(1, 100), content: z.string().min(1, { message: "내용을 입력해주세요" }), textAlign: z.enum(["left", "center", "right"]), fontSize: numberRangeSchema(12, 72), fontWeight: z.enum(["normal", "bold", "light"]), color: colorSchema.optional(), }); // Gallery Section Content 스키마 export const galleryContentSchema = z.object({ title: stringLengthSchema(1, 100).optional(), description: stringLengthSchema(0, 300).optional(), images: z .array( z.object({ id: z.string(), src: urlSchema, alt: z.string(), title: z.string().optional(), description: z.string().optional(), }) ) .min(1, { message: "최소 1개의 이미지가 필요합니다" }), layout: z.enum(["grid", "masonry", "carousel"]), columns: numberRangeSchema(1, 6), }); // Contact Section Content 스키마 export const contactContentSchema = z.object({ title: stringLengthSchema(1, 100), description: stringLengthSchema(0, 300).optional(), form: z.object({ fields: z.array( z.object({ id: z.string(), type: z.enum(["text", "email", "tel", "textarea", "select"]), label: z.string(), placeholder: z.string().optional(), required: z.boolean(), options: z.array(z.string()).optional(), }) ), submitText: z.string(), action: urlSchema.optional(), }), contact: z.object({ email: emailSchema.optional(), phone: z.string().optional(), address: z.string().optional(), hours: z.string().optional(), }), map: z .object({ enabled: z.boolean(), lat: z.number().optional(), lng: z.number().optional(), zoom: numberRangeSchema(1, 20).optional(), }) .optional(), }); // Section Content Union 스키마 export const sectionContentSchema = z.object({ hero: heroSectionContentSchema.optional(), text: textContentSchema.optional(), image: imageContentSchema.optional(), gallery: galleryContentSchema.optional(), contact: contactContentSchema.optional(), custom: z.record(z.any()).optional(), }); // Complete Section 스키마 export const sectionSchema = z.object({ id: z.string().min(1, { message: "섹션 ID는 필수입니다" }), type: z.enum(["hero", "text", "image", "gallery", "contact", "custom"]), name: z.string().min(1, { message: "섹션 이름은 필수입니다" }), order: z.number().int().min(0), isActive: z.boolean(), isLocked: z.boolean(), content: sectionContentSchema, style: z.object({ backgroundColor: colorSchema.optional(), backgroundImage: urlSchema.optional(), padding: z.object({ top: numberRangeSchema(0, 200), bottom: numberRangeSchema(0, 200), left: numberRangeSchema(0, 200), right: numberRangeSchema(0, 200), }), margin: z.object({ top: numberRangeSchema(0, 200), bottom: numberRangeSchema(0, 200), left: numberRangeSchema(0, 200), right: numberRangeSchema(0, 200), }), border: z.object({ width: numberRangeSchema(0, 20), style: z.enum(["none", "solid", "dashed", "dotted"]), color: colorSchema.optional(), radius: numberRangeSchema(0, 50), }), shadow: z.object({ x: numberRangeSchema(-20, 20), y: numberRangeSchema(-20, 20), blur: numberRangeSchema(0, 50), spread: numberRangeSchema(-20, 20), color: colorSchema.optional(), }), animation: z.object({ type: z.enum(["none", "fade", "slide", "zoom", "bounce", "custom"]), duration: numberRangeSchema(0, 5000), delay: numberRangeSchema(0, 2000), easing: z.enum(["ease", "ease-in", "ease-out", "ease-in-out", "linear"]), }), responsive: z.object({ mobile: z.object({ display: z.boolean(), width: z.string().optional(), height: z.string().optional(), }), tablet: z.object({ display: z.boolean(), width: z.string().optional(), height: z.string().optional(), }), desktop: z.object({ display: z.boolean(), width: z.string().optional(), height: z.string().optional(), }), }), customCSS: z.string().optional(), }), metadata: z.object({ createdAt: z.date(), updatedAt: z.date(), version: z.number().int().min(1), author: z.string().optional(), description: z.string().optional(), tags: z.array(z.string()).optional(), }), }); // Validation utility functions export const validateSection = (data: any) => { return sectionSchema.safeParse(data); }; export const validateFormField = (value: any, schema: z.ZodSchema) => { return schema.safeParse(value); }; export const formatValidationErrors = ( error: z.ZodError ): ValidationError[] => { return error.errors.map((err) => ({ field: err.path.join("."), message: err.message, code: err.code, })); }; // Field-specific validation helpers export const validateColor = (color: string) => colorSchema.safeParse(color); export const validateUrl = (url: string) => urlSchema.safeParse(url); export const validateEmail = (email: string) => emailSchema.safeParse(email); // Validation presets for common use cases export const createStringValidator = (min: number = 0, max: number = 1000) => stringLengthSchema(min, max); export const createNumberValidator = (min: number = 0, max: number = 1000) => numberRangeSchema(min, max); // Real-time validation hook export const useFieldValidation = (value: any, schema: z.ZodSchema) => { const [isValid, setIsValid] = React.useState(true); const [error, setError] = React.useState<string | null>(null); React.useEffect(() => { const result = schema.safeParse(value); setIsValid(result.success); setError( result.success ? null : result.error.errors[0]?.message || "유효하지 않은 값입니다" ); }, [value, schema]); return { isValid, error }; }; // Validation error 타입 export interface ValidationError { field: string; message: string; code: string; }