/** * 폼 검증 상태 관리 훅 * 실시간 검증과 사용자 피드백을 위한 커스텀 훅 */ import { useState, useCallback, useEffect, useRef } from "react"; import { ComponentData, ColumnInfo, ScreenDefinition } from "@/types/screen"; import { validateFormData, ValidationResult, ValidationError, ValidationWarning } from "@/lib/utils/formValidation"; import { enhancedFormService, SaveContext, EnhancedSaveResult } from "@/lib/services/enhancedFormService"; import { useToast } from "@/hooks/use-toast"; // 검증 상태 export type ValidationStatus = "idle" | "validating" | "valid" | "invalid"; // 필드별 검증 상태 export interface FieldValidationState { status: ValidationStatus; error?: ValidationError; warning?: ValidationWarning; lastValidated?: Date; } // 폼 검증 상태 export interface FormValidationState { status: ValidationStatus; isValid: boolean; errors: ValidationError[]; warnings: ValidationWarning[]; fieldStates: Record; lastValidated?: Date; validationCount: number; } // 저장 상태 export interface SaveState { status: "idle" | "saving" | "success" | "error"; message?: string; result?: EnhancedSaveResult; lastSaved?: Date; } // 훅 옵션 export interface UseFormValidationOptions { enableRealTimeValidation?: boolean; validationDelay?: number; // debounce 지연시간 (ms) enableAutoSave?: boolean; autoSaveDelay?: number; // 자동저장 지연시간 (ms) showToastMessages?: boolean; validateOnMount?: boolean; } // 훅 반환값 export interface UseFormValidationReturn { // 상태 validationState: FormValidationState; saveState: SaveState; // 액션 validateForm: () => Promise; validateField: (fieldName: string, value: any) => Promise; saveForm: () => Promise; clearValidation: () => void; // 유틸리티 getFieldError: (fieldName: string) => ValidationError | undefined; getFieldWarning: (fieldName: string) => ValidationWarning | undefined; hasFieldError: (fieldName: string) => boolean; isFieldValid: (fieldName: string) => boolean; canSave: boolean; } /** * 폼 검증 관리 훅 */ export const useFormValidation = ( formData: Record, components: ComponentData[], tableColumns: ColumnInfo[], screenInfo: ScreenDefinition, options: UseFormValidationOptions = {}, ): UseFormValidationReturn => { const { enableRealTimeValidation = true, validationDelay = 500, enableAutoSave = false, autoSaveDelay = 2000, showToastMessages = true, validateOnMount = false, } = options; const { toast } = useToast(); // 상태 const [validationState, setValidationState] = useState({ status: "idle", isValid: false, errors: [], warnings: [], fieldStates: {}, validationCount: 0, }); const [saveState, setSaveState] = useState({ status: "idle", }); // 타이머 참조 const validationTimer = useRef(); const autoSaveTimer = useRef(); const lastValidationData = useRef(""); /** * 전체 폼 검증 */ const validateForm = useCallback(async (): Promise => { if (!screenInfo?.tableName) { return { isValid: false, errors: [ { field: "form", code: "NO_TABLE", message: "테이블명이 설정되지 않았습니다.", severity: "error", }, ], warnings: [], }; } setValidationState((prev) => ({ ...prev, status: "validating", })); try { const result = await validateFormData(formData, components, tableColumns, screenInfo.tableName); // 필드별 상태 업데이트 const fieldStates: Record = {}; // 기존 필드 상태 초기화 Object.keys(formData).forEach((fieldName) => { fieldStates[fieldName] = { status: "valid", lastValidated: new Date(), }; }); // 오류가 있는 필드 업데이트 result.errors.forEach((error) => { fieldStates[error.field] = { status: "invalid", error, lastValidated: new Date(), }; }); // 경고가 있는 필드 업데이트 result.warnings.forEach((warning) => { if (fieldStates[warning.field]) { fieldStates[warning.field].warning = warning; } else { fieldStates[warning.field] = { status: "valid", warning, lastValidated: new Date(), }; } }); setValidationState((prev) => ({ status: result.isValid ? "valid" : "invalid", isValid: result.isValid, errors: result.errors, warnings: result.warnings, fieldStates, lastValidated: new Date(), validationCount: prev.validationCount + 1, })); if (showToastMessages) { if (result.isValid && result.warnings.length > 0) { toast({ title: "검증 완료", description: `${result.warnings.length}개의 경고가 있습니다.`, variant: "default", }); } else if (!result.isValid) { toast({ title: "검증 실패", description: `${result.errors.length}개의 오류를 수정해주세요.`, variant: "destructive", }); } } return result; } catch (error) { console.error("❌ 폼 검증 중 오류:", error); const errorResult: ValidationResult = { isValid: false, errors: [ { field: "form", code: "VALIDATION_ERROR", message: `검증 중 오류가 발생했습니다: ${error}`, severity: "error", }, ], warnings: [], }; setValidationState((prev) => ({ ...prev, status: "invalid", isValid: false, errors: errorResult.errors, warnings: [], lastValidated: new Date(), validationCount: prev.validationCount + 1, })); return errorResult; } }, [formData, components, tableColumns, screenInfo, showToastMessages, toast]); /** * 개별 필드 검증 */ const validateField = useCallback( async (fieldName: string, value: any): Promise => { const component = components.find((c) => (c as any).columnName === fieldName || c.id === fieldName); if (!component || component.type !== "widget") return; setValidationState((prev) => ({ ...prev, fieldStates: { ...prev.fieldStates, [fieldName]: { ...prev.fieldStates[fieldName], status: "validating", }, }, })); // 개별 필드 검증 로직 // (실제 구현에서는 validateFieldValue 함수 사용) setValidationState((prev) => ({ ...prev, fieldStates: { ...prev.fieldStates, [fieldName]: { status: "valid", lastValidated: new Date(), }, }, })); }, [components], ); /** * 폼 저장 */ const saveForm = useCallback(async (): Promise => { if (!validationState.isValid) { if (showToastMessages) { toast({ title: "저장 실패", description: "검증 오류를 먼저 수정해주세요.", variant: "destructive", }); } return false; } setSaveState({ status: "saving" }); try { const saveContext: SaveContext = { tableName: screenInfo.tableName, screenInfo, components, formData, options: { transformData: true, showProgress: true, }, }; const result = await enhancedFormService.saveFormData(saveContext); setSaveState({ status: result.success ? "success" : "error", message: result.message, result, lastSaved: new Date(), }); if (showToastMessages) { toast({ title: result.success ? "저장 성공" : "저장 실패", description: result.message, variant: result.success ? "default" : "destructive", }); } return result.success; } catch (error) { console.error("❌ 폼 저장 중 오류:", error); setSaveState({ status: "error", message: `저장 중 오류가 발생했습니다: ${error}`, lastSaved: new Date(), }); if (showToastMessages) { toast({ title: "저장 실패", description: "저장 중 오류가 발생했습니다.", variant: "destructive", }); } return false; } }, [validationState.isValid, screenInfo, components, formData, showToastMessages, toast]); /** * 검증 상태 초기화 */ const clearValidation = useCallback(() => { setValidationState({ status: "idle", isValid: false, errors: [], warnings: [], fieldStates: {}, validationCount: 0, }); setSaveState({ status: "idle" }); }, []); /** * 필드 오류 조회 */ const getFieldError = useCallback( (fieldName: string): ValidationError | undefined => { return validationState.fieldStates[fieldName]?.error; }, [validationState.fieldStates], ); /** * 필드 경고 조회 */ const getFieldWarning = useCallback( (fieldName: string): ValidationWarning | undefined => { return validationState.fieldStates[fieldName]?.warning; }, [validationState.fieldStates], ); /** * 필드 오류 여부 확인 */ const hasFieldError = useCallback( (fieldName: string): boolean => { return validationState.fieldStates[fieldName]?.status === "invalid"; }, [validationState.fieldStates], ); /** * 필드 유효성 확인 */ const isFieldValid = useCallback( (fieldName: string): boolean => { const fieldState = validationState.fieldStates[fieldName]; return fieldState?.status === "valid" || !fieldState; }, [validationState.fieldStates], ); // 저장 가능 여부 const canSave = validationState.isValid && saveState.status !== "saving" && Object.keys(formData).length > 0; // 실시간 검증 (debounced) useEffect(() => { if (!enableRealTimeValidation) return; const currentDataString = JSON.stringify(formData); if (currentDataString === lastValidationData.current) return; // 이전 타이머 클리어 if (validationTimer.current) { clearTimeout(validationTimer.current); } // 새 타이머 설정 validationTimer.current = setTimeout(() => { lastValidationData.current = currentDataString; validateForm(); }, validationDelay); return () => { if (validationTimer.current) { clearTimeout(validationTimer.current); } }; }, [formData, enableRealTimeValidation, validationDelay, validateForm]); // 자동 저장 useEffect(() => { if (!enableAutoSave || !validationState.isValid) return; // 이전 타이머 클리어 if (autoSaveTimer.current) { clearTimeout(autoSaveTimer.current); } // 새 타이머 설정 autoSaveTimer.current = setTimeout(() => { saveForm(); }, autoSaveDelay); return () => { if (autoSaveTimer.current) { clearTimeout(autoSaveTimer.current); } }; }, [validationState.isValid, enableAutoSave, autoSaveDelay, saveForm]); // 마운트 시 검증 useEffect(() => { if (validateOnMount && Object.keys(formData).length > 0) { validateForm(); } }, [validateOnMount]); // formData는 의존성에서 제외 (무한 루프 방지) // 클린업 useEffect(() => { return () => { if (validationTimer.current) { clearTimeout(validationTimer.current); } if (autoSaveTimer.current) { clearTimeout(autoSaveTimer.current); } }; }, []); return { validationState, saveState, validateForm, validateField, saveForm, clearValidation, getFieldError, getFieldWarning, hasFieldError, isFieldValid, canSave, }; };