Migrate from Supabase to Appwrite with core functionality and UI components

This commit is contained in:
hansoo
2025-05-05 08:58:27 +09:00
parent fdfdf15166
commit f83bb384af
79 changed files with 2373 additions and 199 deletions

View File

@@ -1,138 +0,0 @@
import { Transaction } from '@/components/TransactionCard';
import { supabase } from '@/lib/supabase';
import { isSyncEnabled } from '@/utils/syncUtils';
import { formatISO } from 'date-fns';
// ISO 형식으로 날짜 변환 (Supabase 저장용)
const convertDateToISO = (dateStr: string): string => {
try {
// 이미 ISO 형식인 경우 그대로 반환
if (dateStr.match(/^\d{4}-\d{2}-\d{2}T/)) {
return dateStr;
}
// "오늘, 시간" 형식 처리
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 객체로 변환 시도
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return formatISO(date);
}
// 변환 실패 시 현재 시간 반환
console.warn(`날짜 변환 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`);
return formatISO(new Date());
} catch (error) {
console.error(`날짜 변환 오류: "${dateStr}"`, error);
return formatISO(new Date());
}
};
// Supabase와 트랜잭션 동기화 - Cloud 최적화 버전
export const syncTransactionsWithSupabase = async (user: any, transactions: Transaction[]): Promise<Transaction[]> => {
if (!user || !isSyncEnabled()) return transactions;
try {
const { data, error } = await supabase
.from('transactions')
.select('*')
.eq('user_id', user.id);
if (error) {
console.error('Supabase 데이터 조회 오류:', error);
return transactions;
}
if (data && data.length > 0) {
// Supabase 데이터 로컬 형식으로 변환
const supabaseTransactions = data.map(t => ({
id: t.transaction_id || t.id,
title: t.title,
amount: t.amount,
date: t.date,
category: t.category,
type: t.type
}));
// 로컬 데이터와 병합 (중복 ID 제거)
const mergedTransactions = [...transactions];
supabaseTransactions.forEach(newTx => {
const existingIndex = mergedTransactions.findIndex(t => t.id === newTx.id);
if (existingIndex >= 0) {
mergedTransactions[existingIndex] = newTx;
} else {
mergedTransactions.push(newTx);
}
});
return mergedTransactions;
}
} catch (err) {
console.error('Supabase 동기화 오류:', err);
}
return transactions;
};
// Supabase에 트랜잭션 업데이트 - Cloud 최적화 버전
export const updateTransactionInSupabase = async (user: any, transaction: Transaction): Promise<void> => {
if (!user || !isSyncEnabled()) return;
try {
// 날짜를 ISO 형식으로 변환
const isoDate = convertDateToISO(transaction.date);
const { error } = await supabase.from('transactions')
.upsert({
user_id: user.id,
title: transaction.title,
amount: transaction.amount,
date: isoDate, // ISO 형식 사용
category: transaction.category,
type: transaction.type,
transaction_id: transaction.id
});
if (error) {
console.error('트랜잭션 업데이트 오류:', error);
} else {
console.log('Supabase 트랜잭션 업데이트 성공:', transaction.id);
}
} catch (error) {
console.error('Supabase 업데이트 오류:', error);
}
};
// Supabase에서 트랜잭션 삭제 - Cloud 최적화 버전
export const deleteTransactionFromSupabase = async (user: any, transactionId: string): Promise<void> => {
if (!user || !isSyncEnabled()) return;
try {
const { error } = await supabase.from('transactions')
.delete()
.eq('transaction_id', transactionId);
if (error) {
console.error('트랜잭션 삭제 오류:', error);
} else {
console.log('Supabase 트랜잭션 삭제 성공:', transactionId);
}
} catch (error) {
console.error('Supabase 삭제 오류:', error);
}
};

View File

