diff --git a/src/utils/sync/transaction/dateUtils.ts b/src/utils/sync/transaction/dateUtils.ts new file mode 100644 index 0000000..b8334fb --- /dev/null +++ b/src/utils/sync/transaction/dateUtils.ts @@ -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()); + } +}; diff --git a/src/utils/sync/transaction/deleteTransaction.ts b/src/utils/sync/transaction/deleteTransaction.ts new file mode 100644 index 0000000..b73421b --- /dev/null +++ b/src/utils/sync/transaction/deleteTransaction.ts @@ -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 => { + 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" + }); + } +}; diff --git a/src/utils/sync/transaction/downloadTransaction.ts b/src/utils/sync/transaction/downloadTransaction.ts new file mode 100644 index 0000000..0cebea0 --- /dev/null +++ b/src/utils/sync/transaction/downloadTransaction.ts @@ -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 => { + 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; + } +}; diff --git a/src/utils/sync/transaction/index.ts b/src/utils/sync/transaction/index.ts new file mode 100644 index 0000000..b42235f --- /dev/null +++ b/src/utils/sync/transaction/index.ts @@ -0,0 +1,10 @@ + +import { uploadTransactions } from './uploadTransaction'; +import { downloadTransactions } from './downloadTransaction'; +import { deleteTransactionFromServer } from './deleteTransaction'; + +export { + uploadTransactions, + downloadTransactions, + deleteTransactionFromServer +}; diff --git a/src/utils/sync/transaction/uploadTransaction.ts b/src/utils/sync/transaction/uploadTransaction.ts new file mode 100644 index 0000000..7d8f019 --- /dev/null +++ b/src/utils/sync/transaction/uploadTransaction.ts @@ -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 => { + 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; + } +}; diff --git a/src/utils/sync/transactionSync.ts b/src/utils/sync/transactionSync.ts index 985e7f9..d5ad79c 100644 --- a/src/utils/sync/transactionSync.ts +++ b/src/utils/sync/transactionSync.ts @@ -1,233 +1,13 @@ -import { supabase } from '@/lib/supabase'; -import { Transaction } from '@/components/TransactionCard'; -import { isSyncEnabled } from './syncSettings'; -import { toast } from '@/hooks/useToast.wrapper'; -import { formatISO } from 'date-fns'; +// 트랜잭션 동기화 기능을 내보내는 파일 +import { + uploadTransactions, + downloadTransactions, + deleteTransactionFromServer +} from './transaction'; -/** - * 날짜 문자열을 ISO 형식으로 변환하는 함수 - * "오늘, 19:00 PM"과 같은 형식을 처리하기 위한 함수 - */ -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 => { - 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 => { - 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 => { - 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" - }); - } +export { + uploadTransactions, + downloadTransactions, + deleteTransactionFromServer }; diff --git a/src/utils/syncUtils.ts b/src/utils/syncUtils.ts index fba26a3..75c5e07 100644 --- a/src/utils/syncUtils.ts +++ b/src/utils/syncUtils.ts @@ -1,6 +1,6 @@ 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'; // Export all utility functions to maintain the same public API @@ -9,6 +9,7 @@ export { setSyncEnabled, uploadTransactions, downloadTransactions, + deleteTransactionFromServer, uploadBudgets, downloadBudgets, getLastSyncTime,