Fix: Display 0 for expenses
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import React from 'react';
|
import React 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';
|
||||||
|
|
||||||
interface TransactionsHeaderProps {
|
interface TransactionsHeaderProps {
|
||||||
selectedMonth: string;
|
selectedMonth: string;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@@ -12,6 +13,7 @@ interface TransactionsHeaderProps {
|
|||||||
totalExpenses: number;
|
totalExpenses: number;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
|
const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
|
||||||
selectedMonth,
|
selectedMonth,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@@ -29,18 +31,31 @@ const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
|
|||||||
|
|
||||||
// 예산 정보가 없는 경우 기본값 사용
|
// 예산 정보가 없는 경우 기본값 사용
|
||||||
const targetAmount = budgetData?.monthly?.targetAmount || 0;
|
const targetAmount = budgetData?.monthly?.targetAmount || 0;
|
||||||
return <header className="py-4">
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="neuro-pressed mb-5 flex items-center px-4 py-3 rounded-xl">
|
<div className="neuro-pressed mb-5 flex items-center px-4 py-3 rounded-xl">
|
||||||
<Search size={18} className="text-gray-500 mr-2" />
|
<Search size={18} className="text-gray-500 mr-2" />
|
||||||
<input type="text" placeholder="지출 검색..." className="bg-transparent flex-1 outline-none text-sm" value={searchQuery} onChange={e => setSearchQuery(e.target.value)} disabled={isDisabled} />
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="지출 검색..."
|
||||||
|
className="bg-transparent flex-1 outline-none text-sm"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Month Selector */}
|
{/* Month Selector */}
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<button className="neuro-flat p-2 rounded-full" onClick={handlePrevMonth} disabled={isDisabled}>
|
<button
|
||||||
|
className="neuro-flat p-2 rounded-full"
|
||||||
|
onClick={handlePrevMonth}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
<ChevronLeft size={20} />
|
<ChevronLeft size={20} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -49,7 +64,11 @@ const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
|
|||||||
<span className="font-medium text-lg">{selectedMonth}</span>
|
<span className="font-medium text-lg">{selectedMonth}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="neuro-flat p-2 rounded-full" onClick={handleNextMonth} disabled={isDisabled}>
|
<button
|
||||||
|
className="neuro-flat p-2 rounded-full"
|
||||||
|
onClick={handleNextMonth}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
<ChevronRight size={20} />
|
<ChevronRight size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,6 +88,8 @@ const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>;
|
</header>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
export default TransactionsHeader;
|
|
||||||
|
export default React.memo(TransactionsHeader);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useCallback, useEffect } from 'react';
|
|||||||
import { Transaction } from '@/contexts/budget/types';
|
import { Transaction } from '@/contexts/budget/types';
|
||||||
import { getCurrentMonth, getPrevMonth, getNextMonth } from '../dateUtils';
|
import { getCurrentMonth, getPrevMonth, getNextMonth } from '../dateUtils';
|
||||||
import { filterTransactionsByMonth, filterTransactionsByQuery, calculateTotalExpenses } from '../filterUtils';
|
import { filterTransactionsByMonth, filterTransactionsByQuery, calculateTotalExpenses } from '../filterUtils';
|
||||||
|
import { parseTransactionDate } from '@/utils/dateParser';
|
||||||
|
|
||||||
interface UseTransactionsFilteringProps {
|
interface UseTransactionsFilteringProps {
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
@@ -28,7 +29,7 @@ export const useTransactionsFiltering = ({
|
|||||||
console.log('트랜잭션 필터링 적용:', { 선택된월: selectedMonth, 검색어: searchQuery });
|
console.log('트랜잭션 필터링 적용:', { 선택된월: selectedMonth, 검색어: searchQuery });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 먼저 월별 필터링
|
// 먼저 월별 필터링 - 개선된 날짜 처리 기능 사용
|
||||||
const monthFiltered = filterTransactionsByMonth(transactions, selectedMonth);
|
const monthFiltered = filterTransactionsByMonth(transactions, selectedMonth);
|
||||||
console.log('월별 필터링 결과:', monthFiltered.length);
|
console.log('월별 필터링 결과:', monthFiltered.length);
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export const useTransactionsFiltering = ({
|
|||||||
setSelectedMonth(getNextMonth(selectedMonth));
|
setSelectedMonth(getNextMonth(selectedMonth));
|
||||||
}, [selectedMonth, setSelectedMonth]);
|
}, [selectedMonth, setSelectedMonth]);
|
||||||
|
|
||||||
// 총 지출 계산 - date-fns 없이도 사용 가능한 단순 버전
|
// 총 지출 계산 - 개선된 계산 로직 사용
|
||||||
const getTotalExpenses = useCallback((filteredTransactions: Transaction[]): number => {
|
const getTotalExpenses = useCallback((filteredTransactions: Transaction[]): number => {
|
||||||
console.log('총 지출 계산 중...', filteredTransactions.length);
|
console.log('총 지출 계산 중...', filteredTransactions.length);
|
||||||
const total = calculateTotalExpenses(filteredTransactions);
|
const total = calculateTotalExpenses(filteredTransactions);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
|
||||||
import { Transaction } from '@/contexts/budget/types';
|
import { Transaction } from '@/contexts/budget/types';
|
||||||
|
import { parseTransactionDate } from '@/utils/dateParser';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ko } from 'date-fns/locale';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 월별로 트랜잭션 필터링
|
* 월별로 트랜잭션 필터링 - 개선된 버전
|
||||||
*/
|
*/
|
||||||
export const filterTransactionsByMonth = (
|
export const filterTransactionsByMonth = (
|
||||||
transactions: Transaction[],
|
transactions: Transaction[],
|
||||||
@@ -15,6 +18,13 @@ export const filterTransactionsByMonth = (
|
|||||||
console.log('샘플 트랜잭션 날짜:', transactions.slice(0, 3).map(t => t.date));
|
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 [];
|
||||||
|
}
|
||||||
|
|
||||||
const filtered = transactions.filter(transaction => {
|
const filtered = transactions.filter(transaction => {
|
||||||
// 트랜잭션 타입 확인 - 지출 항목만 포함
|
// 트랜잭션 타입 확인 - 지출 항목만 포함
|
||||||
if (transaction.type !== 'expense') {
|
if (transaction.type !== 'expense') {
|
||||||
@@ -28,27 +38,22 @@ export const filterTransactionsByMonth = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 현재 월 포함 확인 (다양한 형식 지원)
|
// 날짜 파싱
|
||||||
const dateIncludes = transaction.date.includes(selectedMonth);
|
const parsedDate = parseTransactionDate(transaction.date);
|
||||||
|
if (!parsedDate) {
|
||||||
// 월 이름으로 확인 (한글)
|
console.warn('날짜 파싱 실패:', transaction.date);
|
||||||
const monthNumberMatch = selectedMonth.match(/\d{4}-(\d{2})/);
|
return false;
|
||||||
let monthNameIncluded = false;
|
|
||||||
|
|
||||||
if (monthNumberMatch) {
|
|
||||||
const monthNumber = parseInt(monthNumberMatch[1]);
|
|
||||||
const koreanMonths = [
|
|
||||||
'1월', '2월', '3월', '4월', '5월', '6월',
|
|
||||||
'7월', '8월', '9월', '10월', '11월', '12월'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 해당 월의 한글 이름이 트랜잭션 날짜에 포함되어 있는지 확인
|
|
||||||
if (monthNumber >= 1 && monthNumber <= 12) {
|
|
||||||
monthNameIncluded = transaction.date.includes(koreanMonths[monthNumber - 1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dateIncludes || monthNameIncluded;
|
// 월 비교
|
||||||
|
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) {
|
} catch (e) {
|
||||||
console.error('트랜잭션 필터링 중 오류:', e, transaction);
|
console.error('트랜잭션 필터링 중 오류:', e, transaction);
|
||||||
return false;
|
return false;
|
||||||
@@ -89,17 +94,31 @@ export const filterTransactionsByQuery = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 총 지출 금액 계산
|
* 총 지출 금액 계산 - 개선된 버전
|
||||||
*/
|
*/
|
||||||
export const calculateTotalExpenses = (transactions: Transaction[]): number => {
|
export const calculateTotalExpenses = (transactions: Transaction[]): number => {
|
||||||
try {
|
try {
|
||||||
const total = transactions.reduce((sum, t) => {
|
// 유효한 트랜잭션만 필터링 (undefined, null 제외)
|
||||||
// 유효한 숫자인지 확인
|
const validTransactions = transactions.filter(t => t && typeof t.amount !== 'undefined');
|
||||||
const amount = typeof t.amount === 'number' ? t.amount : 0;
|
console.log(`유효한 트랜잭션 수: ${validTransactions.length}/${transactions.length}`);
|
||||||
|
|
||||||
|
// 디버깅용 로그
|
||||||
|
if (validTransactions.length > 0) {
|
||||||
|
console.log('첫 번째 트랜잭션 정보:', {
|
||||||
|
title: validTransactions[0].title,
|
||||||
|
amount: validTransactions[0].amount,
|
||||||
|
type: validTransactions[0].type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = validTransactions.reduce((sum, t) => {
|
||||||
|
// 유효한 숫자인지 확인하고 기본값 처리
|
||||||
|
const amount = typeof t.amount === 'number' ? t.amount :
|
||||||
|
parseInt(t.amount as any) || 0;
|
||||||
return sum + amount;
|
return sum + amount;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
console.log(`총 지출 계산: ${total}원 (${transactions.length}개 항목)`);
|
console.log(`총 지출 계산: ${total}원 (${validTransactions.length}개 항목)`);
|
||||||
return total;
|
return total;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('총 지출 계산 중 오류:', e);
|
console.error('총 지출 계산 중 오류:', e);
|
||||||
|
|||||||
@@ -7,17 +7,20 @@ import { ko } from 'date-fns/locale';
|
|||||||
*/
|
*/
|
||||||
export const parseTransactionDate = (dateStr: string): Date | null => {
|
export const parseTransactionDate = (dateStr: string): Date | null => {
|
||||||
// 빈 문자열 체크
|
// 빈 문자열 체크
|
||||||
if (!dateStr) {
|
if (!dateStr || dateStr === '') {
|
||||||
|
console.log('빈 날짜 문자열');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 특수 키워드 처리
|
// 특수 키워드 처리
|
||||||
if (dateStr.includes('오늘')) {
|
if (dateStr.toLowerCase().includes('오늘')) {
|
||||||
|
console.log('오늘 날짜로 변환');
|
||||||
return new Date();
|
return new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dateStr.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;
|
return yesterday;
|
||||||
@@ -25,6 +28,7 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
|
|||||||
|
|
||||||
// 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)) {
|
||||||
return date;
|
return date;
|
||||||
@@ -36,6 +40,7 @@ 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();
|
||||||
@@ -44,6 +49,21 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
|
|||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 쉼표 구분 형식 처리 (예: "오늘, 14:52 PM")
|
||||||
|
const commaSplit = dateStr.split(',');
|
||||||
|
if (commaSplit.length > 1) {
|
||||||
|
console.log('쉼표 구분 날짜 감지');
|
||||||
|
// 첫 부분이 오늘/어제 등의 키워드인 경우 처리
|
||||||
|
const firstPart = commaSplit[0].trim().toLowerCase();
|
||||||
|
if (firstPart === '오늘') {
|
||||||
|
return new Date();
|
||||||
|
} else if (firstPart === '어제') {
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
return yesterday;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parse를 사용한 다양한 형식 시도
|
// parse를 사용한 다양한 형식 시도
|
||||||
const formats = [
|
const formats = [
|
||||||
'yyyy-MM-dd',
|
'yyyy-MM-dd',
|
||||||
@@ -51,13 +71,17 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
|
|||||||
'MM-dd-yyyy',
|
'MM-dd-yyyy',
|
||||||
'MM/dd/yyyy',
|
'MM/dd/yyyy',
|
||||||
'yyyy년 MM월 dd일',
|
'yyyy년 MM월 dd일',
|
||||||
'MM월 dd일'
|
'MM월 dd일',
|
||||||
|
'yyyy년 M월 d일',
|
||||||
|
'M월 d일'
|
||||||
];
|
];
|
||||||
|
|
||||||
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);
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -69,6 +93,7 @@ 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 생성자로 파싱 성공');
|
||||||
return dateFromConstructor;
|
return dateFromConstructor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user