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

View File

@@ -3,29 +3,71 @@ import { useState, useEffect } from 'react';
import { getLastSyncTime } from '@/utils/syncUtils';
/**
* 동기화 상태와 마지막 동기화 시간을 관리하는 커스텀 훅
* 동기화 상태 관리를 위한 커스텀 훅
*/
export const useSyncStatus = () => {
const [lastSync, setLastSync] = useState<string | null>(getLastSyncTime());
// 마지막 동기화 시간 정기적으로 업데이트
useEffect(() => {
const intervalId = setInterval(() => {
setLastSync(getLastSyncTime());
}, 10000); // 10초마다 업데이트
return () => clearInterval(intervalId);
}, []);
// 마지막 동기화 시간 포맷팅
const formatLastSyncTime = () => {
if (!lastSync) return "아직 동기화된 적 없음";
if (lastSync === '부분-다운로드') return "부분 동기화 (다운로드만)";
const date = new Date(lastSync);
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
};
const formatLastSyncTime = (): string => {
if (!lastSync) {
return '없음';
}
return { lastSync, formatLastSyncTime };
try {
const date = new Date(lastSync);
// 유효한 날짜인지 확인
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 '없음';
}
};
// 동기화 시간이 변경될 때 상태 업데이트
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) {
try {
// 인자 수정: userId만 전달
// 동기화 수행
await performSync(user.id);
} catch (error) {
console.error('동기화 중 오류, 로컬 데이터 복원 시도:', error);
@@ -120,7 +120,6 @@ const performSync = async (userId: string) => {
if (!userId) return;
try {
// 인자 수정: userId만 전달
const result = await trySyncAllData(userId);
return result;
} catch (error) {

View File

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

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}`);
} catch (error) {
console.error('[예산 추적] 수정된 예산 정보 저장 실패:', error);
}
// 예산 수정 여부 확인
export const isModifiedBudget = (): boolean => {
return localStorage.getItem('modifiedBudget') === '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 isModifiedCategoryBudgets = (): boolean => {
return localStorage.getItem('modifiedCategoryBudgets') === 'true';
};
/**
* 단일 카테고리 예산 정보를 수정된 것으로 표시
*/
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 setModifiedBudget = (modified: boolean = true): void => {
localStorage.setItem('modifiedBudget', 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 setModifiedCategoryBudgets = (modified: boolean = true): void => {
localStorage.setItem('modifiedCategoryBudgets', modified ? 'true' : 'false');
};
/**
* 수정된 카테고리 예산 정보 가져오기
*/
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 => {
try {
localStorage.removeItem(MODIFIED_BUDGETS_KEY);
console.log('[예산 추적] 수정된 예산 정보 초기화 완료');
} catch (error) {
console.error('[예산 추적] 수정된 예산 정보 초기화 실패:', error);
}
localStorage.setItem('modifiedBudget', 'false');
};
/**
* 카테고리 예산 수정 정보 초기화
*/
// 카테고리 예산 수정 여부 초기화
export const clearModifiedCategoryBudgets = (): void => {
try {
localStorage.removeItem(MODIFIED_CATEGORY_BUDGETS_KEY);
console.log('[예산 추적] 수정된 카테고리 예산 정보 초기화 완료');
} catch (error) {
console.error('[예산 추적] 수정된 카테고리 예산 정보 초기화 실패:', error);
}
localStorage.setItem('modifiedCategoryBudgets', 'false');
};
/**
* 모든 수정 정보 초기화
*/
export const clearAllModifiedBudgets = (): void => {
// 예산 또는 카테고리 예산이 수정되었는지 확인
export const isAnyBudgetModified = (): boolean => {
return isModifiedBudget() || isModifiedCategoryBudgets();
};
// 모든 예산 수정 여부 초기화
export const clearAllModifiedFlags = (): void => {
clearModifiedBudget();
clearModifiedCategoryBudgets();
};

View File

@@ -1,71 +1,55 @@
import { supabase } from '@/lib/supabase';
import { isSyncEnabled } from './syncSettings';
import { toast } from '@/hooks/useToast.wrapper';
/**
* Supabase에 저장된 사용자의 모든 데이터 삭제
* 사용자의 모든 클라우드 데이터 초기화
*/
export const clearCloudData = async (userId: string): Promise<boolean> => {
if (!userId || !isSyncEnabled()) return false;
if (!userId) {
console.error('사용자 ID가 없습니다.');
return false;
}
console.log('클라우드 데이터 초기화 시작');
try {
console.log('클라우드 데이터 초기화 시작...');
// 트랜잭션 데이터 삭제
const { error: txError } = await supabase
// 모든 트랜잭션 삭제
const { error: transactionsError } = await supabase
.from('transactions')
.delete()
.eq('user_id', userId);
if (txError) {
console.error('클라우드 트랜잭션 삭제 오류:', txError);
throw txError;
if (transactionsError) {
console.error('트랜잭션 삭제 오류:', transactionsError);
return false;
}
// 예산 데이터 삭제 (budgets 테이블이 있는 경우)
try {
const { error: budgetError } = await supabase
.from('budgets')
.delete()
.eq('user_id', userId);
if (budgetError) {
console.error('클라우드 예산 삭제 오류:', budgetError);
// 이 오류는 심각하지 않으므로 진행 계속
}
} catch (e) {
console.log('budgets 테이블이 없거나 삭제 중 오류 발생:', e);
// 카테고리 예산 삭제
const { error: categoryBudgetsError } = await supabase
.from('category_budgets')
.delete()
.eq('user_id', userId);
if (categoryBudgetsError) {
console.error('카테고리 예산 삭제 오류:', categoryBudgetsError);
return false;
}
// 카테고리 예산 데이터 삭제 (category_budgets 테이블이 있는 경우)
try {
const { error: catBudgetError } = await supabase
.from('category_budgets')
.delete()
.eq('user_id', userId);
if (catBudgetError) {
console.error('클라우드 카테고리 예산 삭제 오류:', catBudgetError);
// 이 오류는 심각하지 않으므로 진행 계속
}
} catch (e) {
console.log('category_budgets 테이블이 없거나 삭제 중 오류 발생:', e);
// 예산 삭제
const { error: budgetsError } = await supabase
.from('budgets')
.delete()
.eq('user_id', userId);
if (budgetsError) {
console.error('예산 삭제 오류:', budgetsError);
return false;
}
// 동기화 설정 초기화 및 마지막 동기화 시간 초기화
localStorage.removeItem('lastSync');
localStorage.setItem('syncEnabled', 'false'); // 동기화 설정을 OFF로 변경
console.log('클라우드 데이터 초기화 완료 및 동기화 설정 OFF');
console.log('클라우드 데이터 초기화 완료');
return true;
} catch (error) {
console.error('클라우드 데이터 초기화 중 오류 발생:', error);
toast({
title: "클라우드 데이터 초기화 실패",
description: "서버에서 데이터를 삭제하는 중 문제가 발생했습니다.",
variant: "destructive"
});
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 => {
return localStorage.getItem('syncEnabled') === 'true';
};
/**
* 동기화 설정 초기화
*/
export const initSyncSettings = (): void => {
// 동기화 기능이 설정되지 않은 경우 기본값으로 설정
if (localStorage.getItem('syncEnabled') === null) {
localStorage.setItem('syncEnabled', 'false');
try {
const value = localStorage.getItem('syncEnabled');
return value === 'true';
} catch (error) {
console.error('동기화 설정 조회 오류:', error);
return 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 { uploadBudgets, downloadBudgets } from './budget';
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> => {
console.log('안전한 데이터 동기화 시도');
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,
getLastSyncTime,
setLastSyncTime,
initSyncSettings
/**
* 동기화 활성화 여부 확인
*/
export const isSyncEnabled = (): boolean => {
try {
const value = localStorage.getItem('syncEnabled');
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 => {
try {
return localStorage.getItem('lastSyncTime');
} catch (error) {
console.error('마지막 동기화 시간 확인 중 오류:', error);
return null;
}
return localStorage.getItem('lastSync');
};
/**
* 마지막 동기화 시간 설정
* @param customValue 설정할 특정 값 (옵션)
*/
export const setLastSyncTime = (customValue?: string): void => {
// 마지막 동기화 시간 설정
export const setLastSyncTime = (timestamp: string): void => {
localStorage.setItem('lastSync', timestamp);
// 이벤트 발생
window.dispatchEvent(new CustomEvent('syncTimeUpdated', {
detail: { time: timestamp }
}));
};
// 마지막 동기화 시간 포맷
export const formatLastSyncTime = (timestamp: string | null = null): string => {
if (!timestamp) {
timestamp = getLastSyncTime();
}
if (!timestamp) {
return '없음';
}
try {
const value = customValue || new Date().toISOString();
localStorage.setItem('lastSyncTime', value);
const date = new Date(timestamp);
// 유효한 날짜인지 확인
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);
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 => {
// 입력값이 없거나 유효하지 않은 경우 보호
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 {
// "오늘"이라는 표현이 있으면 현재 날짜
// 이미 ISO 형식인 경우 그대
if (dateStr.match(/^\d{4}-\d{2}-\d{2}T/)) {
return dateStr;
}
// "오늘, 시간" 형식 처리
if (dateStr.includes('오늘')) {
const today = new Date();
@@ -33,96 +29,79 @@ export const normalizeDate = (dateStr: string): string => {
return formatISO(today);
}
// 한국어 날짜 형식 처리 (YYYY년 MM월 DD일)
const koreanDateMatch = dateStr.match(/(\d{4})년\s*(\d{1,2})월\s*(\d{1,2})일/);
if (koreanDateMatch) {
const year = parseInt(koreanDateMatch[1], 10);
const month = parseInt(koreanDateMatch[2], 10) - 1; // 월은 0-11
const day = parseInt(koreanDateMatch[3], 10);
// "어제, 시간" 형식 처리
if (dateStr.includes('어제')) {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
// 시간 추출 시도
let hours = 0, minutes = 0;
const timeMatch = dateStr.match(/(\d{1,2}):(\d{2})/);
if (timeMatch) {
hours = parseInt(timeMatch[1], 10);
minutes = parseInt(timeMatch[2], 10);
const hours = parseInt(timeMatch[1], 10);
const minutes = parseInt(timeMatch[2], 10);
yesterday.setHours(hours, minutes, 0, 0);
}
const date = new Date(year, month, day, hours, minutes);
if (isValid(date)) {
return formatISO(date);
}
return formatISO(yesterday);
}
// 일반 날짜 문자열은 그대로 Date 객체로 변환 시도
const date = new Date(dateStr);
if (isValid(date) && !isNaN(date.getTime())) {
if (!isNaN(date.getTime())) {
return formatISO(date);
}
// 변환 실패 시 현재 시간 반환
console.warn(`[날짜 변환] 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`);
console.warn(`날짜 변환 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`);
return formatISO(new Date());
} catch (error) {
console.error(`[날짜 변환] 심각한 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`, error);
// 오류 발생 시 현재 시간 반환 (데이터 손실 방지)
console.error(`날짜 변환 오류: "${dateStr}"`, error);
return formatISO(new Date());
}
};
/**
* ISO 형식의 날짜 문자열을 사용자 친화적 형식으로 변환
* ISO 형식의 날짜 사용자 친화적 형식으로 변환
*/
export const formatDateForDisplay = (isoDateStr: string): string => {
// 입력값이 유효한지 보호 처리
if (!isoDateStr || typeof isoDateStr !== 'string') {
console.warn('[날짜 표시] 유효하지 않은 날짜 입력:', isoDateStr);
return '날짜 없음';
}
try {
// 이미 포맷된 날짜 문자열(예: "오늘, 14:30")이면 그대로 반환
if (isoDateStr.includes('오늘,') ||
(isoDateStr.includes('년') && isoDateStr.includes('월') && isoDateStr.includes('일'))) {
return isoDateStr;
const date = new Date(isoDateStr);
// 유효한 날짜인지 확인
if (isNaN(date.getTime())) {
return isoDateStr; // 변환 실패 시 원본 반환
}
// 유효한 날짜 객체 생성
let date;
if (isoDateStr.match(/^\d{4}-\d{2}-\d{2}T/)) {
// ISO 형식인 경우
date = parseISO(isoDateStr);
} else {
// ISO 형식이 아닌 경우 일반 Date 생성자 시도
date = new Date(isoDateStr);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
// 시간 포맷
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);
return '유효하지 않은 날짜';
// 어제인 경우
if (dateOnly.getTime() === yesterday.getTime()) {
return `어제, ${formattedTime}`;
}
// 현재 날짜와 비교
const today = new Date();
const isToday =
date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
// 그 외의 경우
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
if (isToday) {
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')}`;
}
return `${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}, ${formattedTime}`;
} catch (error) {
console.error('[날짜 표시] 날짜 포맷 변환 오류:', error, isoDateStr);
// 오류 발생 시 기본값 반환
return '날짜 오류';
console.error('날짜 표시 형식 변환 오류:', error);
return isoDateStr; // 오류 시 원본 반환
}
};

View File

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

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';
// 모든 유틸리티 함수를 동일한 공개 API로 유지하기 위해 내보내기
export {
isSyncEnabled,
setSyncEnabled,
getLastSyncTime,
setLastSyncTime,
trySyncAllData,
clearCloudData,
initSyncSettings
};
// App.tsx에서 사용하는 initSyncState 함수 추가
export const initSyncState = async (): Promise<void> => {
// 동기화 설정 초기화
initSyncSettings();
console.log('동기화 상태 초기화 완료');
};
// 모든 동기화 관련 함수들을 하나로 모아서 내보냄
export * from './sync/syncSettings';
export * from './sync/data';
export * from './sync/time';
export * from './sync/transaction';
export * from './sync/budget';