ERP-node/frontend/hooks/useFormValidation.ts

473 lines
12 KiB
TypeScript
Raw Normal View History

/**
*
*
*/
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<string, FieldValidationState>;
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<ValidationResult>;
validateField: (fieldName: string, value: any) => Promise<void>;
saveForm: () => Promise<boolean>;
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<string, any>,
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<FormValidationState>({
status: "idle",
isValid: false,
errors: [],
warnings: [],
fieldStates: {},
validationCount: 0,
});
const [saveState, setSaveState] = useState<SaveState>({
status: "idle",
});
// 타이머 참조
const validationTimer = useRef<NodeJS.Timeout>();
const autoSaveTimer = useRef<NodeJS.Timeout>();
const lastValidationData = useRef<string>("");
/**
*
*/
const validateForm = useCallback(async (): Promise<ValidationResult> => {
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<string, FieldValidationState> = {};
// 기존 필드 상태 초기화
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<void> => {
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<boolean> => {
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,
};
};