@@ -0,0 +1,162 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Transaction } from '@/components/TransactionCard';
import {
syncTransactionsWithAppwrite,
updateTransactionInAppwrite,
deleteTransactionFromAppwrite,
debouncedDeleteTransaction
} from '@/utils/appwriteTransactionUtils';
import { toast } from '@/hooks/useToast.wrapper';
import { isSyncEnabled } from '@/utils/syncUtils';
/**
* Appwrite 트랜잭션 관리 훅
* 트랜잭션 동기화, 추가, 수정, 삭제 기능 제공
*/
export const useAppwriteTransactions = (user: any, localTransactions: Transaction[]) => {
// 트랜잭션 상태 관리
const [transactions, setTransactions] = useState<Transaction[]>(localTransactions);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
// 컴포넌트 마운트 상태 추적
const isMountedRef = useRef<boolean>(true);
// 진행 중인 작업 추적
const pendingOperations = useRef<Set<string>>(new Set());
// 트랜잭션 동기화
const syncTransactions = useCallback(async () => {
if (!user || !isSyncEnabled()) return localTransactions;
try {
setLoading(true);
setError(null);
// UI 스레드 차단 방지
await new Promise(resolve => setTimeout(resolve, 0));
const syncedTransactions = await syncTransactionsWithAppwrite(user, localTransactions);
if (isMountedRef.current) {
setTransactions(syncedTransactions);
setLoading(false);
}
return syncedTransactions;
} catch (err) {
console.error('트랜잭션 동기화 오류:', err);
if (isMountedRef.current) {
setError(err as Error);
setLoading(false);
}
return localTransactions;
}
}, [user, localTransactions]);
// 트랜잭션 추가/수정
const saveTransaction = useCallback(async (transaction: Transaction) => {
if (!user || !isSyncEnabled()) return;
try {
// 작업 추적 시작
pendingOperations.current.add(transaction.id);
// UI 스레드 차단 방지
await new Promise(resolve => requestAnimationFrame(resolve));
await updateTransactionInAppwrite(user, transaction);
if (!isMountedRef.current) return;
// 로컬 상태 업데이트
setTransactions(prev => {
const index = prev.findIndex(t => t.id === transaction.id);
if (index >= 0) {
const updated = [...prev];
updated[index] = transaction;
return updated;
} else {
return [...prev, transaction];
}
});
} catch (err) {
console.error('트랜잭션 저장 오류:', err);
if (isMountedRef.current) {
toast({
title: '저장 실패',
description: '트랜잭션을 저장하는 중 오류가 발생했습니다.',
variant: 'destructive'
});
}
} finally {
// 작업 추적 종료
pendingOperations.current.delete(transaction.id);
}
}, [user]);
// 트랜잭션 삭제
const removeTransaction = useCallback(async (transactionId: string) => {
if (!user || !isSyncEnabled()) return;
try {
// 작업 추적 시작
pendingOperations.current.add(transactionId);
// 로컬 상태 먼저 업데이트 (낙관적 UI 업데이트)
setTransactions(prev => prev.filter(t => t.id !== transactionId));
// 디바운스된 삭제 작업 실행 (여러 번 연속 호출 방지)
await debouncedDeleteTransaction(user, transactionId);
} catch (err) {
console.error('트랜잭션 삭제 오류:', err);
if (isMountedRef.current) {
toast({
title: '삭제 실패',
description: '트랜잭션을 삭제하는 중 오류가 발생했습니다.',
variant: 'destructive'
});
// 실패 시 트랜잭션 복원 (서버에서 가져오기)
syncTransactions();
}
} finally {
// 작업 추적 종료
pendingOperations.current.delete(transactionId);
}
}, [user, syncTransactions]);
// 초기 동기화
useEffect(() => {
if (user && isSyncEnabled()) {
syncTransactions();
} else {
setTransactions(localTransactions);
}
}, [user, localTransactions, syncTransactions]);
// 컴포넌트 언마운트 시 정리
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
return {
transactions,
loading,
error,
syncTransactions,
saveTransaction,
removeTransaction,
hasPendingOperations: pendingOperations.current.size > 0
};
};
export default useAppwriteTransactions;