Review and refine sync logic

This commit reviews and refines the synchronization logic to ensure proper functionality and data integrity.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-21 11:24:40 +00:00
parent e55dfac3a9
commit 8e6eb9d8aa
15 changed files with 567 additions and 357 deletions

View File

@@ -1,10 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import { toast } from '@/hooks/useToast.wrapper'; import { toast } from '@/hooks/useToast.wrapper';
import { trySyncAllData } from '@/utils/syncUtils'; import { trySyncAllData, setLastSyncTime } from '@/utils/syncUtils';
import { getLastSyncTime, setLastSyncTime } from '@/utils/syncUtils';
import { handleSyncResult } from './syncResultHandler'; import { handleSyncResult } from './syncResultHandler';
import type { SyncResult } from '@/utils/syncUtils';
/** /**
* 수동 동기화 기능을 위한 커스텀 훅 * 수동 동기화 기능을 위한 커스텀 훅
@@ -23,6 +21,12 @@ export const useManualSync = (user: any) => {
return; return;
} }
// 이미 동기화 중이면 중복 실행 방지
if (syncing) {
console.log('이미 동기화가 진행 중입니다.');
return;
}
await performSync(user.id); await performSync(user.id);
}; };
@@ -32,11 +36,20 @@ export const useManualSync = (user: any) => {
try { try {
setSyncing(true); setSyncing(true);
// 인자 수정: userId만 전달 console.log('수동 동기화 시작');
// 동기화 실행
const result = await trySyncAllData(userId); const result = await trySyncAllData(userId);
// 동기화 결과 처리
handleSyncResult(result); handleSyncResult(result);
setLastSyncTime(new Date().toISOString());
// 동기화 시간 업데이트
if (result.success) {
setLastSyncTime(new Date().toISOString());
}
return result;
} catch (error) { } catch (error) {
console.error('동기화 오류:', error); console.error('동기화 오류:', error);
toast({ toast({
@@ -46,6 +59,7 @@ export const useManualSync = (user: any) => {
}); });
} finally { } finally {
setSyncing(false); setSyncing(false);
console.log('수동 동기화 종료');
} }
}; };

View File

@@ -3,29 +3,71 @@ import { useState, useEffect } from 'react';
import { getLastSyncTime } from '@/utils/syncUtils'; import { getLastSyncTime } from '@/utils/syncUtils';
/** /**
* 동기화 상태와 마지막 동기화 시간을 관리하는 커스텀 훅 * 동기화 상태 관리를 위한 커스텀 훅
*/ */
export const useSyncStatus = () => { export const useSyncStatus = () => {
const [lastSync, setLastSync] = useState<string | null>(getLastSyncTime()); const [lastSync, setLastSync] = useState<string | null>(getLastSyncTime());
// 마지막 동기화 시간 정기적으로 업데이트
useEffect(() => {
const intervalId = setInterval(() => {
setLastSync(getLastSyncTime());
}, 10000); // 10초마다 업데이트
return () => clearInterval(intervalId);
}, []);
// 마지막 동기화 시간 포맷팅 // 마지막 동기화 시간 포맷팅
const formatLastSyncTime = () => { const formatLastSyncTime = (): string => {
if (!lastSync) return "아직 동기화된 적 없음"; if (!lastSync) {
return '없음';
}
if (lastSync === '부분-다운로드') return "부분 동기화 (다운로드만)"; try {
const date = new Date(lastSync);
const date = new Date(lastSync); // 유효한 날짜인지 확인
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; if (isNaN(date.getTime())) {
return '없음';
}
// 오늘 날짜인 경우
const today = new Date();
const isToday = date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
if (isToday) {
// 시간만 표시
return `오늘 ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
}
// 어제 날짜인 경우
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const isYesterday = date.getDate() === yesterday.getDate() &&
date.getMonth() === yesterday.getMonth() &&
date.getFullYear() === yesterday.getFullYear();
if (isYesterday) {
return `어제 ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
}
// 그 외 날짜
return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
} catch (error) {
console.error('날짜 포맷 오류:', error);
return '없음';
}
}; };
return { lastSync, formatLastSyncTime }; // 동기화 시간이 변경될 때 상태 업데이트
useEffect(() => {
const updateLastSyncTime = () => {
setLastSync(getLastSyncTime());
};
// 이벤트 리스너 등록
window.addEventListener('syncTimeUpdated', updateLastSyncTime);
return () => {
window.removeEventListener('syncTimeUpdated', updateLastSyncTime);
};
}, []);
return {
lastSync,
formatLastSyncTime
};
}; };

View File

