"use client"; /** * UnifiedFormContext * * Unified 컴포넌트들이 폼 상태를 공유하고 * 조건부 로직을 처리할 수 있도록 하는 Context */ import React, { createContext, useContext, useState, useCallback, useMemo } from "react"; import { ConditionalConfig, CascadingConfig } from "@/types/unified-components"; // ===== 타입 정의 ===== export interface FormFieldState { value: unknown; disabled?: boolean; visible?: boolean; error?: string; } export interface FormState { [fieldId: string]: FormFieldState; } export interface UnifiedFormContextValue { // 폼 상태 formData: Record; fieldStates: FormState; // 값 관리 getValue: (fieldId: string) => unknown; setValue: (fieldId: string, value: unknown) => void; setValues: (values: Record) => void; // 조건부 로직 evaluateCondition: (fieldId: string, config?: ConditionalConfig) => { visible: boolean; disabled: boolean; }; // 연쇄 관계 getCascadingFilter: (config?: CascadingConfig) => unknown; // 필드 등록 registerField: (fieldId: string, initialValue?: unknown) => void; unregisterField: (fieldId: string) => void; } // ===== Context 생성 ===== const UnifiedFormContext = createContext(null); // ===== 조건 평가 함수 ===== function evaluateOperator( fieldValue: unknown, operator: ConditionalConfig["operator"], conditionValue: unknown ): boolean { switch (operator) { case "=": return fieldValue === conditionValue; case "!=": return fieldValue !== conditionValue; case ">": return Number(fieldValue) > Number(conditionValue); case "<": return Number(fieldValue) < Number(conditionValue); case "in": if (Array.isArray(conditionValue)) { return conditionValue.includes(fieldValue); } return false; case "notIn": if (Array.isArray(conditionValue)) { return !conditionValue.includes(fieldValue); } return true; case "isEmpty": return fieldValue === null || fieldValue === undefined || fieldValue === "" || (Array.isArray(fieldValue) && fieldValue.length === 0); case "isNotEmpty": return fieldValue !== null && fieldValue !== undefined && fieldValue !== "" && !(Array.isArray(fieldValue) && fieldValue.length === 0); default: return true; } } // ===== Provider 컴포넌트 ===== interface UnifiedFormProviderProps { children: React.ReactNode; initialValues?: Record; onChange?: (formData: Record) => void; } export function UnifiedFormProvider({ children, initialValues = {}, onChange, }: UnifiedFormProviderProps) { const [formData, setFormData] = useState>(initialValues); const [fieldStates, setFieldStates] = useState({}); // 값 가져오기 const getValue = useCallback((fieldId: string): unknown => { return formData[fieldId]; }, [formData]); // 단일 값 설정 const setValue = useCallback((fieldId: string, value: unknown) => { setFormData(prev => { const newData = { ...prev, [fieldId]: value }; onChange?.(newData); return newData; }); }, [onChange]); // 여러 값 한 번에 설정 const setValues = useCallback((values: Record) => { setFormData(prev => { const newData = { ...prev, ...values }; onChange?.(newData); return newData; }); }, [onChange]); // 조건 평가 const evaluateCondition = useCallback(( fieldId: string, config?: ConditionalConfig ): { visible: boolean; disabled: boolean } => { // 조건부 설정이 없으면 기본값 반환 if (!config || !config.enabled) { return { visible: true, disabled: false }; } const { field, operator, value, action } = config; const fieldValue = formData[field]; // 조건 평가 const conditionMet = evaluateOperator(fieldValue, operator, value); // 액션에 따른 결과 switch (action) { case "show": return { visible: conditionMet, disabled: false }; case "hide": return { visible: !conditionMet, disabled: false }; case "enable": return { visible: true, disabled: !conditionMet }; case "disable": return { visible: true, disabled: conditionMet }; default: return { visible: true, disabled: false }; } }, [formData]); // 연쇄 관계 필터값 가져오기 const getCascadingFilter = useCallback((config?: CascadingConfig): unknown => { if (!config) return undefined; return formData[config.parentField]; }, [formData]); // 필드 등록 const registerField = useCallback((fieldId: string, initialValue?: unknown) => { if (initialValue !== undefined && formData[fieldId] === undefined) { setFormData(prev => ({ ...prev, [fieldId]: initialValue })); } setFieldStates(prev => ({ ...prev, [fieldId]: { value: initialValue, visible: true, disabled: false }, })); }, [formData]); // 필드 해제 const unregisterField = useCallback((fieldId: string) => { setFieldStates(prev => { const next = { ...prev }; delete next[fieldId]; return next; }); }, []); // Context 값 const contextValue = useMemo(() => ({ formData, fieldStates, getValue, setValue, setValues, evaluateCondition, getCascadingFilter, registerField, unregisterField, }), [ formData, fieldStates, getValue, setValue, setValues, evaluateCondition, getCascadingFilter, registerField, unregisterField, ]); return ( {children} ); } // ===== 커스텀 훅 ===== export function useUnifiedForm(): UnifiedFormContextValue { const context = useContext(UnifiedFormContext); if (!context) { throw new Error("useUnifiedForm must be used within UnifiedFormProvider"); } return context; } /** * 개별 필드 훅 - 조건부 상태와 값을 한 번에 관리 */ export function useUnifiedField( fieldId: string, conditional?: ConditionalConfig ): { value: unknown; setValue: (value: unknown) => void; visible: boolean; disabled: boolean; } { const { getValue, setValue, evaluateCondition } = useUnifiedForm(); const value = getValue(fieldId); const { visible, disabled } = evaluateCondition(fieldId, conditional); const handleSetValue = useCallback((newValue: unknown) => { setValue(fieldId, newValue); }, [fieldId, setValue]); return { value, setValue: handleSetValue, visible, disabled, }; } /** * 연쇄 선택 훅 - 부모 필드 값에 따라 옵션 필터링 */ export function useCascadingOptions( options: T[], cascading?: CascadingConfig ): T[] { const { getCascadingFilter } = useUnifiedForm(); if (!cascading) return options; const parentValue = getCascadingFilter(cascading); if (parentValue === undefined || parentValue === null || parentValue === "") { return []; // 부모 값이 없으면 빈 배열 } // parentValue로 필터링 return options.filter(opt => opt.parentValue === parentValue); } export default UnifiedFormContext;