Refactor transaction sync module

Refactors the transaction sync module for better organization.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-16 10:05:06 +00:00
parent e3e29d6ebe
commit 74e7ea19fd
7 changed files with 264 additions and 231 deletions

View File

@@ -0,0 +1,37 @@
import { formatISO } from 'date-fns';
/**
* 날짜 문자열을 ISO 형식으로 변환하는 함수
* "오늘, 19:00 PM"과 같은 형식을 처리하기 위한 함수
*/
export const normalizeDate = (dateStr: string): string => {
// 이미 ISO 형식인 경우 그대로 반환
if (dateStr.match(/^\d{4}-\d{2}-\d{2}T/)) {
return dateStr;
}
try {
// "오늘"라는 표현이 있으면 현재 날짜로 변환
if (dateStr.includes('오늘')) {
const today = new Date();
// 시간 추출 시도
const timeMatch = dateStr.match(/(\d{1,2}):(\d{2})/);
if (timeMatch) {
const hours = parseInt(timeMatch[1], 10);
const minutes = parseInt(timeMatch[2], 10);
today.setHours(hours, minutes, 0, 0);
}
return formatISO(today);
}
// 일반 날짜 문자열은 그대로 Date 객체로 변환 시도
return formatISO(new Date(dateStr));
} catch (error) {
console.warn(`날짜 변환 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`, error);
// 오류 발생 시 현재 시간 반환 (데이터 손실 방지)
return formatISO(new Date());
}
};

View File

@@ -0,0 +1,35 @@
import { supabase } from '@/lib/supabase';
import { isSyncEnabled } from '../syncSettings';
import { toast } from '@/hooks/useToast.wrapper';
/**
* 특정 트랜잭션 ID 삭제 처리
*/
export const deleteTransactionFromServer = async (userId: string, transactionId: string): Promise<void> => {
if (!isSyncEnabled()) return;
try {
console.log(`트랜잭션 삭제 요청: ${transactionId}`);
const { error } = await supabase
.from('transactions')
.delete()
.eq('transaction_id', transactionId)
.eq('user_id', userId);
if (error) {
console.error('트랜잭션 삭제 실패:', error);
throw error;
}
console.log(`트랜잭션 ${transactionId} 삭제 완료`);
} catch (error) {
console.error('트랜잭션 삭제 중 오류:', error);
// 에러 발생 시 토스트 알림
toast({
title: "삭제 동기화 실패",
description: "서버에서 트랜잭션을 삭제하는데 문제가 발생했습니다.",
variant: "destructive"
});
}
};

View File

@@ -0,0 +1,72 @@
import { supabase } from '@/lib/supabase';
import { Transaction } from '@/components/TransactionCard';
import { isSyncEnabled } from '../syncSettings';
/**
* Download transaction data from Supabase to local storage
* 서버에서 로컬 스토리지로 데이터 다운로드 (병합 방식)
*/
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);
if (error) {
console.error('트랜잭션 다운로드 실패:', error);
throw error;
}
if (!data || data.length === 0) {
console.log('서버에 저장된 트랜잭션 없음');
return; // 서버에 데이터가 없으면 로컬 데이터 유지
}
console.log(`서버에서 ${data.length}개의 트랜잭션 다운로드`);
// 서버 데이터를 로컬 형식으로 변환
const serverTransactions = data.map(t => ({
id: t.transaction_id || t.id,
title: t.title,
amount: t.amount,
date: t.date,
category: t.category,
type: t.type
}));
// 기존 로컬 데이터 불러오기
const localDataStr = localStorage.getItem('transactions');
const localTransactions = localDataStr ? JSON.parse(localDataStr) : [];
// 로컬 데이터와 서버 데이터 병합 (ID 기준)
const transactionMap = new Map();
// 로컬 데이터를 맵에 추가
localTransactions.forEach((tx: Transaction) => {
transactionMap.set(tx.id, tx);
});
// 서버 데이터로 맵 업데이트 (서버 데이터 우선)
serverTransactions.forEach(tx => {
transactionMap.set(tx.id, tx);
});
// 최종 병합된 데이터 생성
const mergedTransactions = Array.from(transactionMap.values());
// 로컬 스토리지에 저장
localStorage.setItem('transactions', JSON.stringify(mergedTransactions));
console.log(`${mergedTransactions.length}개의 트랜잭션 병합 완료`);
// 이벤트 발생시켜 UI 업데이트
window.dispatchEvent(new Event('transactionUpdated'));
} catch (error) {
console.error('트랜잭션 다운로드 중 오류:', error);
throw error;
}
};

