[React] Filter + React-hook-form

728x90
반응형

1.TabContents.tsx

import { useLocation, useNavigate } from 'react-router-dom';
import BreadCrumb from './BreadCrumb';
import { useRecoilState } from 'recoil';
import { selectedDataAtom } from './store/selectedData';
import Filter from './components/filter/Filter';
import { useState } from 'react';

function TabContents() {
    const [selectedData, setSelectedData] = useRecoilState(selectedDataAtom);
    const [selectedTab, setSelectedTab] = useState('');

    const location = useLocation();
    const { pathname, search } = location;

    const navigate = useNavigate();

    const handleClick = (item: string) => {
        setSelectedTab(item);

        // 현재 pathname을 '/'로 분리하여 배열로 나눔
        const pathSegments = pathname.split('/');

        // 마지막 경로(예: "2")를 변경
        pathSegments[pathSegments.length - 1] = item;

        // 새로운 pathname을 생성
        const newPathname = pathSegments.join('/');

        // 기존 쿼리 문자열은 그대로 유지
        navigate(`${newPathname}${search}`);
    };

    return (
        <div>
            <BreadCrumb />

            <div className="flex gap-1">
                {selectedData.map((item) => {
                    return (
                        <ul key={item}>
                            <li className="bg-slate-300 text-center min-w-[40px] " onClick={() => handleClick(item)}>
                                {item}
                            </li>
                            {/* 선택된 탭에 맞는 필터만 렌더링 */}
                            {selectedTab === item && <Filter dataModel={item} />}
                        </ul>
                    );
                })}
            </div>
        </div>
    );
}

export default TabContents;

* selectedData =[1,2]

 

 

 

2. Filter.tsx

import { useCallback, useEffect, useState } from 'react';
import { FieldValues, useFieldArray, useForm } from 'react-hook-form';
import { useRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { filterData, filterDataAtom } from '../../store/filter';

function debounce<T extends (...args: any[]) => void>(callback: T, delay: number): (...args: Parameters<T>) => void {
    let timer: ReturnType<typeof setTimeout> | null;
    return (...args: Parameters<T>) => {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            callback(...args);
        }, delay);
    };
}

