불필ㅇㅛ 내여ㄱ 삭ㅈ
This commit is contained in:
@@ -1,100 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSearchFilter } from '@/hooks/useSearchFilter';
|
|
||||||
import SearchFilterButton from './SearchFilterButton';
|
|
||||||
import { SearchFilter, formatDateRange } from '@/types/filter';
|
|
||||||
|
|
||||||
interface SearchFilterExampleProps {
|
|
||||||
onFilterChange?: (filter: SearchFilter) => void;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SearchFilterExample: React.FC<SearchFilterExampleProps> = ({
|
|
||||||
onFilterChange,
|
|
||||||
className = ''
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const searchFilterOptions = onFilterChange ? {
|
|
||||||
storageKey: 'transaction_search_filter',
|
|
||||||
onFilterChange
|
|
||||||
} : {
|
|
||||||
storageKey: 'transaction_search_filter'
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
filter,
|
|
||||||
isModalOpen,
|
|
||||||
openModal,
|
|
||||||
closeModal,
|
|
||||||
handleFilterConfirm,
|
|
||||||
getDisplayText,
|
|
||||||
isFilterActive,
|
|
||||||
resetFilter
|
|
||||||
} = useSearchFilter(searchFilterOptions);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`space-y-4 ${className}`}>
|
|
||||||
{/* 조회조건 버튼과 필터 정보 */}
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between">
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<SearchFilterButton
|
|
||||||
displayText={getDisplayText()}
|
|
||||||
isActive={isFilterActive}
|
|
||||||
onClick={openModal}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 초기화 버튼 (필터가 활성화된 경우만 표시) */}
|
|
||||||
{isFilterActive && (
|
|
||||||
<button
|
|
||||||
onClick={resetFilter}
|
|
||||||
className="text-sm text-gray-500 hover:text-gray-700 underline"
|
|
||||||
>
|
|
||||||
{t('common.reset')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 현재 필터 요약 정보 */}
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
{filter.dateRange && (
|
|
||||||
<span>
|
|
||||||
{formatDateRange(filter.dateRange)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 현재 적용된 필터 상세 정보 (디버깅용) */}
|
|
||||||
<div className="bg-gray-50 p-4 rounded-lg">
|
|
||||||
<h3 className="text-sm font-medium text-gray-900 mb-2">{t('filter.currentConditions')}</h3>
|
|
||||||
<div className="space-y-1 text-sm text-gray-600">
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">{t('filter.period')}:</span> {filter.period === 'custom' ? t('filter.periods.custom') : t(`filter.periods.${filter.period}`)}
|
|
||||||
</div>
|
|
||||||
{filter.dateRange && (
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">{t('filter.dateRange')}:</span> {formatDateRange(filter.dateRange)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">{t('filter.transactionType')}:</span> {t(`filter.transactionTypes.${filter.transactionType}`)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">{t('filter.sortOrder')}:</span> {t(`filter.sortOrders.${filter.sortOrder}`)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 조회조건 설정 모달
|
|
||||||
<SearchFilterModal
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
onClose={closeModal}
|
|
||||||
onConfirm={handleFilterConfirm}
|
|
||||||
initialFilter={filter}
|
|
||||||
/>
|
|
||||||
*/}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchFilterExample;
|
|
||||||
@@ -6,5 +6,4 @@ export { default as Services } from './Services';
|
|||||||
export { default as TabBar } from './TabBar';
|
export { default as TabBar } from './TabBar';
|
||||||
export { default as SlideMenu } from './SlideMenu';
|
export { default as SlideMenu } from './SlideMenu';
|
||||||
export { default as SearchFilterButton } from './SearchFilterButton';
|
export { default as SearchFilterButton } from './SearchFilterButton';
|
||||||
export { default as SearchFilterExample } from './SearchFilterExample';
|
|
||||||
export { default as LanguageSwitcher } from './LanguageSwitcher';
|
export { default as LanguageSwitcher } from './LanguageSwitcher';
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
export { useAuth } from './useAuth';
|
export { useAuth } from './useAuth';
|
||||||
export { useLoginForm } from './useLoginForm';
|
|
||||||
export { useAppBridge } from './useAppBridge';
|
export { useAppBridge } from './useAppBridge';
|
||||||
export { useScrollDirection } from './useScrollDirection';
|
export { useScrollDirection } from './useScrollDirection';
|
||||||
export { useSearchFilter } from './useSearchFilter';
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
import { useState, useCallback } from 'react';
|
|
||||||
import { LoginCredentials, RegisterData } from '@/types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
|
|
||||||
interface FormErrors {
|
|
||||||
email?: string;
|
|
||||||
password?: string;
|
|
||||||
name?: string;
|
|
||||||
phone?: string;
|
|
||||||
confirmPassword?: string;
|
|
||||||
general?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseLoginFormReturn {
|
|
||||||
formData: LoginCredentials;
|
|
||||||
errors: FormErrors;
|
|
||||||
isLoading: boolean;
|
|
||||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
handleSubmit: (onSubmit: (data: LoginCredentials) => Promise<void>) => (e: React.FormEvent) => Promise<void>;
|
|
||||||
resetForm: () => void;
|
|
||||||
setError: (field: keyof FormErrors, message: string) => void;
|
|
||||||
clearErrors: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseRegisterFormReturn {
|
|
||||||
formData: RegisterData & { confirmPassword: string };
|
|
||||||
errors: FormErrors;
|
|
||||||
isLoading: boolean;
|
|
||||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
handleSubmit: (onSubmit: (data: RegisterData) => Promise<void>) => (e: React.FormEvent) => Promise<void>;
|
|
||||||
resetForm: () => void;
|
|
||||||
setError: (field: keyof FormErrors, message: string) => void;
|
|
||||||
clearErrors: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useLoginForm = (): UseLoginFormReturn => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [formData, setFormData] = useState<LoginCredentials>({
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
});
|
|
||||||
const [errors, setErrors] = useState<FormErrors>({});
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const validateLoginForm = useCallback((data: LoginCredentials): FormErrors => {
|
|
||||||
const newErrors: FormErrors = {};
|
|
||||||
|
|
||||||
if (!data.email.trim()) {
|
|
||||||
newErrors.email = t('login.emailPlaceholder');
|
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
|
||||||
newErrors.email = t('login.emailPlaceholder');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.password.trim()) {
|
|
||||||
newErrors.password = t('login.passwordPlaceholder');
|
|
||||||
} else if (data.password.length < 6) {
|
|
||||||
newErrors.password = t('login.passwordMinLength');
|
|
||||||
}
|
|
||||||
|
|
||||||
return newErrors;
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
|
||||||
|
|
||||||
// 해당 필드의 에러 제거
|
|
||||||
if (errors[name as keyof FormErrors]) {
|
|
||||||
setErrors(prev => ({ ...prev, [name]: undefined }));
|
|
||||||
}
|
|
||||||
}, [errors]);
|
|
||||||
|
|
||||||
const handleSubmit = useCallback((onSubmit: (data: LoginCredentials) => Promise<void>) => {
|
|
||||||
return async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const validationErrors = validateLoginForm(formData);
|
|
||||||
if (Object.keys(validationErrors).length > 0) {
|
|
||||||
setErrors(validationErrors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setErrors({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await onSubmit(formData);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : t('login.generalError');
|
|
||||||
setErrors({ general: errorMessage });
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [formData, validateLoginForm, t]);
|
|
||||||
|
|
||||||
const resetForm = useCallback(() => {
|
|
||||||
setFormData({ email: '', password: '' });
|
|
||||||
setErrors({});
|
|
||||||
setIsLoading(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setError = useCallback((field: keyof FormErrors, message: string) => {
|
|
||||||
setErrors(prev => ({ ...prev, [field]: message }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearErrors = useCallback(() => {
|
|
||||||
setErrors({});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
formData,
|
|
||||||
errors,
|
|
||||||
isLoading,
|
|
||||||
handleChange,
|
|
||||||
handleSubmit,
|
|
||||||
resetForm,
|
|
||||||
setError,
|
|
||||||
clearErrors,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRegisterForm = (): UseRegisterFormReturn => {
|
|
||||||
const [formData, setFormData] = useState<RegisterData & { confirmPassword: string }>({
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
name: '',
|
|
||||||
phone: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
});
|
|
||||||
const [errors, setErrors] = useState<FormErrors>({});
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const validateRegisterForm = useCallback((data: RegisterData & { confirmPassword: string }): FormErrors => {
|
|
||||||
const newErrors: FormErrors = {};
|
|
||||||
|
|
||||||
if (!data.email.trim()) {
|
|
||||||
newErrors.email = '이메일을 입력해주세요.';
|
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
|
||||||
newErrors.email = '올바른 이메일 형식을 입력해주세요.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.password.trim()) {
|
|
||||||
newErrors.password = t('login.passwordPlaceholder');
|
|
||||||
} else if (data.password.length < 8) {
|
|
||||||
newErrors.password = t('login.passwordMinLength');
|
|
||||||
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(data.password)) {
|
|
||||||
newErrors.password = t('login.passwordComplexity');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.confirmPassword.trim()) {
|
|
||||||
newErrors.confirmPassword = t('login.confirmPasswordPlaceholder');
|
|
||||||
} else if (data.password !== data.confirmPassword) {
|
|
||||||
newErrors.confirmPassword = t('login.confirmPasswordMismatch');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.name.trim()) {
|
|
||||||
newErrors.name = t('login.namePlaceholder');
|
|
||||||
} else if (data.name.length < 2) {
|
|
||||||
newErrors.name = t('login.nameMinLength');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.phone && !/^[0-9-+\s()]+$/.test(data.phone)) {
|
|
||||||
newErrors.phone = t('login.phoneFormat');
|
|
||||||
}
|
|
||||||
|
|
||||||
return newErrors;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
|
||||||
|
|
||||||
// 해당 필드의 에러 제거
|
|
||||||
if (errors[name as keyof FormErrors]) {
|
|
||||||
setErrors(prev => ({ ...prev, [name]: undefined }));
|
|
||||||
}
|
|
||||||
}, [errors]);
|
|
||||||
|
|
||||||
const handleSubmit = useCallback((onSubmit: (data: RegisterData) => Promise<void>) => {
|
|
||||||
return async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const validationErrors = validateRegisterForm(formData);
|
|
||||||
if (Object.keys(validationErrors).length > 0) {
|
|
||||||
setErrors(validationErrors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
setErrors({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { confirmPassword: _confirmPassword, ...registerData } = formData;
|
|
||||||
await onSubmit(registerData);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : '회원가입 중 오류가 발생했습니다.';
|
|
||||||
setErrors({ general: errorMessage });
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [formData, validateRegisterForm]);
|
|
||||||
|
|
||||||
const resetForm = useCallback(() => {
|
|
||||||
setFormData({
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
name: '',
|
|
||||||
phone: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
});
|
|
||||||
setErrors({});
|
|
||||||
setIsLoading(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setError = useCallback((field: keyof FormErrors, message: string) => {
|
|
||||||
setErrors(prev => ({ ...prev, [field]: message }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearErrors = useCallback(() => {
|
|
||||||
setErrors({});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
formData,
|
|
||||||
errors,
|
|
||||||
isLoading,
|
|
||||||
handleChange,
|
|
||||||
handleSubmit,
|
|
||||||
resetForm,
|
|
||||||
setError,
|
|
||||||
clearErrors,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
SearchFilter,
|
|
||||||
DEFAULT_SEARCH_FILTER,
|
|
||||||
getDateRangeFromPeriod,
|
|
||||||
getFilterDisplayText
|
|
||||||
} from '@/types/filter';
|
|
||||||
|
|
||||||
interface UseSearchFilterOptions {
|
|
||||||
initialFilter?: SearchFilter;
|
|
||||||
storageKey?: string; // 로컬 스토리지에 저장할 키
|
|
||||||
onFilterChange?: (filter: SearchFilter) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseSearchFilterReturn {
|
|
||||||
filter: SearchFilter;
|
|
||||||
setFilter: (filter: SearchFilter) => void;
|
|
||||||
resetFilter: () => void;
|
|
||||||
isModalOpen: boolean;
|
|
||||||
openModal: () => void;
|
|
||||||
closeModal: () => void;
|
|
||||||
handleFilterConfirm: (newFilter: SearchFilter) => void;
|
|
||||||
getDisplayText: () => string;
|
|
||||||
isFilterActive: boolean; // 기본값과 다른지 확인
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSearchFilter = (options: UseSearchFilterOptions = {}): UseSearchFilterReturn => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const {
|
|
||||||
initialFilter = DEFAULT_SEARCH_FILTER,
|
|
||||||
storageKey,
|
|
||||||
onFilterChange
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const isInitialMount = useRef(true);
|
|
||||||
|
|
||||||
// 로컬 스토리지에서 저장된 필터 로드
|
|
||||||
const loadStoredFilter = useCallback((): SearchFilter => {
|
|
||||||
if (!storageKey) return initialFilter;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(storageKey);
|
|
||||||
if (stored) {
|
|
||||||
const parsedFilter = JSON.parse(stored);
|
|
||||||
// Date 객체 복원
|
|
||||||
if (parsedFilter.dateRange) {
|
|
||||||
parsedFilter.dateRange = {
|
|
||||||
startDate: new Date(parsedFilter.dateRange.startDate),
|
|
||||||
endDate: new Date(parsedFilter.dateRange.endDate)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { ...initialFilter, ...parsedFilter };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to load stored filter:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return initialFilter;
|
|
||||||
}, [initialFilter, storageKey]);
|
|
||||||
|
|
||||||
const [filter, setFilterState] = useState<SearchFilter>(loadStoredFilter);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
|
|
||||||
// 필터 저장
|
|
||||||
const saveFilter = useCallback((newFilter: SearchFilter) => {
|
|
||||||
if (storageKey) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify(newFilter));
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save filter to storage:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [storageKey]);
|
|
||||||
|
|
||||||
// 필터 설정
|
|
||||||
const setFilter = useCallback((newFilter: SearchFilter) => {
|
|
||||||
// 날짜 범위가 없고 period가 custom이 아닌 경우 자동 계산
|
|
||||||
const finalFilter = {
|
|
||||||
...newFilter,
|
|
||||||
dateRange: newFilter.dateRange || getDateRangeFromPeriod(newFilter.period)
|
|
||||||
};
|
|
||||||
|
|
||||||
//setFilterState(finalFilter);
|
|
||||||
//saveFilter(finalFilter);
|
|
||||||
//onFilterChange?.(finalFilter);
|
|
||||||
}, [saveFilter, onFilterChange]);
|
|
||||||
|
|
||||||
// 필터 초기화
|
|
||||||
const resetFilter = useCallback(() => {
|
|
||||||
setFilter(initialFilter);
|
|
||||||
}, [initialFilter, setFilter]);
|
|
||||||
|
|
||||||
// 모달 제어
|
|
||||||
const openModal = useCallback(() => {
|
|
||||||
setIsModalOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeModal = useCallback(() => {
|
|
||||||
setIsModalOpen(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 모달에서 확인 버튼 클릭 시
|
|
||||||
const handleFilterConfirm = useCallback((newFilter: SearchFilter) => {
|
|
||||||
setFilter(newFilter);
|
|
||||||
closeModal();
|
|
||||||
}, [setFilter, closeModal]);
|
|
||||||
|
|
||||||
// 표시 텍스트 가져오기
|
|
||||||
const getDisplayText = useCallback(() => {
|
|
||||||
return getFilterDisplayText(filter, t);
|
|
||||||
}, [filter, t]);
|
|
||||||
|
|
||||||
// 기본값과 다른지 확인
|
|
||||||
const isFilterActive = useCallback(() => {
|
|
||||||
return JSON.stringify(filter) !== JSON.stringify(DEFAULT_SEARCH_FILTER);
|
|
||||||
}, [filter])();
|
|
||||||
|
|
||||||
// 초기 로드 시 필터 변경 콜백 호출
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInitialMount.current && onFilterChange) {
|
|
||||||
onFilterChange(filter);
|
|
||||||
isInitialMount.current = false;
|
|
||||||
}
|
|
||||||
}, [filter, onFilterChange]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
filter,
|
|
||||||
setFilter,
|
|
||||||
resetFilter,
|
|
||||||
isModalOpen,
|
|
||||||
openModal,
|
|
||||||
closeModal,
|
|
||||||
handleFilterConfirm,
|
|
||||||
getDisplayText,
|
|
||||||
isFilterActive
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { appBridge } from '../utils/appBridge';
|
import { appBridge } from '../utils/appBridge';
|
||||||
import { SearchFilterExample, LanguageSwitcher } from '../components';
|
import { LanguageSwitcher } from '../components';
|
||||||
import { SearchFilter } from '../types/filter';
|
import { SearchFilter } from '../types/filter';
|
||||||
|
|
||||||
interface TestResult {
|
interface TestResult {
|
||||||
@@ -191,12 +191,7 @@ const Test: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 조회조건 테스트 섹션 */}
|
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">조회조건 테스트</h2>
|
|
||||||
<SearchFilterExample onFilterChange={handleFilterChange} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 검색 결과 표시 */}
|
{/* 검색 결과 표시 */}
|
||||||
{searchData.length > 0 && (
|
{searchData.length > 0 && (
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user