View File

@@ -0,0 +1,10 @@
import { uploadTransactions } from './uploadTransaction';
import { downloadTransactions } from './downloadTransaction';
import { deleteTransactionFromServer } from './deleteTransaction';
export {
uploadTransactions,
downloadTransactions,
deleteTransactionFromServer
};

View File

@@ -0,0 +1,98 @@
import { supabase } from '@/lib/supabase';
import { Transaction } from '@/components/TransactionCard';
import { isSyncEnabled } from '../syncSettings';
import { normalizeDate } from './dateUtils';
/**
* Upload transaction data from local storage to Supabase
* 로컬 데이터를 서버에 업로드 (새로운 또는 수정된 데이터만)
*/
export const uploadTransactions = async (userId: string): Promise<void> => {
if (!isSyncEnabled()) return;
try {
const localTransactions = localStorage.getItem('transactions');
if (!localTransactions) return;
const transactions: Transaction[] = JSON.parse(localTransactions);
console.log(`로컬 트랜잭션 ${transactions.length}개 동기화 시작`);
if (transactions.length === 0) return; // 트랜잭션이 없으면 처리하지 않음
// 먼저 서버에서 현재 트랜잭션 목록 가져오기
const { data: existingData, error: fetchError } = await supabase
.from('transactions')
.select('transaction_id')
.eq('user_id', userId);
if (fetchError) {
console.error('기존 트랜잭션 조회 실패:', fetchError);
throw fetchError;
}
// 서버에 이미 있는 트랜잭션 ID 맵 생성
const existingIds = new Set(existingData?.map(t => t.transaction_id) || []);
console.log(`서버에 이미 존재하는 트랜잭션: ${existingIds.size}`);
// 삽입할 새 트랜잭션과 업데이트할 기존 트랜잭션 분리
const newTransactions = [];
const updateTransactions = [];
for (const t of transactions) {
// 날짜 형식 정규화
const normalizedDate = normalizeDate(t.date);
const transactionData = {
user_id: userId,
title: t.title,
amount: t.amount,
date: normalizedDate, // 정규화된 날짜 사용
category: t.category,
type: t.type,
transaction_id: t.id
};
if (existingIds.has(t.id)) {
updateTransactions.push(transactionData);
} else {
newTransactions.push(transactionData);
}
}
// 새 트랜잭션 삽입 (있는 경우)
if (newTransactions.length > 0) {
console.log(`${newTransactions.length}개의 새 트랜잭션 업로드`);
const { error: insertError } = await supabase
.from('transactions')
.insert(newTransactions);
if (insertError) {
console.error('새 트랜잭션 업로드 실패:', insertError);
throw insertError;
}
}
// 기존 트랜잭션 업데이트 (있는 경우)
if (updateTransactions.length > 0) {
console.log(`${updateTransactions.length}개의 기존 트랜잭션 업데이트`);
for (const transaction of updateTransactions) {
const { error: updateError } = await supabase
.from('transactions')
.update(transaction)
.eq('transaction_id', transaction.transaction_id)
.eq('user_id', userId);
if (updateError) {
console.error('트랜잭션 업데이트 실패:', updateError);
// 실패해도 계속 진행
}
}
}
console.log('트랜잭션 업로드 완료');
} catch (error) {
console.error('트랜잭션 업로드 실패:', error);
throw error;
}
};

View File