@@ -82,7 +82,7 @@ export const useSyncToggle = () => {
if (checked && user) { if (checked && user) {
try { try {
// 인자 수정: userId만 전달 // 동기화 수행
await performSync(user.id); await performSync(user.id);
} catch (error) { } catch (error) {
console.error('동기화 중 오류, 로컬 데이터 복원 시도:', error); console.error('동기화 중 오류, 로컬 데이터 복원 시도:', error);
@@ -120,7 +120,6 @@ const performSync = async (userId: string) => {
if (!userId) return; if (!userId) return;
try { try {
// 인자 수정: userId만 전달
const result = await trySyncAllData(userId); const result = await trySyncAllData(userId);
return result; return result;
} catch (error) { } catch (error) {

View File

@@ -1,8 +1,5 @@
import { uploadBudgets } from './uploadBudget'; // 예산 동기화 관련 모듈
import { downloadBudgets } from './downloadBudget'; export * from './uploadBudget';
export * from './downloadBudget';
export { export * from './modifiedBudgetsTracker';
uploadBudgets,
downloadBudgets
};

View File

@@ -1,138 +1,45 @@
/**
* 수정된 예산 데이터를 추적하는 유틸리티
* 로컬 스토리지에 수정된 예산 정보를 저장하고 관리합니다.
*/
const MODIFIED_BUDGETS_KEY = 'modified_budgets';
const MODIFIED_CATEGORY_BUDGETS_KEY = 'modified_category_budgets';
interface ModifiedBudget {
timestamp: number; // 수정 시간 (밀리초)
monthlyAmount: number; // 월간 예산액
}
interface ModifiedCategoryBudgets {
timestamp: number; // 수정 시간 (밀리초)
categories: Record<string, number>; // 카테고리별 예산액
}
/** /**
* 수정된 예산 정보를 로컬 스토리지에 저장 * 수정된 예산 추적 유틸리티
*/ */
export const markBudgetAsModified = (monthlyAmount: number): void => {
try {
const modifiedBudget: ModifiedBudget = {
timestamp: Date.now(),
monthlyAmount
};
localStorage.setItem(MODIFIED_BUDGETS_KEY, JSON.stringify(modifiedBudget)); // 예산 수정 여부 확인
console.log(`[예산 추적] 수정된 예산 정보 저장 완료: ${monthlyAmount}`); export const isModifiedBudget = (): boolean => {
} catch (error) { return localStorage.getItem('modifiedBudget') === 'true';
console.error('[예산 추적] 수정된 예산 정보 저장 실패:', error);
}
}; };
/** // 카테고리 예산 수정 여부 확인
* 수정된 카테고리 예산 정보를 로컬 스토리지에 저장 export const isModifiedCategoryBudgets = (): boolean => {
*/ return localStorage.getItem('modifiedCategoryBudgets') === 'true';
export const markCategoryBudgetsAsModified = (categories: Record<string, number>): void => {
try {
const modifiedCategoryBudgets: ModifiedCategoryBudgets = {
timestamp: Date.now(),
categories
};
localStorage.setItem(MODIFIED_CATEGORY_BUDGETS_KEY, JSON.stringify(modifiedCategoryBudgets));
console.log(`[예산 추적] 수정된 카테고리 예산 정보 저장 완료: ${Object.keys(categories).length}개 카테고리`);
} catch (error) {
console.error('[예산 추적] 수정된 카테고리 예산 정보 저장 실패:', error);
}
}; };
/** // 예산 수정 여부 설정
* 단일 카테고리 예산 정보를 수정된 것으로 표시 export const setModifiedBudget = (modified: boolean = true): void => {
*/ localStorage.setItem('modifiedBudget', modified ? 'true' : 'false');
export const markSingleCategoryBudgetAsModified = (category: string, amount: number): void => {
try {
// 기존 수정 정보 가져오기
const existing = getModifiedCategoryBudgets();
const categories = existing?.categories || {};
// 새 카테고리 예산 정보 추가
categories[category] = amount;
// 수정된 정보 저장
const modifiedCategoryBudgets: ModifiedCategoryBudgets = {
timestamp: Date.now(),
categories
};
localStorage.setItem(MODIFIED_CATEGORY_BUDGETS_KEY, JSON.stringify(modifiedCategoryBudgets));
console.log(`[예산 추적] 카테고리 '${category}' 예산 정보 저장 완료: ${amount}`);
} catch (error) {
console.error(`[예산 추적] 카테고리 '${category}' 예산 정보 저장 실패:`, error);
}
}; };
/** // 카테고리 예산 수정 여부 설정
* 수정된 예산 정보 가져오기 export const setModifiedCategoryBudgets = (modified: boolean = true): void => {
*/ localStorage.setItem('modifiedCategoryBudgets', modified ? 'true' : 'false');
export const getModifiedBudget = (): ModifiedBudget | null => {
try {
const data = localStorage.getItem(MODIFIED_BUDGETS_KEY);
if (!data) return null;
return JSON.parse(data) as ModifiedBudget;
} catch (error) {
console.error('[예산 추적] 수정된 예산 정보 조회 실패:', error);
return null;
}
}; };
/** // 예산 수정 여부 초기화
* 수정된 카테고리 예산 정보 가져오기
*/
export const getModifiedCategoryBudgets = (): ModifiedCategoryBudgets | null => {
try {
const data = localStorage.getItem(MODIFIED_CATEGORY_BUDGETS_KEY);
if (!data) return null;
return JSON.parse(data) as ModifiedCategoryBudgets;
} catch (error) {
console.error('[예산 추적] 수정된 카테고리 예산 정보 조회 실패:', error);
return null;
}
};
/**
* 예산 수정 정보 초기화
*/
export const clearModifiedBudget = (): void => { export const clearModifiedBudget = (): void => {
try { localStorage.setItem('modifiedBudget', 'false');
localStorage.removeItem(MODIFIED_BUDGETS_KEY);
console.log('[예산 추적] 수정된 예산 정보 초기화 완료');
} catch (error) {
console.error('[예산 추적] 수정된 예산 정보 초기화 실패:', error);
}
}; };
/** // 카테고리 예산 수정 여부 초기화
* 카테고리 예산 수정 정보 초기화
*/
export const clearModifiedCategoryBudgets = (): void => { export const clearModifiedCategoryBudgets = (): void => {
try { localStorage.setItem('modifiedCategoryBudgets', 'false');
localStorage.removeItem(MODIFIED_CATEGORY_BUDGETS_KEY);
console.log('[예산 추적] 수정된 카테고리 예산 정보 초기화 완료');
} catch (error) {
console.error('[예산 추적] 수정된 카테고리 예산 정보 초기화 실패:', error);
}
}; };
/** // 예산 또는 카테고리 예산이 수정되었는지 확인
* 모든 수정 정보 초기화 export const isAnyBudgetModified = (): boolean => {
*/ return isModifiedBudget() || isModifiedCategoryBudgets();
export const clearAllModifiedBudgets = (): void => { };
// 모든 예산 수정 여부 초기화
export const clearAllModifiedFlags = (): void => {
clearModifiedBudget(); clearModifiedBudget();
clearModifiedCategoryBudgets(); clearModifiedCategoryBudgets();
}; };

View File

@@ -1,71 +1,55 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import { isSyncEnabled } from './syncSettings';
import { toast } from '@/hooks/useToast.wrapper';
/** /**
* Supabase에 저장된 사용자의 모든 데이터 삭제 * 사용자의 모든 클라우드 데이터 초기화
*/ */
export const clearCloudData = async (userId: string): Promise<boolean> => { export const clearCloudData = async (userId: string): Promise<boolean> => {
if (!userId || !isSyncEnabled()) return false; if (!userId) {
console.error('사용자 ID가 없습니다.');
return false;
}
console.log('클라우드 데이터 초기화 시작');
try { try {
console.log('클라우드 데이터 초기화 시작...'); // 모든 트랜잭션 삭제
const { error: transactionsError } = await supabase
// 트랜잭션 데이터 삭제
const { error: txError } = await supabase
.from('transactions') .from('transactions')
.delete() .delete()
.eq('user_id', userId); .eq('user_id', userId);
if (txError) { if (transactionsError) {
console.error('클라우드 트랜잭션 삭제 오류:', txError); console.error('트랜잭션 삭제 오류:', transactionsError);
throw txError; return false;
} }
// 예산 데이터 삭제 (budgets 테이블이 있는 경우) // 카테고리 예산 삭제
try { const { error: categoryBudgetsError } = await supabase
const { error: budgetError } = await supabase .from('category_budgets')
.from('budgets') .delete()
.delete() .eq('user_id', userId);
.eq('user_id', userId);
if (budgetError) { if (categoryBudgetsError) {
console.error('클라우드 예산 삭제 오류:', budgetError); console.error('카테고리 예산 삭제 오류:', categoryBudgetsError);
// 이 오류는 심각하지 않으므로 진행 계속 return false;
}
} catch (e) {
console.log('budgets 테이블이 없거나 삭제 중 오류 발생:', e);
} }
// 카테고리 예산 데이터 삭제 (category_budgets 테이블이 있는 경우) // 예산 삭제
try { const { error: budgetsError } = await supabase
const { error: catBudgetError } = await supabase .from('budgets')
.from('category_budgets') .delete()
.delete() .eq('user_id', userId);
.eq('user_id', userId);
if (catBudgetError) { if (budgetsError) {
console.error('클라우드 카테고리 예산 삭제 오류:', catBudgetError); console.error('예산 삭제 오류:', budgetsError);
// 이 오류는 심각하지 않으므로 진행 계속 return false;
}
} catch (e) {
console.log('category_budgets 테이블이 없거나 삭제 중 오류 발생:', e);
} }
// 동기화 설정 초기화 및 마지막 동기화 시간 초기화 console.log('클라우드 데이터 초기화 완료');
localStorage.removeItem('lastSync');
localStorage.setItem('syncEnabled', 'false'); // 동기화 설정을 OFF로 변경
console.log('클라우드 데이터 초기화 완료 및 동기화 설정 OFF');
return true; return true;
} catch (error) { } catch (error) {
console.error('클라우드 데이터 초기화 중 오류 발생:', error); console.error('클라우드 데이터 초기화 중 오류 발생:', error);
toast({
title: "클라우드 데이터 초기화 실패",
description: "서버에서 데이터를 삭제하는 중 문제가 발생했습니다.",
variant: "destructive"
});
return false; return false;
} }
}; };

View File

@@ -1,28 +1,43 @@
/** /**
* 동기화 기능 활성화 여부를 localStorage에 저장 * 동기화 설정 관리
* @param enabled 활성화 여부
*/ */
export const setSyncEnabled = (enabled: boolean): void => {
localStorage.setItem('syncEnabled', enabled ? 'true' : 'false');
// 이벤트 발생하여 다른 컴포넌트에 변경 알림
window.dispatchEvent(new Event('syncEnabledChanged'));
};
/** // 동기화 활성화 여부 확인
* 동기화 기능이 현재 활성화되어 있는지 확인
* @returns 활성화 여부
*/
export const isSyncEnabled = (): boolean => { export const isSyncEnabled = (): boolean => {
return localStorage.getItem('syncEnabled') === 'true'; try {
}; const value = localStorage.getItem('syncEnabled');
return value === 'true';
/** } catch (error) {
* 동기화 설정 초기화 console.error('동기화 설정 조회 오류:', error);
*/ return false;
export const initSyncSettings = (): void => {
// 동기화 기능이 설정되지 않은 경우 기본값으로 설정
if (localStorage.getItem('syncEnabled') === null) {
localStorage.setItem('syncEnabled', 'false');
} }
}; };
// 동기화 설정 변경
export const setSyncEnabled = (enabled: boolean): void => {
try {
localStorage.setItem('syncEnabled', enabled ? 'true' : 'false');
// 상태 변경 이벤트 발생
window.dispatchEvent(new Event('syncSettingChanged'));
window.dispatchEvent(new StorageEvent('storage', {
key: 'syncEnabled',
newValue: enabled ? 'true' : 'false'
}));
console.log('동기화 설정이 변경되었습니다:', enabled ? '활성화' : '비활성화');
} catch (error) {
console.error('동기화 설정 변경 오류:', error);
}
};
// 동기화 설정 초기화
export const initSyncSettings = (): void => {
// 이미 설정이 있으면 초기화하지 않음
if (localStorage.getItem('syncEnabled') === null) {
setSyncEnabled(false); // 기본값: 비활성화
}
console.log('동기화 설정 초기화 완료, 현재 상태:', isSyncEnabled() ? '활성화' : '비활성화');
};

View File

@@ -1,3 +1,4 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import { uploadBudgets, downloadBudgets } from './budget'; import { uploadBudgets, downloadBudgets } from './budget';
import { uploadTransactions, downloadTransactions } from './transaction'; import { uploadTransactions, downloadTransactions } from './transaction';
@@ -157,7 +158,7 @@ export const syncAllData = async (userId: string): Promise<SyncResult> => {
} }
}; };
// 서버에 대한 안전한 동기화 래퍼 - 인자 수정 // 서버에 대한 안전한 동기화 래퍼
export const trySyncAllData = async (userId: string): Promise<SyncResult> => { export const trySyncAllData = async (userId: string): Promise<SyncResult> => {
console.log('안전한 데이터 동기화 시도'); console.log('안전한 데이터 동기화 시도');
let attempts = 0; let attempts = 0;

View File

@@ -0,0 +1,111 @@
import { supabase } from '@/lib/supabase';
import { isSyncEnabled } from './syncSettings';
import { clearAllModifiedFlags } from './budget/modifiedBudgetsTracker';
/**
* 서버에서 예산 데이터 다운로드
*/
export const downloadBudgets = async (userId: string): Promise<void> => {
if (!isSyncEnabled()) return;
try {
console.log('서버에서 예산 데이터 다운로드 시작');
// 현재 월/년도 가져오기
const now = new Date();
const currentMonth = now.getMonth() + 1; // 0-11 -> 1-12
const currentYear = now.getFullYear();
// 1. 월간 예산 다운로드
const { data: budgetData, error: budgetError } = await supabase
.from('budgets')
.select('*')
.eq('user_id', userId)
.eq('month', currentMonth)
.eq('year', currentYear)
.order('updated_at', { ascending: false })
.limit(1);
if (budgetError) {
console.error('월간 예산 다운로드 실패:', budgetError);
throw budgetError;
}
if (budgetData && budgetData.length > 0) {
const monthlyBudget = budgetData[0].total_budget;
// 기존 예산 데이터 가져오기
let budgetDataObj = { monthly: { targetAmount: 0, spentAmount: 0, remainingAmount: 0 } };
try {
const storedBudgetData = localStorage.getItem('budgetData');
if (storedBudgetData) {
budgetDataObj = JSON.parse(storedBudgetData);
}
} catch (e) {
console.error('로컬 예산 데이터 파싱 오류:', e);
}
// 월간 예산만 업데이트
budgetDataObj.monthly.targetAmount = monthlyBudget;
// 일간, 주간 예산 계산 (이전 비율 유지)
const daysInMonth = new Date(currentYear, currentMonth, 0).getDate();
budgetDataObj.daily = {
targetAmount: Math.round(monthlyBudget / daysInMonth),
spentAmount: budgetDataObj.daily?.spentAmount || 0,
remainingAmount: Math.round(monthlyBudget / daysInMonth) - (budgetDataObj.daily?.spentAmount || 0)
};
budgetDataObj.weekly = {
targetAmount: Math.round(monthlyBudget * 7 / daysInMonth),
spentAmount: budgetDataObj.weekly?.spentAmount || 0,
remainingAmount: Math.round(monthlyBudget * 7 / daysInMonth) - (budgetDataObj.weekly?.spentAmount || 0)
};
// 업데이트된 예산 데이터 저장
localStorage.setItem('budgetData', JSON.stringify(budgetDataObj));
console.log('월간 예산 다운로드 및 저장 완료:', monthlyBudget);
} else {
console.log('서버에 저장된 월간 예산 데이터가 없습니다.');
}
// 2. 카테고리 예산 다운로드
const { data: categoryData, error: categoryError } = await supabase
.from('category_budgets')
.select('*')
.eq('user_id', userId);
if (categoryError) {
console.error('카테고리 예산 다운로드 실패:', categoryError);
throw categoryError;
}
if (categoryData && categoryData.length > 0) {
// 카테고리 예산 객체 구성
const categoryBudgets: Record<string, number> = {};
categoryData.forEach(item => {
categoryBudgets[item.category] = item.amount;
});
// 카테고리 예산 저장
localStorage.setItem('categoryBudgets', JSON.stringify(categoryBudgets));
console.log('카테고리 예산 다운로드 및 저장 완료:', categoryBudgets);
} else {
console.log('서버에 저장된 카테고리 예산 데이터가 없습니다.');
}
// 수정 플래그 초기화
clearAllModifiedFlags();
// UI 업데이트를 위한 이벤트 발생
window.dispatchEvent(new Event('budgetDataUpdated'));
window.dispatchEvent(new Event('categoryBudgetsUpdated'));
console.log('예산 데이터 다운로드 완료');
} catch (error) {
console.error('예산 데이터 다운로드 오류:', error);
throw error;
}
};

View File

@@ -0,0 +1,91 @@
import { supabase } from '@/lib/supabase';
import { Transaction } from '@/components/TransactionCard';
import { isSyncEnabled } from './syncSettings';
import { formatDateForDisplay } from './transaction/dateUtils';
/**
* 서버에서 트랜잭션 데이터 다운로드
*/
export const downloadTransactions = async (userId: string): Promise<void> => {
if (!isSyncEnabled()) return;
try {
console.log('서버에서 트랜잭션 다운로드 시작');
// 서버에서 모든 트랜잭션 가져오기
const { data, error } = await supabase
.from('transactions')
.select('*')
.eq('user_id', userId)
.order('date', { ascending: false });
if (error) {
console.error('트랜잭션 다운로드 실패:', error);
throw error;
}
if (data && data.length > 0) {
// 기존 로컬 트랜잭션 로드
let localTransactions: Transaction[] = [];
try {
const storedTransactions = localStorage.getItem('transactions');
if (storedTransactions) {
localTransactions = JSON.parse(storedTransactions);
}
} catch (e) {
console.error('로컬 트랜잭션 파싱 오류:', e);
}
// 서버 트랜잭션을 로컬 형식으로 변환
const serverTransactions: Transaction[] = data.map(t => ({
id: t.transaction_id || t.id,
title: t.title,
amount: t.amount,
// 날짜 포맷팅
date: formatDateForDisplay(t.date),
category: t.category,
type: t.type,
notes: t.notes
}));
console.log(`서버에서 ${serverTransactions.length}개의 트랜잭션 다운로드됨`);
// 로컬 데이터와 서버 데이터 병합 (중복 ID 제거)
const mergedTransactions: Transaction[] = [];
const processedIds = new Set<string>();
// 서버 데이터 우선 처리
serverTransactions.forEach(transaction => {
if (!processedIds.has(transaction.id)) {
mergedTransactions.push(transaction);
processedIds.add(transaction.id);
}
});
// 서버에 없는 로컬 트랜잭션 추가
localTransactions.forEach(transaction => {
if (!processedIds.has(transaction.id)) {
mergedTransactions.push(transaction);
processedIds.add(transaction.id);
}
});
// 병합된 트랜잭션 저장
localStorage.setItem('transactions', JSON.stringify(mergedTransactions));
localStorage.setItem('transactions_backup', JSON.stringify(mergedTransactions));
console.log(`${mergedTransactions.length}개의 트랜잭션 병합 완료`);
// UI 업데이트를 위한 이벤트 발생
window.dispatchEvent(new Event('transactionUpdated'));
} else {
console.log('서버에 저장된 트랜잭션이 없습니다.');
}
console.log('트랜잭션 데이터 다운로드 완료');
} catch (error) {
console.error('트랜잭션 다운로드 오류:', error);
throw error;
}
};

View File

@@ -1,17 +1,67 @@
// 동기화 관련 모든 기능을 한 곳에서 내보내는 인덱스 파일 // 동기화 관련 설정 관리
import { isSyncEnabled, setSyncEnabled, initSyncSettings } from './config';
import { getLastSyncTime, setLastSyncTime } from './time';
import { syncAllData as syncData } from './data';
// 단일 진입점에서 모든 기능 내보내기 /**
export { * 동기화 활성화 여부 확인
isSyncEnabled, */
setSyncEnabled, export const isSyncEnabled = (): boolean => {
getLastSyncTime, try {
setLastSyncTime, const value = localStorage.getItem('syncEnabled');
initSyncSettings return value === 'true';
} catch (error) {
console.error('동기화 설정 조회 오류:', error);
return false;
}
}; };
// 이름 충돌 방지를 위해 다른 이름으로 내보내기 /**
export const syncAllData = syncData; * 동기화 설정 변경
*/
export const setSyncEnabled = (enabled: boolean): void => {
try {
localStorage.setItem('syncEnabled', enabled ? 'true' : 'false');
// 상태 변경 이벤트 발생
window.dispatchEvent(new Event('syncSettingChanged'));
window.dispatchEvent(new StorageEvent('storage', {
key: 'syncEnabled',
newValue: enabled ? 'true' : 'false'
}));
console.log('동기화 설정이 변경되었습니다:', enabled ? '활성화' : '비활성화');
} catch (error) {
console.error('동기화 설정 변경 오류:', error);
}
};
/**
* 동기화 설정 초기화
*/
export const initSyncSettings = (): void => {
// 이미 설정이 있으면 초기화하지 않음
if (localStorage.getItem('syncEnabled') === null) {
setSyncEnabled(false); // 기본값: 비활성화
}
console.log('동기화 설정 초기화 완료, 현재 상태:', isSyncEnabled() ? '활성화' : '비활성화');
};
/**
* 마지막 동기화 시간 가져오기
*/
export const getLastSyncTime = (): string | null => {
return localStorage.getItem('lastSync');
};
/**
* 마지막 동기화 시간 설정
*/
export const setLastSyncTime = (timestamp: string): void => {
localStorage.setItem('lastSync', timestamp);
// 이벤트 발생
window.dispatchEvent(new CustomEvent('syncTimeUpdated', {
detail: { time: timestamp }
}));
};
// syncUtils.ts에서 사용하던 함수들도 여기로 통합
export { trySyncAllData } from './data';
export type { SyncResult } from './data';

View File

@@ -1,25 +1,67 @@
/** /**
* 마지막 동기화 시간 가져오기 * 동기화 시간 관리
*/ */
// 마지막 동기화 시간 가져오기
export const getLastSyncTime = (): string | null => { export const getLastSyncTime = (): string | null => {
try { return localStorage.getItem('lastSync');
return localStorage.getItem('lastSyncTime');
} catch (error) {
console.error('마지막 동기화 시간 확인 중 오류:', error);
return null;
}
}; };
/** // 마지막 동기화 시간 설정
* 마지막 동기화 시간 설정 export const setLastSyncTime = (timestamp: string): void => {
* @param customValue 설정할 특정 값 (옵션) localStorage.setItem('lastSync', timestamp);
*/
export const setLastSyncTime = (customValue?: string): void => { // 이벤트 발생
window.dispatchEvent(new CustomEvent('syncTimeUpdated', {
detail: { time: timestamp }
}));
};
// 마지막 동기화 시간 포맷
export const formatLastSyncTime = (timestamp: string | null = null): string => {
if (!timestamp) {
timestamp = getLastSyncTime();
}
if (!timestamp) {
return '없음';
}
try { try {
const value = customValue || new Date().toISOString(); const date = new Date(timestamp);
localStorage.setItem('lastSyncTime', value);
// 유효한 날짜인지 확인
if (isNaN(date.getTime())) {
return '없음';
}
// 오늘 날짜인 경우
const today = new Date();
const isToday = date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
if (isToday) {
// 시간만 표시
return `오늘 ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
}
// 어제 날짜인 경우
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const isYesterday = date.getDate() === yesterday.getDate() &&
date.getMonth() === yesterday.getMonth() &&
date.getFullYear() === yesterday.getFullYear();
if (isYesterday) {
return `어제 ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
}
// 그 외 날짜
return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
} catch (error) { } catch (error) {
console.error('마지막 동기화 시간 업데이트 중 오류:', error); console.error('날짜 포맷 오류:', error);
return '없음';
} }
}; };

View File

@@ -1,24 +1,20 @@
import { formatISO, parseISO, isValid, format } from 'date-fns';
import { ko } from 'date-fns/locale';
/** /**
* 날짜 문자열을 ISO 형식으로 변환하는 함수 * 트랜잭션 날짜 관련 유틸리티
* 다양한 형식의 날짜 문자열을 처리 */
import { formatISO } from 'date-fns';
/**
* 다양한 형식의 날짜 문자열을 ISO 형식으로 변환
*/ */
export const normalizeDate = (dateStr: string): string => { export const normalizeDate = (dateStr: string): string => {
// 입력값이 없거나 유효하지 않은 경우 보호
if (!dateStr || typeof dateStr !== 'string') {
console.warn('[날짜 변환] 유효하지 않은 입력:', dateStr);
return formatISO(new Date());
}
// 이미 ISO 형식인 경우 그대로 반환
if (dateStr.match(/^\d{4}-\d{2}-\d{2}T/)) {
return dateStr;
}
try { try {
// "오늘"이라는 표현이 있으면 현재 날짜 // 이미 ISO 형식인 경우 그대
if (dateStr.match(/^\d{4}-\d{2}-\d{2}T/)) {
return dateStr;
}
// "오늘, 시간" 형식 처리
if (dateStr.includes('오늘')) { if (dateStr.includes('오늘')) {
const today = new Date(); const today = new Date();
@@ -33,96 +29,79 @@ export const normalizeDate = (dateStr: string): string => {
return formatISO(today); return formatISO(today);
} }
// 한국어 날짜 형식 처리 (YYYY년 MM월 DD일) // "어제, 시간" 형식 처리
const koreanDateMatch = dateStr.match(/(\d{4})년\s*(\d{1,2})월\s*(\d{1,2})일/); if (dateStr.includes('어제')) {
if (koreanDateMatch) { const yesterday = new Date();
const year = parseInt(koreanDateMatch[1], 10); yesterday.setDate(yesterday.getDate() - 1);
const month = parseInt(koreanDateMatch[2], 10) - 1; // 월은 0-11
const day = parseInt(koreanDateMatch[3], 10);
// 시간 추출 시도 // 시간 추출 시도
let hours = 0, minutes = 0;
const timeMatch = dateStr.match(/(\d{1,2}):(\d{2})/); const timeMatch = dateStr.match(/(\d{1,2}):(\d{2})/);
if (timeMatch) { if (timeMatch) {
hours = parseInt(timeMatch[1], 10); const hours = parseInt(timeMatch[1], 10);
minutes = parseInt(timeMatch[2], 10); const minutes = parseInt(timeMatch[2], 10);
yesterday.setHours(hours, minutes, 0, 0);
} }
const date = new Date(year, month, day, hours, minutes); return formatISO(yesterday);
if (isValid(date)) {
return formatISO(date);
}
} }
// 일반 날짜 문자열은 그대로 Date 객체로 변환 시도 // 일반 날짜 문자열은 그대로 Date 객체로 변환 시도
const date = new Date(dateStr); const date = new Date(dateStr);
if (isValid(date) && !isNaN(date.getTime())) { if (!isNaN(date.getTime())) {
return formatISO(date); return formatISO(date);
} }
// 변환 실패 시 현재 시간 반환 // 변환 실패 시 현재 시간 반환
console.warn(`[날짜 변환] 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`); console.warn(`날짜 변환 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`);
return formatISO(new Date()); return formatISO(new Date());
} catch (error) { } catch (error) {
console.error(`[날짜 변환] 심각한 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`, error); console.error(`날짜 변환 오류: "${dateStr}"`, error);
// 오류 발생 시 현재 시간 반환 (데이터 손실 방지)
return formatISO(new Date()); return formatISO(new Date());
} }
}; };
/** /**
* ISO 형식의 날짜 문자열을 사용자 친화적 형식으로 변환 * ISO 형식의 날짜 사용자 친화적 형식으로 변환
*/ */
export const formatDateForDisplay = (isoDateStr: string): string => { export const formatDateForDisplay = (isoDateStr: string): string => {
// 입력값이 유효한지 보호 처리
if (!isoDateStr || typeof isoDateStr !== 'string') {
console.warn('[날짜 표시] 유효하지 않은 날짜 입력:', isoDateStr);
return '날짜 없음';
}
try { try {
// 이미 포맷된 날짜 문자열(예: "오늘, 14:30")이면 그대로 반환 const date = new Date(isoDateStr);
if (isoDateStr.includes('오늘,') ||
(isoDateStr.includes('년') && isoDateStr.includes('월') && isoDateStr.includes('일'))) { // 유효한 날짜인지 확인
return isoDateStr; if (isNaN(date.getTime())) {
return isoDateStr; // 변환 실패 시 원본 반환
} }
// 유효한 날짜 객체 생성 const now = new Date();
let date; const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
if (isoDateStr.match(/^\d{4}-\d{2}-\d{2}T/)) { const yesterday = new Date(today);
// ISO 형식인 경우 yesterday.setDate(yesterday.getDate() - 1);
date = parseISO(isoDateStr);
} else { const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
// ISO 형식이 아닌 경우 일반 Date 생성자 시도
date = new Date(isoDateStr); // 시간 포맷
const hours = date.getHours();
const minutes = date.getMinutes();
const formattedTime = `${hours}:${minutes.toString().padStart(2, '0')}`;
// 오늘인 경우
if (dateOnly.getTime() === today.getTime()) {
return `오늘, ${formattedTime}`;
} }
if (!isValid(date) || isNaN(date.getTime())) { // 어제인 경우
console.warn('[날짜 표시] 유효하지 않은 날짜 형식:', isoDateStr); if (dateOnly.getTime() === yesterday.getTime()) {
return '유효하지 않은 날짜'; return `어제, ${formattedTime}`;
} }
// 현재 날짜와 비교 // 그 외의 경우
const today = new Date(); const year = date.getFullYear();
const isToday = const month = date.getMonth() + 1;
date.getDate() === today.getDate() && const day = date.getDate();
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
if (isToday) { return `${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}, ${formattedTime}`;
return `오늘, ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
// date-fns를 사용하여 한국어 형식으로 변환
try {
return format(date, 'yyyy년 M월 d일 HH:mm', { locale: ko });
} catch (formatError) {
// date-fns 포맷 실패 시 수동 포맷 사용
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
} catch (error) { } catch (error) {
console.error('[날짜 표시] 날짜 포맷 변환 오류:', error, isoDateStr); console.error('날짜 표시 형식 변환 오류:', error);
// 오류 발생 시 기본값 반환 return isoDateStr; // 오류 시 원본 반환
return '날짜 오류';
} }
}; };

View File

@@ -1,10 +1,5 @@
import { uploadTransactions } from './uploadTransaction'; // 트랜잭션 동기화 관련 모듈
import { downloadTransactions } from './downloadTransaction'; export * from './uploadTransaction';
import { deleteTransactionFromServer } from './deleteTransaction'; export * from './downloadTransaction';
export * from './dateUtils';
export {
uploadTransactions,
downloadTransactions,
deleteTransactionFromServer
};

View File

@@ -1,26 +1,9 @@
import { isSyncEnabled, setSyncEnabled, initSyncSettings } from './sync/config'; // 편의를 위한 인덱스 파일
import { getLastSyncTime, setLastSyncTime } from './sync/time';
import { trySyncAllData } from './sync/data';
import { clearCloudData } from './sync/clearCloudData';
// SyncResult 타입 내보내기 수정 // 모든 동기화 관련 함수들을 하나로 모아서 내보냄
export type { SyncResult } from './sync/data'; export * from './sync/syncSettings';
export * from './sync/data';
// 모든 유틸리티 함수를 동일한 공개 API로 유지하기 위해 내보내기 export * from './sync/time';
export { export * from './sync/transaction';
isSyncEnabled, export * from './sync/budget';
setSyncEnabled,
getLastSyncTime,
setLastSyncTime,
trySyncAllData,
clearCloudData,
initSyncSettings
};
// App.tsx에서 사용하는 initSyncState 함수 추가
export const initSyncState = async (): Promise<void> => {
// 동기화 설정 초기화
initSyncSettings();
console.log('동기화 상태 초기화 완료');
};