ERP-node/frontend/components/unified/UnifiedFormContext.tsx

280 lines
7.3 KiB
TypeScript

"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<string, unknown>;
fieldStates: FormState;
// 값 관리
getValue: (fieldId: string) => unknown;
setValue: (fieldId: string, value: unknown) => void;
setValues: (values: Record<string, unknown>) => 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<UnifiedFormContextValue | null>(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<string, unknown>;
onChange?: (formData: Record<string, unknown>) => void;
}
export function UnifiedFormProvider({
children,
initialValues = {},
onChange,
}: UnifiedFormProviderProps) {
const [formData, setFormData] = useState<Record<string, unknown>>(initialValues);
const [fieldStates, setFieldStates] = useState<FormState>({});
// 값 가져오기
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<string, unknown>) => {
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<UnifiedFormContextValue>(() => ({
formData,
fieldStates,
getValue,
setValue,
setValues,
evaluateCondition,
getCascadingFilter,
registerField,
unregisterField,
}), [
formData,
fieldStates,
getValue,
setValue,
setValues,
evaluateCondition,
getCascadingFilter,
registerField,
unregisterField,
]);
return (
<UnifiedFormContext.Provider value={contextValue}>
{children}
</UnifiedFormContext.Provider>
);
}
// ===== 커스텀 훅 =====
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<T extends { parentValue?: unknown }>(
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;