@@ -1,233 +1,13 @@
import { supabase } from '@/lib/supabase'; // 트랜잭션 동기화 기능을 내보내는 파일
import { Transaction } from '@/components/TransactionCard'; import {
import { isSyncEnabled } from './syncSettings'; uploadTransactions,
import { toast } from '@/hooks/useToast.wrapper'; downloadTransactions,
import { formatISO } from 'date-fns'; deleteTransactionFromServer
} from './transaction';
/** export {
* 날짜 문자열을 ISO 형식으로 변환하는 함수 uploadTransactions,
* "오늘, 19:00 PM"과 같은 형식을 처리하기 위한 함수 downloadTransactions,
*/ deleteTransactionFromServer
const normalizeDate = (dateStr: string): string => {
// 이미 ISO 형식인 경우 그대로 반환
if (dateStr.match(/^\d{4}-\d{2}-\d{2}T/)) {
return dateStr;
}
try {
// "오늘"라는 표현이 있으면 현재 날짜로 변환
if (dateStr.includes('오늘')) {
const today = new Date();
// 시간 추출 시도
const timeMatch = dateStr.match(/(\d{1,2}):(\d{2})/);
if (timeMatch) {
const hours = parseInt(timeMatch[1], 10);
const minutes = parseInt(timeMatch[2], 10);
today.setHours(hours, minutes, 0, 0);
}
return formatISO(today);
}
// 일반 날짜 문자열은 그대로 Date 객체로 변환 시도
return formatISO(new Date(dateStr));
} catch (error) {
console.warn(`날짜 변환 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`, error);
// 오류 발생 시 현재 시간 반환 (데이터 손실 방지)
return formatISO(new Date());
}
};
/**
* Upload transaction data from local storage to Supabase
* 로컬 데이터를 서버에 업로드 (새로운 또는 수정된 데이터만)
*/
export const uploadTransactions = async (userId: string): Promise<void> => {
if (!isSyncEnabled()) return;
try {
const localTransactions = localStorage.getItem('transactions');
if (!localTransactions) return;
const transactions: Transaction[] = JSON.parse(localTransactions);
console.log(`로컬 트랜잭션 ${transactions.length}개 동기화 시작`);
if (transactions.length === 0) return; // 트랜잭션이 없으면 처리하지 않음
// 먼저 서버에서 현재 트랜잭션 목록 가져오기
const { data: existingData, error: fetchError } = await supabase
.from('transactions')
.select('transaction_id')
.eq('user_id', userId);
if (fetchError) {
console.error('기존 트랜잭션 조회 실패:', fetchError);
throw fetchError;
}
// 서버에 이미 있는 트랜잭션 ID 맵 생성
const existingIds = new Set(existingData?.map(t => t.transaction_id) || []);
console.log(`서버에 이미 존재하는 트랜잭션: ${existingIds.size}`);
// 삽입할 새 트랜잭션과 업데이트할 기존 트랜잭션 분리
const newTransactions = [];
const updateTransactions = [];
for (const t of transactions) {
// 날짜 형식 정규화
const normalizedDate = normalizeDate(t.date);
const transactionData = {
user_id: userId,
title: t.title,
amount: t.amount,
date: normalizedDate, // 정규화된 날짜 사용
category: t.category,
type: t.type,
transaction_id: t.id
};
if (existingIds.has(t.id)) {
updateTransactions.push(transactionData);
} else {
newTransactions.push(transactionData);
}
}
// 새 트랜잭션 삽입 (있는 경우)
if (newTransactions.length > 0) {
console.log(`${newTransactions.length}개의 새 트랜잭션 업로드`);
const { error: insertError } = await supabase
.from('transactions')
.insert(newTransactions);
if (insertError) {
console.error('새 트랜잭션 업로드 실패:', insertError);
throw insertError;
}
}
// 기존 트랜잭션 업데이트 (있는 경우)
if (updateTransactions.length > 0) {
console.log(`${updateTransactions.length}개의 기존 트랜잭션 업데이트`);
for (const transaction of updateTransactions) {
const { error: updateError } = await supabase
.from('transactions')
.update(transaction)
.eq('transaction_id', transaction.transaction_id)
.eq('user_id', userId);
if (updateError) {
console.error('트랜잭션 업데이트 실패:', updateError);
// 실패해도 계속 진행
}
}
}
console.log('트랜잭션 업로드 완료');
} catch (error) {
console.error('트랜잭션 업로드 실패:', error);
throw error;
}
};
/**
* Download transaction data from Supabase to local storage
* 서버에서 로컬 스토리지로 데이터 다운로드 (병합 방식)
*/
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);
if (error) {
console.error('트랜잭션 다운로드 실패:', error);
throw error;
}
if (!data || data.length === 0) {
console.log('서버에 저장된 트랜잭션 없음');
return; // 서버에 데이터가 없으면 로컬 데이터 유지
}
console.log(`서버에서 ${data.length}개의 트랜잭션 다운로드`);
// 서버 데이터를 로컬 형식으로 변환
const serverTransactions = data.map(t => ({
id: t.transaction_id || t.id,
title: t.title,
amount: t.amount,
date: t.date,
category: t.category,
type: t.type
}));
// 기존 로컬 데이터 불러오기
const localDataStr = localStorage.getItem('transactions');
const localTransactions = localDataStr ? JSON.parse(localDataStr) : [];
// 로컬 데이터와 서버 데이터 병합 (ID 기준)
const transactionMap = new Map();
// 로컬 데이터를 맵에 추가
localTransactions.forEach((tx: Transaction) => {
transactionMap.set(tx.id, tx);
});
// 서버 데이터로 맵 업데이트 (서버 데이터 우선)
serverTransactions.forEach(tx => {
transactionMap.set(tx.id, tx);
});
// 최종 병합된 데이터 생성
const mergedTransactions = Array.from(transactionMap.values());
// 로컬 스토리지에 저장
localStorage.setItem('transactions', JSON.stringify(mergedTransactions));
console.log(`${mergedTransactions.length}개의 트랜잭션 병합 완료`);
// 이벤트 발생시켜 UI 업데이트
window.dispatchEvent(new Event('transactionUpdated'));
} catch (error) {
console.error('트랜잭션 다운로드 중 오류:', error);
throw error;
}
};
/**
* 특정 트랜잭션 ID 삭제 처리
*/
export const deleteTransactionFromServer = async (userId: string, transactionId: string): Promise<void> => {
if (!isSyncEnabled()) return;
try {
console.log(`트랜잭션 삭제 요청: ${transactionId}`);
const { error } = await supabase
.from('transactions')
.delete()
.eq('transaction_id', transactionId)
.eq('user_id', userId);
if (error) {
console.error('트랜잭션 삭제 실패:', error);
throw error;
}
console.log(`트랜잭션 ${transactionId} 삭제 완료`);
} catch (error) {
console.error('트랜잭션 삭제 중 오류:', error);
// 에러 발생 시 토스트 알림
toast({
title: "삭제 동기화 실패",
description: "서버에서 트랜잭션을 삭제하는데 문제가 발생했습니다.",
variant: "destructive"
});
}
}; };

View File

@@ -1,6 +1,6 @@
import { isSyncEnabled, setSyncEnabled, getLastSyncTime, setLastSyncTime, initSyncSettings } from './sync/syncSettings'; import { isSyncEnabled, setSyncEnabled, getLastSyncTime, setLastSyncTime, initSyncSettings } from './sync/syncSettings';
import { uploadTransactions, downloadTransactions } from './sync/transactionSync'; import { uploadTransactions, downloadTransactions, deleteTransactionFromServer } from './sync/transactionSync';
import { uploadBudgets, downloadBudgets } from './sync/budgetSync'; import { uploadBudgets, downloadBudgets } from './sync/budgetSync';
// Export all utility functions to maintain the same public API // Export all utility functions to maintain the same public API
@@ -9,6 +9,7 @@ export {
setSyncEnabled, setSyncEnabled,
uploadTransactions, uploadTransactions,
downloadTransactions, downloadTransactions,
deleteTransactionFromServer,
uploadBudgets, uploadBudgets,
downloadBudgets, downloadBudgets,
getLastSyncTime, getLastSyncTime,