Migrate from Supabase to Appwrite with core functionality and UI components
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
162
src/hooks/transactions/useAppwriteTransactions.ts
Normal file
162
src/hooks/transactions/useAppwriteTransactions.ts
Normal 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;
|
||||
Reference in New Issue
Block a user