From 8e6eb9d8aa6abb6e0e06d021d9a961c15cae5891 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 11:24:40 +0000 Subject: [PATCH] Review and refine sync logic This commit reviews and refines the synchronization logic to ensure proper functionality and data integrity. --- src/hooks/sync/useManualSync.ts | 24 ++- src/hooks/sync/useSyncStatus.ts | 82 +++++++--- src/hooks/sync/useSyncToggle.ts | 3 +- src/utils/sync/budget/index.ts | 11 +- .../sync/budget/modifiedBudgetsTracker.ts | 143 +++--------------- src/utils/sync/clearCloudData.ts | 78 ++++------ src/utils/sync/config.ts | 57 ++++--- src/utils/sync/data.ts | 3 +- src/utils/sync/downloadBudget.ts | 111 ++++++++++++++ src/utils/sync/downloadTransaction.ts | 91 +++++++++++ src/utils/sync/syncSettings.ts | 76 ++++++++-- src/utils/sync/time.ts | 72 +++++++-- src/utils/sync/transaction/dateUtils.ts | 129 +++++++--------- src/utils/sync/transaction/index.ts | 13 +- src/utils/syncUtils.ts | 31 +--- 15 files changed, 567 insertions(+), 357 deletions(-) create mode 100644 src/utils/sync/downloadBudget.ts create mode 100644 src/utils/sync/downloadTransaction.ts diff --git a/src/hooks/sync/useManualSync.ts b/src/hooks/sync/useManualSync.ts index b80d210..53d9839 100644 --- a/src/hooks/sync/useManualSync.ts +++ b/src/hooks/sync/useManualSync.ts @@ -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('수동 동기화 종료'); } }; diff --git a/src/hooks/sync/useSyncStatus.ts b/src/hooks/sync/useSyncStatus.ts index 3360f50..c39beaf 100644 --- a/src/hooks/sync/useSyncStatus.ts +++ b/src/hooks/sync/useSyncStatus.ts @@ -3,29 +3,71 @@ import { useState, useEffect } from 'react'; import { getLastSyncTime } from '@/utils/syncUtils'; /** - * 동기화 상태와 마지막 동기화 시간을 관리하는 커스텀 훅 + * 동기화 상태 관리를 위한 커스텀 훅 */ export const useSyncStatus = () => { const [lastSync, setLastSync] = useState(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 + }; }; diff --git a/src/hooks/sync/useSyncToggle.ts b/src/hooks/sync/useSyncToggle.ts index 9df3001..23cbbb7 100644 --- a/src/hooks/sync/useSyncToggle.ts +++ b/src/hooks/sync/useSyncToggle.ts @@ -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) { diff --git a/src/utils/sync/budget/index.ts b/src/utils/sync/budget/index.ts index 82279ef..bc2cd64 100644 --- a/src/utils/sync/budget/index.ts +++ b/src/utils/sync/budget/index.ts @@ -1,8 +1,5 @@ -import { uploadBudgets } from './uploadBudget'; -import { downloadBudgets } from './downloadBudget'; - -export { - uploadBudgets, - downloadBudgets -}; +// 예산 동기화 관련 모듈 +export * from './uploadBudget'; +export * from './downloadBudget'; +export * from './modifiedBudgetsTracker'; diff --git a/src/utils/sync/budget/modifiedBudgetsTracker.ts b/src/utils/sync/budget/modifiedBudgetsTracker.ts index 0615eea..37d7930 100644 --- a/src/utils/sync/budget/modifiedBudgetsTracker.ts +++ b/src/utils/sync/budget/modifiedBudgetsTracker.ts @@ -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; // 카테고리별 예산액 -} /** - * 수정된 예산 정보를 로컬 스토리지에 저장 + * 수정된 예산 추적 유틸리티 */ -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): 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(); }; diff --git a/src/utils/sync/clearCloudData.ts b/src/utils/sync/clearCloudData.ts index bb07d4c..8b759d6 100644 --- a/src/utils/sync/clearCloudData.ts +++ b/src/utils/sync/clearCloudData.ts @@ -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 => { - 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; } }; diff --git a/src/utils/sync/config.ts b/src/utils/sync/config.ts index 97f8bb7..a3b4b4a 100644 --- a/src/utils/sync/config.ts +++ b/src/utils/sync/config.ts @@ -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() ? '활성화' : '비활성화'); +}; diff --git a/src/utils/sync/data.ts b/src/utils/sync/data.ts index 6522109..a431107 100644 --- a/src/utils/sync/data.ts +++ b/src/utils/sync/data.ts @@ -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 => { } }; -// 서버에 대한 안전한 동기화 래퍼 - 인자 수정 +// 서버에 대한 안전한 동기화 래퍼 export const trySyncAllData = async (userId: string): Promise => { console.log('안전한 데이터 동기화 시도'); let attempts = 0; diff --git a/src/utils/sync/downloadBudget.ts b/src/utils/sync/downloadBudget.ts new file mode 100644 index 0000000..4eb09c4 --- /dev/null +++ b/src/utils/sync/downloadBudget.ts @@ -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 => { + 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 = {}; + + 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; + } +}; diff --git a/src/utils/sync/downloadTransaction.ts b/src/utils/sync/downloadTransaction.ts new file mode 100644 index 0000000..819d943 --- /dev/null +++ b/src/utils/sync/downloadTransaction.ts @@ -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 => { + 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(); + + // 서버 데이터 우선 처리 + 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; + } +}; diff --git a/src/utils/sync/syncSettings.ts b/src/utils/sync/syncSettings.ts index e990d8f..607a6f2 100644 --- a/src/utils/sync/syncSettings.ts +++ b/src/utils/sync/syncSettings.ts @@ -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'; diff --git a/src/utils/sync/time.ts b/src/utils/sync/time.ts index 188763d..2f74b6d 100644 --- a/src/utils/sync/time.ts +++ b/src/utils/sync/time.ts @@ -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 '없음'; } }; diff --git a/src/utils/sync/transaction/dateUtils.ts b/src/utils/sync/transaction/dateUtils.ts index 68d0cf3..ada068e 100644 --- a/src/utils/sync/transaction/dateUtils.ts +++ b/src/utils/sync/transaction/dateUtils.ts @@ -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; // 오류 시 원본 반환 } }; diff --git a/src/utils/sync/transaction/index.ts b/src/utils/sync/transaction/index.ts index b42235f..04928ba 100644 --- a/src/utils/sync/transaction/index.ts +++ b/src/utils/sync/transaction/index.ts @@ -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'; diff --git a/src/utils/syncUtils.ts b/src/utils/syncUtils.ts index 621c486..518cf3f 100644 --- a/src/utils/syncUtils.ts +++ b/src/utils/syncUtils.ts @@ -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 => { - // 동기화 설정 초기화 - initSyncSettings(); - console.log('동기화 상태 초기화 완료'); -}; +// 모든 동기화 관련 함수들을 하나로 모아서 내보냄 +export * from './sync/syncSettings'; +export * from './sync/data'; +export * from './sync/time'; +export * from './sync/transaction'; +export * from './sync/budget';