function Filter({ dataModel }: { dataModel: string }) {
    const [savedFilters, setSavedFilters] = useRecoilState<filterData[]>(filterDataAtom);

    const {
        control,
        handleSubmit,
        register,
        setValue,
        getValues,
        watch,
        formState: { errors },
        reset,
    } = useForm({
        defaultValues: {
            filters: [
                {
                    id: uuidv4(),
                    logic: 'AND',
                    itemName: 'Name',
                    operation: '==',
                    value: '',
                    minValue: '',
                    maxValue: '',
                },
            ],
        },
    });

    // useFieldArray를 사용하여 배열 관리
    const { fields, append, remove } = useFieldArray({
        control,
        name: 'filters',
    });

    useEffect(() => {
        const filterDataForModel = savedFilters.find((filterData) => filterData.dataModel === dataModel);

        if (filterDataForModel) {
            reset({ filters: filterDataForModel.detailFilters });
        }
    }, [savedFilters, dataModel, reset]);

    //input의 경우 debounce처리
    const debounceUpdate = useCallback(
        debounce((index: number, fieldName: string, value: string) => {
            setValue(`filters.${index}.${fieldName}` as any, value);
        }, 300),
        [setValue]
    );

    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>, index: number, fieldName: string) => {
        const { value } = e.target;
        debounceUpdate(index, fieldName, value);
    };

    //submit
    const onSubmit = (data: any) => {
        //리코일에 저장
        const updatedFilterData = {
            dataModel: dataModel, // dataModel 값을 그대로 사용
            detailFilters: data.filters, // 필터 데이터를 detailFilters에 저장
        };

        console.log('updatedFilterData', updatedFilterData);
        setSavedFilters((prev) => {
            const existingIndex = prev.findIndex((filter) => filter.dataModel === dataModel);

            if (existingIndex !== -1) {
                //존재하면 해당 항목만 업데이트
                const updateFilters = [...prev];
                updateFilters[existingIndex] = updatedFilterData;
                return updateFilters;
            } else {
                //없으면 항목 추가
                return [...prev, updatedFilterData];
            }
        });
    };

    const handleRemoveFilter = (index: number) => {
        // useFieldArray 사용해서 해당항목 제거
        remove(index);

        //Recoil
        setSavedFilters((prev) => {
            const existingIndex = prev.findIndex((filter) => filter.dataModel === dataModel);
            if (existingIndex !== -1) {
                const updatedFilters = prev[existingIndex].detailFilters.filter((filter, idx) => idx !== index);
                const updatedFilterData = {
                    dataModel: dataModel,
                    detailFilters: updatedFilters,
                };
                const updatedSavedFilters = [...prev];
                updatedSavedFilters[existingIndex] = updatedFilterData;
                return updatedSavedFilters;
            }
            return prev;
        });
    };

    return (
        <div className="p-[20px] max-w-[600px] m-auto">
            <h2>Filter Manager</h2>
            <form onSubmit={handleSubmit(onSubmit)}>
                {fields.map((filter, index) => (
                    <>
                        <p>{dataModel}</p>
                        <div
                            key={filter.id}
                            style={{
                                marginBottom: '10px',
                                padding: '10px',
                                border: '1px solid #ddd',
                                borderRadius: '4px',
                            }}
                        >
                            <div>
                                <label>Logic: </label>
                                <select {...register(`filters.${index}.logic`)} defaultValue={filter.logic}>
                                    <option value="AND">AND</option>
                                    <option value="OR">OR</option>
                                </select>
                            </div>

                            <div>
                                <label>Item Name: </label>
                                <select
                                    {...register(`filters.${index}.itemName`, { required: 'Item Name is required' })}
                                    defaultValue={filter.itemName}
                                >
                                    <option value="Name">Name</option>
                                    <option value="Id">Id</option>
                                    <option value="Password">Password</option>
                                </select>
                                {errors.filters?.[index]?.itemName && (
                                    <span style={{ color: 'red' }}>{errors.filters[index]?.itemName?.message}</span>
                                )}
                            </div>

                            <div>
                                <label>Operation: </label>
                                <select {...register(`filters.${index}.operation`)} defaultValue={filter.operation}>
                                    <option value="==">==</option>
                                    <option value="!=">!=</option>
                                    <option value="~=">~=</option>
                                </select>
                                {errors.filters?.[index]?.operation && (
                                    <span style={{ color: 'red' }}>{errors.filters[index]?.operation?.message}</span>
                                )}
                            </div>

                            <div>
                                <label>Value: </label>
                                <input
                                    type="text"
                                    defaultValue={filter.value}
                                    {...register(`filters.${index}.value`, { required: '!' })}
                                    onChange={(e) => handleInputChange(e, index, 'value')}
                                />
                                {errors.filters?.[index]?.value && (
                                    <span style={{ color: 'red' }}>{errors.filters[index]?.value?.message}</span>
                                )}
                            </div>

                            <div>
                                <label>Min Value: </label>
                                <input
                                    type="text"
                                    defaultValue={filter.minValue}
                                    {...register(`filters.${index}.minValue`, {
                                        required: filter.operation && 'Min Value is required',
                                    })}
                                    onChange={(e) => handleInputChange(e, index, 'minValue')}
                                />
                                {errors.filters?.[index]?.minValue && (
                                    <span style={{ color: 'red' }}>{errors.filters[index]?.minValue?.message}</span>
                                )}
                            </div>

                            <div>
                                <label>Max Value: </label>
                                <input
                                    type="text"
                                    defaultValue={filter.maxValue}
                                    {...register(`filters.${index}.maxValue`, {
                                        required: filter.operation && 'Max Value is required',
                                    })}
                                    onChange={(e) => handleInputChange(e, index, 'maxValue')}
                                />{' '}
                                {errors.filters?.[index]?.maxValue && (
                                    <span style={{ color: 'red' }}>{errors.filters[index]?.maxValue?.message}</span>
                                )}
                            </div>

                            <button
                                type="button"
                                className="mt-[10px] text-red-400"
                                onClick={() => handleRemoveFilter(index)}
                            >
                                Filter 삭제
                            </button>
                        </div>
                    </>
                ))}

                <button
                    type="button"
                    onClick={() =>
                        append({
                            id: uuidv4(),
                            logic: 'AND',
                            itemName: 'Name',
                            operation: '==',
                            value: '',
                            minValue: '',
                            maxValue: '',
                        })
                    }
                >
                    Filter 추가
                </button>

                <button type="submit" style={{ marginTop: '2-100px', color: 'red' }}>
                    Filter 저장
                </button>
            </form>
        </div>
    );
}
export default Filter;

 

3. Recoil

import { atom } from 'recoil';

export const filterDataAtom = atom<filterData[]>({
    key: 'filterData',
    default: [
        {
            dataModel: '',
            detailFilters: [],
        },
    ],
});

export interface filterData {
    dataModel: string;
    detailFilters: detailFilters[];
}

export interface detailFilters {
    id: string;
    logic: string;
    itemName: string;
    operation: string;
    value: string;
    minValue: string;
    maxValue: string;
}
반응형

'React' 카테고리의 다른 글

[React] Chart.js  (0) 2025.01.01
React : BreadCrumb + Tab  (0) 2024.12.01
[React] 드래그 가능한 사이즈 조절 컴포넌트 구현  (2) 2024.09.22
페이지네이션  (0) 2024.08.12
리액트 폴더구조 : 재귀적으로 컴포넌트 구조  (0) 2024.07.28