불필ㅇㅛ 내여ㄱ 삭ㅈ

This commit is contained in:
focp212@naver.com
2025-09-19 10:00:47 +09:00
parent 028b0a96a7
commit f54c654ad6
6 changed files with 2 additions and 485 deletions

View File

@@ -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;

View File

@@ -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';

View File

@@ -1,5 +1,3 @@
export { useAuth } from './useAuth';
export { useLoginForm } from './useLoginForm';
export { useAppBridge } from './useAppBridge';
export { useScrollDirection } from './useScrollDirection';
export { useSearchFilter } from './useSearchFilter';

View File

@@ -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,
};
};

View File

@@ -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
};
};

View File

@@ -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">