불필ㅇㅛ 내여ㄱ 삭ㅈ
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 SlideMenu } from './SlideMenu';
|
||||
export { default as SearchFilterButton } from './SearchFilterButton';
|
||||
export { default as SearchFilterExample } from './SearchFilterExample';
|
||||
export { default as LanguageSwitcher } from './LanguageSwitcher';
|
||||
@@ -1,5 +1,3 @@
|
||||
export { useAuth } from './useAuth';
|
||||
export { useLoginForm } from './useLoginForm';
|
||||
export { useAppBridge } from './useAppBridge';
|
||||
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 { appBridge } from '../utils/appBridge';
|
||||
import { SearchFilterExample, LanguageSwitcher } from '../components';
|
||||
import { LanguageSwitcher } from '../components';
|
||||
import { SearchFilter } from '../types/filter';
|
||||
|
||||
interface TestResult {
|
||||
@@ -191,12 +191,7 @@ const Test: React.FC = () => {
|
||||
)}
|
||||
</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 && (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
|
||||
Reference in New Issue
Block a user