import { supabase } from '@/lib/supabase'; import { Transaction } from '@/components/TransactionCard'; import { isSyncEnabled } from '../syncSettings'; import { formatDateForDisplay } from './dateUtils'; import { getDeletedTransactions, isTransactionDeleted } from './deletedTransactionsTracker'; /** * Download transaction data from Supabase to local storage * 서버에서 로컬 스토리지로 데이터 다운로드 (병합 방식) */ export const downloadTransactions = async (userId: string): Promise => { if (!isSyncEnabled()) { console.log('[동기화] 다운로드: 동기화 비활성화 상태, 작업 건너뜀'); return; } try { console.log('[동기화] 서버에서 트랜잭션 데이터 다운로드 시작'); // 다운로드 시간 기록 (충돌 감지용) const downloadStartTime = new Date().toISOString(); console.log(`[동기화] 다운로드 시작 시간: ${downloadStartTime}`); // 대용량 데이터 처리를 위한 페이지네이션 설정 const pageSize = 500; // 한 번에 가져올 최대 레코드 수 let lastId = null; let allServerData = []; let hasMore = true; // 페이지네이션을 사용하여 모든 데이터 가져오기 while (hasMore) { let query = supabase .from('transactions') .select('*') .eq('user_id', userId) .order('id', { ascending: true }) .limit(pageSize); // 마지막 ID 이후의 데이터만 가져오기 if (lastId) { query = query.gt('id', lastId); } const { data, error } = await query; if (error) { console.error('[동기화] 트랜잭션 다운로드 실패:', error); console.error('[동기화] 오류 상세:', JSON.stringify(error, null, 2)); throw error; } if (!data || data.length === 0) { hasMore = false; } else { allServerData = [...allServerData, ...data]; lastId = data[data.length - 1].id; // 마지막 페이지인지 확인 if (data.length < pageSize) { hasMore = false; } console.log(`[동기화] 페이지 다운로드 완료: ${data.length}개 항목`); } } console.log(`[동기화] 서버 데이터 다운로드 완료: 총 ${allServerData.length}개 항목`); if (allServerData.length === 0) { console.log('[동기화] 서버에 저장된 트랜잭션 없음'); return; // 서버에 데이터가 없으면 로컬 데이터 유지 } // 삭제된 트랜잭션 ID 목록 가져오기 const deletedIds = getDeletedTransactions(); console.log(`[동기화] 삭제된 트랜잭션 ${deletedIds.length}개 필터링 적용`); if (deletedIds.length > 0) { console.log(`[동기화] 삭제 추적 항목: ${deletedIds.slice(0, 5).join(', ')}${deletedIds.length > 5 ? '...' : ''}`); } // 서버 데이터를 로컬 형식으로 변환 (삭제된 항목 제외) const serverTransactions = allServerData .filter(t => { const transactionId = t.transaction_id || t.id; const isDeleted = isTransactionDeleted(transactionId); if (isDeleted) { console.log(`[동기화] 삭제된 트랜잭션 필터링: ${transactionId}`); } return !isDeleted; // 삭제된 항목 제외 }) .map(t => { try { // 날짜 형식 변환 시 오류 방지 처리 let formattedDate = '날짜 없음'; try { if (t.date) { // ISO 형식이 아닌 경우 기본 변환 수행 if (!t.date.match(/^\d{4}-\d{2}-\d{2}T/)) { console.log(`[동기화] 비표준 날짜 형식 감지: ${t.date}, ID: ${t.transaction_id || t.id}`); // 유효한 Date 객체로 변환 가능한지 확인 const testDate = new Date(t.date); if (isNaN(testDate.getTime())) { console.warn(`[동기화] 잘못된 날짜 형식 감지, 현재 날짜 사용: ${t.date}`); t.date = new Date().toISOString(); // 잘못된 날짜는 현재 날짜로 대체 } } formattedDate = formatDateForDisplay(t.date); } } catch (err) { console.error(`[동기화] 날짜 변환 오류 (ID: ${t.transaction_id || t.id}):`, err); // 오류 발생 시 기본값 사용 formattedDate = new Date().toLocaleString('ko-KR'); } return { id: t.transaction_id || t.id, title: t.title || '무제', amount: t.amount || 0, date: formattedDate, category: t.category || '기타', type: t.type || 'expense', notes: t.notes || '', serverTimestamp: t.updated_at || t.created_at || downloadStartTime }; } catch (itemError) { console.error(`[동기화] 트랜잭션 변환 오류 (ID: ${t.transaction_id || t.id}):`, itemError); // 오류 발생 시 기본 객체 반환 return { id: t.transaction_id || t.id, title: '데이터 오류', amount: 0, date: new Date().toLocaleString('ko-KR'), category: '기타', type: 'expense', notes: '데이터 변환 중 오류 발생', serverTimestamp: downloadStartTime }; } }); console.log(`[동기화] 서버 트랜잭션 변환 완료: ${serverTransactions.length}개 항목`); // 기존 로컬 데이터 불러오기 const localDataStr = localStorage.getItem('transactions'); const localTransactions: Transaction[] = localDataStr ? JSON.parse(localDataStr) : []; console.log(`[동기화] 로컬 트랜잭션: ${localTransactions.length}개 항목`); // 로컬 데이터와 서버 데이터 병합 (ID 기준) const transactionMap = new Map(); // 로컬 데이터를 맵에 추가 localTransactions.forEach((tx: Transaction) => { if (tx && tx.id) { // 유효성 검사 추가 // 로컬 항목에 타임스탬프 추가 (없는 경우) const txWithTimestamp = { ...tx, localTimestamp: tx.localTimestamp || downloadStartTime }; transactionMap.set(tx.id, txWithTimestamp); } }); // 충돌 카운터 let overwrittenCount = 0; let preservedCount = 0; // 서버 데이터로 맵 업데이트 (타임스탬프 비교) serverTransactions.forEach(tx => { if (tx && tx.id) { // 유효성 검사 추가 const existingTx = transactionMap.get(tx.id); if (!existingTx) { // 로컬에 없는 새 항목 transactionMap.set(tx.id, tx); console.log(`[동기화] 새 항목 추가: ${tx.id} - ${tx.title}`); } else { // 타임스탬프 비교로 최신 데이터 결정 const serverTime = tx.serverTimestamp || downloadStartTime; const localTime = existingTx.localTimestamp || '1970-01-01T00:00:00Z'; if (serverTime > localTime) { // 서버 데이터가 더 최신 transactionMap.set(tx.id, tx); overwrittenCount++; console.log(`[동기화] 서버 데이터로 업데이트: ${tx.id} - ${tx.title} (서버: ${serverTime}, 로컬: ${localTime})`); } else { // 로컬 데이터 유지 preservedCount++; console.log(`[동기화] 로컬 데이터 유지: ${tx.id} - ${existingTx.title} (서버: ${serverTime}, 로컬: ${localTime})`); } } } }); // 최종 병합된 데이터 생성 const mergedTransactions = Array.from(transactionMap.values()); console.log(`[동기화] 병합 결과: 총 ${mergedTransactions.length}개 항목 (서버 데이터로 업데이트: ${overwrittenCount}, 로컬 데이터 유지: ${preservedCount})`); // 로컬 스토리지에 저장 localStorage.setItem('transactions', JSON.stringify(mergedTransactions)); console.log(`[동기화] 병합된 트랜잭션 저장 완료`); // 백업 저장 localStorage.setItem('transactions_backup', JSON.stringify(mergedTransactions)); console.log(`[동기화] 트랜잭션 백업 저장 완료`); // 이벤트 발생시켜 UI 업데이트 window.dispatchEvent(new Event('transactionUpdated')); console.log(`[동기화] 트랜잭션 업데이트 이벤트 발생`); } catch (error) { console.error('[동기화] 트랜잭션 다운로드 중 오류:', error); console.error('[동기화] 오류 상세:', JSON.stringify(error, null, 2)); throw error; } };