feat: Add CI/CD pipeline and code quality improvements
- Add GitHub Actions workflow for automated CI/CD - Configure Node.js 18.x and 20.x matrix testing - Add TypeScript type checking step - Add ESLint code quality checks with enhanced rules - Add Prettier formatting verification - Add production build validation - Upload build artifacts for deployment - Set up automated testing on push/PR - Replace console.log with environment-aware logger - Add pre-commit hooks for code quality - Exclude archive folder from linting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
|
||||
import { format, parse, addMonths, subMonths } from 'date-fns';
|
||||
import { ko } from 'date-fns/locale';
|
||||
import { format, parse, addMonths, subMonths } from "date-fns";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { ko } from "date-fns/locale";
|
||||
|
||||
/**
|
||||
* 월 이름 배열 (한국어)
|
||||
*/
|
||||
export const MONTHS_KR = [
|
||||
'1월', '2월', '3월', '4월', '5월', '6월',
|
||||
'7월', '8월', '9월', '10월', '11월', '12월'
|
||||
"1월",
|
||||
"2월",
|
||||
"3월",
|
||||
"4월",
|
||||
"5월",
|
||||
"6월",
|
||||
"7월",
|
||||
"8월",
|
||||
"9월",
|
||||
"10월",
|
||||
"11월",
|
||||
"12월",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -22,7 +32,7 @@ export const isValidMonth = (month: string): boolean => {
|
||||
* 현재 년월 가져오기
|
||||
*/
|
||||
export const getCurrentMonth = (): string => {
|
||||
return format(new Date(), 'yyyy-MM');
|
||||
return format(new Date(), "yyyy-MM");
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -31,19 +41,19 @@ export const getCurrentMonth = (): string => {
|
||||
export const getPrevMonth = (month: string): string => {
|
||||
// 입력값 검증
|
||||
if (!isValidMonth(month)) {
|
||||
console.warn('유효하지 않은 월 형식:', month);
|
||||
logger.warn("유효하지 않은 월 형식:", month);
|
||||
return getCurrentMonth();
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 월 문자열을 날짜로 파싱
|
||||
const date = parse(month, 'yyyy-MM', new Date());
|
||||
const date = parse(month, "yyyy-MM", new Date());
|
||||
// 한 달 이전
|
||||
const prevMonth = subMonths(date, 1);
|
||||
// yyyy-MM 형식으로 반환
|
||||
return format(prevMonth, 'yyyy-MM');
|
||||
return format(prevMonth, "yyyy-MM");
|
||||
} catch (error) {
|
||||
console.error('이전 월 계산 중 오류:', error);
|
||||
logger.error("이전 월 계산 중 오류:", error);
|
||||
return getCurrentMonth();
|
||||
}
|
||||
};
|
||||
@@ -54,19 +64,19 @@ export const getPrevMonth = (month: string): string => {
|
||||
export const getNextMonth = (month: string): string => {
|
||||
// 입력값 검증
|
||||
if (!isValidMonth(month)) {
|
||||
console.warn('유효하지 않은 월 형식:', month);
|
||||
logger.warn("유효하지 않은 월 형식:", month);
|
||||
return getCurrentMonth();
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 월 문자열을 날짜로 파싱
|
||||
const date = parse(month, 'yyyy-MM', new Date());
|
||||
const date = parse(month, "yyyy-MM", new Date());
|
||||
// 한 달 이후
|
||||
const nextMonth = addMonths(date, 1);
|
||||
// yyyy-MM 형식으로 반환
|
||||
return format(nextMonth, 'yyyy-MM');
|
||||
return format(nextMonth, "yyyy-MM");
|
||||
} catch (error) {
|
||||
console.error('다음 월 계산 중 오류:', error);
|
||||
logger.error("다음 월 계산 중 오류:", error);
|
||||
return getCurrentMonth();
|
||||
}
|
||||
};
|
||||
@@ -78,16 +88,16 @@ export const formatMonthForDisplay = (month: string): string => {
|
||||
try {
|
||||
// 입력값 검증
|
||||
if (!isValidMonth(month)) {
|
||||
console.warn('유효하지 않은 월 형식:', month);
|
||||
return format(new Date(), 'yyyy년 MM월', { locale: ko });
|
||||
logger.warn("유효하지 않은 월 형식:", month);
|
||||
return format(new Date(), "yyyy년 MM월", { locale: ko });
|
||||
}
|
||||
|
||||
|
||||
// 월 문자열을 날짜로 파싱
|
||||
const date = parse(month, 'yyyy-MM', new Date());
|
||||
const date = parse(month, "yyyy-MM", new Date());
|
||||
// yyyy년 MM월 형식으로 반환 (한국어 로케일)
|
||||
return format(date, 'yyyy년 MM월', { locale: ko });
|
||||
return format(date, "yyyy년 MM월", { locale: ko });
|
||||
} catch (error) {
|
||||
console.error('월 형식 변환 중 오류:', error);
|
||||
logger.error("월 형식 변환 중 오류:", error);
|
||||
return month;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { useAuth } from '@/contexts/auth/useAuth';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import { saveTransactionsToStorage } from './storageUtils';
|
||||
import { deleteTransactionFromSupabase } from './supabaseUtils';
|
||||
import { addToDeletedTransactions } from '@/utils/sync/transaction/deletedTransactionsTracker';
|
||||
import { useCallback } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { Transaction } from "@/components/TransactionCard";
|
||||
import { useAuth } from "@/contexts/auth/useAuth";
|
||||
import { toast } from "@/hooks/useToast.wrapper";
|
||||
import { saveTransactionsToStorage } from "./storageUtils";
|
||||
import { deleteTransactionFromSupabase } from "./supabaseUtils";
|
||||
import { addToDeletedTransactions } from "@/utils/sync/transaction/deletedTransactionsTracker";
|
||||
|
||||
/**
|
||||
* 트랜잭션 삭제 기능을 위한 훅
|
||||
@@ -19,72 +19,87 @@ export const useDeleteTransaction = (
|
||||
/**
|
||||
* 트랜잭션 삭제 처리
|
||||
*/
|
||||
const deleteTransaction = useCallback(async (transactionId: string): Promise<boolean> => {
|
||||
try {
|
||||
console.log(`[트랜잭션 삭제] 시작: ID=${transactionId}`);
|
||||
|
||||
// 트랜잭션 존재 확인
|
||||
const transaction = transactions.find(t => t.id === transactionId);
|
||||
if (!transaction) {
|
||||
console.warn(`[트랜잭션 삭제] 트랜잭션을 찾을 수 없음: ${transactionId}`);
|
||||
const deleteTransaction = useCallback(
|
||||
async (transactionId: string): Promise<boolean> => {
|
||||
try {
|
||||
logger.info(`[트랜잭션 삭제] 시작: ID=${transactionId}`);
|
||||
|
||||
// 트랜잭션 존재 확인
|
||||
const transaction = transactions.find((t) => t.id === transactionId);
|
||||
if (!transaction) {
|
||||
logger.warn(
|
||||
`[트랜잭션 삭제] 트랜잭션을 찾을 수 없음: ${transactionId}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[트랜잭션 삭제] 삭제 대상: "${transaction.title}", 금액: ${transaction.amount}원`
|
||||
);
|
||||
|
||||
// 트랜잭션 목록에서 제거
|
||||
const updatedTransactions = transactions.filter(
|
||||
(t) => t.id !== transactionId
|
||||
);
|
||||
|
||||
// 로컬 스토리지 업데이트
|
||||
saveTransactionsToStorage(updatedTransactions);
|
||||
logger.info(`[트랜잭션 삭제] 로컬 저장소 업데이트 완료`);
|
||||
|
||||
// 상태 업데이트
|
||||
setTransactions(updatedTransactions);
|
||||
|
||||
// 클라우드 동기화 (Supabase)
|
||||
if (user) {
|
||||
try {
|
||||
logger.info(`[트랜잭션 삭제] Supabase 삭제 시작: ${transactionId}`);
|
||||
await deleteTransactionFromSupabase(user, transactionId);
|
||||
logger.info(`[트랜잭션 삭제] Supabase 삭제 성공: ${transactionId}`);
|
||||
} catch (syncError) {
|
||||
logger.error(`[트랜잭션 삭제] Supabase 삭제 실패:`, syncError);
|
||||
// 삭제 실패하더라도 로컬에서는 삭제됨, 추적 목록에 추가
|
||||
addToDeletedTransactions(transactionId);
|
||||
logger.info(
|
||||
`[트랜잭션 삭제] 삭제 트랜잭션 추적 목록에 추가: ${transactionId}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 오프라인 상태이거나 로그인되지 않은 경우 추적 목록에 추가
|
||||
addToDeletedTransactions(transactionId);
|
||||
logger.info(
|
||||
`[트랜잭션 삭제] 오프라인 삭제: 추적 목록에 추가 ${transactionId}`
|
||||
);
|
||||
}
|
||||
|
||||
// 이벤트 발생
|
||||
window.dispatchEvent(new Event("transactionUpdated"));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("transactionDeleted", {
|
||||
detail: { id: transactionId },
|
||||
})
|
||||
);
|
||||
|
||||
// 토스트 메시지 표시
|
||||
toast({
|
||||
title: "지출이 삭제되었습니다",
|
||||
description: `${transaction.title} 항목이 삭제되었습니다.`,
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
logger.info(`[트랜잭션 삭제] 완료: ${transactionId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`[트랜잭션 삭제] 오류 발생:`, error);
|
||||
toast({
|
||||
title: "삭제 실패",
|
||||
description: "지출 항목 삭제 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[트랜잭션 삭제] 삭제 대상: "${transaction.title}", 금액: ${transaction.amount}원`);
|
||||
|
||||
// 트랜잭션 목록에서 제거
|
||||
const updatedTransactions = transactions.filter(t => t.id !== transactionId);
|
||||
|
||||
// 로컬 스토리지 업데이트
|
||||
saveTransactionsToStorage(updatedTransactions);
|
||||
console.log(`[트랜잭션 삭제] 로컬 저장소 업데이트 완료`);
|
||||
|
||||
// 상태 업데이트
|
||||
setTransactions(updatedTransactions);
|
||||
|
||||
// 클라우드 동기화 (Supabase)
|
||||
if (user) {
|
||||
try {
|
||||
console.log(`[트랜잭션 삭제] Supabase 삭제 시작: ${transactionId}`);
|
||||
await deleteTransactionFromSupabase(user, transactionId);
|
||||
console.log(`[트랜잭션 삭제] Supabase 삭제 성공: ${transactionId}`);
|
||||
} catch (syncError) {
|
||||
console.error(`[트랜잭션 삭제] Supabase 삭제 실패:`, syncError);
|
||||
// 삭제 실패하더라도 로컬에서는 삭제됨, 추적 목록에 추가
|
||||
addToDeletedTransactions(transactionId);
|
||||
console.log(`[트랜잭션 삭제] 삭제 트랜잭션 추적 목록에 추가: ${transactionId}`);
|
||||
}
|
||||
} else {
|
||||
// 오프라인 상태이거나 로그인되지 않은 경우 추적 목록에 추가
|
||||
addToDeletedTransactions(transactionId);
|
||||
console.log(`[트랜잭션 삭제] 오프라인 삭제: 추적 목록에 추가 ${transactionId}`);
|
||||
}
|
||||
|
||||
// 이벤트 발생
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new CustomEvent('transactionDeleted', {
|
||||
detail: { id: transactionId }
|
||||
}));
|
||||
|
||||
// 토스트 메시지 표시
|
||||
toast({
|
||||
title: "지출이 삭제되었습니다",
|
||||
description: `${transaction.title} 항목이 삭제되었습니다.`,
|
||||
duration: 3000
|
||||
});
|
||||
|
||||
console.log(`[트랜잭션 삭제] 완료: ${transactionId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[트랜잭션 삭제] 오류 발생:`, error);
|
||||
toast({
|
||||
title: "삭제 실패",
|
||||
description: "지출 항목 삭제 중 오류가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, [transactions, setTransactions, user]);
|
||||
},
|
||||
[transactions, setTransactions, user]
|
||||
);
|
||||
|
||||
return { deleteTransaction };
|
||||
};
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Transaction } from '@/contexts/budget/types';
|
||||
import { getCurrentMonth, getPrevMonth, getNextMonth } from '../dateUtils';
|
||||
import { filterTransactionsByMonth, filterTransactionsByQuery, calculateTotalExpenses } from '../filterUtils';
|
||||
import { parseTransactionDate } from '@/utils/dateParser';
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { Transaction } from "@/contexts/budget/types";
|
||||
import { getCurrentMonth, getPrevMonth, getNextMonth } from "../dateUtils";
|
||||
import {
|
||||
filterTransactionsByMonth,
|
||||
filterTransactionsByQuery,
|
||||
calculateTotalExpenses,
|
||||
} from "../filterUtils";
|
||||
import { parseTransactionDate } from "@/utils/dateParser";
|
||||
|
||||
interface UseTransactionsFilteringProps {
|
||||
transactions: Transaction[];
|
||||
@@ -22,27 +26,32 @@ export const useTransactionsFiltering = ({
|
||||
selectedMonth,
|
||||
setSelectedMonth,
|
||||
searchQuery,
|
||||
setFilteredTransactions
|
||||
setFilteredTransactions,
|
||||
}: UseTransactionsFilteringProps) => {
|
||||
// 필터링 적용
|
||||
useEffect(() => {
|
||||
console.log('트랜잭션 필터링 적용:', { 선택된월: selectedMonth, 검색어: searchQuery });
|
||||
|
||||
logger.info("트랜잭션 필터링 적용:", {
|
||||
선택된월: selectedMonth,
|
||||
검색어: searchQuery,
|
||||
});
|
||||
|
||||
try {
|
||||
// 먼저 월별 필터링 - 개선된 날짜 처리 기능 사용
|
||||
const monthFiltered = filterTransactionsByMonth(transactions, selectedMonth);
|
||||
console.log('월별 필터링 결과:', monthFiltered.length);
|
||||
|
||||
const monthFiltered = filterTransactionsByMonth(
|
||||
transactions,
|
||||
selectedMonth
|
||||
);
|
||||
logger.info("월별 필터링 결과:", monthFiltered.length);
|
||||
|
||||
// 그 다음 검색어 필터링
|
||||
const searchFiltered = searchQuery
|
||||
const searchFiltered = searchQuery
|
||||
? filterTransactionsByQuery(monthFiltered, searchQuery)
|
||||
: monthFiltered;
|
||||
|
||||
console.log('최종 필터링 결과:', searchFiltered.length);
|
||||
|
||||
logger.info("최종 필터링 결과:", searchFiltered.length);
|
||||
setFilteredTransactions(searchFiltered);
|
||||
|
||||
} catch (error) {
|
||||
console.error('트랜잭션 필터링 중 오류 발생:', error);
|
||||
logger.error("트랜잭션 필터링 중 오류 발생:", error);
|
||||
// 오류 발생 시 원본 데이터 유지
|
||||
setFilteredTransactions(transactions);
|
||||
}
|
||||
@@ -59,16 +68,19 @@ export const useTransactionsFiltering = ({
|
||||
}, [selectedMonth, setSelectedMonth]);
|
||||
|
||||
// 총 지출 계산 - 개선된 계산 로직 사용
|
||||
const getTotalExpenses = useCallback((filteredTransactions: Transaction[]): number => {
|
||||
console.log('총 지출 계산 중...', filteredTransactions.length);
|
||||
const total = calculateTotalExpenses(filteredTransactions);
|
||||
console.log('계산된 총 지출:', total);
|
||||
return total;
|
||||
}, []);
|
||||
const getTotalExpenses = useCallback(
|
||||
(filteredTransactions: Transaction[]): number => {
|
||||
logger.info("총 지출 계산 중...", filteredTransactions.length);
|
||||
const total = calculateTotalExpenses(filteredTransactions);
|
||||
logger.info("계산된 총 지출:", total);
|
||||
return total;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
handlePrevMonth,
|
||||
handleNextMonth,
|
||||
getTotalExpenses
|
||||
getTotalExpenses,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { Transaction } from "@/components/TransactionCard";
|
||||
|
||||
export interface FilteringProps {
|
||||
transactions: Transaction[];
|
||||
|
||||
@@ -1,55 +1,65 @@
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { FilteringProps } from './types';
|
||||
import { MONTHS_KR } from '../dateUtils';
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { Transaction } from "@/components/TransactionCard";
|
||||
import { FilteringProps } from "./types";
|
||||
import { MONTHS_KR } from "../dateUtils";
|
||||
|
||||
/**
|
||||
* 거래 필터링 로직
|
||||
* 선택된 월과 검색어를 기준으로 거래를 필터링합니다.
|
||||
*/
|
||||
export const useFilterApplication = ({
|
||||
transactions,
|
||||
selectedMonth,
|
||||
searchQuery,
|
||||
setFilteredTransactions
|
||||
}: Pick<FilteringProps, 'transactions' | 'selectedMonth' | 'searchQuery' | 'setFilteredTransactions'>) => {
|
||||
|
||||
export const useFilterApplication = ({
|
||||
transactions,
|
||||
selectedMonth,
|
||||
searchQuery,
|
||||
setFilteredTransactions,
|
||||
}: Pick<
|
||||
FilteringProps,
|
||||
"transactions" | "selectedMonth" | "searchQuery" | "setFilteredTransactions"
|
||||
>) => {
|
||||
// 거래 필터링 함수
|
||||
const filterTransactions = useCallback(() => {
|
||||
try {
|
||||
console.log('필터링 시작, 전체 트랜잭션:', transactions.length);
|
||||
console.log('선택된 월:', selectedMonth);
|
||||
|
||||
logger.info("필터링 시작, 전체 트랜잭션:", transactions.length);
|
||||
logger.info("선택된 월:", selectedMonth);
|
||||
|
||||
// 선택된 월 정보 파싱
|
||||
const selectedMonthName = selectedMonth;
|
||||
const monthNumber = MONTHS_KR.findIndex(month => month === selectedMonthName) + 1;
|
||||
|
||||
const monthNumber =
|
||||
MONTHS_KR.findIndex((month) => month === selectedMonthName) + 1;
|
||||
|
||||
// 월별 필터링
|
||||
let filtered = transactions.filter(transaction => {
|
||||
if (!transaction.date) return false;
|
||||
|
||||
console.log(`트랜잭션 날짜 확인: "${transaction.date}", 타입: ${typeof transaction.date}`);
|
||||
|
||||
let filtered = transactions.filter((transaction) => {
|
||||
if (!transaction.date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`트랜잭션 날짜 확인: "${transaction.date}", 타입: ${typeof transaction.date}`
|
||||
);
|
||||
|
||||
// 다양한 날짜 형식 처리
|
||||
if (transaction.date.includes(selectedMonthName)) {
|
||||
return true; // 선택된 월 이름이 포함된 경우
|
||||
}
|
||||
|
||||
if (transaction.date.includes('오늘')) {
|
||||
|
||||
if (transaction.date.includes("오늘")) {
|
||||
// 오늘 날짜가 해당 월인지 확인
|
||||
const today = new Date();
|
||||
const currentMonth = today.getMonth() + 1; // 0부터 시작하므로 +1
|
||||
return currentMonth === monthNumber;
|
||||
}
|
||||
|
||||
|
||||
// 다른 형식의 날짜도 시도
|
||||
try {
|
||||
// ISO 형식이 아닌 경우 처리
|
||||
if (transaction.date.includes('년') || transaction.date.includes('월')) {
|
||||
if (
|
||||
transaction.date.includes("년") ||
|
||||
transaction.date.includes("월")
|
||||
) {
|
||||
return transaction.date.includes(selectedMonthName);
|
||||
}
|
||||
|
||||
|
||||
// 표준 날짜 문자열 처리 시도
|
||||
const date = new Date(transaction.date);
|
||||
if (!isNaN(date.getTime())) {
|
||||
@@ -57,31 +67,32 @@ export const useFilterApplication = ({
|
||||
return transactionMonth === monthNumber;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('날짜 파싱 오류:', e);
|
||||
logger.error("날짜 파싱 오류:", e);
|
||||
}
|
||||
|
||||
|
||||
// 기본적으로 모든 트랜잭션 포함
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log(`월별 필터링 결과: ${filtered.length} 트랜잭션`);
|
||||
|
||||
logger.info(`월별 필터링 결과: ${filtered.length} 트랜잭션`);
|
||||
|
||||
// 검색어에 따른 필터링
|
||||
if (searchQuery.trim()) {
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(transaction =>
|
||||
transaction.title.toLowerCase().includes(searchLower) ||
|
||||
transaction.category.toLowerCase().includes(searchLower) ||
|
||||
transaction.amount.toString().includes(searchQuery)
|
||||
filtered = filtered.filter(
|
||||
(transaction) =>
|
||||
transaction.title.toLowerCase().includes(searchLower) ||
|
||||
transaction.category.toLowerCase().includes(searchLower) ||
|
||||
transaction.amount.toString().includes(searchQuery)
|
||||
);
|
||||
console.log(`검색어 필터링 결과: ${filtered.length} 트랜잭션`);
|
||||
logger.info(`검색어 필터링 결과: ${filtered.length} 트랜잭션`);
|
||||
}
|
||||
|
||||
|
||||
// 결과 설정
|
||||
setFilteredTransactions(filtered);
|
||||
console.log('최종 필터링 결과:', filtered);
|
||||
logger.info("최종 필터링 결과:", filtered);
|
||||
} catch (error) {
|
||||
console.error('거래 필터링 중 오류:', error);
|
||||
logger.error("거래 필터링 중 오류:", error);
|
||||
setFilteredTransactions([]);
|
||||
}
|
||||
}, [transactions, selectedMonth, searchQuery, setFilteredTransactions]);
|
||||
@@ -92,6 +103,6 @@ export const useFilterApplication = ({
|
||||
}, [transactions, selectedMonth, searchQuery, filterTransactions]);
|
||||
|
||||
return {
|
||||
filterTransactions
|
||||
filterTransactions,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { getPrevMonth, getNextMonth } from '../dateUtils';
|
||||
import { useCallback } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { getPrevMonth, getNextMonth } from "../dateUtils";
|
||||
|
||||
/**
|
||||
* 월 선택 관련 훅
|
||||
* 이전/다음 월 이동 기능을 제공합니다.
|
||||
*/
|
||||
export const useMonthSelection = ({
|
||||
selectedMonth,
|
||||
setSelectedMonth
|
||||
}: {
|
||||
export const useMonthSelection = ({
|
||||
selectedMonth,
|
||||
setSelectedMonth,
|
||||
}: {
|
||||
selectedMonth: string;
|
||||
setSelectedMonth: (month: string) => void;
|
||||
}) => {
|
||||
// 이전 월로 이동
|
||||
const handlePrevMonth = useCallback(() => {
|
||||
const prevMonth = getPrevMonth(selectedMonth);
|
||||
console.log(`월 변경: ${selectedMonth} -> ${prevMonth}`);
|
||||
logger.info(`월 변경: ${selectedMonth} -> ${prevMonth}`);
|
||||
setSelectedMonth(prevMonth);
|
||||
}, [selectedMonth, setSelectedMonth]);
|
||||
|
||||
// 다음 월로 이동
|
||||
const handleNextMonth = useCallback(() => {
|
||||
const nextMonth = getNextMonth(selectedMonth);
|
||||
console.log(`월 변경: ${selectedMonth} -> ${nextMonth}`);
|
||||
logger.info(`월 변경: ${selectedMonth} -> ${nextMonth}`);
|
||||
setSelectedMonth(nextMonth);
|
||||
}, [selectedMonth, setSelectedMonth]);
|
||||
|
||||
return {
|
||||
handlePrevMonth,
|
||||
handleNextMonth
|
||||
handleNextMonth,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { calculateTotalExpenses } from '../filterUtils';
|
||||
import { Transaction } from "@/components/TransactionCard";
|
||||
import { calculateTotalExpenses } from "../filterUtils";
|
||||
|
||||
/**
|
||||
* 총 지출 계산 관련 훅
|
||||
@@ -13,6 +12,6 @@ export const useTotalCalculation = () => {
|
||||
};
|
||||
|
||||
return {
|
||||
getTotalExpenses
|
||||
getTotalExpenses,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,45 +1,54 @@
|
||||
|
||||
import { Transaction } from '@/contexts/budget/types';
|
||||
import { parseTransactionDate } from '@/utils/dateParser';
|
||||
import { format } from 'date-fns';
|
||||
import { Transaction } from "@/contexts/budget/types";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { parseTransactionDate } from "@/utils/dateParser";
|
||||
import { format } from "date-fns";
|
||||
|
||||
/**
|
||||
* 트랜잭션을 월별로 필터링
|
||||
*/
|
||||
export const filterTransactionsByMonth = (transactions: Transaction[], selectedMonth: string): Transaction[] => {
|
||||
export const filterTransactionsByMonth = (
|
||||
transactions: Transaction[],
|
||||
selectedMonth: string
|
||||
): Transaction[] => {
|
||||
if (!transactions || transactions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`월별 필터링 시작: ${selectedMonth}, 트랜잭션 수: ${transactions.length}`);
|
||||
|
||||
logger.info(
|
||||
`월별 필터링 시작: ${selectedMonth}, 트랜잭션 수: ${transactions.length}`
|
||||
);
|
||||
|
||||
try {
|
||||
const [year, month] = selectedMonth.split('-').map(Number);
|
||||
|
||||
const filtered = transactions.filter(transaction => {
|
||||
const [year, month] = selectedMonth.split("-").map(Number);
|
||||
|
||||
const filtered = transactions.filter((transaction) => {
|
||||
const date = parseTransactionDate(transaction.date);
|
||||
|
||||
|
||||
if (!date) {
|
||||
console.warn(`날짜를 파싱할 수 없음: ${transaction.date}, 트랜잭션 ID: ${transaction.id}`);
|
||||
logger.warn(
|
||||
`날짜를 파싱할 수 없음: ${transaction.date}, 트랜잭션 ID: ${transaction.id}`
|
||||
);
|
||||
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}`);
|
||||
logger.info(
|
||||
`트랜잭션 매칭: ${transaction.id}, 제목: ${transaction.title}, 날짜: ${transaction.date}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
console.log(`월별 필터링 결과: ${filtered.length}개 트랜잭션`);
|
||||
|
||||
logger.info(`월별 필터링 결과: ${filtered.length}개 트랜잭션`);
|
||||
return filtered;
|
||||
} catch (error) {
|
||||
console.error('월별 필터링 중 오류:', error);
|
||||
logger.error("월별 필터링 중 오류:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -47,18 +56,25 @@ export const filterTransactionsByMonth = (transactions: Transaction[], selectedM
|
||||
/**
|
||||
* 트랜잭션을 검색어로 필터링
|
||||
*/
|
||||
export const filterTransactionsByQuery = (transactions: Transaction[], searchQuery: string): Transaction[] => {
|
||||
if (!searchQuery || searchQuery.trim() === '') {
|
||||
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);
|
||||
|
||||
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;
|
||||
});
|
||||
};
|
||||
@@ -68,49 +84,55 @@ export const filterTransactionsByQuery = (transactions: Transaction[], searchQue
|
||||
*/
|
||||
export const calculateTotalExpenses = (transactions: Transaction[]): number => {
|
||||
if (!transactions || transactions.length === 0) {
|
||||
console.log('계산할 트랜잭션이 없습니다.');
|
||||
logger.info("계산할 트랜잭션이 없습니다.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(`총 지출 계산 시작: 트랜잭션 ${transactions.length}개`);
|
||||
|
||||
|
||||
logger.info(`총 지출 계산 시작: 트랜잭션 ${transactions.length}개`);
|
||||
|
||||
// 지출 타입만 필터링하고 합산
|
||||
const expenses = transactions
|
||||
.filter(t => t.type === 'expense')
|
||||
.filter((t) => t.type === "expense")
|
||||
.reduce((sum, transaction) => {
|
||||
const amount = Number(transaction.amount);
|
||||
if (isNaN(amount)) {
|
||||
console.warn(`유효하지 않은 금액: ${transaction.amount}, 트랜잭션 ID: ${transaction.id}`);
|
||||
logger.warn(
|
||||
`유효하지 않은 금액: ${transaction.amount}, 트랜잭션 ID: ${transaction.id}`
|
||||
);
|
||||
return sum;
|
||||
}
|
||||
return sum + amount;
|
||||
}, 0);
|
||||
|
||||
console.log(`총 지출 계산 결과: ${expenses}원`);
|
||||
|
||||
logger.info(`총 지출 계산 결과: ${expenses}원`);
|
||||
return expenses;
|
||||
};
|
||||
|
||||
/**
|
||||
* 트랜잭션을 날짜별로 그룹화
|
||||
*/
|
||||
export const groupTransactionsByDate = (transactions: Transaction[]): Record<string, Transaction[]> => {
|
||||
export const groupTransactionsByDate = (
|
||||
transactions: Transaction[]
|
||||
): Record<string, Transaction[]> => {
|
||||
const groups: Record<string, Transaction[]> = {};
|
||||
|
||||
transactions.forEach(transaction => {
|
||||
|
||||
transactions.forEach((transaction) => {
|
||||
const date = parseTransactionDate(transaction.date);
|
||||
if (!date) {
|
||||
console.warn(`날짜를 파싱할 수 없음: ${transaction.date}, 트랜잭션 ID: ${transaction.id}`);
|
||||
logger.warn(
|
||||
`날짜를 파싱할 수 없음: ${transaction.date}, 트랜잭션 ID: ${transaction.id}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedDate = format(date, 'yyyy-MM-dd');
|
||||
|
||||
|
||||
const formattedDate = format(date, "yyyy-MM-dd");
|
||||
|
||||
if (!groups[formattedDate]) {
|
||||
groups[formattedDate] = [];
|
||||
}
|
||||
|
||||
|
||||
groups[formattedDate].push(transaction);
|
||||
});
|
||||
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
|
||||
// 트랜잭션 관련 모든 훅과 유틸리티 함수를 재내보내기
|
||||
export { useTransactions } from './useTransactions';
|
||||
export { MONTHS_KR, getCurrentMonth, getPrevMonth, getNextMonth } from './dateUtils';
|
||||
export { filterTransactionsByMonth, filterTransactionsByQuery, calculateTotalExpenses } from './filterUtils';
|
||||
export { useTransactions } from "./useTransactions";
|
||||
export {
|
||||
MONTHS_KR,
|
||||
getCurrentMonth,
|
||||
getPrevMonth,
|
||||
getNextMonth,
|
||||
} from "./dateUtils";
|
||||
export {
|
||||
filterTransactionsByMonth,
|
||||
filterTransactionsByQuery,
|
||||
calculateTotalExpenses,
|
||||
} from "./filterUtils";
|
||||
|
||||
@@ -1,90 +1,94 @@
|
||||
|
||||
import { Transaction } from '@/contexts/budget/types';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
|
||||
import { Transaction } from "@/contexts/budget/types";
|
||||
import { storageLogger } from "@/utils/logger";
|
||||
import { toast } from "@/hooks/useToast.wrapper";
|
||||
import { EXPENSE_CATEGORIES } from "@/constants/categoryIcons";
|
||||
|
||||
// 로컬 스토리지에서 트랜잭션 데이터 로드
|
||||
export const loadTransactionsFromStorage = (): Transaction[] => {
|
||||
try {
|
||||
// 로컬 스토리지에서 트랜잭션 데이터 가져오기
|
||||
const localDataStr = localStorage.getItem('transactions');
|
||||
console.log('로컬 트랜잭션 데이터:', localDataStr);
|
||||
|
||||
const localDataStr = localStorage.getItem("transactions");
|
||||
storageLogger.info("로컬 트랜잭션 데이터:", localDataStr);
|
||||
|
||||
if (localDataStr) {
|
||||
try {
|
||||
const localData = JSON.parse(localDataStr);
|
||||
|
||||
|
||||
// 지원되는 카테고리로 필터링 및 카테고리명 변환
|
||||
const filteredData = localData.map((transaction: Transaction) => {
|
||||
if (transaction.type === 'expense') {
|
||||
if (transaction.type === "expense") {
|
||||
// 기존 카테고리명 변환
|
||||
if (transaction.category === '식비') {
|
||||
return {
|
||||
...transaction,
|
||||
category: '음식',
|
||||
paymentMethod: transaction.paymentMethod || '신용카드' // 기존 데이터에 없으면 기본값 추가
|
||||
if (transaction.category === "식비") {
|
||||
return {
|
||||
...transaction,
|
||||
category: "음식",
|
||||
paymentMethod: transaction.paymentMethod || "신용카드", // 기존 데이터에 없으면 기본값 추가
|
||||
};
|
||||
} else if (transaction.category === '생활비') {
|
||||
return {
|
||||
...transaction,
|
||||
category: '쇼핑',
|
||||
paymentMethod: transaction.paymentMethod || '신용카드' // 기존 데이터에 없으면 기본값 추가
|
||||
} else if (transaction.category === "생활비") {
|
||||
return {
|
||||
...transaction,
|
||||
category: "쇼핑",
|
||||
paymentMethod: transaction.paymentMethod || "신용카드", // 기존 데이터에 없으면 기본값 추가
|
||||
};
|
||||
} else if (!EXPENSE_CATEGORIES.includes(transaction.category)) {
|
||||
return {
|
||||
...transaction,
|
||||
category: '쇼핑',
|
||||
paymentMethod: transaction.paymentMethod || '신용카드' // 기존 데이터에 없으면 기본값 추가
|
||||
return {
|
||||
...transaction,
|
||||
category: "쇼핑",
|
||||
paymentMethod: transaction.paymentMethod || "신용카드", // 기존 데이터에 없으면 기본값 추가
|
||||
}; // 지원되지 않는 카테고리는 '쇼핑'으로
|
||||
}
|
||||
|
||||
|
||||
// 기존 데이터에 paymentMethod가 없으면 기본값 추가
|
||||
if (!transaction.paymentMethod) {
|
||||
return {
|
||||
...transaction,
|
||||
paymentMethod: '신용카드'
|
||||
paymentMethod: "신용카드",
|
||||
};
|
||||
}
|
||||
}
|
||||
return transaction;
|
||||
});
|
||||
|
||||
console.log('필터링된 트랜잭션:', filteredData.length);
|
||||
|
||||
storageLogger.info("필터링된 트랜잭션:", filteredData.length);
|
||||
return filteredData;
|
||||
} catch (parseError) {
|
||||
console.error('트랜잭션 데이터 파싱 오류:', parseError);
|
||||
storageLogger.error("트랜잭션 데이터 파싱 오류:", parseError);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('트랜잭션 로드 중 오류:', err);
|
||||
storageLogger.error("트랜잭션 로드 중 오류:", err);
|
||||
}
|
||||
|
||||
console.log('로컬 트랜잭션 데이터 없음');
|
||||
|
||||
storageLogger.info("로컬 트랜잭션 데이터 없음");
|
||||
return [];
|
||||
};
|
||||
|
||||
// 로컬 스토리지에 트랜잭션 데이터 저장
|
||||
export const saveTransactionsToStorage = (transactions: Transaction[]): void => {
|
||||
export const saveTransactionsToStorage = (
|
||||
transactions: Transaction[]
|
||||
): void => {
|
||||
try {
|
||||
const dataString = JSON.stringify(transactions);
|
||||
localStorage.setItem('transactions', dataString);
|
||||
localStorage.setItem('transactions_backup', dataString); // 백업도 저장
|
||||
|
||||
localStorage.setItem("transactions", dataString);
|
||||
localStorage.setItem("transactions_backup", dataString); // 백업도 저장
|
||||
|
||||
// 이벤트 발생
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new StorageEvent('storage', {
|
||||
key: 'transactions',
|
||||
newValue: dataString
|
||||
}));
|
||||
|
||||
console.log('트랜잭션 저장 완료:', transactions.length, '개');
|
||||
window.dispatchEvent(new Event("transactionUpdated"));
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", {
|
||||
key: "transactions",
|
||||
newValue: dataString,
|
||||
})
|
||||
);
|
||||
|
||||
storageLogger.info("트랜잭션 저장 완료:", transactions.length, "개");
|
||||
} catch (error) {
|
||||
console.error('트랜잭션 저장 오류:', error);
|
||||
storageLogger.error("트랜잭션 저장 오류:", error);
|
||||
toast({
|
||||
title: "데이터 저장 실패",
|
||||
description: "트랜잭션 데이터를 저장하는데 실패했습니다.",
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -92,13 +96,13 @@ export const saveTransactionsToStorage = (transactions: Transaction[]): void =>
|
||||
// 예산 데이터 로드
|
||||
export const loadBudgetFromStorage = (): number => {
|
||||
try {
|
||||
const budgetDataStr = localStorage.getItem('budgetData');
|
||||
const budgetDataStr = localStorage.getItem("budgetData");
|
||||
if (budgetDataStr) {
|
||||
const budgetData = JSON.parse(budgetDataStr);
|
||||
return budgetData.monthly.targetAmount;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('예산 데이터 파싱 오류:', e);
|
||||
storageLogger.error("예산 데이터 파싱 오류:", e);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Transaction } from '@/contexts/budget/types';
|
||||
import { useBudget } from '@/contexts/budget/BudgetContext';
|
||||
import { useCallback } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { Transaction } from "@/contexts/budget/types";
|
||||
import { useBudget } from "@/contexts/budget/BudgetContext";
|
||||
|
||||
export const useTransactionsOperations = (transactions: Transaction[]) => {
|
||||
const { updateTransaction: budgetUpdateTransaction, deleteTransaction: budgetDeleteTransaction } = useBudget();
|
||||
const {
|
||||
updateTransaction: budgetUpdateTransaction,
|
||||
deleteTransaction: budgetDeleteTransaction,
|
||||
} = useBudget();
|
||||
|
||||
// 트랜잭션 업데이트 함수
|
||||
const updateTransaction = useCallback((updatedTransaction: Transaction): void => {
|
||||
try {
|
||||
budgetUpdateTransaction(updatedTransaction);
|
||||
} catch (error) {
|
||||
console.error('트랜잭션 업데이트 중 오류:', error);
|
||||
}
|
||||
}, [budgetUpdateTransaction]);
|
||||
const updateTransaction = useCallback(
|
||||
(updatedTransaction: Transaction): void => {
|
||||
try {
|
||||
budgetUpdateTransaction(updatedTransaction);
|
||||
} catch (error) {
|
||||
logger.error("트랜잭션 업데이트 중 오류:", error);
|
||||
}
|
||||
},
|
||||
[budgetUpdateTransaction]
|
||||
);
|
||||
|
||||
// 트랜잭션 삭제 함수
|
||||
const deleteTransaction = useCallback(async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
budgetDeleteTransaction(id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('트랜잭션 삭제 중 오류:', error);
|
||||
return false;
|
||||
}
|
||||
}, [budgetDeleteTransaction]);
|
||||
const deleteTransaction = useCallback(
|
||||
async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
budgetDeleteTransaction(id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("트랜잭션 삭제 중 오류:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[budgetDeleteTransaction]
|
||||
);
|
||||
|
||||
return {
|
||||
updateTransaction,
|
||||
deleteTransaction
|
||||
deleteTransaction,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { Transaction } from "@/components/TransactionCard";
|
||||
|
||||
export interface TransactionOperationProps {
|
||||
transactions: Transaction[];
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { useAuth } from '@/contexts/auth/useAuth';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import { saveTransactionsToStorage } from '../storageUtils';
|
||||
import { updateTransactionInSupabase } from '../supabaseUtils';
|
||||
import { TransactionOperationProps } from './types';
|
||||
import { normalizeDate } from '@/utils/sync/transaction/dateUtils';
|
||||
import { useCallback } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { Transaction } from "@/components/TransactionCard";
|
||||
import { useAuth } from "@/contexts/auth/useAuth";
|
||||
import { toast } from "@/hooks/useToast.wrapper";
|
||||
import { saveTransactionsToStorage } from "../storageUtils";
|
||||
import { updateTransactionInSupabase } from "../supabaseUtils";
|
||||
import { TransactionOperationProps } from "./types";
|
||||
import { normalizeDate } from "@/utils/sync/transaction/dateUtils";
|
||||
|
||||
/**
|
||||
* 트랜잭션 업데이트 기능
|
||||
@@ -18,88 +18,104 @@ export const useUpdateTransaction = (
|
||||
) => {
|
||||
const { user } = useAuth();
|
||||
|
||||
return useCallback((updatedTransaction: Transaction) => {
|
||||
try {
|
||||
console.log(`[트랜잭션] 업데이트 시작: ID=${updatedTransaction.id}, 제목=${updatedTransaction.title}`);
|
||||
|
||||
// 트랜잭션 존재 여부 확인
|
||||
const existingIndex = transactions.findIndex(t => t.id === updatedTransaction.id);
|
||||
if (existingIndex === -1) {
|
||||
console.warn(`[트랜잭션] 업데이트 오류: ID ${updatedTransaction.id}를 찾을 수 없음`);
|
||||
toast({
|
||||
title: "업데이트 실패",
|
||||
description: "해당 지출 항목을 찾을 수 없습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 데이터와 변경 감지
|
||||
const oldTransaction = transactions[existingIndex];
|
||||
const hasChanges = JSON.stringify(oldTransaction) !== JSON.stringify(updatedTransaction);
|
||||
|
||||
if (!hasChanges) {
|
||||
console.log(`[트랜잭션] 변경사항 없음: ${updatedTransaction.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 변경 내용 로깅
|
||||
console.log(`[트랜잭션] 변경 감지:
|
||||
return useCallback(
|
||||
(updatedTransaction: Transaction) => {
|
||||
try {
|
||||
logger.info(
|
||||
`[트랜잭션] 업데이트 시작: ID=${updatedTransaction.id}, 제목=${updatedTransaction.title}`
|
||||
);
|
||||
|
||||
// 트랜잭션 존재 여부 확인
|
||||
const existingIndex = transactions.findIndex(
|
||||
(t) => t.id === updatedTransaction.id
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
logger.warn(
|
||||
`[트랜잭션] 업데이트 오류: ID ${updatedTransaction.id}를 찾을 수 없음`
|
||||
);
|
||||
toast({
|
||||
title: "업데이트 실패",
|
||||
description: "해당 지출 항목을 찾을 수 없습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 데이터와 변경 감지
|
||||
const oldTransaction = transactions[existingIndex];
|
||||
const hasChanges =
|
||||
JSON.stringify(oldTransaction) !== JSON.stringify(updatedTransaction);
|
||||
|
||||
if (!hasChanges) {
|
||||
logger.info(`[트랜잭션] 변경사항 없음: ${updatedTransaction.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 변경 내용 로깅
|
||||
logger.info(`[트랜잭션] 변경 감지:
|
||||
제목: ${oldTransaction.title} -> ${updatedTransaction.title}
|
||||
금액: ${oldTransaction.amount} -> ${updatedTransaction.amount}
|
||||
카테고리: ${oldTransaction.category} -> ${updatedTransaction.category}
|
||||
날짜: ${oldTransaction.date} -> ${updatedTransaction.date}
|
||||
`);
|
||||
|
||||
// 로컬 스토리지 업데이트
|
||||
const updatedTransactions = transactions.map(transaction =>
|
||||
transaction.id === updatedTransaction.id ? updatedTransaction : transaction
|
||||
);
|
||||
|
||||
saveTransactionsToStorage(updatedTransactions);
|
||||
console.log(`[트랜잭션] 로컬 저장소 업데이트 완료`);
|
||||
|
||||
// 상태 업데이트
|
||||
setTransactions(updatedTransactions);
|
||||
|
||||
// Supabase 업데이트 시도 (날짜 형식 변환 추가)
|
||||
if (user) {
|
||||
// ISO 형식으로 날짜 변환
|
||||
const transactionWithIsoDate = {
|
||||
...updatedTransaction,
|
||||
dateForSync: normalizeDate(updatedTransaction.date)
|
||||
};
|
||||
|
||||
console.log(`[트랜잭션] Supabase 업데이트 시작: ${updatedTransaction.id}`);
|
||||
updateTransactionInSupabase(user, transactionWithIsoDate)
|
||||
.then(() => {
|
||||
console.log(`[트랜잭션] Supabase 업데이트 성공: ${updatedTransaction.id}`);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`[트랜잭션] Supabase 업데이트 실패:`, err);
|
||||
|
||||
// 로컬 스토리지 업데이트
|
||||
const updatedTransactions = transactions.map((transaction) =>
|
||||
transaction.id === updatedTransaction.id
|
||||
? updatedTransaction
|
||||
: transaction
|
||||
);
|
||||
|
||||
saveTransactionsToStorage(updatedTransactions);
|
||||
logger.info(`[트랜잭션] 로컬 저장소 업데이트 완료`);
|
||||
|
||||
// 상태 업데이트
|
||||
setTransactions(updatedTransactions);
|
||||
|
||||
// Supabase 업데이트 시도 (날짜 형식 변환 추가)
|
||||
if (user) {
|
||||
// ISO 형식으로 날짜 변환
|
||||
const transactionWithIsoDate = {
|
||||
...updatedTransaction,
|
||||
dateForSync: normalizeDate(updatedTransaction.date),
|
||||
};
|
||||
|
||||
logger.info(
|
||||
`[트랜잭션] Supabase 업데이트 시작: ${updatedTransaction.id}`
|
||||
);
|
||||
updateTransactionInSupabase(user, transactionWithIsoDate)
|
||||
.then(() => {
|
||||
logger.info(
|
||||
`[트랜잭션] Supabase 업데이트 성공: ${updatedTransaction.id}`
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(`[트랜잭션] Supabase 업데이트 실패:`, err);
|
||||
});
|
||||
} else {
|
||||
logger.info(`[트랜잭션] 로그인 상태 아님: Supabase 업데이트 건너뜀`);
|
||||
}
|
||||
|
||||
// 이벤트 발생
|
||||
window.dispatchEvent(new Event("transactionUpdated"));
|
||||
|
||||
// 약간의 지연을 두고 토스트 표시
|
||||
setTimeout(() => {
|
||||
toast({
|
||||
title: "지출이 수정되었습니다",
|
||||
description: `${updatedTransaction.title} 항목이 업데이트되었습니다.`,
|
||||
duration: 3000,
|
||||
});
|
||||
} else {
|
||||
console.log(`[트랜잭션] 로그인 상태 아님: Supabase 업데이트 건너뜀`);
|
||||
}
|
||||
|
||||
// 이벤트 발생
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
|
||||
// 약간의 지연을 두고 토스트 표시
|
||||
setTimeout(() => {
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
logger.error(`[트랜잭션] 업데이트 중 오류 발생:`, error);
|
||||
toast({
|
||||
title: "지출이 수정되었습니다",
|
||||
description: `${updatedTransaction.title} 항목이 업데이트되었습니다.`,
|
||||
duration: 3000
|
||||
title: "업데이트 실패",
|
||||
description: "지출 수정 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error(`[트랜잭션] 업데이트 중 오류 발생:`, error);
|
||||
toast({
|
||||
title: "업데이트 실패",
|
||||
description: "지출 수정 중 오류가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
}, [transactions, setTransactions, user]);
|
||||
}
|
||||
},
|
||||
[transactions, setTransactions, user]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Transaction } from '@/contexts/budget/types';
|
||||
import { useBudget } from '@/contexts/budget/BudgetContext';
|
||||
import { useCallback } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { Transaction } from "@/contexts/budget/types";
|
||||
import { useBudget } from "@/contexts/budget/BudgetContext";
|
||||
|
||||
export const useTransactionsOperations = (transactions: Transaction[]) => {
|
||||
const { updateTransaction: budgetUpdateTransaction, deleteTransaction: budgetDeleteTransaction } = useBudget();
|
||||
const {
|
||||
updateTransaction: budgetUpdateTransaction,
|
||||
deleteTransaction: budgetDeleteTransaction,
|
||||
} = useBudget();
|
||||
|
||||
// 트랜잭션 업데이트 함수
|
||||
const updateTransaction = useCallback((updatedTransaction: Transaction): void => {
|
||||
try {
|
||||
budgetUpdateTransaction(updatedTransaction);
|
||||
} catch (error) {
|
||||
console.error('트랜잭션 업데이트 중 오류:', error);
|
||||
}
|
||||
}, [budgetUpdateTransaction]);
|
||||
const updateTransaction = useCallback(
|
||||
(updatedTransaction: Transaction): void => {
|
||||
try {
|
||||
budgetUpdateTransaction(updatedTransaction);
|
||||
} catch (error) {
|
||||
logger.error("트랜잭션 업데이트 중 오류:", error);
|
||||
}
|
||||
},
|
||||
[budgetUpdateTransaction]
|
||||
);
|
||||
|
||||
// 트랜잭션 삭제 함수
|
||||
const deleteTransaction = useCallback(async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
budgetDeleteTransaction(id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('트랜잭션 삭제 중 오류:', error);
|
||||
return false;
|
||||
}
|
||||
}, [budgetDeleteTransaction]);
|
||||
const deleteTransaction = useCallback(
|
||||
async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
budgetDeleteTransaction(id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("트랜잭션 삭제 중 오류:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[budgetDeleteTransaction]
|
||||
);
|
||||
|
||||
return {
|
||||
updateTransaction,
|
||||
deleteTransaction
|
||||
deleteTransaction,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,137 +1,157 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import {
|
||||
syncTransactionsWithAppwrite,
|
||||
updateTransactionInAppwrite,
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { appwriteLogger } from "@/utils/logger";
|
||||
import { Transaction } from "@/components/TransactionCard";
|
||||
import {
|
||||
syncTransactionsWithAppwrite,
|
||||
updateTransactionInAppwrite,
|
||||
deleteTransactionFromAppwrite,
|
||||
debouncedDeleteTransaction
|
||||
} from '@/utils/appwriteTransactionUtils';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import { isSyncEnabled } from '@/utils/syncUtils';
|
||||
debouncedDeleteTransaction,
|
||||
} from "@/utils/appwriteTransactionUtils";
|
||||
import { toast } from "@/hooks/useToast.wrapper";
|
||||
import { isSyncEnabled } from "@/utils/syncUtils";
|
||||
|
||||
/**
|
||||
* Appwrite 트랜잭션 관리 훅
|
||||
* 트랜잭션 동기화, 추가, 수정, 삭제 기능 제공
|
||||
*/
|
||||
export const useAppwriteTransactions = (user: any, localTransactions: Transaction[]) => {
|
||||
export const useAppwriteTransactions = (
|
||||
user: any,
|
||||
localTransactions: Transaction[]
|
||||
) => {
|
||||
// 트랜잭션 상태 관리
|
||||
const [transactions, setTransactions] = useState<Transaction[]>(localTransactions);
|
||||
const [transactions, setTransactions] =
|
||||
useState<Transaction[]>(localTransactions);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
|
||||
// 컴포넌트 마운트 상태 추적
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
|
||||
// 진행 중인 작업 추적
|
||||
const pendingOperations = useRef<Set<string>>(new Set());
|
||||
|
||||
|
||||
// 트랜잭션 동기화
|
||||
const syncTransactions = useCallback(async () => {
|
||||
if (!user || !isSyncEnabled()) return localTransactions;
|
||||
|
||||
if (!user || !isSyncEnabled()) {
|
||||
return localTransactions;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
||||
// UI 스레드 차단 방지
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const syncedTransactions = await syncTransactionsWithAppwrite(user, localTransactions);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const syncedTransactions = await syncTransactionsWithAppwrite(
|
||||
user,
|
||||
localTransactions
|
||||
);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
setTransactions(syncedTransactions);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
|
||||
return syncedTransactions;
|
||||
} catch (err) {
|
||||
console.error('트랜잭션 동기화 오류:', err);
|
||||
|
||||
appwriteLogger.error("트랜잭션 동기화 오류:", err);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
setError(err as Error);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
|
||||
return localTransactions;
|
||||
}
|
||||
}, [user, localTransactions]);
|
||||
|
||||
|
||||
// 트랜잭션 추가/수정
|
||||
const saveTransaction = useCallback(async (transaction: Transaction) => {
|
||||
if (!user || !isSyncEnabled()) return;
|
||||
|
||||
try {
|
||||
// 작업 추적 시작
|
||||
pendingOperations.current.add(transaction.id);
|
||||
|
||||
// UI 스레드 차단 방지
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
await updateTransactionInAppwrite(user, transaction);
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// 로컬 상태 업데이트
|
||||
setTransactions(prev => {
|
||||
const index = prev.findIndex(t => t.id === transaction.id);
|
||||
if (index >= 0) {
|
||||
const updated = [...prev];
|
||||
updated[index] = transaction;
|
||||
return updated;
|
||||
} else {
|
||||
return [...prev, transaction];
|
||||
const saveTransaction = useCallback(
|
||||
async (transaction: Transaction) => {
|
||||
if (!user || !isSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 작업 추적 시작
|
||||
pendingOperations.current.add(transaction.id);
|
||||
|
||||
// UI 스레드 차단 방지
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
|
||||
await updateTransactionInAppwrite(user, transaction);
|
||||
|
||||
if (!isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('트랜잭션 저장 오류:', err);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: '저장 실패',
|
||||
description: '트랜잭션을 저장하는 중 오류가 발생했습니다.',
|
||||
variant: 'destructive'
|
||||
|
||||
// 로컬 상태 업데이트
|
||||
setTransactions((prev) => {
|
||||
const index = prev.findIndex((t) => t.id === transaction.id);
|
||||
if (index >= 0) {
|
||||
const updated = [...prev];
|
||||
updated[index] = transaction;
|
||||
return updated;
|
||||
} else {
|
||||
return [...prev, transaction];
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
appwriteLogger.error("트랜잭션 저장 오류:", err);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "저장 실패",
|
||||
description: "트랜잭션을 저장하는 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
// 작업 추적 종료
|
||||
pendingOperations.current.delete(transaction.id);
|
||||
}
|
||||
} finally {
|
||||
// 작업 추적 종료
|
||||
pendingOperations.current.delete(transaction.id);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
// 트랜잭션 삭제
|
||||
const removeTransaction = useCallback(async (transactionId: string) => {
|
||||
if (!user || !isSyncEnabled()) return;
|
||||
|
||||
try {
|
||||
// 작업 추적 시작
|
||||
pendingOperations.current.add(transactionId);
|
||||
|
||||
// 로컬 상태 먼저 업데이트 (낙관적 UI 업데이트)
|
||||
setTransactions(prev => prev.filter(t => t.id !== transactionId));
|
||||
|
||||
// 디바운스된 삭제 작업 실행 (여러 번 연속 호출 방지)
|
||||
await debouncedDeleteTransaction(user, transactionId);
|
||||
|
||||
} catch (err) {
|
||||
console.error('트랜잭션 삭제 오류:', err);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: '삭제 실패',
|
||||
description: '트랜잭션을 삭제하는 중 오류가 발생했습니다.',
|
||||
variant: 'destructive'
|
||||
});
|
||||
|
||||
// 실패 시 트랜잭션 복원 (서버에서 가져오기)
|
||||
syncTransactions();
|
||||
const removeTransaction = useCallback(
|
||||
async (transactionId: string) => {
|
||||
if (!user || !isSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
// 작업 추적 종료
|
||||
pendingOperations.current.delete(transactionId);
|
||||
}
|
||||
}, [user, syncTransactions]);
|
||||
|
||||
|
||||
try {
|
||||
// 작업 추적 시작
|
||||
pendingOperations.current.add(transactionId);
|
||||
|
||||
// 로컬 상태 먼저 업데이트 (낙관적 UI 업데이트)
|
||||
setTransactions((prev) => prev.filter((t) => t.id !== transactionId));
|
||||
|
||||
// 디바운스된 삭제 작업 실행 (여러 번 연속 호출 방지)
|
||||
await debouncedDeleteTransaction(user, transactionId);
|
||||
} catch (err) {
|
||||
appwriteLogger.error("트랜잭션 삭제 오류:", err);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "삭제 실패",
|
||||
description: "트랜잭션을 삭제하는 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
// 실패 시 트랜잭션 복원 (서버에서 가져오기)
|
||||
syncTransactions();
|
||||
}
|
||||
} finally {
|
||||
// 작업 추적 종료
|
||||
pendingOperations.current.delete(transactionId);
|
||||
}
|
||||
},
|
||||
[user, syncTransactions]
|
||||
);
|
||||
|
||||
// 초기 동기화
|
||||
useEffect(() => {
|
||||
if (user && isSyncEnabled()) {
|
||||
@@ -140,14 +160,14 @@ export const useAppwriteTransactions = (user: any, localTransactions: Transactio
|
||||
setTransactions(localTransactions);
|
||||
}
|
||||
}, [user, localTransactions, syncTransactions]);
|
||||
|
||||
|
||||
// 컴포넌트 언마운트 시 정리
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return {
|
||||
transactions,
|
||||
loading,
|
||||
@@ -155,7 +175,7 @@ export const useAppwriteTransactions = (user: any, localTransactions: Transactio
|
||||
syncTransactions,
|
||||
saveTransaction,
|
||||
removeTransaction,
|
||||
hasPendingOperations: pendingOperations.current.size > 0
|
||||
hasPendingOperations: pendingOperations.current.size > 0,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
|
||||
import { logger } from "@/utils/logger";
|
||||
/**
|
||||
* 트랜잭션 삭제 알림 관련 로직을 담당하는 커스텀 훅
|
||||
*/
|
||||
export const useDeleteAlert = (onDelete: () => Promise<boolean> | boolean) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
|
||||
// 타임아웃 참조 저장 (메모리 누수 방지용)
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
|
||||
// 클린업 함수 - 메모리 누수 방지
|
||||
const clearTimeouts = () => {
|
||||
if (timeoutRef.current) {
|
||||
@@ -18,32 +18,34 @@ export const useDeleteAlert = (onDelete: () => Promise<boolean> | boolean) => {
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 컴포넌트 언마운트 시 모든 타임아웃 제거
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeouts();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const handleDelete = async () => {
|
||||
// 이미 삭제 중이면 중복 실행 방지
|
||||
if (isDeleting) return;
|
||||
|
||||
if (isDeleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 삭제 상태 활성화
|
||||
setIsDeleting(true);
|
||||
|
||||
|
||||
// 다이얼로그 즉시 닫기 (UI 응답성 개선)
|
||||
setIsOpen(false);
|
||||
|
||||
|
||||
// UI 애니메이션 완료 후 삭제 실행
|
||||
timeoutRef.current = setTimeout(async () => {
|
||||
try {
|
||||
// 삭제 함수 실행
|
||||
await onDelete();
|
||||
} catch (error) {
|
||||
console.error('삭제 처리 오류:', error);
|
||||
logger.error("삭제 처리 오류:", error);
|
||||
} finally {
|
||||
// 모든 작업 완료 후 상태 초기화 (약간 지연)
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
@@ -52,23 +54,25 @@ export const useDeleteAlert = (onDelete: () => Promise<boolean> | boolean) => {
|
||||
}
|
||||
}, 150);
|
||||
} catch (error) {
|
||||
console.error('삭제 핸들러 오류:', error);
|
||||
logger.error("삭제 핸들러 오류:", error);
|
||||
setIsDeleting(false);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 다이얼로그 상태 관리
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
// 삭제 중에는 상태 변경 방지
|
||||
if (isDeleting && !open) return;
|
||||
if (isDeleting && !open) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(open);
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
isDeleting,
|
||||
handleDelete,
|
||||
handleOpenChange
|
||||
handleOpenChange,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { toast } from "@/hooks/useToast.wrapper";
|
||||
|
||||
/**
|
||||
* 최근 거래내역 관련 로직을 처리하는 커스텀 훅
|
||||
@@ -10,7 +10,7 @@ export const useRecentTransactions = (
|
||||
deleteTransaction: (id: string) => void
|
||||
) => {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
|
||||
// 삭제 중인 ID 추적
|
||||
const deletingIdRef = useRef<string | null>(null);
|
||||
|
||||
@@ -21,101 +21,107 @@ export const useRecentTransactions = (
|
||||
const lastDeleteTimeRef = useRef<Record<string, number>>({});
|
||||
|
||||
// 완전히 새로운 삭제 처리 함수
|
||||
const handleDeleteTransaction = useCallback(async (id: string): Promise<boolean> => {
|
||||
return new Promise(resolve => {
|
||||
try {
|
||||
// 삭제 진행 중인지 확인
|
||||
if (isDeleting || deletingIdRef.current === id) {
|
||||
console.log('이미 삭제 작업이 진행 중입니다');
|
||||
const handleDeleteTransaction = useCallback(
|
||||
async (id: string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
// 삭제 진행 중인지 확인
|
||||
if (isDeleting || deletingIdRef.current === id) {
|
||||
logger.info("이미 삭제 작업이 진행 중입니다");
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 급발진 방지 (300ms)
|
||||
const now = Date.now();
|
||||
if (
|
||||
lastDeleteTimeRef.current[id] &&
|
||||
now - lastDeleteTimeRef.current[id] < 300
|
||||
) {
|
||||
logger.warn("삭제 요청이 너무 빠릅니다. 무시합니다.");
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 타임스탬프 업데이트
|
||||
lastDeleteTimeRef.current[id] = now;
|
||||
|
||||
// 삭제 상태 설정
|
||||
setIsDeleting(true);
|
||||
deletingIdRef.current = id;
|
||||
|
||||
// 안전장치: 타임아웃 설정 (최대 900ms)
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
logger.warn("삭제 타임아웃 - 상태 초기화");
|
||||
setIsDeleting(false);
|
||||
deletingIdRef.current = null;
|
||||
resolve(true); // UI 응답성 위해 성공 간주
|
||||
}, 900);
|
||||
|
||||
// 비동기 작업을 동기적으로 처리하여 UI 블로킹 방지
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// BudgetContext의 deleteTransaction 함수 호출
|
||||
deleteTransaction(id);
|
||||
|
||||
// 안전장치 타임아웃 제거
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
|
||||
// 상태 초기화 (지연 적용)
|
||||
setTimeout(() => {
|
||||
setIsDeleting(false);
|
||||
deletingIdRef.current = null;
|
||||
}, 100);
|
||||
|
||||
// 성공 메시지 표시
|
||||
toast({
|
||||
title: "항목이 삭제되었습니다",
|
||||
description: "지출 내역이 성공적으로 삭제되었습니다.",
|
||||
duration: 1500,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("삭제 처리 오류:", err);
|
||||
|
||||
// 에러 메시지 표시
|
||||
toast({
|
||||
title: "삭제 실패",
|
||||
description: "항목을 삭제하는 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// 즉시 성공 반환 (UI 응답성 향상)
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("삭제 처리 전체 오류:", error);
|
||||
|
||||
// 급발진 방지 (300ms)
|
||||
const now = Date.now();
|
||||
if (lastDeleteTimeRef.current[id] && now - lastDeleteTimeRef.current[id] < 300) {
|
||||
console.warn('삭제 요청이 너무 빠릅니다. 무시합니다.');
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 타임스탬프 업데이트
|
||||
lastDeleteTimeRef.current[id] = now;
|
||||
|
||||
// 삭제 상태 설정
|
||||
setIsDeleting(true);
|
||||
deletingIdRef.current = id;
|
||||
|
||||
// 안전장치: 타임아웃 설정 (최대 900ms)
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
console.warn('삭제 타임아웃 - 상태 초기화');
|
||||
// 항상 상태 정리
|
||||
setIsDeleting(false);
|
||||
deletingIdRef.current = null;
|
||||
resolve(true); // UI 응답성 위해 성공 간주
|
||||
}, 900);
|
||||
|
||||
// 비동기 작업을 동기적으로 처리하여 UI 블로킹 방지
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// BudgetContext의 deleteTransaction 함수 호출
|
||||
deleteTransaction(id);
|
||||
|
||||
// 안전장치 타임아웃 제거
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
|
||||
// 상태 초기화 (지연 적용)
|
||||
setTimeout(() => {
|
||||
setIsDeleting(false);
|
||||
deletingIdRef.current = null;
|
||||
}, 100);
|
||||
|
||||
// 성공 메시지 표시
|
||||
toast({
|
||||
title: "항목이 삭제되었습니다",
|
||||
description: "지출 내역이 성공적으로 삭제되었습니다.",
|
||||
duration: 1500
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('삭제 처리 오류:', err);
|
||||
|
||||
// 에러 메시지 표시
|
||||
toast({
|
||||
title: "삭제 실패",
|
||||
description: "항목을 삭제하는 중 오류가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
duration: 1500
|
||||
});
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// 즉시 성공 반환 (UI 응답성 향상)
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
console.error('삭제 처리 전체 오류:', error);
|
||||
|
||||
// 항상 상태 정리
|
||||
setIsDeleting(false);
|
||||
deletingIdRef.current = null;
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
toast({
|
||||
title: "오류 발생",
|
||||
description: "처리 중 문제가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
duration: 1500,
|
||||
});
|
||||
resolve(false);
|
||||
}
|
||||
toast({
|
||||
title: "오류 발생",
|
||||
description: "처리 중 문제가 발생했습니다.",
|
||||
variant: "destructive",
|
||||
duration: 1500
|
||||
});
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}, [deleteTransaction, isDeleting]);
|
||||
});
|
||||
},
|
||||
[deleteTransaction, isDeleting]
|
||||
);
|
||||
|
||||
// 컴포넌트 언마운트 시 타임아웃 정리 (리액트 컴포넌트에서 처리해야함)
|
||||
const cleanupTimeouts = useCallback(() => {
|
||||
@@ -128,6 +134,6 @@ export const useRecentTransactions = (
|
||||
return {
|
||||
handleDeleteTransaction,
|
||||
isDeleting,
|
||||
cleanupTimeouts
|
||||
cleanupTimeouts,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Transaction } from '@/contexts/budget/types';
|
||||
import { useState } from "react";
|
||||
import { Transaction } from "@/contexts/budget/types";
|
||||
|
||||
/**
|
||||
* 최근 거래내역의 다이얼로그 상태를 관리하는 커스텀 훅
|
||||
*/
|
||||
export const useRecentTransactionsDialog = () => {
|
||||
const [selectedTransaction, setSelectedTransaction] = useState<Transaction | null>(null);
|
||||
const [selectedTransaction, setSelectedTransaction] =
|
||||
useState<Transaction | null>(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
const handleTransactionClick = (transaction: Transaction) => {
|
||||
@@ -27,6 +27,6 @@ export const useRecentTransactionsDialog = () => {
|
||||
isDialogOpen,
|
||||
handleTransactionClick,
|
||||
handleCloseDialog,
|
||||
setIsDialogOpen
|
||||
setIsDialogOpen,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
import { useTransactionsCore } from './useTransactionsCore';
|
||||
import { useTransactionsCore } from "./useTransactionsCore";
|
||||
|
||||
/**
|
||||
* 메인 트랜잭션 훅
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useTransactionsState } from './useTransactionsState';
|
||||
import { useTransactionsFiltering } from './useTransactionsFiltering';
|
||||
import { useTransactionsLoader } from './useTransactionsLoader';
|
||||
import { useTransactionsOperations } from './transactionOperations/useTransactionsOperations';
|
||||
import { useTransactionsEvents } from './useTransactionsEvents';
|
||||
import { useCallback } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { useTransactionsState } from "./useTransactionsState";
|
||||
import { useTransactionsFiltering } from "./useTransactionsFiltering";
|
||||
import { useTransactionsLoader } from "./useTransactionsLoader";
|
||||
import { useTransactionsOperations } from "./transactionOperations/useTransactionsOperations";
|
||||
import { useTransactionsEvents } from "./useTransactionsEvents";
|
||||
|
||||
/**
|
||||
* 핵심 트랜잭션 훅 - 성능 및 안정성 최적화 버전
|
||||
@@ -12,7 +12,7 @@ import { useTransactionsEvents } from './useTransactionsEvents';
|
||||
*/
|
||||
export const useTransactionsCore = () => {
|
||||
// 상태 관리
|
||||
const {
|
||||
const {
|
||||
transactions,
|
||||
setTransactions,
|
||||
filteredTransactions,
|
||||
@@ -28,42 +28,37 @@ export const useTransactionsCore = () => {
|
||||
totalBudget,
|
||||
setTotalBudget,
|
||||
refreshKey,
|
||||
setRefreshKey
|
||||
setRefreshKey,
|
||||
} = useTransactionsState();
|
||||
|
||||
// 데이터 로딩
|
||||
const { loadTransactions } = useTransactionsLoader(
|
||||
setTransactions,
|
||||
setTotalBudget,
|
||||
setIsLoading,
|
||||
setTransactions,
|
||||
setTotalBudget,
|
||||
setIsLoading,
|
||||
setError
|
||||
);
|
||||
|
||||
// 필터링 - 성능 개선 버전
|
||||
const {
|
||||
handlePrevMonth,
|
||||
handleNextMonth,
|
||||
getTotalExpenses
|
||||
} = useTransactionsFiltering({
|
||||
transactions,
|
||||
selectedMonth,
|
||||
setSelectedMonth,
|
||||
searchQuery,
|
||||
setFilteredTransactions
|
||||
});
|
||||
const { handlePrevMonth, handleNextMonth, getTotalExpenses } =
|
||||
useTransactionsFiltering({
|
||||
transactions,
|
||||
selectedMonth,
|
||||
setSelectedMonth,
|
||||
searchQuery,
|
||||
setFilteredTransactions,
|
||||
});
|
||||
|
||||
// 트랜잭션 작업 - 단순화된 버전
|
||||
const {
|
||||
deleteTransaction
|
||||
} = useTransactionsOperations(transactions);
|
||||
const { deleteTransaction } = useTransactionsOperations(transactions);
|
||||
|
||||
// 이벤트 리스너 - 메모리 누수 방지 버전
|
||||
useTransactionsEvents(loadTransactions, refreshKey);
|
||||
|
||||
// 데이터 강제 새로고침 - 성능 최적화
|
||||
const refreshTransactions = useCallback(() => {
|
||||
console.log('[트랜잭션 코어] 강제 새로고침');
|
||||
setRefreshKey(prev => prev + 1);
|
||||
logger.info("[트랜잭션 코어] 강제 새로고침");
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
loadTransactions();
|
||||
}, [loadTransactions, setRefreshKey]);
|
||||
|
||||
@@ -71,26 +66,26 @@ export const useTransactionsCore = () => {
|
||||
// 데이터
|
||||
transactions: filteredTransactions,
|
||||
allTransactions: transactions,
|
||||
|
||||
|
||||
// 상태
|
||||
isLoading,
|
||||
error,
|
||||
totalBudget,
|
||||
|
||||
|
||||
// 필터링
|
||||
selectedMonth,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
handlePrevMonth,
|
||||
handleNextMonth,
|
||||
|
||||
|
||||
// 작업
|
||||
deleteTransaction,
|
||||
|
||||
|
||||
// 합계
|
||||
totalExpenses: getTotalExpenses(filteredTransactions),
|
||||
|
||||
|
||||
// 새로고침
|
||||
refreshTransactions
|
||||
refreshTransactions,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { logger } from "@/utils/logger";
|
||||
/**
|
||||
* 트랜잭션 이벤트 리스너 훅 - 성능 및 메모리 누수 방지 개선 버전
|
||||
*/
|
||||
@@ -11,76 +11,86 @@ export const useTransactionsEvents = (
|
||||
// 바운싱 방지 및 이벤트 제어를 위한 참조
|
||||
const isProcessingRef = useRef(false);
|
||||
const timeoutIdsRef = useRef<number[]>([]);
|
||||
|
||||
|
||||
// 타임아웃 클리어 도우미 함수
|
||||
const clearAllTimeouts = () => {
|
||||
timeoutIdsRef.current.forEach(id => window.clearTimeout(id));
|
||||
timeoutIdsRef.current.forEach((id) => window.clearTimeout(id));
|
||||
timeoutIdsRef.current = [];
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[이벤트] 이벤트 리스너 설정');
|
||||
|
||||
logger.info("[이벤트] 이벤트 리스너 설정");
|
||||
|
||||
// 이벤트 핸들러 - 부하 조절(throttle) 적용
|
||||
const handleEvent = (name: string, delay: number = 200) => {
|
||||
const handleEvent = (name: string, delay = 200) => {
|
||||
return (e?: any) => {
|
||||
// 이미 처리 중인 경우 건너뜀
|
||||
if (isProcessingRef.current) return;
|
||||
|
||||
console.log(`[이벤트] ${name} 이벤트 감지:`, e?.detail?.type || '');
|
||||
if (isProcessingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`[이벤트] ${name} 이벤트 감지:`, e?.detail?.type || "");
|
||||
isProcessingRef.current = true;
|
||||
|
||||
|
||||
// 딜레이 적용 (이벤트 폭주 방지)
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
loadTransactions();
|
||||
isProcessingRef.current = false;
|
||||
|
||||
|
||||
// 타임아웃 ID 목록에서 제거
|
||||
timeoutIdsRef.current = timeoutIdsRef.current.filter(id => id !== timeoutId);
|
||||
timeoutIdsRef.current = timeoutIdsRef.current.filter(
|
||||
(id) => id !== timeoutId
|
||||
);
|
||||
}, delay);
|
||||
|
||||
|
||||
// 타임아웃 ID 기록 (나중에 정리하기 위함)
|
||||
timeoutIdsRef.current.push(timeoutId);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// 각 이벤트별 핸들러 생성
|
||||
const handleTransactionUpdate = handleEvent('트랜잭션 업데이트', 150);
|
||||
const handleTransactionDelete = handleEvent('트랜잭션 삭제', 200);
|
||||
const handleTransactionChange = handleEvent('트랜잭션 변경', 150);
|
||||
const handleTransactionUpdate = handleEvent("트랜잭션 업데이트", 150);
|
||||
const handleTransactionDelete = handleEvent("트랜잭션 삭제", 200);
|
||||
const handleTransactionChange = handleEvent("트랜잭션 변경", 150);
|
||||
const handleStorageEvent = (e: StorageEvent) => {
|
||||
if (e.key === 'transactions' || e.key === null) {
|
||||
handleEvent('스토리지', 150)();
|
||||
if (e.key === "transactions" || e.key === null) {
|
||||
handleEvent("스토리지", 150)();
|
||||
}
|
||||
};
|
||||
const handleFocus = handleEvent('포커스', 200);
|
||||
|
||||
const handleFocus = handleEvent("포커스", 200);
|
||||
|
||||
// 이벤트 리스너 등록
|
||||
window.addEventListener('transactionUpdated', handleTransactionUpdate);
|
||||
window.addEventListener('transactionDeleted', handleTransactionDelete);
|
||||
window.addEventListener('transactionChanged', handleTransactionChange as EventListener);
|
||||
window.addEventListener('storage', handleStorageEvent);
|
||||
window.addEventListener('focus', handleFocus);
|
||||
|
||||
window.addEventListener("transactionUpdated", handleTransactionUpdate);
|
||||
window.addEventListener("transactionDeleted", handleTransactionDelete);
|
||||
window.addEventListener(
|
||||
"transactionChanged",
|
||||
handleTransactionChange as EventListener
|
||||
);
|
||||
window.addEventListener("storage", handleStorageEvent);
|
||||
window.addEventListener("focus", handleFocus);
|
||||
|
||||
// 초기 데이터 로드
|
||||
if (!isProcessingRef.current) {
|
||||
loadTransactions();
|
||||
}
|
||||
|
||||
|
||||
// 클린업 함수
|
||||
return () => {
|
||||
console.log('[이벤트] 이벤트 리스너 정리');
|
||||
|
||||
logger.info("[이벤트] 이벤트 리스너 정리");
|
||||
|
||||
// 모든 이벤트 리스너 제거
|
||||
window.removeEventListener('transactionUpdated', handleTransactionUpdate);
|
||||
window.removeEventListener('transactionDeleted', handleTransactionDelete);
|
||||
window.removeEventListener('transactionChanged', handleTransactionChange as EventListener);
|
||||
window.removeEventListener('storage', handleStorageEvent);
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
|
||||
window.removeEventListener("transactionUpdated", handleTransactionUpdate);
|
||||
window.removeEventListener("transactionDeleted", handleTransactionDelete);
|
||||
window.removeEventListener(
|
||||
"transactionChanged",
|
||||
handleTransactionChange as EventListener
|
||||
);
|
||||
window.removeEventListener("storage", handleStorageEvent);
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
|
||||
// 모든 진행 중인 타임아웃 정리
|
||||
clearAllTimeouts();
|
||||
|
||||
|
||||
// 처리 상태 초기화
|
||||
isProcessingRef.current = false;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
import { useTransactionsFiltering } from './filterOperations';
|
||||
import { useTransactionsFiltering } from "./filterOperations";
|
||||
|
||||
// 기존 훅을 그대로 내보내기
|
||||
export { useTransactionsFiltering };
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import {
|
||||
loadTransactionsFromStorage
|
||||
} from './storageUtils';
|
||||
import { useCallback } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { toast } from "@/hooks/useToast.wrapper";
|
||||
import { loadTransactionsFromStorage } from "./storageUtils";
|
||||
|
||||
/**
|
||||
* 트랜잭션 로딩 관련 훅
|
||||
@@ -19,41 +17,45 @@ export const useTransactionsLoader = (
|
||||
const loadTransactions = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
||||
try {
|
||||
const localTransactions = loadTransactionsFromStorage();
|
||||
setTransactions(localTransactions);
|
||||
|
||||
|
||||
// 예산 데이터에서 직접 월간 예산 값을 가져옴
|
||||
try {
|
||||
const budgetDataStr = localStorage.getItem('budgetData');
|
||||
const budgetDataStr = localStorage.getItem("budgetData");
|
||||
if (budgetDataStr) {
|
||||
const budgetData = JSON.parse(budgetDataStr);
|
||||
// 월간 예산 값만 사용
|
||||
if (budgetData && budgetData.monthly && typeof budgetData.monthly.targetAmount === 'number') {
|
||||
if (
|
||||
budgetData &&
|
||||
budgetData.monthly &&
|
||||
typeof budgetData.monthly.targetAmount === "number"
|
||||
) {
|
||||
const monthlyBudget = budgetData.monthly.targetAmount;
|
||||
setTotalBudget(monthlyBudget);
|
||||
console.log('월간 예산 설정:', monthlyBudget);
|
||||
logger.info("월간 예산 설정:", monthlyBudget);
|
||||
} else {
|
||||
console.log('유효한 월간 예산 데이터가 없습니다. 기본값 0 사용');
|
||||
logger.info("유효한 월간 예산 데이터가 없습니다. 기본값 0 사용");
|
||||
setTotalBudget(0);
|
||||
}
|
||||
} else {
|
||||
console.log('예산 데이터가 없습니다. 기본값 0 사용');
|
||||
logger.info("예산 데이터가 없습니다. 기본값 0 사용");
|
||||
setTotalBudget(0);
|
||||
}
|
||||
} catch (budgetErr) {
|
||||
console.error('예산 데이터 파싱 오류:', budgetErr);
|
||||
logger.error("예산 데이터 파싱 오류:", budgetErr);
|
||||
setTotalBudget(0);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('트랜잭션 로드 중 오류:', err);
|
||||
setError('데이터를 불러오는 중 문제가 발생했습니다.');
|
||||
logger.error("트랜잭션 로드 중 오류:", err);
|
||||
setError("데이터를 불러오는 중 문제가 발생했습니다.");
|
||||
toast({
|
||||
title: "데이터 로드 실패",
|
||||
description: "지출 내역을 불러오는데 실패했습니다.",
|
||||
variant: "destructive",
|
||||
duration: 4000
|
||||
duration: 4000,
|
||||
});
|
||||
} finally {
|
||||
// 로딩 상태를 약간 지연시켜 UI 업데이트가 원활하게 이루어지도록 함
|
||||
@@ -62,6 +64,6 @@ export const useTransactionsLoader = (
|
||||
}, [setTransactions, setTotalBudget, setIsLoading, setError]);
|
||||
|
||||
return {
|
||||
loadTransactions
|
||||
loadTransactions,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { useCallback } from "react";
|
||||
import { Transaction } from "@/components/TransactionCard";
|
||||
|
||||
export const useTransactionsOperations = (
|
||||
transactions: Transaction[],
|
||||
setTransactions: React.Dispatch<React.SetStateAction<Transaction[]>>
|
||||
) => {
|
||||
const updateTransaction = useCallback((updatedTransaction: Transaction) => {
|
||||
setTransactions(prev =>
|
||||
prev.map(t => t.id === updatedTransaction.id ? updatedTransaction : t)
|
||||
);
|
||||
}, [setTransactions]);
|
||||
const updateTransaction = useCallback(
|
||||
(updatedTransaction: Transaction) => {
|
||||
setTransactions((prev) =>
|
||||
prev.map((t) =>
|
||||
t.id === updatedTransaction.id ? updatedTransaction : t
|
||||
)
|
||||
);
|
||||
},
|
||||
[setTransactions]
|
||||
);
|
||||
|
||||
return {
|
||||
updateTransaction
|
||||
updateTransaction,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { getCurrentMonth } from './dateUtils';
|
||||
import { useState } from "react";
|
||||
import { Transaction } from "@/components/TransactionCard";
|
||||
import { getCurrentMonth } from "./dateUtils";
|
||||
|
||||
/**
|
||||
* 트랜잭션 관련 상태 관리 훅
|
||||
@@ -10,19 +9,21 @@ import { getCurrentMonth } from './dateUtils';
|
||||
export const useTransactionsState = () => {
|
||||
// 트랜잭션 상태
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [filteredTransactions, setFilteredTransactions] = useState<Transaction[]>([]);
|
||||
|
||||
const [filteredTransactions, setFilteredTransactions] = useState<
|
||||
Transaction[]
|
||||
>([]);
|
||||
|
||||
// 필터링 상태
|
||||
const [selectedMonth, setSelectedMonth] = useState(getCurrentMonth());
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
// 로딩 및 에러 상태
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
|
||||
// 예산 상태
|
||||
const [totalBudget, setTotalBudget] = useState(0);
|
||||
|
||||
|
||||
// 새로고침 키
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
@@ -32,25 +33,25 @@ export const useTransactionsState = () => {
|
||||
setTransactions,
|
||||
filteredTransactions,
|
||||
setFilteredTransactions,
|
||||
|
||||
|
||||
// 필터링 상태
|
||||
selectedMonth,
|
||||
setSelectedMonth,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
|
||||
|
||||
// 로딩 및 에러 상태
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
error,
|
||||
setError,
|
||||
|
||||
|
||||
// 예산 상태
|
||||
totalBudget,
|
||||
setTotalBudget,
|
||||
|
||||
|
||||
// 새로고침 키
|
||||
refreshKey,
|
||||
setRefreshKey
|
||||
setRefreshKey,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user