diff --git a/src/utils/sync/budget/index.ts b/src/utils/sync/budget/index.ts index bc2cd64..3b427a2 100644 --- a/src/utils/sync/budget/index.ts +++ b/src/utils/sync/budget/index.ts @@ -1,5 +1,20 @@ -// 예산 동기화 관련 모듈 +/** + * 예산 동기화 관련 모듈 + * 모든 예산 관련 함수를 하나의 파일에서 내보냅니다. + */ + +// 예산 업로드 관련 함수 export * from './uploadBudget'; +export * from './uploadMonthlyBudget'; +export * from './uploadCategoryBudgets'; + +// 예산 다운로드 관련 함수 export * from './downloadBudget'; + +// 예산 추적 관련 함수 export * from './modifiedBudgetsTracker'; + +// 타입 정의 및 유틸리티 +export * from './types'; +export * from './validators'; diff --git a/src/utils/sync/budget/types.ts b/src/utils/sync/budget/types.ts new file mode 100644 index 0000000..2cd51be --- /dev/null +++ b/src/utils/sync/budget/types.ts @@ -0,0 +1,50 @@ + +/** + * 예산 동기화 관련 타입 정의 + */ + +// 예산 데이터 타입 +export interface BudgetData { + daily: BudgetPeriod; + weekly: BudgetPeriod; + monthly: BudgetPeriod; + [key: string]: BudgetPeriod; +} + +// 기간별 예산 타입 +export interface BudgetPeriod { + targetAmount: number; + spentAmount: number; + remainingAmount: number; + [key: string]: number; +} + +// 카테고리 예산 타입 +export type CategoryBudgets = Record; + +// 예산 업로드 결과 타입 +export interface BudgetUploadResult { + success: boolean; + message: string; +} + +// 데이터베이스 예산 항목 타입 +export interface BudgetRecord { + id?: string; + user_id: string; + month: number; + year: number; + total_budget: number; + created_at?: string; + updated_at: string; +} + +// 데이터베이스 카테고리 예산 항목 타입 +export interface CategoryBudgetRecord { + id?: string; + user_id: string; + category: string; + amount: number; + created_at?: string; + updated_at: string; +} diff --git a/src/utils/sync/budget/uploadBudget.ts b/src/utils/sync/budget/uploadBudget.ts index 80615bf..0f4f819 100644 --- a/src/utils/sync/budget/uploadBudget.ts +++ b/src/utils/sync/budget/uploadBudget.ts @@ -1,10 +1,16 @@ -import { supabase } from '@/lib/supabase'; +/** + * 예산 데이터 업로드 메인 모듈 + */ import { isSyncEnabled } from '../syncSettings'; import { clearModifiedBudget, clearModifiedCategoryBudgets } from './modifiedBudgetsTracker'; +import { uploadMonthlyBudget } from './uploadMonthlyBudget'; +import { uploadCategoryBudgets } from './uploadCategoryBudgets'; +import { isValidMonthlyBudget, isValidCategoryBudgets } from './validators'; +import { BudgetData, CategoryBudgets } from './types'; /** * 예산 데이터를 서버에 업로드 @@ -20,16 +26,23 @@ export const uploadBudgets = async (userId: string): Promise => { // 예산 데이터 업로드 if (budgetDataStr) { - const budgetData = JSON.parse(budgetDataStr); - - // 월간 예산이 0보다 클 때만 업로드 - if (budgetData.monthly && typeof budgetData.monthly.targetAmount === 'number' && budgetData.monthly.targetAmount > 0) { - console.log('유효한 월간 예산 발견:', budgetData.monthly.targetAmount); - await uploadBudgetData(userId, budgetData); - // 업로드 성공 후 수정 추적 정보 초기화 - clearModifiedBudget(); - } else { - console.log('월간 예산이 0 이하거나 없어서 업로드 건너뜀'); + try { + const budgetData: BudgetData = JSON.parse(budgetDataStr); + + // 월간 예산이 유효할 때만 업로드 + if (isValidMonthlyBudget(budgetData)) { + console.log('유효한 월간 예산 발견:', budgetData.monthly.targetAmount); + const uploadSuccess = await uploadMonthlyBudget(userId, budgetData); + + // 업로드 성공 후 수정 추적 정보 초기화 + if (uploadSuccess) { + clearModifiedBudget(); + } + } else { + console.log('월간 예산이 0 이하거나 없어서 업로드 건너뜀'); + } + } catch (error) { + console.error('월간 예산 데이터 파싱 또는 업로드 오류:', error); } } else { console.log('업로드할 예산 데이터가 없음'); @@ -37,17 +50,23 @@ export const uploadBudgets = async (userId: string): Promise => { // 카테고리 예산 업로드 if (categoryBudgetsStr) { - const categoryBudgets = JSON.parse(categoryBudgetsStr); - - // 총 카테고리 예산이 0보다 클 때만 업로드 - const totalCategoryBudget = Object.values(categoryBudgets).reduce((sum: number, val: number) => sum + val, 0); - if (totalCategoryBudget > 0) { - console.log('유효한 카테고리 예산 발견:', totalCategoryBudget); - await uploadCategoryBudgets(userId, categoryBudgets); - // 업로드 성공 후 수정 추적 정보 초기화 - clearModifiedCategoryBudgets(); - } else { - console.log('카테고리 예산이 모두 0이어서 업로드 건너뜀'); + try { + const categoryBudgets: CategoryBudgets = JSON.parse(categoryBudgetsStr); + + // 총 카테고리 예산이 유효할 때만 업로드 + if (isValidCategoryBudgets(categoryBudgets)) { + console.log('유효한 카테고리 예산 발견'); + const uploadSuccess = await uploadCategoryBudgets(userId, categoryBudgets); + + // 업로드 성공 후 수정 추적 정보 초기화 + if (uploadSuccess) { + clearModifiedCategoryBudgets(); + } + } else { + console.log('카테고리 예산이 모두 0이어서 업로드 건너뜀'); + } + } catch (error) { + console.error('카테고리 예산 데이터 파싱 또는 업로드 오류:', error); } } else { console.log('업로드할 카테고리 예산이 없음'); @@ -59,171 +78,3 @@ export const uploadBudgets = async (userId: string): Promise => { throw error; } }; - -/** - * 일반 예산 데이터 업로드 - */ -async function uploadBudgetData(userId: string, parsedBudgetData: Record): Promise { - console.log('예산 데이터 업로드:', parsedBudgetData); - - // 현재 월/년도 가져오기 - const now = new Date(); - const currentMonth = now.getMonth() + 1; // 0-11 -> 1-12 - const currentYear = now.getFullYear(); - - // 기존 예산 데이터 확인 - const { data: existingBudgets, error: fetchError } = await supabase - .from('budgets') - .select('*') - .eq('user_id', userId) - .eq('month', currentMonth) - .eq('year', currentYear); - - if (fetchError) { - console.error('기존 예산 데이터 조회 실패:', fetchError); - throw fetchError; - } - - // 월간 타겟 금액 가져오기 - const monthlyTarget = parsedBudgetData.monthly.targetAmount; - - // 예산이 0 이하면 업로드하지 않음 - if (typeof monthlyTarget !== 'number' || monthlyTarget <= 0) { - console.log('월간 예산이 0 이하여서 업로드 건너뜀:', monthlyTarget); - return; - } - - console.log('업로드할 월간 예산:', monthlyTarget); - - // 현재 타임스탬프 - const currentTimestamp = new Date().toISOString(); - - // 가능한 경우 서버 데이터와 비교하여 필요한 경우만 업데이트 - if (existingBudgets && existingBudgets.length > 0) { - const existingBudget = existingBudgets[0]; - // 새 예산이 기존 예산보다 클 때만 업데이트 - if (typeof existingBudget.total_budget === 'number' && monthlyTarget > existingBudget.total_budget) { - console.log(`새 예산(${monthlyTarget})이 기존 예산(${existingBudget.total_budget})보다 큼, 업데이트 실행`); - - // 기존 데이터 업데이트 - const { error } = await supabase - .from('budgets') - .update({ - total_budget: monthlyTarget, - updated_at: currentTimestamp - }) - .eq('id', existingBudget.id); - - if (error) { - console.error('예산 데이터 업데이트 실패:', error); - throw error; - } - - console.log('예산 데이터 업데이트 성공'); - } else { - console.log(`새 예산(${monthlyTarget})이 기존 예산(${existingBudget.total_budget})보다 작거나 같음, 업데이트 건너뜀`); - } - } else { - // 새 데이터 삽입 - const { error } = await supabase - .from('budgets') - .insert({ - user_id: userId, - month: currentMonth, - year: currentYear, - total_budget: monthlyTarget, - created_at: currentTimestamp, - updated_at: currentTimestamp - }); - - if (error) { - console.error('예산 데이터 삽입 실패:', error); - throw error; - } - - console.log('예산 데이터 삽입 성공'); - } -} - -/** - * 카테고리 예산 데이터 업로드 - */ -async function uploadCategoryBudgets(userId: string, parsedCategoryBudgets: Record): Promise { - console.log('카테고리 예산 업로드:', parsedCategoryBudgets); - - // 기존 카테고리 예산 확인 - const { data: existingCategoryBudgets, error: fetchError } = await supabase - .from('category_budgets') - .select('*') - .eq('user_id', userId); - - if (fetchError) { - console.error('기존 카테고리 예산 조회 실패:', fetchError); - throw fetchError; - } - - // 기존 카테고리 예산의 총액 계산 - let existingTotal = 0; - if (existingCategoryBudgets && existingCategoryBudgets.length > 0) { - existingTotal = existingCategoryBudgets.reduce((sum, item) => { - return sum + (typeof item.amount === 'number' ? item.amount : 0); - }, 0); - } - - // 새 카테고리 예산의 총액 계산 - const newTotal = Object.values(parsedCategoryBudgets).reduce((sum: number, val: number) => sum + val, 0); - - // 새 카테고리 예산 총액이 기존 카테고리 예산 총액보다 작거나 같으면 업로드 건너뜀 - if (newTotal <= existingTotal && existingTotal > 0) { - console.log(`새 카테고리 예산 총액(${newTotal})이 기존 예산 총액(${existingTotal})보다 작거나 같음, 업로드 건너뜀`); - return; - } - - // 새 카테고리 예산 총액이 0이면 업로드 건너뜀 - if (newTotal <= 0) { - console.log('새 카테고리 예산 총액이 0이하여서 업로드 건너뜀'); - return; - } - - console.log(`새 카테고리 예산 총액(${newTotal})이 기존 예산 총액(${existingTotal})보다 큼, 업로드 실행`); - - // 기존 카테고리 예산 삭제 - const { error: deleteError } = await supabase - .from('category_budgets') - .delete() - .eq('user_id', userId); - - if (deleteError) { - console.error('기존 카테고리 예산 삭제 실패:', deleteError); - // 오류가 나도 계속 진행 (중요 데이터가 아니기 때문) - } - - // 현재 타임스탬프 - const currentTimestamp = new Date().toISOString(); - - // 카테고리별 예산 데이터 변환 및 삽입 - const categoryEntries = Object.entries(parsedCategoryBudgets) - .filter(([_, amount]) => typeof amount === 'number' && amount > 0) // 금액이 0보다 큰 것만 저장 - .map(([category, amount]) => ({ - user_id: userId, - category, - amount, - created_at: currentTimestamp, - updated_at: currentTimestamp - })); - - if (categoryEntries.length > 0) { - const { error } = await supabase - .from('category_budgets') - .insert(categoryEntries); - - if (error) { - console.error('카테고리 예산 삽입 실패:', error); - throw error; - } - - console.log('카테고리 예산 삽입 성공:', categoryEntries.length, '개'); - } else { - console.log('저장할 카테고리 예산이 없음'); - } -} diff --git a/src/utils/sync/budget/uploadCategoryBudgets.ts b/src/utils/sync/budget/uploadCategoryBudgets.ts new file mode 100644 index 0000000..0751c97 --- /dev/null +++ b/src/utils/sync/budget/uploadCategoryBudgets.ts @@ -0,0 +1,92 @@ + +/** + * 카테고리 예산 업로드 기능 + */ +import { supabase } from '@/lib/supabase'; +import { CategoryBudgets, CategoryBudgetRecord } from './types'; +import { calculateTotalCategoryBudget, filterValidCategoryBudgets } from './validators'; + +/** + * 카테고리 예산 데이터 업로드 + */ +export async function uploadCategoryBudgets(userId: string, parsedCategoryBudgets: CategoryBudgets): Promise { + console.log('카테고리 예산 업로드:', parsedCategoryBudgets); + + // 기존 카테고리 예산 확인 + const { data: existingCategoryBudgets, error: fetchError } = await supabase + .from('category_budgets') + .select('*') + .eq('user_id', userId); + + if (fetchError) { + console.error('기존 카테고리 예산 조회 실패:', fetchError); + throw fetchError; + } + + // 기존 카테고리 예산의 총액 계산 + let existingTotal = 0; + if (existingCategoryBudgets && existingCategoryBudgets.length > 0) { + existingTotal = existingCategoryBudgets.reduce((sum, item) => { + return sum + (typeof item.amount === 'number' ? item.amount : 0); + }, 0); + } + + // 새 카테고리 예산의 총액 계산 + const newTotal = calculateTotalCategoryBudget(parsedCategoryBudgets); + + // 새 카테고리 예산 총액이 기존 카테고리 예산 총액보다 작거나 같으면 업로드 건너뜀 + if (newTotal <= existingTotal && existingTotal > 0) { + console.log(`새 카테고리 예산 총액(${newTotal})이 기존 예산 총액(${existingTotal})보다 작거나 같음, 업로드 건너뜀`); + return false; + } + + // 새 카테고리 예산 총액이 0이면 업로드 건너뜀 + if (newTotal <= 0) { + console.log('새 카테고리 예산 총액이 0이하여서 업로드 건너뜀'); + return false; + } + + console.log(`새 카테고리 예산 총액(${newTotal})이 기존 예산 총액(${existingTotal})보다 큼, 업로드 실행`); + + // 기존 카테고리 예산 삭제 + const { error: deleteError } = await supabase + .from('category_budgets') + .delete() + .eq('user_id', userId); + + if (deleteError) { + console.error('기존 카테고리 예산 삭제 실패:', deleteError); + // 오류가 나도 계속 진행 (중요 데이터가 아니기 때문) + } + + // 현재 타임스탬프 + const currentTimestamp = new Date().toISOString(); + + // 카테고리별 예산 데이터 변환 및 삽입 + const validCategoryBudgets = filterValidCategoryBudgets(parsedCategoryBudgets); + const categoryEntries: CategoryBudgetRecord[] = Object.entries(validCategoryBudgets) + .map(([category, amount]) => ({ + user_id: userId, + category, + amount, + created_at: currentTimestamp, + updated_at: currentTimestamp + })); + + if (categoryEntries.length > 0) { + const { error } = await supabase + .from('category_budgets') + .insert(categoryEntries); + + if (error) { + console.error('카테고리 예산 삽입 실패:', error); + throw error; + } + + console.log('카테고리 예산 삽입 성공:', categoryEntries.length, '개'); + return true; + } else { + console.log('저장할 카테고리 예산이 없음'); + return false; + } +} diff --git a/src/utils/sync/budget/uploadMonthlyBudget.ts b/src/utils/sync/budget/uploadMonthlyBudget.ts new file mode 100644 index 0000000..0e9108f --- /dev/null +++ b/src/utils/sync/budget/uploadMonthlyBudget.ts @@ -0,0 +1,98 @@ + +/** + * 월간 예산 업로드 기능 + */ +import { supabase } from '@/lib/supabase'; +import { BudgetData, BudgetRecord } from './types'; +import { isValidMonthlyBudget } from './validators'; + +/** + * 월간 예산 데이터 업로드 + */ +export async function uploadMonthlyBudget(userId: string, parsedBudgetData: BudgetData): Promise { + console.log('월간 예산 데이터 업로드:', parsedBudgetData); + + // 월간 타겟 금액 가져오기 + const monthlyTarget = parsedBudgetData.monthly.targetAmount; + + // 예산이 0 이하면 업로드하지 않음 + if (typeof monthlyTarget !== 'number' || monthlyTarget <= 0) { + console.log('월간 예산이 0 이하여서 업로드 건너뜀:', monthlyTarget); + return false; + } + + // 현재 월/년도 가져오기 + const now = new Date(); + const currentMonth = now.getMonth() + 1; // 0-11 -> 1-12 + const currentYear = now.getFullYear(); + + // 기존 예산 데이터 확인 + const { data: existingBudgets, error: fetchError } = await supabase + .from('budgets') + .select('*') + .eq('user_id', userId) + .eq('month', currentMonth) + .eq('year', currentYear); + + if (fetchError) { + console.error('기존 예산 데이터 조회 실패:', fetchError); + throw fetchError; + } + + console.log('업로드할 월간 예산:', monthlyTarget); + + // 현재 타임스탬프 + const currentTimestamp = new Date().toISOString(); + + // 가능한 경우 서버 데이터와 비교하여 필요한 경우만 업데이트 + if (existingBudgets && existingBudgets.length > 0) { + const existingBudget = existingBudgets[0]; + + // 새 예산이 기존 예산보다 클 때만 업데이트 + if (typeof existingBudget.total_budget === 'number' && monthlyTarget > existingBudget.total_budget) { + console.log(`새 예산(${monthlyTarget})이 기존 예산(${existingBudget.total_budget})보다 큼, 업데이트 실행`); + + // 기존 데이터 업데이트 + const { error } = await supabase + .from('budgets') + .update({ + total_budget: monthlyTarget, + updated_at: currentTimestamp + }) + .eq('id', existingBudget.id); + + if (error) { + console.error('예산 데이터 업데이트 실패:', error); + throw error; + } + + console.log('예산 데이터 업데이트 성공'); + return true; + } else { + console.log(`새 예산(${monthlyTarget})이 기존 예산(${existingBudget.total_budget})보다 작거나 같음, 업데이트 건너뜀`); + return false; + } + } else { + // 새 데이터 삽입 + const newBudget: BudgetRecord = { + user_id: userId, + month: currentMonth, + year: currentYear, + total_budget: monthlyTarget, + created_at: currentTimestamp, + updated_at: currentTimestamp + }; + + const { error } = await supabase + .from('budgets') + .insert(newBudget); + + if (error) { + console.error('예산 데이터 삽입 실패:', error); + throw error; + } + + console.log('예산 데이터 삽입 성공'); + return true; + } +} diff --git a/src/utils/sync/budget/validators.ts b/src/utils/sync/budget/validators.ts new file mode 100644 index 0000000..ddc8383 --- /dev/null +++ b/src/utils/sync/budget/validators.ts @@ -0,0 +1,50 @@ + +/** + * 예산 데이터 검증 유틸리티 + */ +import { BudgetData, CategoryBudgets } from './types'; + +/** + * 월간 예산이 유효한지 확인 + */ +export const isValidMonthlyBudget = (budgetData: BudgetData | null): boolean => { + if (!budgetData || !budgetData.monthly) return false; + + const { targetAmount } = budgetData.monthly; + return typeof targetAmount === 'number' && targetAmount > 0; +}; + +/** + * 카테고리 예산이 유효한지 확인 + */ +export const isValidCategoryBudgets = (categoryBudgets: CategoryBudgets | null): boolean => { + if (!categoryBudgets) return false; + + // 총 예산 금액 계산 + const totalAmount = Object.values(categoryBudgets).reduce((sum, amount) => { + return sum + (typeof amount === 'number' ? amount : 0); + }, 0); + + return totalAmount > 0; +}; + +/** + * 카테고리 예산 총액 계산 + */ +export const calculateTotalCategoryBudget = (categoryBudgets: CategoryBudgets | null): number => { + if (!categoryBudgets) return 0; + + return Object.values(categoryBudgets).reduce((sum, amount) => { + return sum + (typeof amount === 'number' ? amount : 0); + }, 0); +}; + +/** + * 유효한 카테고리 예산 항목만 필터링 + */ +export const filterValidCategoryBudgets = (categoryBudgets: CategoryBudgets): CategoryBudgets => { + return Object.fromEntries( + Object.entries(categoryBudgets) + .filter(([_, amount]) => typeof amount === 'number' && amount > 0) + ); +};