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 |