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:
@@ -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);
|
||||
|
||||
// 동기화 시간 업데이트
|
||||
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('수동 동기화 종료');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 formatLastSyncTime = (): string => {
|
||||
if (!lastSync) {
|
||||
return '없음';
|
||||
}
|
||||
|
||||
try {
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
|
||||
import { uploadBudgets } from './uploadBudget';
|
||||
import { downloadBudgets } from './downloadBudget';
|
||||
|
||||
export {
|
||||
uploadBudgets,
|
||||
downloadBudgets
|
||||
};
|
||||
// 예산 동기화 관련 모듈
|
||||
export * from './uploadBudget';
|
||||
export * from './downloadBudget';
|
||||
export * from './modifiedBudgetsTracker';
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 카테고리 예산 데이터 삭제 (category_budgets 테이블이 있는 경우)
|
||||
try {
|
||||
const { error: catBudgetError } = await supabase
|
||||
// 카테고리 예산 삭제
|
||||
const { error: categoryBudgetsError } = await supabase
|
||||
.from('category_budgets')
|
||||
.delete()
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (catBudgetError) {
|
||||
console.error('클라우드 카테고리 예산 삭제 오류:', catBudgetError);
|
||||
// 이 오류는 심각하지 않으므로 진행 계속
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('category_budgets 테이블이 없거나 삭제 중 오류 발생:', e);
|
||||
if (categoryBudgetsError) {
|
||||
console.error('카테고리 예산 삭제 오류:', categoryBudgetsError);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 동기화 설정 초기화 및 마지막 동기화 시간 초기화
|
||||
localStorage.removeItem('lastSync');
|
||||
localStorage.setItem('syncEnabled', 'false'); // 동기화 설정을 OFF로 변경
|
||||
// 예산 삭제
|
||||
const { error: budgetsError } = await supabase
|
||||
.from('budgets')
|
||||
.delete()
|
||||
.eq('user_id', userId);
|
||||
|
||||
console.log('클라우드 데이터 초기화 완료 및 동기화 설정 OFF');
|
||||
if (budgetsError) {
|
||||
console.error('예산 삭제 오류:', budgetsError);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('클라우드 데이터 초기화 완료');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('클라우드 데이터 초기화 중 오류 발생:', error);
|
||||
toast({
|
||||
title: "클라우드 데이터 초기화 실패",
|
||||
description: "서버에서 데이터를 삭제하는 중 문제가 발생했습니다.",
|
||||
variant: "destructive"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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() ? '활성화' : '비활성화');
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
111
src/utils/sync/downloadBudget.ts
Normal file
111
src/utils/sync/downloadBudget.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
91
src/utils/sync/downloadTransaction.ts
Normal file
91
src/utils/sync/downloadTransaction.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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 '없음';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
try {
|
||||
// 이미 ISO 형식인 경우 그대로 반환
|
||||
if (dateStr.match(/^\d{4}-\d{2}-\d{2}T/)) {
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
try {
|
||||
// "오늘"이라는 표현이 있으면 현재 날짜로 변환
|
||||
// "오늘, 시간" 형식 처리
|
||||
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; // 오류 시 원본 반환
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user