import React, { useCallback, useEffect, useMemo } from "react"; import { useForm, Controller, useWatch, Control, FieldValues, } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { debounce } from "lodash"; import { colorSchema, urlSchema, emailSchema, numberRangeSchema, stringLengthSchema, validateFormField, ValidationError, } from "./validation"; import { useCMSEditor } from "./store"; // Form field interfaces interface BaseFormFieldProps { name: string; label: string; description?: string; required?: boolean; disabled?: boolean; className?: string; } interface TextFieldProps extends BaseFormFieldProps { type?: "text" | "email" | "url" | "tel" | "password"; placeholder?: string; maxLength?: number; minLength?: number; } interface NumberFieldProps extends BaseFormFieldProps { min?: number; max?: number; step?: number; placeholder?: string; } interface SelectFieldProps extends BaseFormFieldProps { options: Array<{ value: string | number; label: string; disabled?: boolean }>; placeholder?: string; } interface ColorFieldProps extends BaseFormFieldProps { allowTransparent?: boolean; } interface ToggleFieldProps extends BaseFormFieldProps { size?: "small" | "medium" | "large"; } interface RangeFieldProps extends BaseFormFieldProps { min: number; max: number; step?: number; showValue?: boolean; formatValue?: (value: number) => string; } // Error display component const FieldError: React.FC<{ error?: string }> = ({ error }) => { if (!error) return null; return (
{error}
); }; // Success indicator const FieldSuccess: React.FC<{ show: boolean }> = ({ show }) => { if (!show) return null; return (
유효한 값입니다
); }; // Text input component export const TextField: React.FC< TextFieldProps & { control: Control } > = ({ name, label, description, type = "text", placeholder, required = false, disabled = false, maxLength, minLength, className = "", control, }) => { return (
{description &&

{description}

} (
)} />
); }; // Number input component export const NumberField: React.FC< NumberFieldProps & { control: Control } > = ({ name, label, description, min, max, step = 1, placeholder, required = false, disabled = false, className = "", control, }) => { return (
{description &&

{description}

} (
field.onChange(parseFloat(e.target.value) || 0)} className={` w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-50 disabled:text-gray-500 ${ invalid ? "border-red-300 focus:ring-red-500 focus:border-red-500" : "" } ${ !invalid && field.value !== undefined ? "border-green-300" : "" } `} />
)} />
); }; // Select dropdown component export const SelectField: React.FC< SelectFieldProps & { control: Control } > = ({ name, label, description, options, placeholder, required = false, disabled = false, className = "", control, }) => { return (
{description &&

{description}

} (
)} />
); }; // Color picker component export const ColorField: React.FC = ({ name, label, description, required = false, disabled = false, allowTransparent = false, className = "", control, }) => { return (
{description &&

{description}

} (
{allowTransparent && ( )}
)} />
); }; // Toggle switch component export const ToggleField: React.FC = ({ name, label, description, required = false, disabled = false, size = "medium", className = "", control, }) => { const sizeClasses = { small: "w-8 h-4", medium: "w-10 h-5", large: "w-12 h-6", }; const thumbSizeClasses = { small: "w-3 h-3", medium: "w-4 h-4", large: "w-5 h-5", }; return (
(
{description && (

{description}

)}
)} />
); }; // Range slider component export const RangeField: React.FC = ({ name, label, description, min, max, step = 1, showValue = true, formatValue, required = false, disabled = false, className = "", control, }) => { return (
{showValue && ( ( {formatValue ? formatValue(field.value) : field.value} )} /> )}
{description &&

{description}

} (
field.onChange(parseFloat(e.target.value))} className={` w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed ${invalid ? "accent-red-500" : "accent-blue-500"} `} />
{min} {max}
)} />
); }; // Optimistic form wrapper interface OptimisticFormProps { sectionId: string; schema: z.ZodSchema; defaultValues: any; onSubmit: (data: any) => Promise; children: (props: { control: any; formState: any; handleSubmit: any; }) => React.ReactNode; className?: string; } export const OptimisticForm: React.FC = ({ sectionId, schema, defaultValues, onSubmit, children, className = "", }) => { const { updateSection, startOptimisticUpdate, finishOptimisticUpdate, rollbackOptimisticUpdate, } = useCMSEditor(); const form = useForm({ resolver: zodResolver(schema), defaultValues, mode: "onChange", }); const { control, handleSubmit, formState, watch, getValues } = form; // Watch for changes and apply optimistic updates const watchedValues = useWatch({ control }); const debouncedOptimisticUpdate = useMemo( () => debounce((values: any) => { startOptimisticUpdate(sectionId); updateSection(sectionId, values); }, 300), [sectionId, startOptimisticUpdate, updateSection] ); useEffect(() => { if (formState.isValid && !formState.isSubmitting) { debouncedOptimisticUpdate(watchedValues); } return () => { debouncedOptimisticUpdate.cancel(); }; }, [ watchedValues, formState.isValid, formState.isSubmitting, debouncedOptimisticUpdate, ]); const handleFormSubmit = useCallback( async (data: any) => { try { startOptimisticUpdate(sectionId); await onSubmit(data); finishOptimisticUpdate(sectionId, true); } catch (error) { console.error("Form submission failed:", error); rollbackOptimisticUpdate(sectionId); finishOptimisticUpdate(sectionId, false); } }, [ sectionId, onSubmit, startOptimisticUpdate, finishOptimisticUpdate, rollbackOptimisticUpdate, ] ); return (
{children({ control, formState, handleSubmit: handleSubmit(handleFormSubmit), })}
); }; // Real-time validation indicator export const ValidationIndicator: React.FC<{ sectionId: string; className?: string; }> = ({ sectionId, className = "" }) => { const { validationErrors, pendingUpdates } = useCMSEditor(); const errors = validationErrors[sectionId] || []; const isPending = pendingUpdates.has(sectionId); if (isPending) { return (
저장 중...
); } if (errors.length > 0) { return (
{errors.length}개 오류
); } return (
저장됨
); };