280 lines
7.3 KiB
TypeScript
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;
|
|
|
|
|