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,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { account } from '@/lib/appwrite';
|
||||
import { ID } from 'appwrite';
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { account } from "@/lib/appwrite";
|
||||
import { ID } from "appwrite";
|
||||
|
||||
// 인증 상태 인터페이스
|
||||
interface AuthState {
|
||||
@@ -29,7 +29,7 @@ export const useAppwriteAuth = () => {
|
||||
const [authState, setAuthState] = useState<AuthState>({
|
||||
user: null,
|
||||
loading: true,
|
||||
error: null
|
||||
error: null,
|
||||
});
|
||||
|
||||
// 컴포넌트 마운트 상태 추적
|
||||
@@ -43,7 +43,7 @@ export const useAppwriteAuth = () => {
|
||||
setAuthState({
|
||||
user,
|
||||
loading: false,
|
||||
error: null
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
return user;
|
||||
@@ -52,7 +52,7 @@ export const useAppwriteAuth = () => {
|
||||
setAuthState({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: error as Error
|
||||
error: error as Error,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
@@ -60,97 +60,101 @@ export const useAppwriteAuth = () => {
|
||||
}, [isMounted]);
|
||||
|
||||
// 이메일/비밀번호로 로그인
|
||||
const login = useCallback(async ({ email, password }: LoginCredentials) => {
|
||||
try {
|
||||
setAuthState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
// 비동기 작업 시작 전 UI 스레드 차단 방지
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const session = await account.createEmailPasswordSession(email, password);
|
||||
const user = await account.get();
|
||||
|
||||
if (isMounted) {
|
||||
setAuthState({
|
||||
user,
|
||||
loading: false,
|
||||
error: null
|
||||
});
|
||||
const login = useCallback(
|
||||
async ({ email, password }: LoginCredentials) => {
|
||||
try {
|
||||
setAuthState((prev) => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
// 비동기 작업 시작 전 UI 스레드 차단 방지
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const session = await account.createEmailPasswordSession(
|
||||
email,
|
||||
password
|
||||
);
|
||||
const user = await account.get();
|
||||
|
||||
if (isMounted) {
|
||||
setAuthState({
|
||||
user,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
|
||||
return { user, session };
|
||||
} catch (error) {
|
||||
if (isMounted) {
|
||||
setAuthState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error as Error,
|
||||
}));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { user, session };
|
||||
} catch (error) {
|
||||
if (isMounted) {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error as Error
|
||||
}));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}, [isMounted]);
|
||||
},
|
||||
[isMounted]
|
||||
);
|
||||
|
||||
// 회원가입
|
||||
const signup = useCallback(async ({ email, password, name }: SignupCredentials) => {
|
||||
try {
|
||||
setAuthState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
// 비동기 작업 시작 전 UI 스레드 차단 방지
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const user = await account.create(
|
||||
ID.unique(),
|
||||
email,
|
||||
password,
|
||||
name
|
||||
);
|
||||
|
||||
// 회원가입 후 자동 로그인
|
||||
await account.createEmailPasswordSession(email, password);
|
||||
|
||||
if (isMounted) {
|
||||
setAuthState({
|
||||
user,
|
||||
loading: false,
|
||||
error: null
|
||||
});
|
||||
const signup = useCallback(
|
||||
async ({ email, password, name }: SignupCredentials) => {
|
||||
try {
|
||||
setAuthState((prev) => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
// 비동기 작업 시작 전 UI 스레드 차단 방지
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const user = await account.create(ID.unique(), email, password, name);
|
||||
|
||||
// 회원가입 후 자동 로그인
|
||||
await account.createEmailPasswordSession(email, password);
|
||||
|
||||
if (isMounted) {
|
||||
setAuthState({
|
||||
user,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
if (isMounted) {
|
||||
setAuthState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error as Error,
|
||||
}));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
if (isMounted) {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error as Error
|
||||
}));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}, [isMounted]);
|
||||
},
|
||||
[isMounted]
|
||||
);
|
||||
|
||||
// 로그아웃
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
setAuthState(prev => ({ ...prev, loading: true }));
|
||||
|
||||
setAuthState((prev) => ({ ...prev, loading: true }));
|
||||
|
||||
// 현재 세션 삭제
|
||||
await account.deleteSession('current');
|
||||
|
||||
await account.deleteSession("current");
|
||||
|
||||
if (isMounted) {
|
||||
setAuthState({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isMounted) {
|
||||
setAuthState(prev => ({
|
||||
setAuthState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error as Error
|
||||
error: error as Error,
|
||||
}));
|
||||
}
|
||||
throw error;
|
||||
@@ -161,7 +165,7 @@ export const useAppwriteAuth = () => {
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
getCurrentUser();
|
||||
|
||||
|
||||
// 정리 함수
|
||||
return () => {
|
||||
setIsMounted(false);
|
||||
@@ -175,7 +179,7 @@ export const useAppwriteAuth = () => {
|
||||
login,
|
||||
signup,
|
||||
logout,
|
||||
getCurrentUser
|
||||
getCurrentUser,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { EXPENSE_CATEGORIES } from '@/constants/categoryIcons';
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { EXPENSE_CATEGORIES } from "@/constants/categoryIcons";
|
||||
|
||||
interface BudgetData {
|
||||
targetAmount: number;
|
||||
@@ -11,7 +11,10 @@ interface BudgetData {
|
||||
interface UseBudgetTabContentProps {
|
||||
data: BudgetData;
|
||||
calculatePercentage: (spent: number, target: number) => number;
|
||||
onSaveBudget: (amount: number, categoryBudgets?: Record<string, number>) => void;
|
||||
onSaveBudget: (
|
||||
amount: number,
|
||||
categoryBudgets?: Record<string, number>
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface UseBudgetTabContentReturn {
|
||||
@@ -33,53 +36,64 @@ interface UseBudgetTabContentReturn {
|
||||
export const useBudgetTabContent = ({
|
||||
data,
|
||||
calculatePercentage,
|
||||
onSaveBudget
|
||||
onSaveBudget,
|
||||
}: UseBudgetTabContentProps): UseBudgetTabContentReturn => {
|
||||
const [categoryBudgets, setCategoryBudgets] = useState<Record<string, number>>({});
|
||||
const [categoryBudgets, setCategoryBudgets] = useState<
|
||||
Record<string, number>
|
||||
>({});
|
||||
const spentAmount = data.spentAmount;
|
||||
const targetAmount = data.targetAmount;
|
||||
|
||||
// 로그 추가 - 받은 데이터 확인
|
||||
useEffect(() => {
|
||||
console.log(`BudgetTabContent 수신 데이터:`, data);
|
||||
logger.info(`BudgetTabContent 수신 데이터:`, data);
|
||||
}, [data]);
|
||||
|
||||
// 전역 예산 데이터가 변경되었을 때 로컬 상태 갱신
|
||||
useEffect(() => {
|
||||
const handleBudgetDataUpdated = () => {
|
||||
console.log(`BudgetTabContent: 전역 예산 데이터 이벤트 감지, 현재 targetAmount=${targetAmount}`);
|
||||
logger.info(
|
||||
`BudgetTabContent: 전역 예산 데이터 이벤트 감지, 현재 targetAmount=${targetAmount}`
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener('budgetDataUpdated', handleBudgetDataUpdated);
|
||||
return () => window.removeEventListener('budgetDataUpdated', handleBudgetDataUpdated);
|
||||
window.addEventListener("budgetDataUpdated", handleBudgetDataUpdated);
|
||||
return () =>
|
||||
window.removeEventListener("budgetDataUpdated", handleBudgetDataUpdated);
|
||||
}, [targetAmount]);
|
||||
|
||||
// 예산 설정 여부 확인 - 데이터 targetAmount가 실제로 0보다 큰지 확인
|
||||
const isBudgetSet = targetAmount > 0;
|
||||
|
||||
// 실제 백분율 계산 (초과해도 실제 퍼센트로 표시)
|
||||
const actualPercentage = targetAmount > 0 ? Math.round((spentAmount / targetAmount) * 100) : 0;
|
||||
const actualPercentage =
|
||||
targetAmount > 0 ? Math.round((spentAmount / targetAmount) * 100) : 0;
|
||||
const percentage = Math.min(actualPercentage, 100); // 대시보드 표시용으로는 100% 제한
|
||||
|
||||
|
||||
// 예산 초과 여부 계산
|
||||
const isOverBudget = spentAmount > targetAmount && targetAmount > 0;
|
||||
// 예산이 얼마 남지 않은 경우 (10% 미만)
|
||||
const isLowBudget = targetAmount > 0 && actualPercentage >= 90 && actualPercentage < 100;
|
||||
const isLowBudget =
|
||||
targetAmount > 0 && actualPercentage >= 90 && actualPercentage < 100;
|
||||
|
||||
// 프로그레스 바 색상 결정
|
||||
const progressBarColor = isOverBudget ? 'bg-red-500' : isLowBudget ? 'bg-yellow-400' : 'bg-neuro-income';
|
||||
const progressBarColor = isOverBudget
|
||||
? "bg-red-500"
|
||||
: isLowBudget
|
||||
? "bg-yellow-400"
|
||||
: "bg-neuro-income";
|
||||
|
||||
// 남은 예산 또는 초과 예산 텍스트 및 금액
|
||||
const budgetStatusText = isOverBudget ? '예산 초과: ' : '남은 예산: ';
|
||||
const budgetAmount = isOverBudget ?
|
||||
Math.abs(targetAmount - spentAmount).toLocaleString() :
|
||||
Math.max(0, targetAmount - spentAmount).toLocaleString();
|
||||
|
||||
const budgetStatusText = isOverBudget ? "예산 초과: " : "남은 예산: ";
|
||||
const budgetAmount = isOverBudget
|
||||
? Math.abs(targetAmount - spentAmount).toLocaleString()
|
||||
: Math.max(0, targetAmount - spentAmount).toLocaleString();
|
||||
|
||||
const handleCategoryInputChange = (value: string, category: string) => {
|
||||
const numValue = parseInt(value.replace(/,/g, ''), 10) || 0;
|
||||
setCategoryBudgets(prev => ({
|
||||
const numValue = parseInt(value.replace(/,/g, ""), 10) || 0;
|
||||
setCategoryBudgets((prev) => ({
|
||||
...prev,
|
||||
[category]: numValue
|
||||
[category]: numValue,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -87,10 +101,10 @@ export const useBudgetTabContent = ({
|
||||
const calculateTotalBudget = () => {
|
||||
// 모든 EXPENSE_CATEGORIES에 있는 카테고리 포함해서 합계 계산
|
||||
let total = 0;
|
||||
EXPENSE_CATEGORIES.forEach(category => {
|
||||
EXPENSE_CATEGORIES.forEach((category) => {
|
||||
total += categoryBudgets[category] || 0;
|
||||
});
|
||||
console.log('카테고리 예산 총합:', total, categoryBudgets);
|
||||
logger.info("카테고리 예산 총합:", total, categoryBudgets);
|
||||
return total;
|
||||
};
|
||||
|
||||
@@ -98,25 +112,29 @@ export const useBudgetTabContent = ({
|
||||
const handleSaveCategoryBudgets = () => {
|
||||
// 카테고리 예산 기본값 설정 - 모든 카테고리 포함
|
||||
const updatedCategoryBudgets: Record<string, number> = {};
|
||||
EXPENSE_CATEGORIES.forEach(category => {
|
||||
EXPENSE_CATEGORIES.forEach((category) => {
|
||||
updatedCategoryBudgets[category] = categoryBudgets[category] || 0;
|
||||
});
|
||||
|
||||
|
||||
const totalBudget = calculateTotalBudget();
|
||||
console.log('카테고리 예산 저장 및 총 예산 설정:', totalBudget, updatedCategoryBudgets);
|
||||
|
||||
logger.info(
|
||||
"카테고리 예산 저장 및 총 예산 설정:",
|
||||
totalBudget,
|
||||
updatedCategoryBudgets
|
||||
);
|
||||
|
||||
// 총액이 0이 아닐 때만 저장 처리
|
||||
if (totalBudget > 0) {
|
||||
// 명시적으로 월간 예산으로 설정 - 항상 월간 예산만 저장
|
||||
onSaveBudget(totalBudget, updatedCategoryBudgets);
|
||||
|
||||
|
||||
// 이벤트 발생 추가 (데이터 저장 후 즉시 UI 업데이트를 위해)
|
||||
setTimeout(() => {
|
||||
console.log('예산 데이터 저장 후 이벤트 발생');
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
logger.info("예산 데이터 저장 후 이벤트 발생");
|
||||
window.dispatchEvent(new Event("budgetDataUpdated"));
|
||||
}, 200);
|
||||
} else {
|
||||
alert('예산을 입력해주세요.');
|
||||
alert("예산을 입력해주세요.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,14 +142,14 @@ export const useBudgetTabContent = ({
|
||||
useEffect(() => {
|
||||
// 로컬 스토리지에서 카테고리 예산 불러오기
|
||||
try {
|
||||
const storedCategoryBudgets = localStorage.getItem('categoryBudgets');
|
||||
const storedCategoryBudgets = localStorage.getItem("categoryBudgets");
|
||||
if (storedCategoryBudgets) {
|
||||
const parsedBudgets = JSON.parse(storedCategoryBudgets);
|
||||
console.log('저장된 카테고리 예산 불러옴:', parsedBudgets);
|
||||
logger.info("저장된 카테고리 예산 불러옴:", parsedBudgets);
|
||||
setCategoryBudgets(parsedBudgets);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('카테고리 예산 불러오기 오류:', error);
|
||||
logger.error("카테고리 예산 불러오기 오류:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -139,7 +157,10 @@ export const useBudgetTabContent = ({
|
||||
const budgetButtonText = isBudgetSet ? "예산 수정하기" : "예산 입력하기";
|
||||
|
||||
// 화면에 표시할 내용 - 디버깅을 위한 로그 추가
|
||||
console.log(`BudgetTabContent 렌더링: targetAmount=${targetAmount}, isBudgetSet=${isBudgetSet}, 표시될 화면:`, isBudgetSet ? "예산 진행 상황" : "예산 입력하기 버튼");
|
||||
logger.info(
|
||||
`BudgetTabContent 렌더링: targetAmount=${targetAmount}, isBudgetSet=${isBudgetSet}, 표시될 화면:`,
|
||||
isBudgetSet ? "예산 진행 상황" : "예산 입력하기 버튼"
|
||||
);
|
||||
|
||||
return {
|
||||
categoryBudgets,
|
||||
@@ -154,6 +175,6 @@ export const useBudgetTabContent = ({
|
||||
budgetStatusText,
|
||||
budgetAmount,
|
||||
budgetButtonText,
|
||||
calculateTotalBudget
|
||||
calculateTotalBudget,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
// 주요 동기화 훅 내보내기
|
||||
export * from './useSyncToggle';
|
||||
export * from './useManualSync';
|
||||
export * from './useSyncStatus';
|
||||
export * from './syncTime';
|
||||
export * from './syncResultHandler';
|
||||
export * from './syncPerformer';
|
||||
export * from './syncNetworkChecker';
|
||||
export * from './syncBackupUtils';
|
||||
export * from "./useSyncToggle";
|
||||
export * from "./useManualSync";
|
||||
export * from "./useSyncStatus";
|
||||
export * from "./syncTime";
|
||||
export * from "./syncResultHandler";
|
||||
export * from "./syncPerformer";
|
||||
export * from "./syncNetworkChecker";
|
||||
export * from "./syncBackupUtils";
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
|
||||
/**
|
||||
* 로컬 데이터 백업 만들기
|
||||
*/
|
||||
export const createLocalDataBackup = () => {
|
||||
const budgetDataBackup = localStorage.getItem('budgetData');
|
||||
const categoryBudgetsBackup = localStorage.getItem('categoryBudgets');
|
||||
const transactionsBackup = localStorage.getItem('transactions');
|
||||
|
||||
console.log('로컬 데이터 백업:', {
|
||||
budgetData: budgetDataBackup ? '있음' : '없음',
|
||||
categoryBudgets: categoryBudgetsBackup ? '있음' : '없음',
|
||||
transactions: transactionsBackup ? '있음' : '없음'
|
||||
const budgetDataBackup = localStorage.getItem("budgetData");
|
||||
const categoryBudgetsBackup = localStorage.getItem("categoryBudgets");
|
||||
const transactionsBackup = localStorage.getItem("transactions");
|
||||
|
||||
syncLogger.info("로컬 데이터 백업:", {
|
||||
budgetData: budgetDataBackup ? "있음" : "없음",
|
||||
categoryBudgets: categoryBudgetsBackup ? "있음" : "없음",
|
||||
transactions: transactionsBackup ? "있음" : "없음",
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
budgetDataBackup,
|
||||
categoryBudgetsBackup,
|
||||
transactionsBackup
|
||||
transactionsBackup,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,29 +23,30 @@ export const createLocalDataBackup = () => {
|
||||
* 로컬 데이터 복원하기
|
||||
*/
|
||||
export const restoreLocalDataBackup = (backup: {
|
||||
budgetDataBackup: string | null,
|
||||
categoryBudgetsBackup: string | null,
|
||||
transactionsBackup: string | null
|
||||
budgetDataBackup: string | null;
|
||||
categoryBudgetsBackup: string | null;
|
||||
transactionsBackup: string | null;
|
||||
}) => {
|
||||
const { budgetDataBackup, categoryBudgetsBackup, transactionsBackup } = backup;
|
||||
|
||||
console.log('로컬 데이터 복원 시도');
|
||||
|
||||
const { budgetDataBackup, categoryBudgetsBackup, transactionsBackup } =
|
||||
backup;
|
||||
|
||||
syncLogger.info("로컬 데이터 복원 시도");
|
||||
|
||||
// 오류 발생 시 백업 데이터 복원
|
||||
if (budgetDataBackup) {
|
||||
localStorage.setItem('budgetData', budgetDataBackup);
|
||||
localStorage.setItem("budgetData", budgetDataBackup);
|
||||
}
|
||||
if (categoryBudgetsBackup) {
|
||||
localStorage.setItem('categoryBudgets', categoryBudgetsBackup);
|
||||
localStorage.setItem("categoryBudgets", categoryBudgetsBackup);
|
||||
}
|
||||
if (transactionsBackup) {
|
||||
localStorage.setItem('transactions', transactionsBackup);
|
||||
localStorage.setItem("transactions", transactionsBackup);
|
||||
}
|
||||
|
||||
|
||||
// 이벤트 발생시켜 UI 업데이트
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
|
||||
console.log('로컬 데이터 복원 완료');
|
||||
window.dispatchEvent(new Event("budgetDataUpdated"));
|
||||
window.dispatchEvent(new Event("categoryBudgetsUpdated"));
|
||||
window.dispatchEvent(new Event("transactionUpdated"));
|
||||
|
||||
syncLogger.info("로컬 데이터 복원 완료");
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { checkNetworkStatus } from '@/utils/network/checker';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import { checkNetworkStatus } from "@/utils/network/checker";
|
||||
import { syncLogger } from "@/utils/logger";
|
||||
import { toast } from "@/hooks/useToast.wrapper";
|
||||
|
||||
/**
|
||||
* 동기화를 위한 네트워크 상태 확인
|
||||
@@ -8,20 +8,27 @@ import { toast } from '@/hooks/useToast.wrapper';
|
||||
export const checkSyncNetworkStatus = async (): Promise<boolean> => {
|
||||
// 기본 네트워크 상태 확인 - navigator.onLine 우선 사용
|
||||
const navigatorOnline = navigator.onLine;
|
||||
console.log(`[동기화] 기본 네트워크 상태 확인: ${navigatorOnline ? '온라인' : '오프라인'}`);
|
||||
|
||||
syncLogger.info(
|
||||
`[동기화] 기본 네트워크 상태 확인: ${navigatorOnline ? "온라인" : "오프라인"}`
|
||||
);
|
||||
|
||||
if (!navigatorOnline) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 강화된 네트워크 확인 시도 (실패해도 계속 진행)
|
||||
try {
|
||||
const isOnline = await checkNetworkStatus();
|
||||
console.log(`[동기화] 강화된 네트워크 확인 결과: ${isOnline ? '온라인' : '오프라인'}`);
|
||||
syncLogger.info(
|
||||
`[동기화] 강화된 네트워크 확인 결과: ${isOnline ? "온라인" : "오프라인"}`
|
||||
);
|
||||
return isOnline;
|
||||
} catch (error) {
|
||||
// 네트워크 확인 실패해도 navigator.onLine이 true면 진행
|
||||
console.warn('[동기화] 강화된 네트워크 확인 실패, 기본 상태 사용:', error);
|
||||
syncLogger.warn(
|
||||
"[동기화] 강화된 네트워크 확인 실패, 기본 상태 사용:",
|
||||
error
|
||||
);
|
||||
return navigatorOnline;
|
||||
}
|
||||
};
|
||||
@@ -34,12 +41,12 @@ export const showNetworkErrorNotification = (
|
||||
) => {
|
||||
const title = "네트워크 연결 필요";
|
||||
const description = "동기화를 위해 인터넷 연결이 필요합니다.";
|
||||
|
||||
|
||||
toast({
|
||||
title,
|
||||
description,
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
|
||||
addNotification(title, description);
|
||||
};
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
|
||||
import { trySyncAllData } from '@/utils/syncUtils';
|
||||
import { setLastSyncTime } from '@/utils/syncUtils';
|
||||
import { trySyncAllData } from "@/utils/syncUtils";
|
||||
import { syncLogger } from "@/utils/logger";
|
||||
import { setLastSyncTime } from "@/utils/syncUtils";
|
||||
|
||||
/**
|
||||
* 실제 동기화 수행 함수 (최대 2회까지 자동 재시도)
|
||||
*/
|
||||
export const performSync = async (userId: string) => {
|
||||
if (!userId) return;
|
||||
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 2;
|
||||
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
attempts++;
|
||||
console.log(`동기화 시도 ${attempts}/${maxAttempts}`);
|
||||
|
||||
syncLogger.info(`동기화 시도 ${attempts}/${maxAttempts}`);
|
||||
|
||||
// 네트워크 상태 확인 - 기본 navigator.onLine 사용
|
||||
if (!navigator.onLine) {
|
||||
console.log('네트워크 연결 없음, 동기화 건너뜀');
|
||||
throw new Error('네트워크 연결 필요');
|
||||
syncLogger.info("네트워크 연결 없음, 동기화 건너뜀");
|
||||
throw new Error("네트워크 연결 필요");
|
||||
}
|
||||
|
||||
|
||||
const result = await trySyncAllData(userId);
|
||||
|
||||
|
||||
// 동기화 성공 시 마지막 동기화 시간 업데이트
|
||||
if (result && result.success) {
|
||||
const currentTime = new Date().toISOString();
|
||||
console.log('동기화 성공, 시간 업데이트:', currentTime);
|
||||
syncLogger.info("동기화 성공, 시간 업데이트:", currentTime);
|
||||
setLastSyncTime(currentTime);
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`동기화 시도 ${attempts} 실패:`, error);
|
||||
|
||||
syncLogger.error(`동기화 시도 ${attempts} 실패:`, error);
|
||||
|
||||
if (attempts < maxAttempts) {
|
||||
// 재시도 전 잠시 대기
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
console.log(`${attempts+1}번째 동기화 재시도 중...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
syncLogger.info(`${attempts + 1}번째 동기화 재시도 중...`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { SyncResult } from '@/utils/sync/data';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import useNotifications from '@/hooks/useNotifications';
|
||||
import React, { useEffect } from "react";
|
||||
import { syncLogger } from "@/utils/logger";
|
||||
import { SyncResult } from "@/utils/sync/data";
|
||||
import { toast } from "@/hooks/useToast.wrapper";
|
||||
import useNotifications from "@/hooks/useNotifications";
|
||||
|
||||
// 알림 인스턴스 얻기 위한 전역 변수
|
||||
let notificationAdder: ((title: string, message: string) => void) | null = null;
|
||||
|
||||
// 알림 함수 설정
|
||||
export const setSyncNotificationAdder = (adder: (title: string, message: string) => void) => {
|
||||
export const setSyncNotificationAdder = (
|
||||
adder: (title: string, message: string) => void
|
||||
) => {
|
||||
notificationAdder = adder;
|
||||
};
|
||||
|
||||
@@ -21,10 +23,10 @@ export const handleSyncResult = (result: SyncResult) => {
|
||||
if (result.success) {
|
||||
// 성공 시 실패 카운터 초기화
|
||||
syncFailureCount = 0;
|
||||
|
||||
let title = '';
|
||||
let description = '';
|
||||
|
||||
|
||||
let title = "";
|
||||
let description = "";
|
||||
|
||||
if (result.uploadSuccess && result.downloadSuccess) {
|
||||
// 양방향 동기화 성공
|
||||
title = "동기화 완료";
|
||||
@@ -38,53 +40,54 @@ export const handleSyncResult = (result: SyncResult) => {
|
||||
title = "동기화 완료";
|
||||
description = "클라우드 데이터가 기기에 다운로드되었습니다.";
|
||||
}
|
||||
|
||||
|
||||
// 토스트 표시
|
||||
toast({
|
||||
title,
|
||||
description,
|
||||
});
|
||||
|
||||
|
||||
// 알림 추가 (설정된 경우)
|
||||
if (notificationAdder) {
|
||||
notificationAdder(title, description);
|
||||
}
|
||||
|
||||
|
||||
// 상세 결과 로깅
|
||||
console.log("동기화 세부 결과:", {
|
||||
예산업로드: result.details?.budgetUpload ? '성공' : '실패',
|
||||
예산다운로드: result.details?.budgetDownload ? '성공' : '실패',
|
||||
트랜잭션업로드: result.details?.transactionUpload ? '성공' : '실패',
|
||||
트랜잭션다운로드: result.details?.transactionDownload ? '성공' : '실패'
|
||||
syncLogger.info("동기화 세부 결과:", {
|
||||
예산업로드: result.details?.budgetUpload ? "성공" : "실패",
|
||||
예산다운로드: result.details?.budgetDownload ? "성공" : "실패",
|
||||
트랜잭션업로드: result.details?.transactionUpload ? "성공" : "실패",
|
||||
트랜잭션다운로드: result.details?.transactionDownload ? "성공" : "실패",
|
||||
});
|
||||
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// 동기화 실패
|
||||
console.error("동기화 실패 세부 결과:", result.details);
|
||||
|
||||
syncLogger.error("동기화 실패 세부 결과:", result.details);
|
||||
|
||||
// 실패 카운터 증가 및 최대 알림 횟수 제한
|
||||
syncFailureCount++;
|
||||
|
||||
|
||||
if (syncFailureCount <= MAX_SYNC_FAILURE_NOTIFICATIONS) {
|
||||
const title = "동기화 실패";
|
||||
const description = "데이터 동기화 중 문제가 발생했습니다. 나중에 다시 시도합니다.";
|
||||
|
||||
const description =
|
||||
"데이터 동기화 중 문제가 발생했습니다. 나중에 다시 시도합니다.";
|
||||
|
||||
// 토스트 표시
|
||||
toast({
|
||||
title,
|
||||
description,
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
|
||||
// 알림 추가 (설정된 경우)
|
||||
if (notificationAdder) {
|
||||
notificationAdder(title, description);
|
||||
}
|
||||
} else {
|
||||
console.log(`동기화 실패 알림 제한 (${syncFailureCount}회 실패)`);
|
||||
syncLogger.info(`동기화 실패 알림 제한 (${syncFailureCount}회 실패)`);
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -92,7 +95,7 @@ export const handleSyncResult = (result: SyncResult) => {
|
||||
// 커스텀 훅: 동기화 알림 관리
|
||||
export const useSyncNotifications = () => {
|
||||
const { addNotification } = useNotifications();
|
||||
|
||||
|
||||
// 컴포넌트 마운트 시 알림 함수 설정
|
||||
useEffect(() => {
|
||||
setSyncNotificationAdder(addNotification);
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
|
||||
export * from './useSyncTimeFormatting';
|
||||
export * from './useSyncTimeEvents';
|
||||
export * from "./useSyncTimeFormatting";
|
||||
export * from "./useSyncTimeEvents";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { getLastSyncTime } from '@/utils/syncUtils';
|
||||
import { useCallback } from "react";
|
||||
import { syncLogger } from "@/utils/logger";
|
||||
import { getLastSyncTime } from "@/utils/syncUtils";
|
||||
|
||||
/**
|
||||
* 동기화 시간 관련 이벤트를 처리하는 커스텀 훅
|
||||
*/
|
||||
export const useSyncTimeEvents = (
|
||||
lastSync: string | null,
|
||||
lastSync: string | null,
|
||||
setLastSync: React.Dispatch<React.SetStateAction<string | null>>
|
||||
) => {
|
||||
/**
|
||||
@@ -15,44 +15,49 @@ export const useSyncTimeEvents = (
|
||||
const setupSyncTimeEventListeners = useCallback(() => {
|
||||
const updateLastSyncTime = (event?: Event | CustomEvent) => {
|
||||
const newTime = getLastSyncTime();
|
||||
const eventDetails = event instanceof CustomEvent ? ` (이벤트 상세: ${JSON.stringify(event.detail)})` : '';
|
||||
console.log(`마지막 동기화 시간 업데이트됨: ${newTime} ${eventDetails}`);
|
||||
const eventDetails =
|
||||
event instanceof CustomEvent
|
||||
? ` (이벤트 상세: ${JSON.stringify(event.detail)})`
|
||||
: "";
|
||||
syncLogger.info(
|
||||
`마지막 동기화 시간 업데이트됨: ${newTime} ${eventDetails}`
|
||||
);
|
||||
setLastSync(newTime);
|
||||
};
|
||||
|
||||
|
||||
// 이벤트 리스너 등록 - 커스텀 이벤트 사용
|
||||
window.addEventListener('syncTimeUpdated', updateLastSyncTime);
|
||||
|
||||
window.addEventListener("syncTimeUpdated", updateLastSyncTime);
|
||||
|
||||
// 스토리지 이벤트도 모니터링
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.key === 'lastSync' || event.key === null) {
|
||||
console.log('스토리지 변경 감지 (lastSync):', event.newValue);
|
||||
if (event.key === "lastSync" || event.key === null) {
|
||||
syncLogger.info("스토리지 변경 감지 (lastSync):", event.newValue);
|
||||
updateLastSyncTime();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
|
||||
// 동기화 완료 이벤트도 모니터링
|
||||
window.addEventListener('syncComplete', updateLastSyncTime);
|
||||
|
||||
// 초기 상태 업데이트
|
||||
window.addEventListener("syncComplete", updateLastSyncTime);
|
||||
|
||||
// 초기 상태 업데이트
|
||||
updateLastSyncTime();
|
||||
|
||||
|
||||
// 주기적 시간 확인 기능 설정
|
||||
const checkInterval = setupPeriodicTimeCheck(lastSync, setLastSync);
|
||||
|
||||
|
||||
// 정리 함수 반환
|
||||
return () => {
|
||||
window.removeEventListener('syncTimeUpdated', updateLastSyncTime);
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
window.removeEventListener('syncComplete', updateLastSyncTime);
|
||||
window.removeEventListener("syncTimeUpdated", updateLastSyncTime);
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
window.removeEventListener("syncComplete", updateLastSyncTime);
|
||||
clearInterval(checkInterval);
|
||||
};
|
||||
}, [lastSync, setLastSync]);
|
||||
|
||||
|
||||
return {
|
||||
setupSyncTimeEventListeners
|
||||
setupSyncTimeEventListeners,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -60,14 +65,14 @@ export const useSyncTimeEvents = (
|
||||
* 주기적으로 동기화 시간을 확인하는 기능 설정
|
||||
*/
|
||||
const setupPeriodicTimeCheck = (
|
||||
lastSync: string | null,
|
||||
lastSync: string | null,
|
||||
setLastSync: React.Dispatch<React.SetStateAction<string | null>>
|
||||
): number => {
|
||||
// 1초마다 업데이트 상태 확인 (문제 해결을 위한 임시 방안)
|
||||
return window.setInterval(() => {
|
||||
const currentTime = getLastSyncTime();
|
||||
if (currentTime !== lastSync) {
|
||||
console.log('주기적 확인에서 동기화 시간 변경 감지:', currentTime);
|
||||
syncLogger.info("주기적 확인에서 동기화 시간 변경 감지:", currentTime);
|
||||
setLastSync(currentTime);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* 마지막 동기화 시간 포맷팅을 위한 커스텀 훅
|
||||
*/
|
||||
@@ -8,26 +7,26 @@ export const useLastSyncTimeFormatting = (lastSync: string | null) => {
|
||||
*/
|
||||
const formatLastSyncTime = (): string => {
|
||||
if (!lastSync) {
|
||||
return '없음';
|
||||
return "없음";
|
||||
}
|
||||
|
||||
try {
|
||||
const date = new Date(lastSync);
|
||||
|
||||
|
||||
// 유효한 날짜인지 확인
|
||||
if (isNaN(date.getTime())) {
|
||||
return '없음';
|
||||
return "없음";
|
||||
}
|
||||
|
||||
|
||||
return formatDateByRelativeTime(date);
|
||||
} catch (error) {
|
||||
console.error('날짜 포맷 오류:', error);
|
||||
return '없음';
|
||||
syncLogger.error("날짜 포맷 오류:", error);
|
||||
return "없음";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
formatLastSyncTime
|
||||
formatLastSyncTime,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,21 +37,21 @@ const formatDateByRelativeTime = (date: Date): string => {
|
||||
// 오늘 날짜인 경우
|
||||
const today = new Date();
|
||||
const isToday = isSameDay(date, today);
|
||||
|
||||
|
||||
if (isToday) {
|
||||
// 시간만 표시
|
||||
return `오늘 ${formatTime(date)}`;
|
||||
}
|
||||
|
||||
|
||||
// 어제 날짜인 경우
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const isYesterday = isSameDay(date, yesterday);
|
||||
|
||||
|
||||
if (isYesterday) {
|
||||
return `어제 ${formatTime(date)}`;
|
||||
}
|
||||
|
||||
|
||||
// 그 외 날짜
|
||||
return `${formatFullDate(date)} ${formatTime(date)}`;
|
||||
};
|
||||
@@ -61,21 +60,23 @@ const formatDateByRelativeTime = (date: Date): string => {
|
||||
* 두 날짜가 같은 날인지 확인
|
||||
*/
|
||||
const isSameDay = (date1: Date, date2: Date): boolean => {
|
||||
return date1.getDate() === date2.getDate() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getFullYear() === date2.getFullYear();
|
||||
return (
|
||||
date1.getDate() === date2.getDate() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getFullYear() === date2.getFullYear()
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 시간을 HH:MM 형식으로 포맷팅
|
||||
*/
|
||||
const formatTime = (date: Date): string => {
|
||||
return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
return `${date.getHours()}:${String(date.getMinutes()).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 전체 날짜를 YYYY/MM/DD 형식으로 포맷팅
|
||||
*/
|
||||
const formatFullDate = (date: Date): string => {
|
||||
return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`;
|
||||
return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import { trySyncAllData, setLastSyncTime } from '@/utils/syncUtils';
|
||||
import { handleSyncResult } from './syncResultHandler';
|
||||
import useNotifications from '@/hooks/useNotifications';
|
||||
import { useState } from "react";
|
||||
import { syncLogger } from "@/utils/logger";
|
||||
import { toast } from "@/hooks/useToast.wrapper";
|
||||
import { trySyncAllData, setLastSyncTime } from "@/utils/syncUtils";
|
||||
import { handleSyncResult } from "./syncResultHandler";
|
||||
import useNotifications from "@/hooks/useNotifications";
|
||||
import { Models } from "appwrite";
|
||||
|
||||
/**
|
||||
* 수동 동기화 기능을 위한 커스텀 훅
|
||||
*/
|
||||
export const useManualSync = (user: any) => {
|
||||
export const useManualSync = (user: Models.User<Models.Preferences> | null) => {
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const { addNotification } = useNotifications();
|
||||
|
||||
@@ -18,67 +19,66 @@ export const useManualSync = (user: any) => {
|
||||
toast({
|
||||
title: "로그인 필요",
|
||||
description: "데이터 동기화를 위해 로그인이 필요합니다.",
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
|
||||
addNotification(
|
||||
"로그인 필요",
|
||||
"로그인 필요",
|
||||
"데이터 동기화를 위해 로그인이 필요합니다."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 이미 동기화 중이면 중복 실행 방지
|
||||
if (syncing) {
|
||||
console.log('이미 동기화가 진행 중입니다.');
|
||||
syncLogger.info("이미 동기화가 진행 중입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await performSync(user.id);
|
||||
};
|
||||
|
||||
// 실제 동기화 수행 함수
|
||||
const performSync = async (userId: string) => {
|
||||
if (!userId) return;
|
||||
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSyncing(true);
|
||||
console.log('수동 동기화 시작');
|
||||
|
||||
addNotification(
|
||||
"동기화 시작",
|
||||
"데이터 동기화가 시작되었습니다."
|
||||
);
|
||||
|
||||
syncLogger.info("수동 동기화 시작");
|
||||
|
||||
addNotification("동기화 시작", "데이터 동기화가 시작되었습니다.");
|
||||
|
||||
// 동기화 실행
|
||||
const result = await trySyncAllData(userId);
|
||||
|
||||
|
||||
// 동기화 결과 처리
|
||||
handleSyncResult(result);
|
||||
|
||||
|
||||
// 동기화 시간 업데이트
|
||||
if (result.success) {
|
||||
const currentTime = new Date().toISOString();
|
||||
console.log('수동 동기화 성공, 시간 업데이트:', currentTime);
|
||||
syncLogger.info("수동 동기화 성공, 시간 업데이트:", currentTime);
|
||||
setLastSyncTime(currentTime);
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('동기화 오류:', error);
|
||||
syncLogger.error("동기화 오류:", error);
|
||||
toast({
|
||||
title: "동기화 오류",
|
||||
description: "동기화 중 문제가 발생했습니다. 다시 시도해주세요.",
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
|
||||
addNotification(
|
||||
"동기화 오류",
|
||||
"동기화 오류",
|
||||
"동기화 중 문제가 발생했습니다. 다시 시도해주세요."
|
||||
);
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
console.log('수동 동기화 종료');
|
||||
syncLogger.info("수동 동기화 종료");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getLastSyncTime } from '@/utils/syncUtils';
|
||||
import { useLastSyncTimeFormatting } from './syncTime/useSyncTimeFormatting';
|
||||
import { useSyncTimeEvents } from './syncTime/useSyncTimeEvents';
|
||||
import { useState, useEffect } from "react";
|
||||
import { syncLogger } from "@/utils/logger";
|
||||
import { getLastSyncTime } from "@/utils/syncUtils";
|
||||
import { useLastSyncTimeFormatting } from "./syncTime/useSyncTimeFormatting";
|
||||
import { useSyncTimeEvents } from "./syncTime/useSyncTimeEvents";
|
||||
|
||||
/**
|
||||
* 동기화 상태 관리를 위한 커스텀 훅
|
||||
@@ -10,20 +10,26 @@ import { useSyncTimeEvents } from './syncTime/useSyncTimeEvents';
|
||||
export const useSyncStatus = () => {
|
||||
const [lastSync, setLastSync] = useState<string | null>(getLastSyncTime());
|
||||
const { formatLastSyncTime } = useLastSyncTimeFormatting(lastSync);
|
||||
const { setupSyncTimeEventListeners } = useSyncTimeEvents(lastSync, setLastSync);
|
||||
|
||||
const { setupSyncTimeEventListeners } = useSyncTimeEvents(
|
||||
lastSync,
|
||||
setLastSync
|
||||
);
|
||||
|
||||
// 동기화 시간이 변경될 때 상태 업데이트 및 이벤트 리스너 설정
|
||||
useEffect(() => {
|
||||
console.log('useSyncStatus 훅 초기화, 현재 마지막 동기화 시간:', lastSync);
|
||||
|
||||
syncLogger.info(
|
||||
"useSyncStatus 훅 초기화, 현재 마지막 동기화 시간:",
|
||||
lastSync
|
||||
);
|
||||
|
||||
// 이벤트 리스너 및 주기적 확인 설정
|
||||
const cleanup = setupSyncTimeEventListeners();
|
||||
|
||||
|
||||
return cleanup;
|
||||
}, [lastSync, setupSyncTimeEventListeners]);
|
||||
|
||||
|
||||
return {
|
||||
lastSync,
|
||||
formatLastSyncTime
|
||||
formatLastSyncTime,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/contexts/auth';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
import {
|
||||
isSyncEnabled,
|
||||
import { useState, useEffect } from "react";
|
||||
import { syncLogger } from "@/utils/logger";
|
||||
import { useAuth } from "@/contexts/auth";
|
||||
import { toast } from "@/hooks/useToast.wrapper";
|
||||
import {
|
||||
isSyncEnabled,
|
||||
setSyncEnabled,
|
||||
setLastSyncTime
|
||||
} from '@/utils/syncUtils';
|
||||
import useNotifications from '@/hooks/useNotifications';
|
||||
import { resetSyncFailureCount } from './syncResultHandler';
|
||||
import { checkSyncNetworkStatus, showNetworkErrorNotification } from './syncNetworkChecker';
|
||||
import { performSync } from './syncPerformer';
|
||||
import { createLocalDataBackup, restoreLocalDataBackup } from './syncBackupUtils';
|
||||
setLastSyncTime,
|
||||
} from "@/utils/syncUtils";
|
||||
import useNotifications from "@/hooks/useNotifications";
|
||||
import { resetSyncFailureCount } from "./syncResultHandler";
|
||||
import {
|
||||
checkSyncNetworkStatus,
|
||||
showNetworkErrorNotification,
|
||||
} from "./syncNetworkChecker";
|
||||
import { performSync } from "./syncPerformer";
|
||||
import {
|
||||
createLocalDataBackup,
|
||||
restoreLocalDataBackup,
|
||||
} from "./syncBackupUtils";
|
||||
|
||||
/**
|
||||
* 동기화 토글 기능을 위한 커스텀 훅
|
||||
* 동기화 토글 기능을 위한 커스텀 훅
|
||||
*/
|
||||
export const useSyncToggle = () => {
|
||||
const [enabled, setEnabled] = useState(isSyncEnabled());
|
||||
@@ -30,35 +36,38 @@ export const useSyncToggle = () => {
|
||||
// 사용자가 로그아웃했고 동기화가 활성화되어 있으면 비활성화
|
||||
setSyncEnabled(false);
|
||||
setEnabled(false);
|
||||
console.log('로그아웃으로 인해 동기화 설정이 비활성화되었습니다.');
|
||||
syncLogger.info("로그아웃으로 인해 동기화 설정이 비활성화되었습니다.");
|
||||
}
|
||||
|
||||
|
||||
// 동기화 상태 업데이트
|
||||
setEnabled(isSyncEnabled());
|
||||
|
||||
|
||||
// 로그인/로그아웃 시 실패 카운터 초기화
|
||||
resetSyncFailureCount();
|
||||
};
|
||||
|
||||
// 초기 호출
|
||||
updateSyncState();
|
||||
|
||||
|
||||
// 인증 상태 변경 이벤트 리스너
|
||||
window.addEventListener('auth-state-changed', updateSyncState);
|
||||
|
||||
window.addEventListener("auth-state-changed", updateSyncState);
|
||||
|
||||
// 스토리지 변경 이벤트에도 동기화 상태 확인 추가
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.key === 'syncEnabled' || event.key === null) {
|
||||
if (event.key === "syncEnabled" || event.key === null) {
|
||||
setEnabled(isSyncEnabled());
|
||||
console.log('스토리지 변경으로 동기화 상태 업데이트:', isSyncEnabled() ? '활성화' : '비활성화');
|
||||
syncLogger.info(
|
||||
"스토리지 변경으로 동기화 상태 업데이트:",
|
||||
isSyncEnabled() ? "활성화" : "비활성화"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('auth-state-changed', updateSyncState);
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
window.removeEventListener("auth-state-changed", updateSyncState);
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
@@ -68,16 +77,16 @@ export const useSyncToggle = () => {
|
||||
toast({
|
||||
title: "로그인 필요",
|
||||
description: "데이터 동기화를 위해 로그인이 필요합니다.",
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
|
||||
addNotification(
|
||||
"로그인 필요",
|
||||
"로그인 필요",
|
||||
"데이터 동기화를 위해 로그인이 필요합니다."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 네트워크 상태 확인
|
||||
if (checked) {
|
||||
@@ -87,56 +96,57 @@ export const useSyncToggle = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 현재 로컬 데이터 백업
|
||||
const dataBackup = createLocalDataBackup();
|
||||
|
||||
|
||||
// 동기화 설정 변경
|
||||
setEnabled(checked);
|
||||
setSyncEnabled(checked);
|
||||
|
||||
|
||||
// 실패 카운터 초기화
|
||||
resetSyncFailureCount();
|
||||
|
||||
|
||||
// 동기화 활성화/비활성화 알림 추가
|
||||
addNotification(
|
||||
checked ? "동기화 활성화" : "동기화 비활성화",
|
||||
checked
|
||||
? "데이터가 클라우드와 동기화됩니다."
|
||||
checked ? "동기화 활성화" : "동기화 비활성화",
|
||||
checked
|
||||
? "데이터가 클라우드와 동기화됩니다."
|
||||
: "클라우드 동기화가 중지되었습니다."
|
||||
);
|
||||
|
||||
|
||||
// 이벤트 트리거
|
||||
window.dispatchEvent(new Event('auth-state-changed'));
|
||||
|
||||
window.dispatchEvent(new Event("auth-state-changed"));
|
||||
|
||||
if (checked && user) {
|
||||
try {
|
||||
// 동기화 수행
|
||||
await performSync(user.id);
|
||||
} catch (error) {
|
||||
console.error('동기화 중 오류, 로컬 데이터 복원 시도:', error);
|
||||
|
||||
syncLogger.error("동기화 중 오류, 로컬 데이터 복원 시도:", error);
|
||||
|
||||
// 오류 발생 시 백업 데이터 복원
|
||||
restoreLocalDataBackup(dataBackup);
|
||||
|
||||
|
||||
toast({
|
||||
title: "동기화 오류",
|
||||
description: "동기화 중 문제가 발생하여 로컬 데이터가 복원되었습니다.",
|
||||
variant: "destructive"
|
||||
description:
|
||||
"동기화 중 문제가 발생하여 로컬 데이터가 복원되었습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
|
||||
addNotification(
|
||||
"동기화 오류",
|
||||
"동기화 오류",
|
||||
"동기화 중 문제가 발생하여 로컬 데이터가 복원되었습니다."
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('동기화 설정 변경 중 예상치 못한 오류:', error);
|
||||
syncLogger.error("동기화 설정 변경 중 예상치 못한 오류:", error);
|
||||
toast({
|
||||
title: "동기화 설정 오류",
|
||||
description: "설정 변경 중 문제가 발생했습니다. 다시 시도해 주세요.",
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
|
||||
export const TOAST_LIMIT = 5 // 최대 5개로 제한
|
||||
export const TOAST_REMOVE_DELAY = 3000 // 3초 후 DOM에서 제거
|
||||
export const TOAST_LIMIT = 5; // 최대 5개로 제한
|
||||
export const TOAST_REMOVE_DELAY = 3000; // 3초 후 DOM에서 제거
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { Toast, ToasterToast, State } from "./types";
|
||||
import { actionTypes } from "./types";
|
||||
import { listeners, memoryState } from "./store";
|
||||
import { genId, dispatch } from "./toastManager";
|
||||
|
||||
import * as React from "react"
|
||||
import { Toast, ToasterToast, State } from "./types"
|
||||
import { actionTypes } from "./types"
|
||||
import { listeners, memoryState } from "./store"
|
||||
import { genId, dispatch } from "./toastManager"
|
||||
|
||||
export { TOAST_LIMIT, TOAST_REMOVE_DELAY } from "./constants"
|
||||
export type { ToasterToast } from "./types"
|
||||
export { TOAST_LIMIT, TOAST_REMOVE_DELAY } from "./constants";
|
||||
export type { ToasterToast } from "./types";
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: actionTypes.UPDATE_TOAST,
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id })
|
||||
});
|
||||
const dismiss = () =>
|
||||
dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: actionTypes.ADD_TOAST,
|
||||
@@ -25,37 +25,40 @@ function toast({ ...props }: Toast) {
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
if (!open) {
|
||||
dismiss();
|
||||
}
|
||||
},
|
||||
duration: props.duration || 3000, // 기본 지속 시간 3초로 설정
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),
|
||||
}
|
||||
dismiss: (toastId?: string) =>
|
||||
dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
export { useToast, toast };
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
import { TOAST_REMOVE_DELAY, TOAST_LIMIT } from './constants'
|
||||
import { Action, State, actionTypes } from './types'
|
||||
import { dispatch } from './toastManager'
|
||||
import { TOAST_REMOVE_DELAY, TOAST_LIMIT } from "./constants";
|
||||
import { Action, State, actionTypes } from "./types";
|
||||
import { dispatch } from "./toastManager";
|
||||
|
||||
// 토스트 타임아웃 맵
|
||||
export const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
export const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
// 토스트 자동 제거 함수
|
||||
export const addToRemoveQueue = (toastId: string) => {
|
||||
@@ -15,15 +14,15 @@ export const addToRemoveQueue = (toastId: string) => {
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: actionTypes.REMOVE_TOAST,
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
@@ -35,7 +34,7 @@ export const reducer = (state: State, action: Action): State => {
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
};
|
||||
|
||||
case actionTypes.UPDATE_TOAST:
|
||||
return {
|
||||
@@ -43,17 +42,17 @@ export const reducer = (state: State, action: Action): State => {
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
case actionTypes.DISMISS_TOAST: {
|
||||
const { toastId } = action
|
||||
const { toastId } = action;
|
||||
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -66,20 +65,20 @@ export const reducer = (state: State, action: Action): State => {
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
};
|
||||
}
|
||||
case actionTypes.REMOVE_TOAST:
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
};
|
||||
default:
|
||||
return state
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
import { State } from './types'
|
||||
import { State } from "./types";
|
||||
|
||||
// 전역 상태 및 리스너
|
||||
export const listeners: Array<(state: State) => void> = []
|
||||
export const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
// memoryState와 lastAction은 toastManager.ts에서 관리
|
||||
export { memoryState } from './toastManager';
|
||||
export { memoryState } from "./toastManager";
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
|
||||
import { Action, actionTypes } from './types'
|
||||
import { TOAST_LIMIT } from './constants'
|
||||
import { reducer } from './reducer'
|
||||
import { listeners } from './store'
|
||||
import { Action, actionTypes } from "./types";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { TOAST_LIMIT } from "./constants";
|
||||
import { reducer } from "./reducer";
|
||||
import { listeners } from "./store";
|
||||
|
||||
// 전역 상태 관리
|
||||
let memoryState = { toasts: [] };
|
||||
let lastAction = null;
|
||||
|
||||
// ID 생성기
|
||||
let count = 0
|
||||
let count = 0;
|
||||
|
||||
export function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
export function dispatch(action: Action) {
|
||||
// 마지막 액션 정보 추출
|
||||
let actionId: string | undefined = undefined;
|
||||
if ('toast' in action && action.toast) {
|
||||
if ("toast" in action && action.toast) {
|
||||
actionId = action.toast.id;
|
||||
} else if ('toastId' in action) {
|
||||
} else if ("toastId" in action) {
|
||||
actionId = action.toastId;
|
||||
}
|
||||
|
||||
|
||||
// 동일한 토스트에 대한 중복 액션 방지
|
||||
const now = Date.now();
|
||||
const isSameAction = lastAction &&
|
||||
lastAction.type === action.type &&
|
||||
((action.type === actionTypes.ADD_TOAST &&
|
||||
lastAction.time > now - 1000) || // ADD 액션은 1초 내 중복 방지
|
||||
(action.type !== actionTypes.ADD_TOAST &&
|
||||
actionId === lastAction.id &&
|
||||
lastAction.time > now - 300)); // 다른 액션은 300ms 내 중복 방지
|
||||
|
||||
const isSameAction =
|
||||
lastAction &&
|
||||
lastAction.type === action.type &&
|
||||
((action.type === actionTypes.ADD_TOAST && lastAction.time > now - 1000) || // ADD 액션은 1초 내 중복 방지
|
||||
(action.type !== actionTypes.ADD_TOAST &&
|
||||
actionId === lastAction.id &&
|
||||
lastAction.time > now - 300)); // 다른 액션은 300ms 내 중복 방지
|
||||
|
||||
if (isSameAction) {
|
||||
console.log('중복 토스트 액션 무시:', action.type);
|
||||
logger.info("중복 토스트 액션 무시:", action.type);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 액션 추적 업데이트
|
||||
lastAction = {
|
||||
type: action.type,
|
||||
id: actionId,
|
||||
time: now
|
||||
lastAction = {
|
||||
type: action.type,
|
||||
id: actionId,
|
||||
time: now,
|
||||
};
|
||||
|
||||
|
||||
// REMOVE_TOAST 액션 우선순위 높임
|
||||
if (action.type === actionTypes.REMOVE_TOAST) {
|
||||
// 즉시 처리
|
||||
@@ -56,7 +56,7 @@ export function dispatch(action: Action) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 실제 상태 업데이트 및 리스너 호출
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
export type ToasterToast = {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: React.ReactNode
|
||||
variant?: "default" | "destructive"
|
||||
duration?: number
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
variant?: "default" | "destructive";
|
||||
duration?: number;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
export type ActionType = typeof actionTypes
|
||||
export type ActionType = typeof actionTypes;
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast> & { id: string }
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast> & { id: string };
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: string
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: string;
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: string
|
||||
}
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: string;
|
||||
};
|
||||
|
||||
export interface State {
|
||||
toasts: ToasterToast[]
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
export type Toast = Omit<ToasterToast, "id">
|
||||
export type Toast = Omit<ToasterToast, "id">;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
import * as React from "react";
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean>(
|
||||
typeof window !== 'undefined' ? window.innerWidth < MOBILE_BREAKPOINT : false
|
||||
)
|
||||
typeof window !== "undefined"
|
||||
? window.innerWidth < MOBILE_BREAKPOINT
|
||||
: false
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
|
||||
// 모바일 화면인 경우 body에 클래스 추가
|
||||
if (window.innerWidth < MOBILE_BREAKPOINT) {
|
||||
document.body.classList.add('is-mobile');
|
||||
document.body.classList.add("is-mobile");
|
||||
} else {
|
||||
document.body.classList.remove('is-mobile');
|
||||
document.body.classList.remove("is-mobile");
|
||||
}
|
||||
}
|
||||
|
||||
// 초기 확인
|
||||
checkMobile()
|
||||
|
||||
// 리사이즈 이벤트 리스너 추가
|
||||
window.addEventListener('resize', checkMobile)
|
||||
|
||||
// 클린업 함수
|
||||
return () => window.removeEventListener('resize', checkMobile)
|
||||
}, [])
|
||||
};
|
||||
|
||||
return isMobile
|
||||
// 초기 확인
|
||||
checkMobile();
|
||||
|
||||
// 리사이즈 이벤트 리스너 추가
|
||||
window.addEventListener("resize", checkMobile);
|
||||
|
||||
// 클린업 함수
|
||||
return () => window.removeEventListener("resize", checkMobile);
|
||||
}, []);
|
||||
|
||||
return isMobile;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
// 이 파일은 기존 import 경로 호환성을 위한 리디렉션입니다
|
||||
export { useToast, toast, TOAST_LIMIT, TOAST_REMOVE_DELAY } from "./toast";
|
||||
export type { ToasterToast } from "./toast/types";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { logger } from "@/utils/logger";
|
||||
/**
|
||||
* 앱이 포커스를 얻었을 때나 가시성이 변경될 때 데이터를 새로고침하는 커스텀 훅
|
||||
*/
|
||||
@@ -8,68 +8,70 @@ export const useAppFocusEvents = () => {
|
||||
useEffect(() => {
|
||||
const handleFocus = () => {
|
||||
try {
|
||||
console.log('창이 포커스를 얻음 - 데이터 새로고침');
|
||||
logger.info("창이 포커스를 얻음 - 데이터 새로고침");
|
||||
// 이미 리프레시 중인지 확인하는 플래그
|
||||
if (sessionStorage.getItem('isRefreshing') === 'true') {
|
||||
console.log('이미 리프레시 진행 중, 중복 실행 방지');
|
||||
if (sessionStorage.getItem("isRefreshing") === "true") {
|
||||
logger.info("이미 리프레시 진행 중, 중복 실행 방지");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
sessionStorage.setItem('isRefreshing', 'true');
|
||||
|
||||
sessionStorage.setItem("isRefreshing", "true");
|
||||
|
||||
// 이벤트 발생시켜 데이터 새로고침
|
||||
window.dispatchEvent(new Event('storage'));
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||
|
||||
window.dispatchEvent(new Event("storage"));
|
||||
window.dispatchEvent(new Event("transactionUpdated"));
|
||||
window.dispatchEvent(new Event("budgetDataUpdated"));
|
||||
window.dispatchEvent(new Event("categoryBudgetsUpdated"));
|
||||
|
||||
// 리프레시 완료 표시 (300ms 후에 플래그 해제)
|
||||
setTimeout(() => {
|
||||
sessionStorage.setItem('isRefreshing', 'false');
|
||||
sessionStorage.setItem("isRefreshing", "false");
|
||||
}, 300);
|
||||
} catch (e) {
|
||||
console.error('이벤트 발생 오류:', e);
|
||||
sessionStorage.setItem('isRefreshing', 'false');
|
||||
logger.error("이벤트 발생 오류:", e);
|
||||
sessionStorage.setItem("isRefreshing", "false");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('포커스 이벤트 처리 중 오류:', error);
|
||||
logger.error("포커스 이벤트 처리 중 오류:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 포커스 이벤트
|
||||
window.addEventListener('focus', handleFocus);
|
||||
|
||||
window.addEventListener("focus", handleFocus);
|
||||
|
||||
// 가시성 변경 이벤트 (백그라운드에서 전경으로 돌아올 때)
|
||||
const handleVisibilityChange = () => {
|
||||
try {
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.log('페이지가 다시 보임 - 데이터 새로고침');
|
||||
if (document.visibilityState === "visible") {
|
||||
logger.info("페이지가 다시 보임 - 데이터 새로고침");
|
||||
handleFocus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('가시성 이벤트 처리 중 오류:', error);
|
||||
logger.error("가시성 이벤트 처리 중 오류:", error);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
// 정기적인 데이터 새로고침 (60초마다로 변경 - 너무 빈번한 리프레시 방지)
|
||||
const refreshInterval = setInterval(() => {
|
||||
try {
|
||||
if (document.visibilityState === 'visible' &&
|
||||
sessionStorage.getItem('isRefreshing') !== 'true') {
|
||||
console.log('정기 새로고침 - 데이터 업데이트');
|
||||
if (
|
||||
document.visibilityState === "visible" &&
|
||||
sessionStorage.getItem("isRefreshing") !== "true"
|
||||
) {
|
||||
logger.info("정기 새로고침 - 데이터 업데이트");
|
||||
handleFocus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('정기 새로고침 처리 중 오류:', error);
|
||||
logger.error("정기 새로고침 처리 중 오류:", error);
|
||||
}
|
||||
}, 60000); // 60초마다
|
||||
|
||||
}, 60000); // 60초마다
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
clearInterval(refreshInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -1,138 +1,158 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { resetAllData } from '@/contexts/budget/storage';
|
||||
import { resetAllStorageData } from '@/utils/storageUtils';
|
||||
import { clearCloudData } from '@/utils/sync/clearCloudData';
|
||||
import { useAuth } from '@/contexts/auth';
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { resetAllData } from "@/contexts/budget/storage";
|
||||
import { resetAllStorageData } from "@/utils/storageUtils";
|
||||
import { clearCloudData } from "@/utils/sync/clearCloudData";
|
||||
import { useAuth } from "@/contexts/auth";
|
||||
|
||||
export const useDataInitialization = (resetBudgetData?: () => void) => {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const { user } = useAuth();
|
||||
|
||||
|
||||
// 모든 데이터 초기화 함수
|
||||
const initializeAllData = useCallback(async () => {
|
||||
try {
|
||||
// 중요: 이미 방문한 적이 있으면 절대 초기화하지 않음
|
||||
const hasVisitedBefore = localStorage.getItem('hasVisitedBefore') === 'true';
|
||||
const hasVisitedBefore =
|
||||
localStorage.getItem("hasVisitedBefore") === "true";
|
||||
if (hasVisitedBefore) {
|
||||
console.log('이미 앱을 방문한 적이 있으므로 데이터를 초기화하지 않습니다.');
|
||||
logger.info(
|
||||
"이미 앱을 방문한 적이 있으므로 데이터를 초기화하지 않습니다."
|
||||
);
|
||||
setIsInitialized(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('첫 방문: 모든 데이터 초기화 시작');
|
||||
|
||||
|
||||
logger.info("첫 방문: 모든 데이터 초기화 시작");
|
||||
|
||||
// 현재 dontShowWelcome 값 백업
|
||||
const dontShowWelcomeValue = localStorage.getItem('dontShowWelcome');
|
||||
console.log('useDataInitialization - 초기화 전 dontShowWelcome 값:', dontShowWelcomeValue);
|
||||
|
||||
const dontShowWelcomeValue = localStorage.getItem("dontShowWelcome");
|
||||
logger.info(
|
||||
"useDataInitialization - 초기화 전 dontShowWelcome 값:",
|
||||
dontShowWelcomeValue
|
||||
);
|
||||
|
||||
try {
|
||||
// 로그인 상태라면 클라우드 데이터도 초기화 (첫 방문 시)
|
||||
if (user) {
|
||||
console.log('로그인 상태: 클라우드 데이터도 초기화 시도');
|
||||
logger.info("로그인 상태: 클라우드 데이터도 초기화 시도");
|
||||
await clearCloudData(user.id);
|
||||
}
|
||||
|
||||
|
||||
// 모든 데이터 완전히 삭제 및 초기화 (한 번만 실행)
|
||||
resetAllData();
|
||||
resetAllStorageData();
|
||||
|
||||
|
||||
// 컨텍스트 데이터 리셋 (필요한 경우)
|
||||
if (resetBudgetData) {
|
||||
resetBudgetData();
|
||||
}
|
||||
|
||||
|
||||
// 초기화 후 dontShowWelcome 값 확인
|
||||
const afterResetValue = localStorage.getItem('dontShowWelcome');
|
||||
console.log('useDataInitialization - 초기화 후 dontShowWelcome 값:', afterResetValue);
|
||||
|
||||
const afterResetValue = localStorage.getItem("dontShowWelcome");
|
||||
logger.info(
|
||||
"useDataInitialization - 초기화 후 dontShowWelcome 값:",
|
||||
afterResetValue
|
||||
);
|
||||
|
||||
// 값이 유지되지 않았다면 복원
|
||||
if (dontShowWelcomeValue && afterResetValue !== dontShowWelcomeValue) {
|
||||
console.log('useDataInitialization - dontShowWelcome 값 복원:', dontShowWelcomeValue);
|
||||
localStorage.setItem('dontShowWelcome', dontShowWelcomeValue);
|
||||
logger.info(
|
||||
"useDataInitialization - dontShowWelcome 값 복원:",
|
||||
dontShowWelcomeValue
|
||||
);
|
||||
localStorage.setItem("dontShowWelcome", dontShowWelcomeValue);
|
||||
}
|
||||
|
||||
console.log('모든 데이터 초기화 완료');
|
||||
|
||||
logger.info("모든 데이터 초기화 완료");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('데이터 초기화 중 오류 발생:', error);
|
||||
logger.error("데이터 초기화 중 오류 발생:", error);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('initializeAllData 함수 실행 중 오류:', error);
|
||||
logger.error("initializeAllData 함수 실행 중 오류:", error);
|
||||
setIsInitialized(true); // 오류가 발생해도 앱을 사용할 수 있도록 초기화 완료로 설정
|
||||
return false;
|
||||
}
|
||||
}, [resetBudgetData, user]);
|
||||
|
||||
|
||||
// 분석 페이지 데이터 초기화 함수
|
||||
const clearAllAnalyticsData = useCallback(() => {
|
||||
try {
|
||||
// 분석 관련 데이터만 선택적으로 삭제 (전체 데이터는 건드리지 않음)
|
||||
const analyticsKeys = [
|
||||
'analytics', 'monthlyTotals', 'chartData',
|
||||
'expenseHistory', 'budgetHistory', 'categorySpending',
|
||||
'monthlyData', 'expenseData', 'analyticData'
|
||||
"analytics",
|
||||
"monthlyTotals",
|
||||
"chartData",
|
||||
"expenseHistory",
|
||||
"budgetHistory",
|
||||
"categorySpending",
|
||||
"monthlyData",
|
||||
"expenseData",
|
||||
"analyticData",
|
||||
];
|
||||
|
||||
analyticsKeys.forEach(key => {
|
||||
|
||||
analyticsKeys.forEach((key) => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
|
||||
|
||||
// 월간, 차트, 분석 관련 키워드가 포함된 항목만 삭제
|
||||
for (let i = localStorage.length - 1; i >= 0; i--) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && (
|
||||
key.includes('month') ||
|
||||
key.includes('chart') ||
|
||||
key.includes('analytics') ||
|
||||
key.includes('expense') ||
|
||||
key.includes('budget') ||
|
||||
key.includes('total')
|
||||
) &&
|
||||
// 핵심 데이터는 건드리지 않도록 제외
|
||||
!key.includes('budgetData') &&
|
||||
!key.includes('transactions') &&
|
||||
!key.includes('categoryBudgets')) {
|
||||
console.log(`분석 데이터 삭제: ${key}`);
|
||||
if (
|
||||
key &&
|
||||
(key.includes("month") ||
|
||||
key.includes("chart") ||
|
||||
key.includes("analytics") ||
|
||||
key.includes("expense") ||
|
||||
key.includes("budget") ||
|
||||
key.includes("total")) &&
|
||||
// 핵심 데이터는 건드리지 않도록 제외
|
||||
!key.includes("budgetData") &&
|
||||
!key.includes("transactions") &&
|
||||
!key.includes("categoryBudgets")
|
||||
) {
|
||||
logger.info(`분석 데이터 삭제: ${key}`);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('분석 데이터 초기화 중 오류:', error);
|
||||
logger.error("분석 데이터 초기화 중 오류:", error);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
// 데이터 초기화 실행 - 첫 방문시에만
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (!isInitialized) {
|
||||
// 이미 방문한 적이 있는지 체크 (이미 있다면 초기화하지 않음)
|
||||
const hasVisitedBefore = localStorage.getItem('hasVisitedBefore') === 'true';
|
||||
const hasVisitedBefore =
|
||||
localStorage.getItem("hasVisitedBefore") === "true";
|
||||
if (hasVisitedBefore) {
|
||||
console.log('이미 방문 기록이 있어 초기화를 건너뜁니다.');
|
||||
logger.info("이미 방문 기록이 있어 초기화를 건너뜁니다.");
|
||||
setIsInitialized(true);
|
||||
} else {
|
||||
initializeAllData().then(result => {
|
||||
initializeAllData().then((result) => {
|
||||
setIsInitialized(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 첫 방문 여부 체크용 키 설정 (항상 true로 설정)
|
||||
localStorage.setItem('hasVisitedBefore', 'true');
|
||||
localStorage.setItem("hasVisitedBefore", "true");
|
||||
} catch (error) {
|
||||
console.error('데이터 초기화 useEffect 내 오류:', error);
|
||||
logger.error("데이터 초기화 useEffect 내 오류:", error);
|
||||
setIsInitialized(true); // 오류 발생해도 앱을 사용할 수 있도록 초기화 완료로 설정
|
||||
}
|
||||
}, [isInitialized, initializeAllData]);
|
||||
|
||||
|
||||
return {
|
||||
isInitialized,
|
||||
initializeAllData,
|
||||
clearAllAnalyticsData
|
||||
clearAllAnalyticsData,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useToast } from '@/hooks/useToast.wrapper';
|
||||
import { resetAllStorageData } from '@/utils/storageUtils';
|
||||
import { clearCloudData } from '@/utils/sync/clearCloudData';
|
||||
import { useAuth } from '@/contexts/auth';
|
||||
import { isSyncEnabled, setSyncEnabled } from '@/utils/sync/syncSettings';
|
||||
import { useState } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useToast } from "@/hooks/useToast.wrapper";
|
||||
import { resetAllStorageData } from "@/utils/storageUtils";
|
||||
import { clearCloudData } from "@/utils/sync/clearCloudData";
|
||||
import { useAuth } from "@/contexts/auth";
|
||||
import { isSyncEnabled, setSyncEnabled } from "@/utils/sync/syncSettings";
|
||||
|
||||
export interface DataResetResult {
|
||||
isCloudResetSuccess: boolean | null;
|
||||
@@ -13,7 +13,9 @@ export interface DataResetResult {
|
||||
|
||||
export const useDataReset = () => {
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
const [isCloudResetSuccess, setIsCloudResetSuccess] = useState<boolean | null>(null);
|
||||
const [isCloudResetSuccess, setIsCloudResetSuccess] = useState<
|
||||
boolean | null
|
||||
>(null);
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
@@ -21,122 +23,131 @@ export const useDataReset = () => {
|
||||
const resetAllData = async (): Promise<DataResetResult> => {
|
||||
try {
|
||||
setIsResetting(true);
|
||||
console.log('모든 데이터 초기화 시작');
|
||||
|
||||
logger.info("모든 데이터 초기화 시작");
|
||||
|
||||
// 현재 동기화 설정 저장
|
||||
const syncWasEnabled = isSyncEnabled();
|
||||
console.log('데이터 초기화 전 동기화 상태:', syncWasEnabled ? '활성화' : '비활성화');
|
||||
|
||||
logger.info(
|
||||
"데이터 초기화 전 동기화 상태:",
|
||||
syncWasEnabled ? "활성화" : "비활성화"
|
||||
);
|
||||
|
||||
// 중요: 클라우드 데이터 초기화 먼저 시도 (로그인 상태인 경우)
|
||||
let cloudResetSuccess = false;
|
||||
if (user) {
|
||||
console.log('로그인 상태: 클라우드 데이터 초기화 시도');
|
||||
|
||||
logger.info("로그인 상태: 클라우드 데이터 초기화 시도");
|
||||
|
||||
// 여러 번 시도하여 클라우드 데이터 초기화 확실히 하기
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
console.log(`클라우드 데이터 초기화 시도 ${attempt}/3`);
|
||||
logger.info(`클라우드 데이터 초기화 시도 ${attempt}/3`);
|
||||
cloudResetSuccess = await clearCloudData(user.id);
|
||||
|
||||
|
||||
if (cloudResetSuccess) {
|
||||
console.log('클라우드 데이터 초기화 성공');
|
||||
logger.info("클라우드 데이터 초기화 성공");
|
||||
break;
|
||||
} else {
|
||||
console.warn(`클라우드 데이터 초기화 시도 ${attempt} 실패`);
|
||||
logger.warn(`클라우드 데이터 초기화 시도 ${attempt} 실패`);
|
||||
// 잠시 대기 후 재시도
|
||||
if (attempt < 3) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setIsCloudResetSuccess(cloudResetSuccess);
|
||||
} else {
|
||||
console.log('로그인하지 않음: 클라우드 초기화 건너뜀');
|
||||
logger.info("로그인하지 않음: 클라우드 초기화 건너뜀");
|
||||
setIsCloudResetSuccess(null);
|
||||
}
|
||||
|
||||
|
||||
// 초기화 실행 전에 사용자 설정 백업
|
||||
const dontShowWelcomeValue = localStorage.getItem('dontShowWelcome');
|
||||
const hasVisitedBefore = localStorage.getItem('hasVisitedBefore');
|
||||
|
||||
const dontShowWelcomeValue = localStorage.getItem("dontShowWelcome");
|
||||
const hasVisitedBefore = localStorage.getItem("hasVisitedBefore");
|
||||
|
||||
// 로그인 관련 설정 백업 (supabase 관련 모든 설정)
|
||||
const authBackupItems: Record<string, string | null> = {};
|
||||
|
||||
|
||||
// 로그인 관련 항목 수집
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && (
|
||||
key.includes('supabase') ||
|
||||
key.includes('auth') ||
|
||||
key.includes('sb-') ||
|
||||
key.includes('token') ||
|
||||
key.includes('user') ||
|
||||
key.includes('session')
|
||||
)) {
|
||||
if (
|
||||
key &&
|
||||
(key.includes("supabase") ||
|
||||
key.includes("auth") ||
|
||||
key.includes("sb-") ||
|
||||
key.includes("token") ||
|
||||
key.includes("user") ||
|
||||
key.includes("session"))
|
||||
) {
|
||||
authBackupItems[key] = localStorage.getItem(key);
|
||||
console.log(`백업 항목: ${key}`);
|
||||
logger.info(`백업 항목: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 데이터 초기화 (개선된 메소드 사용)
|
||||
resetAllStorageData();
|
||||
|
||||
|
||||
// 추가 초기화를 위해 빈 데이터 명시적 설정
|
||||
localStorage.setItem('transactions', JSON.stringify([]));
|
||||
localStorage.setItem('budgetData', JSON.stringify({
|
||||
daily: {targetAmount: 0, spentAmount: 0, remainingAmount: 0},
|
||||
weekly: {targetAmount: 0, spentAmount: 0, remainingAmount: 0},
|
||||
monthly: {targetAmount: 0, spentAmount: 0, remainingAmount: 0}
|
||||
}));
|
||||
localStorage.setItem('categoryBudgets', JSON.stringify({}));
|
||||
|
||||
localStorage.setItem("transactions", JSON.stringify([]));
|
||||
localStorage.setItem(
|
||||
"budgetData",
|
||||
JSON.stringify({
|
||||
daily: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 },
|
||||
weekly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 },
|
||||
monthly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 },
|
||||
})
|
||||
);
|
||||
localStorage.setItem("categoryBudgets", JSON.stringify({}));
|
||||
|
||||
// 사용자 설정 복원
|
||||
if (dontShowWelcomeValue) {
|
||||
localStorage.setItem('dontShowWelcome', dontShowWelcomeValue);
|
||||
localStorage.setItem("dontShowWelcome", dontShowWelcomeValue);
|
||||
}
|
||||
|
||||
|
||||
if (hasVisitedBefore) {
|
||||
localStorage.setItem('hasVisitedBefore', hasVisitedBefore);
|
||||
localStorage.setItem("hasVisitedBefore", hasVisitedBefore);
|
||||
}
|
||||
|
||||
|
||||
// 로그인 관련 설정 복원 (로그인 화면이 나타나지 않도록)
|
||||
Object.entries(authBackupItems).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
localStorage.setItem(key, value);
|
||||
console.log(`복원 항목: ${key}`);
|
||||
logger.info(`복원 항목: ${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 중요: 동기화 설정은 초기화 후 강제로 비활성화
|
||||
setSyncEnabled(false);
|
||||
console.log('동기화 설정이 비활성화되었습니다.');
|
||||
|
||||
logger.info("동기화 설정이 비활성화되었습니다.");
|
||||
|
||||
// 마지막 동기화 시간은 초기화
|
||||
localStorage.removeItem('lastSync');
|
||||
|
||||
localStorage.removeItem("lastSync");
|
||||
|
||||
// 삭제 플래그 초기화 (강제로 삭제 목록 초기화)
|
||||
localStorage.removeItem('deletedTransactions');
|
||||
localStorage.removeItem('modifiedBudgets');
|
||||
|
||||
localStorage.removeItem("deletedTransactions");
|
||||
localStorage.removeItem("modifiedBudgets");
|
||||
|
||||
// 스토리지 이벤트 트리거하여 다른 컴포넌트에 변경 알림
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||
window.dispatchEvent(new StorageEvent('storage'));
|
||||
window.dispatchEvent(new Event('auth-state-changed')); // 동기화 상태 변경 이벤트 추가
|
||||
|
||||
window.dispatchEvent(new Event("transactionUpdated"));
|
||||
window.dispatchEvent(new Event("budgetDataUpdated"));
|
||||
window.dispatchEvent(new Event("categoryBudgetsUpdated"));
|
||||
window.dispatchEvent(new StorageEvent("storage"));
|
||||
window.dispatchEvent(new Event("auth-state-changed")); // 동기화 상태 변경 이벤트 추가
|
||||
|
||||
// 클라우드 초기화 상태에 따라 다른 메시지 표시
|
||||
if (user) {
|
||||
if (cloudResetSuccess) {
|
||||
toast({
|
||||
title: "모든 데이터가 초기화되었습니다.",
|
||||
description: "로컬 및 클라우드의 모든 데이터가 초기화되었으며, 동기화 설정이 비활성화되었습니다.",
|
||||
description:
|
||||
"로컬 및 클라우드의 모든 데이터가 초기화되었으며, 동기화 설정이 비활성화되었습니다.",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "로컬 데이터만 초기화됨",
|
||||
description: "로컬 데이터는 초기화되었지만, 클라우드 데이터 초기화 중 문제가 발생했습니다. 동기화 설정이 비활성화되었습니다.",
|
||||
variant: "destructive"
|
||||
description:
|
||||
"로컬 데이터는 초기화되었지만, 클라우드 데이터 초기화 중 문제가 발생했습니다. 동기화 설정이 비활성화되었습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -145,17 +156,17 @@ export const useDataReset = () => {
|
||||
description: "모든 예산, 지출 내역, 설정이 초기화되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
console.log('모든 데이터 초기화 완료');
|
||||
|
||||
|
||||
logger.info("모든 데이터 초기화 완료");
|
||||
|
||||
// 페이지 리프레시를 위해 잠시 후에 새로고침
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
|
||||
|
||||
return { isCloudResetSuccess: cloudResetSuccess };
|
||||
} catch (error) {
|
||||
console.error('데이터 초기화 실패:', error);
|
||||
logger.error("데이터 초기화 실패:", error);
|
||||
toast({
|
||||
title: "데이터 초기화 실패",
|
||||
description: "데이터를 초기화하는 중 문제가 발생했습니다.",
|
||||
@@ -170,6 +181,6 @@ export const useDataReset = () => {
|
||||
return {
|
||||
isResetting,
|
||||
isCloudResetSuccess,
|
||||
resetAllData
|
||||
resetAllData,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,57 +1,62 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { logger } from "@/utils/logger";
|
||||
/**
|
||||
* 앱 첫 실행 시 로컬스토리지 데이터를 로드하는 커스텀 훅
|
||||
*/
|
||||
export const useInitialDataLoading = () => {
|
||||
useEffect(() => {
|
||||
try {
|
||||
console.log('Index 페이지 마운트, 데이터 확인 중...');
|
||||
|
||||
logger.info("Index 페이지 마운트, 데이터 확인 중...");
|
||||
|
||||
// 페이지 첫 마운트 시에만 실행되는 로직
|
||||
const isFirstMount = sessionStorage.getItem('initialDataLoaded') !== 'true';
|
||||
|
||||
const isFirstMount =
|
||||
sessionStorage.getItem("initialDataLoaded") !== "true";
|
||||
|
||||
if (isFirstMount) {
|
||||
try {
|
||||
// 백업된 데이터 복구 확인 (메인 데이터가 없는 경우만)
|
||||
if (!localStorage.getItem('budgetData')) {
|
||||
const budgetBackup = localStorage.getItem('budgetData_backup');
|
||||
if (!localStorage.getItem("budgetData")) {
|
||||
const budgetBackup = localStorage.getItem("budgetData_backup");
|
||||
if (budgetBackup) {
|
||||
console.log('예산 데이터 백업에서 복구');
|
||||
localStorage.setItem('budgetData', budgetBackup);
|
||||
logger.info("예산 데이터 백업에서 복구");
|
||||
localStorage.setItem("budgetData", budgetBackup);
|
||||
}
|
||||
}
|
||||
|
||||
if (!localStorage.getItem('categoryBudgets')) {
|
||||
const categoryBackup = localStorage.getItem('categoryBudgets_backup');
|
||||
|
||||
if (!localStorage.getItem("categoryBudgets")) {
|
||||
const categoryBackup = localStorage.getItem(
|
||||
"categoryBudgets_backup"
|
||||
);
|
||||
if (categoryBackup) {
|
||||
console.log('카테고리 예산 백업에서 복구');
|
||||
localStorage.setItem('categoryBudgets', categoryBackup);
|
||||
logger.info("카테고리 예산 백업에서 복구");
|
||||
localStorage.setItem("categoryBudgets", categoryBackup);
|
||||
}
|
||||
}
|
||||
|
||||
if (!localStorage.getItem('transactions')) {
|
||||
const transactionBackup = localStorage.getItem('transactions_backup');
|
||||
|
||||
if (!localStorage.getItem("transactions")) {
|
||||
const transactionBackup = localStorage.getItem(
|
||||
"transactions_backup"
|
||||
);
|
||||
if (transactionBackup) {
|
||||
console.log('트랜잭션 백업에서 복구');
|
||||
localStorage.setItem('transactions', transactionBackup);
|
||||
logger.info("트랜잭션 백업에서 복구");
|
||||
localStorage.setItem("transactions", transactionBackup);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 한 번만 이벤트 발생
|
||||
window.dispatchEvent(new Event('transactionUpdated'));
|
||||
window.dispatchEvent(new Event('budgetDataUpdated'));
|
||||
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
|
||||
|
||||
window.dispatchEvent(new Event("transactionUpdated"));
|
||||
window.dispatchEvent(new Event("budgetDataUpdated"));
|
||||
window.dispatchEvent(new Event("categoryBudgetsUpdated"));
|
||||
|
||||
// 초기 로드 완료 표시
|
||||
sessionStorage.setItem('initialDataLoaded', 'true');
|
||||
sessionStorage.setItem("initialDataLoaded", "true");
|
||||
} catch (error) {
|
||||
console.error('백업 복구 시도 중 오류:', error);
|
||||
logger.error("백업 복구 시도 중 오류:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Index 페이지 초기화 중 오류:', error);
|
||||
logger.error("Index 페이지 초기화 중 오류:", error);
|
||||
}
|
||||
}, []); // 컴포넌트 마운트 시 한 번만 실행
|
||||
}, []); // 컴포넌트 마운트 시 한 번만 실행
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useToast } from "@/hooks/useToast.wrapper";
|
||||
import { useAuth } from "@/contexts/auth";
|
||||
@@ -11,7 +11,7 @@ export function useLogin() {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState<string | null>(null);
|
||||
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const { signIn } = useAuth();
|
||||
@@ -20,75 +20,77 @@ export function useLogin() {
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoginError(null);
|
||||
|
||||
|
||||
if (!email || !password) {
|
||||
toast({
|
||||
title: "입력 오류",
|
||||
description: "이메일과 비밀번호를 모두 입력해주세요.",
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const { error, user } = await signIn(email, password);
|
||||
|
||||
|
||||
if (error) {
|
||||
console.error("로그인 실패:", error);
|
||||
|
||||
logger.error("로그인 실패:", error);
|
||||
|
||||
let errorMessage = "로그인에 실패했습니다.";
|
||||
|
||||
|
||||
if (error.message) {
|
||||
if (error.message.includes("Invalid login credentials")) {
|
||||
errorMessage = "이메일 또는 비밀번호가 올바르지 않습니다.";
|
||||
} else if (error.message.includes("Email not confirmed")) {
|
||||
errorMessage = "이메일 인증이 완료되지 않았습니다. 이메일을 확인해주세요.";
|
||||
errorMessage =
|
||||
"이메일 인증이 완료되지 않았습니다. 이메일을 확인해주세요.";
|
||||
} else {
|
||||
errorMessage = `오류: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setLoginError(errorMessage);
|
||||
|
||||
|
||||
toast({
|
||||
title: "로그인 실패",
|
||||
description: errorMessage,
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
} else if (user) {
|
||||
// 로그인 성공
|
||||
toast({
|
||||
title: "로그인 성공",
|
||||
description: "환영합니다! 대시보드로 이동합니다.",
|
||||
variant: "default"
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
|
||||
await setupTables();
|
||||
navigate("/");
|
||||
} else {
|
||||
// user가 없지만 error도 없는 경우 (드문 경우)
|
||||
console.warn("로그인 성공했지만 사용자 정보가 없습니다.");
|
||||
|
||||
logger.warn("로그인 성공했지만 사용자 정보가 없습니다.");
|
||||
|
||||
toast({
|
||||
title: "로그인 상태 확인 중",
|
||||
description: "로그인은 성공했지만 사용자 정보를 확인하지 못했습니다. 페이지를 새로고침해보세요.",
|
||||
variant: "default"
|
||||
description:
|
||||
"로그인은 성공했지만 사용자 정보를 확인하지 못했습니다. 페이지를 새로고침해보세요.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
|
||||
navigate("/");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("로그인 과정에서 예외 발생:", err);
|
||||
|
||||
logger.error("로그인 과정에서 예외 발생:", err);
|
||||
|
||||
const errorMessage = err.message || "알 수 없는 오류가 발생했습니다.";
|
||||
setLoginError(errorMessage);
|
||||
|
||||
|
||||
toast({
|
||||
title: "로그인 오류",
|
||||
description: errorMessage,
|
||||
variant: "destructive"
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -106,6 +108,6 @@ export function useLogin() {
|
||||
isSettingUpTables,
|
||||
loginError,
|
||||
setLoginError,
|
||||
handleLogin
|
||||
handleLogin,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Notification } from '@/components/notification/NotificationPopover';
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Notification } from "@/components/notification/NotificationPopover";
|
||||
|
||||
export const useNotifications = () => {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
@@ -9,18 +9,20 @@ export const useNotifications = () => {
|
||||
// 로컬 스토리지에서 알림 불러오기
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedNotifications = localStorage.getItem('notifications');
|
||||
const savedNotifications = localStorage.getItem("notifications");
|
||||
if (savedNotifications) {
|
||||
const parsedNotifications = JSON.parse(savedNotifications);
|
||||
// 시간 문자열을 Date 객체로 변환
|
||||
const formattedNotifications = parsedNotifications.map((notification: any) => ({
|
||||
...notification,
|
||||
timestamp: new Date(notification.timestamp)
|
||||
}));
|
||||
const formattedNotifications = parsedNotifications.map(
|
||||
(notification: any) => ({
|
||||
...notification,
|
||||
timestamp: new Date(notification.timestamp),
|
||||
})
|
||||
);
|
||||
setNotifications(formattedNotifications);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('알림 데이터 로드 중 오류 발생:', error);
|
||||
logger.error("알림 데이터 로드 중 오류 발생:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -31,25 +33,31 @@ export const useNotifications = () => {
|
||||
title,
|
||||
message,
|
||||
timestamp: new Date(),
|
||||
read: false
|
||||
read: false,
|
||||
};
|
||||
|
||||
setNotifications(prevNotifications => {
|
||||
setNotifications((prevNotifications) => {
|
||||
const updatedNotifications = [newNotification, ...prevNotifications];
|
||||
// 로컬 스토리지 업데이트
|
||||
localStorage.setItem('notifications', JSON.stringify(updatedNotifications));
|
||||
localStorage.setItem(
|
||||
"notifications",
|
||||
JSON.stringify(updatedNotifications)
|
||||
);
|
||||
return updatedNotifications;
|
||||
});
|
||||
};
|
||||
|
||||
// 알림 읽음 표시
|
||||
const markAsRead = (id: string) => {
|
||||
setNotifications(prevNotifications => {
|
||||
const updatedNotifications = prevNotifications.map(notification =>
|
||||
setNotifications((prevNotifications) => {
|
||||
const updatedNotifications = prevNotifications.map((notification) =>
|
||||
notification.id === id ? { ...notification, read: true } : notification
|
||||
);
|
||||
// 로컬 스토리지 업데이트
|
||||
localStorage.setItem('notifications', JSON.stringify(updatedNotifications));
|
||||
localStorage.setItem(
|
||||
"notifications",
|
||||
JSON.stringify(updatedNotifications)
|
||||
);
|
||||
return updatedNotifications;
|
||||
});
|
||||
};
|
||||
@@ -57,14 +65,14 @@ export const useNotifications = () => {
|
||||
// 모든 알림 삭제
|
||||
const clearAllNotifications = () => {
|
||||
setNotifications([]);
|
||||
localStorage.removeItem('notifications');
|
||||
localStorage.removeItem("notifications");
|
||||
};
|
||||
|
||||
return {
|
||||
notifications,
|
||||
addNotification,
|
||||
markAsRead,
|
||||
clearAllNotifications
|
||||
clearAllNotifications,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/contexts/auth';
|
||||
import { useSyncToggle, useManualSync, useSyncStatus } from './sync';
|
||||
import { useState, useEffect } from "react";
|
||||
import { syncLogger } from "@/utils/logger";
|
||||
import { useAuth } from "@/contexts/auth";
|
||||
import { useSyncToggle, useManualSync, useSyncStatus } from "./sync";
|
||||
|
||||
/**
|
||||
* 동기화 설정 관리를 위한 커스텀 훅
|
||||
@@ -11,14 +11,14 @@ export const useSyncSettings = () => {
|
||||
const { enabled, setEnabled, handleSyncToggle } = useSyncToggle();
|
||||
const { syncing, handleManualSync } = useManualSync(user);
|
||||
const { lastSync, formatLastSyncTime } = useSyncStatus();
|
||||
|
||||
|
||||
// 콘솔에 상태 기록
|
||||
useEffect(() => {
|
||||
console.log(`[동기화설정] 상태 변경:
|
||||
- 활성화: ${enabled ? '예' : '아니오'}
|
||||
- 진행중: ${syncing ? '예' : '아니오'}
|
||||
- 마지막동기화: ${lastSync || '없음'}
|
||||
- 사용자: ${user ? '로그인됨' : '로그인안됨'}`);
|
||||
syncLogger.info(`[동기화설정] 상태 변경:
|
||||
- 활성화: ${enabled ? "예" : "아니오"}
|
||||
- 진행중: ${syncing ? "예" : "아니오"}
|
||||
- 마지막동기화: ${lastSync || "없음"}
|
||||
- 사용자: ${user ? "로그인됨" : "로그인안됨"}`);
|
||||
}, [enabled, syncing, lastSync, user]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { useToast } from "@/hooks/useToast.wrapper";
|
||||
import { createRequiredTables } from "@/archive/lib/supabase/setup";
|
||||
|
||||
@@ -17,23 +17,24 @@ export function useTableSetup() {
|
||||
try {
|
||||
setIsSettingUpTables(true);
|
||||
const { success, message } = await createRequiredTables();
|
||||
|
||||
|
||||
if (success) {
|
||||
console.log("테이블 설정 성공:", message);
|
||||
logger.info("테이블 설정 성공:", message);
|
||||
return true;
|
||||
} else {
|
||||
console.warn("테이블 설정 문제:", message);
|
||||
logger.warn("테이블 설정 문제:", message);
|
||||
// 사용자에게 경고 표시 (선택적)
|
||||
toast({
|
||||
title: "테이블 설정 문제",
|
||||
description: "일부 테이블 설정에 문제가 있었지만, 기본 기능은 사용할 수 있습니다.",
|
||||
variant: "default"
|
||||
description:
|
||||
"일부 테이블 설정에 문제가 있었지만, 기본 기능은 사용할 수 있습니다.",
|
||||
variant: "default",
|
||||
});
|
||||
// 테이블 설정 실패해도 로그인은 진행
|
||||
return false;
|
||||
}
|
||||
} catch (setupError) {
|
||||
console.error("테이블 설정 중 오류:", setupError);
|
||||
logger.error("테이블 설정 중 오류:", setupError);
|
||||
return false;
|
||||
} finally {
|
||||
setIsSettingUpTables(false);
|
||||
@@ -42,6 +43,6 @@ export function useTableSetup() {
|
||||
|
||||
return {
|
||||
isSettingUpTables,
|
||||
setupTables
|
||||
setupTables,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
|
||||
import { useToast as useOriginalToast, toast as originalToast } from '@/hooks/toast';
|
||||
import type { ToasterToast } from '@/hooks/toast/types';
|
||||
import {
|
||||
useToast as useOriginalToast,
|
||||
toast as originalToast,
|
||||
} from "@/hooks/toast";
|
||||
import { logger } from "@/utils/logger";
|
||||
import type { ToasterToast } from "@/hooks/toast/types";
|
||||
|
||||
/**
|
||||
* 토스트 중복 방지를 위한 설정값
|
||||
*/
|
||||
const TOAST_CONFIG = {
|
||||
DEFAULT_DURATION: 3000, // 기본 토스트 표시 시간 (ms)
|
||||
DEBOUNCE_TIME: 1500, // 동일 메시지 무시 시간 (ms)
|
||||
HISTORY_LIMIT: 10, // 히스토리에 저장할 최대 토스트 수
|
||||
CLEANUP_INTERVAL: 30000, // 히스토리 정리 주기 (ms)
|
||||
HISTORY_RETENTION: 10000 // 히스토리 보관 기간 (ms)
|
||||
DEFAULT_DURATION: 3000, // 기본 토스트 표시 시간 (ms)
|
||||
DEBOUNCE_TIME: 1500, // 동일 메시지 무시 시간 (ms)
|
||||
HISTORY_LIMIT: 10, // 히스토리에 저장할 최대 토스트 수
|
||||
CLEANUP_INTERVAL: 30000, // 히스토리 정리 주기 (ms)
|
||||
HISTORY_RETENTION: 10000, // 히스토리 보관 기간 (ms)
|
||||
};
|
||||
|
||||
/**
|
||||
* 토스트 메시지 히스토리 인터페이스
|
||||
*/
|
||||
interface ToastHistoryItem {
|
||||
message: string; // 메시지 내용 (title + description)
|
||||
timestamp: number; // 생성 시간
|
||||
variant?: string; // 토스트 종류 (default/destructive)
|
||||
message: string; // 메시지 내용 (title + description)
|
||||
timestamp: number; // 생성 시간
|
||||
variant?: string; // 토스트 종류 (default/destructive)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +35,7 @@ class ToastHistoryManager {
|
||||
constructor() {
|
||||
// 주기적으로 오래된 히스토리 정리
|
||||
this.cleanupInterval = setInterval(
|
||||
() => this.cleanup(),
|
||||
() => this.cleanup(),
|
||||
TOAST_CONFIG.CLEANUP_INTERVAL
|
||||
);
|
||||
}
|
||||
@@ -44,7 +47,7 @@ class ToastHistoryManager {
|
||||
this.history.push({
|
||||
message,
|
||||
timestamp: Date.now(),
|
||||
variant
|
||||
variant,
|
||||
});
|
||||
|
||||
// 히스토리 크기 제한
|
||||
@@ -59,7 +62,7 @@ class ToastHistoryManager {
|
||||
cleanup(): void {
|
||||
const now = Date.now();
|
||||
this.history = this.history.filter(
|
||||
item => (now - item.timestamp) < TOAST_CONFIG.HISTORY_RETENTION
|
||||
(item) => now - item.timestamp < TOAST_CONFIG.HISTORY_RETENTION
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,11 +71,12 @@ class ToastHistoryManager {
|
||||
*/
|
||||
isDuplicate(message: string, variant?: string): boolean {
|
||||
const now = Date.now();
|
||||
|
||||
return this.history.some(item =>
|
||||
item.message === message &&
|
||||
item.variant === variant &&
|
||||
(now - item.timestamp) < TOAST_CONFIG.DEBOUNCE_TIME
|
||||
|
||||
return this.history.some(
|
||||
(item) =>
|
||||
item.message === message &&
|
||||
item.variant === variant &&
|
||||
now - item.timestamp < TOAST_CONFIG.DEBOUNCE_TIME
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,7 +86,7 @@ class ToastHistoryManager {
|
||||
clear(): void {
|
||||
this.history = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 정리 타이머 해제 (메모리 누수 방지)
|
||||
*/
|
||||
@@ -98,10 +102,9 @@ const toastHistory = new ToastHistoryManager();
|
||||
* 메시지 내용 추출 (title + description)
|
||||
*/
|
||||
const extractMessage = (params: Omit<ToasterToast, "id">): string => {
|
||||
return [
|
||||
params.title?.toString() || '',
|
||||
params.description?.toString() || ''
|
||||
].filter(Boolean).join(' - ');
|
||||
return [params.title?.toString() || "", params.description?.toString() || ""]
|
||||
.filter(Boolean)
|
||||
.join(" - ");
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -109,22 +112,22 @@ const extractMessage = (params: Omit<ToasterToast, "id">): string => {
|
||||
*/
|
||||
const debouncedToast = (params: Omit<ToasterToast, "id">) => {
|
||||
const message = extractMessage(params);
|
||||
|
||||
|
||||
// 빈 메시지 무시
|
||||
if (!message.trim()) {
|
||||
console.warn('빈 토스트 메시지가 무시되었습니다');
|
||||
logger.warn("빈 토스트 메시지가 무시되었습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 중복 검사
|
||||
if (toastHistory.isDuplicate(message, params.variant)) {
|
||||
console.log('중복 토스트 감지로 무시됨:', message);
|
||||
logger.info("중복 토스트 감지로 무시됨:", message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 히스토리에 추가
|
||||
toastHistory.add(message, params.variant);
|
||||
|
||||
|
||||
// 실제 토스트 표시
|
||||
originalToast({
|
||||
...params,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
|
||||
// 이 파일은 이제 단순히 새로운 구조의 파일들을 재내보내기만 합니다
|
||||
export {
|
||||
export {
|
||||
useTransactions,
|
||||
MONTHS_KR,
|
||||
getCurrentMonth,
|
||||
getPrevMonth,
|
||||
getPrevMonth,
|
||||
getNextMonth,
|
||||
filterTransactionsByMonth,
|
||||
filterTransactionsByQuery,
|
||||
calculateTotalExpenses
|
||||
} from './transactions';
|
||||
calculateTotalExpenses,
|
||||
} from "./transactions";
|
||||
|
||||
@@ -1,60 +1,69 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import { logger } from "@/utils/logger";
|
||||
export const useWelcomeDialog = () => {
|
||||
const [showWelcome, setShowWelcome] = useState(false);
|
||||
|
||||
|
||||
// 환영 다이얼로그 표시 여부 확인
|
||||
const checkWelcomeDialogState = useCallback(() => {
|
||||
// 현재 세션에서 이미 환영 메시지를 닫았는지 확인
|
||||
const sessionClosed = sessionStorage.getItem('welcomeClosedThisSession') === 'true';
|
||||
|
||||
const sessionClosed =
|
||||
sessionStorage.getItem("welcomeClosedThisSession") === "true";
|
||||
|
||||
if (sessionClosed) {
|
||||
console.log('useWelcomeDialog - 이번 세션에서 이미 환영 메시지를 닫았습니다');
|
||||
logger.info(
|
||||
"useWelcomeDialog - 이번 세션에서 이미 환영 메시지를 닫았습니다"
|
||||
);
|
||||
setShowWelcome(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const dontShowWelcome = localStorage.getItem('dontShowWelcome');
|
||||
console.log('useWelcomeDialog - dontShowWelcome 값:', dontShowWelcome);
|
||||
|
||||
|
||||
const dontShowWelcome = localStorage.getItem("dontShowWelcome");
|
||||
logger.info("useWelcomeDialog - dontShowWelcome 값:", dontShowWelcome);
|
||||
|
||||
// 명시적으로 'true' 문자열인 경우에만 숨김 처리
|
||||
if (dontShowWelcome === 'true') {
|
||||
console.log('useWelcomeDialog - 환영 메시지 표시하지 않음 (저장된 설정)');
|
||||
if (dontShowWelcome === "true") {
|
||||
logger.info("useWelcomeDialog - 환영 메시지 표시하지 않음 (저장된 설정)");
|
||||
setShowWelcome(false);
|
||||
} else {
|
||||
console.log('useWelcomeDialog - 환영 메시지 표시함');
|
||||
logger.info("useWelcomeDialog - 환영 메시지 표시함");
|
||||
setShowWelcome(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
// 환영 다이얼로그 닫기 핸들러
|
||||
const handleCloseWelcome = useCallback((dontShowAgain: boolean) => {
|
||||
setShowWelcome(false);
|
||||
|
||||
|
||||
// 이번 세션에서 닫았음을 기록
|
||||
sessionStorage.setItem('welcomeClosedThisSession', 'true');
|
||||
|
||||
sessionStorage.setItem("welcomeClosedThisSession", "true");
|
||||
|
||||
// 사용자가 더 이상 보지 않기를 선택한 경우
|
||||
if (dontShowAgain) {
|
||||
localStorage.setItem('dontShowWelcome', 'true');
|
||||
sessionStorage.setItem('dontShowWelcome', 'true');
|
||||
console.log('useWelcomeDialog - 환영 팝업 더 이상 표시하지 않기 설정됨:', dontShowAgain);
|
||||
|
||||
localStorage.setItem("dontShowWelcome", "true");
|
||||
sessionStorage.setItem("dontShowWelcome", "true");
|
||||
logger.info(
|
||||
"useWelcomeDialog - 환영 팝업 더 이상 표시하지 않기 설정됨:",
|
||||
dontShowAgain
|
||||
);
|
||||
|
||||
// 설정 확인
|
||||
const savedValue = localStorage.getItem('dontShowWelcome');
|
||||
console.log('useWelcomeDialog - 설정 후 dontShowWelcome 저장값:', savedValue);
|
||||
const savedValue = localStorage.getItem("dontShowWelcome");
|
||||
logger.info(
|
||||
"useWelcomeDialog - 설정 후 dontShowWelcome 저장값:",
|
||||
savedValue
|
||||
);
|
||||
} else {
|
||||
// 체크하지 않은 경우 명시적으로 false 저장
|
||||
localStorage.setItem('dontShowWelcome', 'false');
|
||||
sessionStorage.setItem('dontShowWelcome', 'false');
|
||||
localStorage.setItem("dontShowWelcome", "false");
|
||||
sessionStorage.setItem("dontShowWelcome", "false");
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
return {
|
||||
showWelcome,
|
||||
setShowWelcome,
|
||||
checkWelcomeDialogState,
|
||||
handleCloseWelcome
|
||||
handleCloseWelcome,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useAuth } from '@/contexts/auth';
|
||||
import useNotifications from '@/hooks/useNotifications';
|
||||
import { useEffect } from "react";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { useAuth } from "@/contexts/auth";
|
||||
import useNotifications from "@/hooks/useNotifications";
|
||||
|
||||
/**
|
||||
* 앱 초기화 후 환영 메시지 알림을 표시하는 커스텀 훅
|
||||
@@ -13,23 +13,25 @@ export const useWelcomeNotification = (isInitialized: boolean) => {
|
||||
useEffect(() => {
|
||||
try {
|
||||
// 환영 메시지가 이미 표시되었는지 확인하는 키
|
||||
const welcomeNotificationSent = sessionStorage.getItem('welcomeNotificationSent');
|
||||
|
||||
const welcomeNotificationSent = sessionStorage.getItem(
|
||||
"welcomeNotificationSent"
|
||||
);
|
||||
|
||||
if (isInitialized && user && !welcomeNotificationSent) {
|
||||
// 사용자 로그인 시 알림 예시 (한 번만 실행)
|
||||
const timeoutId = setTimeout(() => {
|
||||
addNotification(
|
||||
'환영합니다!',
|
||||
'젤리의 적자탈출에 오신 것을 환영합니다. 예산을 설정하고 지출을 기록해보세요.'
|
||||
"환영합니다!",
|
||||
"젤리의 적자탈출에 오신 것을 환영합니다. 예산을 설정하고 지출을 기록해보세요."
|
||||
);
|
||||
// 세션 스토리지에 환영 메시지 표시 여부 저장
|
||||
sessionStorage.setItem('welcomeNotificationSent', 'true');
|
||||
sessionStorage.setItem("welcomeNotificationSent", "true");
|
||||
}, 2000);
|
||||
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('환영 메시지 알림 표시 중 오류:', error);
|
||||
logger.error("환영 메시지 알림 표시 중 오류:", error);
|
||||
}
|
||||
}, [isInitialized, user, addNotification]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user