Refactor: Codebase review and cleanup
Review the entire codebase for potential issues and perform necessary cleanup.
This commit is contained in:
@@ -1,80 +1,31 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState, ReactNode } from 'react';
|
import React from 'react';
|
||||||
import { isIOSPlatform } from '@/utils/platform';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface SafeAreaContainerProps {
|
interface SafeAreaContainerProps {
|
||||||
children: ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
topOnly?: boolean;
|
extraBottomPadding?: boolean;
|
||||||
bottomOnly?: boolean;
|
|
||||||
extraBottomPadding?: boolean; // 추가 하단 여백 옵션
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 플랫폼별 안전 영역(Safe Area)을 고려한 컨테이너 컴포넌트
|
* iOS의 안전 영역(notch, home indicator 등)을 고려한 컨테이너
|
||||||
* iOS에서는 노치/다이나믹 아일랜드를 고려한 여백 적용
|
* 모든 페이지 최상위 컴포넌트로 사용해야 함
|
||||||
*/
|
*/
|
||||||
const SafeAreaContainer: React.FC<SafeAreaContainerProps> = ({
|
const SafeAreaContainer: React.FC<SafeAreaContainerProps> = ({
|
||||||
children,
|
children,
|
||||||
className = '',
|
className = '',
|
||||||
topOnly = false,
|
extraBottomPadding = false
|
||||||
bottomOnly = false,
|
|
||||||
extraBottomPadding = false // 기본값은 false
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isIOS, setIsIOS] = useState(false);
|
|
||||||
|
|
||||||
// 마운트 시 플랫폼 확인
|
|
||||||
useEffect(() => {
|
|
||||||
const checkPlatform = async () => {
|
|
||||||
const isiOS = isIOSPlatform();
|
|
||||||
console.log('SafeAreaContainer: 플랫폼 확인 - iOS:', isiOS);
|
|
||||||
setIsIOS(isiOS);
|
|
||||||
};
|
|
||||||
|
|
||||||
checkPlatform();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 플랫폼에 따른 클래스 결정
|
|
||||||
let safeAreaClass = 'safe-area-container';
|
|
||||||
|
|
||||||
if (isIOS) {
|
|
||||||
if (!bottomOnly) safeAreaClass += ' has-safe-area-top'; // iOS 상단 안전 영역
|
|
||||||
if (!topOnly) safeAreaClass += ' has-safe-area-bottom'; // iOS 하단 안전 영역
|
|
||||||
safeAreaClass += ' ios-safe-area'; // iOS 전용 클래스 추가
|
|
||||||
} else {
|
|
||||||
if (!bottomOnly) safeAreaClass += ' pt-4'; // 안드로이드 상단 여백
|
|
||||||
if (!topOnly) safeAreaClass += ' pb-4'; // 안드로이드 하단 여백
|
|
||||||
}
|
|
||||||
|
|
||||||
// 추가 하단 여백 적용
|
|
||||||
const extraBottomClass = extraBottomPadding ? 'pb-[80px]' : '';
|
|
||||||
|
|
||||||
// 디버그용 로그 추가
|
|
||||||
useEffect(() => {
|
|
||||||
if (isIOS) {
|
|
||||||
console.log('SafeAreaContainer: iOS 안전 영역 적용됨', {
|
|
||||||
topOnly,
|
|
||||||
bottomOnly,
|
|
||||||
extraBottomPadding
|
|
||||||
});
|
|
||||||
|
|
||||||
// 안전 영역 값 확인 (CSS 변수)
|
|
||||||
try {
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement);
|
|
||||||
console.log('Safe area 변수 값:', {
|
|
||||||
top: computedStyle.getPropertyValue('--safe-area-top'),
|
|
||||||
bottom: computedStyle.getPropertyValue('--safe-area-bottom'),
|
|
||||||
left: computedStyle.getPropertyValue('--safe-area-left'),
|
|
||||||
right: computedStyle.getPropertyValue('--safe-area-right')
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('CSS 변수 확인 중 오류:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isIOS, topOnly, bottomOnly]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${safeAreaClass} ${extraBottomClass} ${className}`}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'min-h-screen bg-neuro-background',
|
||||||
|
'pt-safe pb-safe pl-safe pr-safe', // iOS 안전 영역 적용
|
||||||
|
extraBottomPadding ? 'pb-24' : '',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Calendar, Search, ChevronLeft, ChevronRight } from 'lucide-react';
|
import { Calendar, Search, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
import { formatCurrency } from '@/utils/formatters';
|
import { formatCurrency } from '@/utils/formatters';
|
||||||
|
import { formatMonthForDisplay } from '@/hooks/transactions/dateUtils';
|
||||||
|
|
||||||
interface TransactionsHeaderProps {
|
interface TransactionsHeaderProps {
|
||||||
selectedMonth: string;
|
selectedMonth: string;
|
||||||
@@ -24,14 +25,25 @@ const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
|
|||||||
totalExpenses,
|
totalExpenses,
|
||||||
isDisabled
|
isDisabled
|
||||||
}) => {
|
}) => {
|
||||||
console.log('TransactionsHeader 렌더링:', {
|
// 월 표시 형식 변환 (2024-04 -> 2024년 04월)
|
||||||
selectedMonth,
|
const displayMonth = useMemo(() =>
|
||||||
totalExpenses
|
formatMonthForDisplay(selectedMonth),
|
||||||
});
|
[selectedMonth]
|
||||||
|
);
|
||||||
|
|
||||||
// 예산 정보가 없는 경우 기본값 사용
|
// 예산 정보가 없는 경우 기본값 사용
|
||||||
const targetAmount = budgetData?.monthly?.targetAmount || 0;
|
const targetAmount = budgetData?.monthly?.targetAmount || 0;
|
||||||
|
|
||||||
|
// 디버깅을 위한 로그
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log('TransactionsHeader 렌더링:', {
|
||||||
|
selectedMonth,
|
||||||
|
displayMonth,
|
||||||
|
totalExpenses,
|
||||||
|
targetAmount
|
||||||
|
});
|
||||||
|
}, [selectedMonth, displayMonth, totalExpenses, targetAmount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="py-4">
|
<header className="py-4">
|
||||||
<h1 className="font-bold neuro-text mb-3 text-xl">지출 내역</h1>
|
<h1 className="font-bold neuro-text mb-3 text-xl">지출 내역</h1>
|
||||||
@@ -61,7 +73,7 @@ const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
|
|||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar size={18} className="text-neuro-income" />
|
<Calendar size={18} className="text-neuro-income" />
|
||||||
<span className="font-medium text-lg">{selectedMonth}</span>
|
<span className="font-medium text-lg">{displayMonth}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AuthProvider } from './auth/AuthProvider';
|
import { AuthProvider } from './auth/AuthProvider';
|
||||||
import { useAuth } from './auth/useAuth';
|
|
||||||
|
|
||||||
export { AuthProvider, useAuth };
|
export { AuthProvider } from './auth/AuthProvider';
|
||||||
|
export { useAuth } from './auth/useAuth';
|
||||||
|
|
||||||
export default function AuthContextWrapper({ children }: { children: React.ReactNode }) {
|
export default function AuthContextWrapper({ children }: { children: React.ReactNode }) {
|
||||||
return <AuthProvider>{children}</AuthProvider>;
|
return <AuthProvider>{children}</AuthProvider>;
|
||||||
|
|||||||
6
src/contexts/auth/AuthContext.tsx
Normal file
6
src/contexts/auth/AuthContext.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
import React, { createContext } from 'react';
|
||||||
|
import { AuthContextType } from './types';
|
||||||
|
|
||||||
|
// AuthContext 생성
|
||||||
|
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
@@ -6,7 +6,7 @@ import { toast } from '@/hooks/useToast.wrapper';
|
|||||||
import { AuthContextType } from './types';
|
import { AuthContextType } from './types';
|
||||||
import * as authActions from './authActions';
|
import * as authActions from './authActions';
|
||||||
import { clearAllToasts } from '@/hooks/toast/toastManager';
|
import { clearAllToasts } from '@/hooks/toast/toastManager';
|
||||||
import { AuthContext } from './useAuth';
|
import { AuthContext } from './AuthContext';
|
||||||
|
|
||||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
|
|
||||||
import { useContext, createContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
import { AuthContext } from './AuthContext';
|
||||||
import { AuthContextType } from './types';
|
import { AuthContextType } from './types';
|
||||||
|
|
||||||
// AuthContext 생성
|
|
||||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 인증 컨텍스트에 접근하기 위한 커스텀 훅
|
* 인증 컨텍스트에 접근하기 위한 커스텀 훅
|
||||||
* AuthProvider 내부에서만 사용해야 함
|
* AuthProvider 내부에서만 사용해야 함
|
||||||
|
|||||||
@@ -1,47 +1,85 @@
|
|||||||
|
|
||||||
|
import { format, parse, addMonths, subMonths } from 'date-fns';
|
||||||
|
import { ko } from 'date-fns/locale';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 한글 월 이름 배열
|
* 월 형식 검증 함수 (YYYY-MM 형식)
|
||||||
*/
|
*/
|
||||||
export const MONTHS_KR = [
|
export const isValidMonth = (month: string): boolean => {
|
||||||
'1월', '2월', '3월', '4월', '5월', '6월',
|
const regex = /^\d{4}-(0[1-9]|1[0-2])$/;
|
||||||
'7월', '8월', '9월', '10월', '11월', '12월'
|
return regex.test(month);
|
||||||
];
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 현재 월 가져오기
|
* 현재 년월 가져오기
|
||||||
*/
|
*/
|
||||||
export const getCurrentMonth = (): string => {
|
export const getCurrentMonth = (): string => {
|
||||||
const now = new Date();
|
return format(new Date(), 'yyyy-MM');
|
||||||
const month = now.getMonth(); // 0-indexed
|
|
||||||
return `${MONTHS_KR[month]}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이전 월 가져오기
|
* 이전 월 가져오기
|
||||||
*/
|
*/
|
||||||
export const getPrevMonth = (currentMonth: string): string => {
|
export const getPrevMonth = (month: string): string => {
|
||||||
const currentMonthIdx = MONTHS_KR.findIndex(m => m === currentMonth);
|
// 입력값 검증
|
||||||
|
if (!isValidMonth(month)) {
|
||||||
|
console.warn('유효하지 않은 월 형식:', month);
|
||||||
|
return getCurrentMonth();
|
||||||
|
}
|
||||||
|
|
||||||
if (currentMonthIdx === 0) {
|
try {
|
||||||
// 1월인 경우 12월로 변경
|
// 월 문자열을 날짜로 파싱
|
||||||
return `${MONTHS_KR[11]}`;
|
const date = parse(month, 'yyyy-MM', new Date());
|
||||||
} else {
|
// 한 달 이전
|
||||||
const prevMonthIdx = currentMonthIdx - 1;
|
const prevMonth = subMonths(date, 1);
|
||||||
return `${MONTHS_KR[prevMonthIdx]}`;
|
// yyyy-MM 형식으로 반환
|
||||||
|
return format(prevMonth, 'yyyy-MM');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('이전 월 계산 중 오류:', error);
|
||||||
|
return getCurrentMonth();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 다음 월 가져오기
|
* 다음 월 가져오기
|
||||||
*/
|
*/
|
||||||
export const getNextMonth = (currentMonth: string): string => {
|
export const getNextMonth = (month: string): string => {
|
||||||
const currentMonthIdx = MONTHS_KR.findIndex(m => m === currentMonth);
|
// 입력값 검증
|
||||||
|
if (!isValidMonth(month)) {
|
||||||
|
console.warn('유효하지 않은 월 형식:', month);
|
||||||
|
return getCurrentMonth();
|
||||||
|
}
|
||||||
|
|
||||||
if (currentMonthIdx === 11) {
|
try {
|
||||||
// 12월인 경우 1월로 변경
|
// 월 문자열을 날짜로 파싱
|
||||||
return `${MONTHS_KR[0]}`;
|
const date = parse(month, 'yyyy-MM', new Date());
|
||||||
} else {
|
// 한 달 이후
|
||||||
const nextMonthIdx = currentMonthIdx + 1;
|
const nextMonth = addMonths(date, 1);
|
||||||
return `${MONTHS_KR[nextMonthIdx]}`;
|
// yyyy-MM 형식으로 반환
|
||||||
|
return format(nextMonth, 'yyyy-MM');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('다음 월 계산 중 오류:', error);
|
||||||
|
return getCurrentMonth();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 표시 형식으로 변환 (yyyy년 MM월)
|
||||||
|
*/
|
||||||
|
export const formatMonthForDisplay = (month: string): string => {
|
||||||
|
try {
|
||||||
|
// 입력값 검증
|
||||||
|
if (!isValidMonth(month)) {
|
||||||
|
console.warn('유효하지 않은 월 형식:', month);
|
||||||
|
return format(new Date(), 'yyyy년 MM월', { locale: ko });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 월 문자열을 날짜로 파싱
|
||||||
|
const date = parse(month, 'yyyy-MM', new Date());
|
||||||
|
// yyyy년 MM월 형식으로 반환 (한국어 로케일)
|
||||||
|
return format(date, 'yyyy년 MM월', { locale: ko });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('월 형식 변환 중 오류:', error);
|
||||||
|
return month;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,126 +2,115 @@
|
|||||||
import { Transaction } from '@/contexts/budget/types';
|
import { Transaction } from '@/contexts/budget/types';
|
||||||
import { parseTransactionDate } from '@/utils/dateParser';
|
import { parseTransactionDate } from '@/utils/dateParser';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { ko } from 'date-fns/locale';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 월별로 트랜잭션 필터링 - 개선된 버전
|
* 트랜잭션을 월별로 필터링
|
||||||
*/
|
*/
|
||||||
export const filterTransactionsByMonth = (
|
export const filterTransactionsByMonth = (transactions: Transaction[], selectedMonth: string): Transaction[] => {
|
||||||
transactions: Transaction[],
|
if (!transactions || transactions.length === 0) {
|
||||||
selectedMonth: string
|
|
||||||
): Transaction[] => {
|
|
||||||
console.log(`월별 트랜잭션 필터링: ${selectedMonth}, 총 데이터 수: ${transactions.length}`);
|
|
||||||
|
|
||||||
// 필터링 전 샘플 데이터 로그
|
|
||||||
if (transactions.length > 0) {
|
|
||||||
console.log('샘플 트랜잭션 날짜:', transactions.slice(0, 3).map(t => t.date));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 선택된 월의 숫자 추출 (ex: "4월" -> 4)
|
|
||||||
const selectedMonthNumber = parseInt(selectedMonth.replace('월', ''));
|
|
||||||
if (isNaN(selectedMonthNumber) || selectedMonthNumber < 1 || selectedMonthNumber > 12) {
|
|
||||||
console.error('잘못된 월 형식:', selectedMonth);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = transactions.filter(transaction => {
|
|
||||||
// 트랜잭션 타입 확인 - 지출 항목만 포함
|
|
||||||
if (transaction.type !== 'expense') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 날짜가 없는 경우 필터링 제외
|
|
||||||
if (!transaction.date) {
|
|
||||||
console.warn('날짜 없는 트랜잭션:', transaction);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 날짜 파싱
|
|
||||||
const parsedDate = parseTransactionDate(transaction.date);
|
|
||||||
if (!parsedDate) {
|
|
||||||
console.warn('날짜 파싱 실패:', transaction.date);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 월 비교
|
|
||||||
const transactionMonth = parsedDate.getMonth() + 1; // 0-based -> 1-based
|
|
||||||
const isMatchingMonth = transactionMonth === selectedMonthNumber;
|
|
||||||
|
|
||||||
if (isMatchingMonth) {
|
|
||||||
console.log(`트랜잭션 매칭: ${transaction.title}, 날짜: ${transaction.date}, 월: ${transactionMonth}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isMatchingMonth;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('트랜잭션 필터링 중 오류:', e, transaction);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`월별 필터링 결과: ${filtered.length}개 항목 (${selectedMonth})`);
|
|
||||||
return filtered;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
console.log(`월별 필터링 시작: ${selectedMonth}, 트랜잭션 수: ${transactions.length}`);
|
||||||
* 검색어로 트랜잭션 필터링
|
|
||||||
*/
|
|
||||||
export const filterTransactionsByQuery = (
|
|
||||||
transactions: Transaction[],
|
|
||||||
searchQuery: string
|
|
||||||
): Transaction[] => {
|
|
||||||
if (!searchQuery.trim()) return transactions;
|
|
||||||
|
|
||||||
const query = searchQuery.toLowerCase().trim();
|
|
||||||
console.log(`검색어 필터링: "${query}"`);
|
|
||||||
|
|
||||||
const filtered = transactions.filter(transaction => {
|
|
||||||
try {
|
|
||||||
return (
|
|
||||||
(transaction.title?.toLowerCase().includes(query)) ||
|
|
||||||
(transaction.category?.toLowerCase().includes(query)) ||
|
|
||||||
(transaction.paymentMethod?.toLowerCase().includes(query))
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('검색어 필터링 중 오류:', e, transaction);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`검색어 필터링 결과: ${filtered.length}개 항목`);
|
|
||||||
return filtered;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 총 지출 금액 계산 - 개선된 버전
|
|
||||||
*/
|
|
||||||
export const calculateTotalExpenses = (transactions: Transaction[]): number => {
|
|
||||||
try {
|
try {
|
||||||
// 유효한 트랜잭션만 필터링 (undefined, null 제외)
|
const [year, month] = selectedMonth.split('-').map(Number);
|
||||||
const validTransactions = transactions.filter(t => t && typeof t.amount !== 'undefined');
|
|
||||||
console.log(`유효한 트랜잭션 수: ${validTransactions.length}/${transactions.length}`);
|
|
||||||
|
|
||||||
// 디버깅용 로그
|
const filtered = transactions.filter(transaction => {
|
||||||
if (validTransactions.length > 0) {
|
const date = parseTransactionDate(transaction.date);
|
||||||
console.log('첫 번째 트랜잭션 정보:', {
|
|
||||||
title: validTransactions[0].title,
|
if (!date) {
|
||||||
amount: validTransactions[0].amount,
|
console.warn(`날짜를 파싱할 수 없음: ${transaction.date}, 트랜잭션 ID: ${transaction.id}`);
|
||||||
type: validTransactions[0].type
|
return false;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
const transactionYear = date.getFullYear();
|
||||||
|
const transactionMonth = date.getMonth() + 1; // JavaScript 월은 0부터 시작하므로 +1
|
||||||
|
|
||||||
|
const match = transactionYear === year && transactionMonth === month;
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
console.log(`트랜잭션 매칭: ${transaction.id}, 제목: ${transaction.title}, 날짜: ${transaction.date}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
|
||||||
const total = validTransactions.reduce((sum, t) => {
|
console.log(`월별 필터링 결과: ${filtered.length}개 트랜잭션`);
|
||||||
// 유효한 숫자인지 확인하고 기본값 처리
|
return filtered;
|
||||||
const amount = typeof t.amount === 'number' ? t.amount :
|
} catch (error) {
|
||||||
parseInt(t.amount as any) || 0;
|
console.error('월별 필터링 중 오류:', error);
|
||||||
return sum + amount;
|
return [];
|
||||||
}, 0);
|
|
||||||
|
|
||||||
console.log(`총 지출 계산: ${total}원 (${validTransactions.length}개 항목)`);
|
|
||||||
return total;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('총 지출 계산 중 오류:', e);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 트랜잭션을 검색어로 필터링
|
||||||
|
*/
|
||||||
|
export const filterTransactionsByQuery = (transactions: Transaction[], searchQuery: string): Transaction[] => {
|
||||||
|
if (!searchQuery || searchQuery.trim() === '') {
|
||||||
|
return transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedQuery = searchQuery.toLowerCase().trim();
|
||||||
|
|
||||||
|
return transactions.filter(transaction => {
|
||||||
|
const titleMatch = transaction.title.toLowerCase().includes(normalizedQuery);
|
||||||
|
const categoryMatch = transaction.category.toLowerCase().includes(normalizedQuery);
|
||||||
|
const amountMatch = transaction.amount.toString().includes(normalizedQuery);
|
||||||
|
|
||||||
|
return titleMatch || categoryMatch || amountMatch;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 총 지출 계산 (지출 타입만 포함)
|
||||||
|
*/
|
||||||
|
export const calculateTotalExpenses = (transactions: Transaction[]): number => {
|
||||||
|
if (!transactions || transactions.length === 0) {
|
||||||
|
console.log('계산할 트랜잭션이 없습니다.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`총 지출 계산 시작: 트랜잭션 ${transactions.length}개`);
|
||||||
|
|
||||||
|
// 지출 타입만 필터링하고 합산
|
||||||
|
const expenses = transactions
|
||||||
|
.filter(t => t.type === 'expense')
|
||||||
|
.reduce((sum, transaction) => {
|
||||||
|
const amount = Number(transaction.amount);
|
||||||
|
if (isNaN(amount)) {
|
||||||
|
console.warn(`유효하지 않은 금액: ${transaction.amount}, 트랜잭션 ID: ${transaction.id}`);
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
return sum + amount;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
console.log(`총 지출 계산 결과: ${expenses}원`);
|
||||||
|
return expenses;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 트랜잭션을 날짜별로 그룹화
|
||||||
|
*/
|
||||||
|
export const groupTransactionsByDate = (transactions: Transaction[]): Record<string, Transaction[]> => {
|
||||||
|
const groups: Record<string, Transaction[]> = {};
|
||||||
|
|
||||||
|
transactions.forEach(transaction => {
|
||||||
|
const date = parseTransactionDate(transaction.date);
|
||||||
|
if (!date) {
|
||||||
|
console.warn(`날짜를 파싱할 수 없음: ${transaction.date}, 트랜잭션 ID: ${transaction.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDate = format(date, 'yyyy-MM-dd');
|
||||||
|
|
||||||
|
if (!groups[formattedDate]) {
|
||||||
|
groups[formattedDate] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
groups[formattedDate].push(transaction);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,35 +2,47 @@
|
|||||||
import { format, parse, isValid, parseISO } from 'date-fns';
|
import { format, parse, isValid, parseISO } from 'date-fns';
|
||||||
import { ko } from 'date-fns/locale';
|
import { ko } from 'date-fns/locale';
|
||||||
|
|
||||||
|
// 날짜 파싱 결과를 캐싱하기 위한 Map
|
||||||
|
const dateParseCache = new Map<string, Date | null>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 다양한 형식의 날짜 문자열을 Date 객체로 변환하는 유틸리티
|
* 다양한 형식의 날짜 문자열을 Date 객체로 변환하는 유틸리티
|
||||||
|
* 성능 최적화를 위해 결과 캐싱 기능 추가
|
||||||
*/
|
*/
|
||||||
export const parseTransactionDate = (dateStr: string): Date | null => {
|
export const parseTransactionDate = (dateStr: string): Date | null => {
|
||||||
// 빈 문자열 체크
|
// 빈 문자열 체크
|
||||||
if (!dateStr || dateStr === '') {
|
if (!dateStr || dateStr === '') {
|
||||||
console.log('빈 날짜 문자열');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 캐시된 결과가 있으면 반환
|
||||||
|
if (dateParseCache.has(dateStr)) {
|
||||||
|
return dateParseCache.get(dateStr) || null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let result: Date | null = null;
|
||||||
|
|
||||||
// 특수 키워드 처리
|
// 특수 키워드 처리
|
||||||
if (dateStr.toLowerCase().includes('오늘')) {
|
if (dateStr.toLowerCase().includes('오늘')) {
|
||||||
console.log('오늘 날짜로 변환');
|
result = new Date();
|
||||||
return new Date();
|
dateParseCache.set(dateStr, result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dateStr.toLowerCase().includes('어제')) {
|
if (dateStr.toLowerCase().includes('어제')) {
|
||||||
console.log('어제 날짜로 변환');
|
|
||||||
const yesterday = new Date();
|
const yesterday = new Date();
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
return yesterday;
|
result = yesterday;
|
||||||
|
dateParseCache.set(dateStr, result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ISO 형식 (yyyy-MM-dd) 시도
|
// ISO 형식 (yyyy-MM-dd) 시도
|
||||||
if (/^\d{4}-\d{2}-\d{2}/.test(dateStr)) {
|
if (/^\d{4}-\d{2}-\d{2}/.test(dateStr)) {
|
||||||
console.log('ISO 형식 날짜 감지');
|
|
||||||
const date = parseISO(dateStr);
|
const date = parseISO(dateStr);
|
||||||
if (isValid(date)) {
|
if (isValid(date)) {
|
||||||
|
dateParseCache.set(dateStr, date);
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,27 +52,30 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
|
|||||||
const koreanMatch = dateStr.match(koreanDatePattern);
|
const koreanMatch = dateStr.match(koreanDatePattern);
|
||||||
|
|
||||||
if (koreanMatch) {
|
if (koreanMatch) {
|
||||||
console.log('한국어 날짜 형식 감지', koreanMatch);
|
|
||||||
const month = parseInt(koreanMatch[1]) - 1; // 0-based month
|
const month = parseInt(koreanMatch[1]) - 1; // 0-based month
|
||||||
const day = parseInt(koreanMatch[2]);
|
const day = parseInt(koreanMatch[2]);
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
date.setMonth(month);
|
date.setMonth(month);
|
||||||
date.setDate(day);
|
date.setDate(day);
|
||||||
|
dateParseCache.set(dateStr, date);
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 쉼표 구분 형식 처리 (예: "오늘, 14:52 PM")
|
// 쉼표 구분 형식 처리 (예: "오늘, 14:52 PM")
|
||||||
const commaSplit = dateStr.split(',');
|
const commaSplit = dateStr.split(',');
|
||||||
if (commaSplit.length > 1) {
|
if (commaSplit.length > 1) {
|
||||||
console.log('쉼표 구분 날짜 감지');
|
|
||||||
// 첫 부분이 오늘/어제 등의 키워드인 경우 처리
|
// 첫 부분이 오늘/어제 등의 키워드인 경우 처리
|
||||||
const firstPart = commaSplit[0].trim().toLowerCase();
|
const firstPart = commaSplit[0].trim().toLowerCase();
|
||||||
if (firstPart === '오늘') {
|
if (firstPart === '오늘') {
|
||||||
return new Date();
|
result = new Date();
|
||||||
|
dateParseCache.set(dateStr, result);
|
||||||
|
return result;
|
||||||
} else if (firstPart === '어제') {
|
} else if (firstPart === '어제') {
|
||||||
const yesterday = new Date();
|
const yesterday = new Date();
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
return yesterday;
|
result = yesterday;
|
||||||
|
dateParseCache.set(dateStr, result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,13 +93,12 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
|
|||||||
|
|
||||||
for (const formatStr of formats) {
|
for (const formatStr of formats) {
|
||||||
try {
|
try {
|
||||||
console.log(`날짜 형식 시도: ${formatStr}`);
|
|
||||||
const date = parse(dateStr, formatStr, new Date(), { locale: ko });
|
const date = parse(dateStr, formatStr, new Date(), { locale: ko });
|
||||||
if (isValid(date)) {
|
if (isValid(date)) {
|
||||||
console.log('날짜 파싱 성공:', formatStr);
|
dateParseCache.set(dateStr, date);
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// 이 형식이 실패하면 다음 형식 시도
|
// 이 형식이 실패하면 다음 형식 시도
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -93,19 +107,27 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
|
|||||||
// 위 모든 형식이 실패하면 마지막으로 Date 생성자 시도
|
// 위 모든 형식이 실패하면 마지막으로 Date 생성자 시도
|
||||||
const dateFromConstructor = new Date(dateStr);
|
const dateFromConstructor = new Date(dateStr);
|
||||||
if (isValid(dateFromConstructor)) {
|
if (isValid(dateFromConstructor)) {
|
||||||
console.log('Date 생성자로 파싱 성공');
|
dateParseCache.set(dateStr, dateFromConstructor);
|
||||||
return dateFromConstructor;
|
return dateFromConstructor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모든 방법이 실패하면 null 반환
|
// 모든 방법이 실패하면 null 반환
|
||||||
console.warn(`날짜 파싱 실패: ${dateStr}`);
|
dateParseCache.set(dateStr, null);
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`날짜 파싱 중 오류 발생: ${dateStr}`, error);
|
console.error(`날짜 파싱 중 오류 발생: ${dateStr}`, error);
|
||||||
|
dateParseCache.set(dateStr, null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 캐시를 정리하는 함수 (필요시 호출)
|
||||||
|
*/
|
||||||
|
export const clearDateParseCache = () => {
|
||||||
|
dateParseCache.clear();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Date 객체를 yyyy-MM-dd 형식의 문자열로 변환
|
* Date 객체를 yyyy-MM-dd 형식의 문자열로 변환
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user