Refactor form component

Refactor the form component to use a hybrid approach.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-15 05:09:25 +00:00
parent df257a948b
commit 1ad6e5b685
7 changed files with 518 additions and 5 deletions

133
package-lock.json generated
View File

@@ -40,6 +40,7 @@
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.4",
"@supabase/supabase-js": "^2.49.1",
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -2833,6 +2834,80 @@
"win32" "win32"
] ]
}, },
"node_modules/@supabase/auth-js": {
"version": "2.68.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.68.0.tgz",
"integrity": "sha512-odG7nb7aOmZPUXk6SwL2JchSsn36Ppx11i2yWMIc/meUO2B2HK9YwZHPK06utD9Ql9ke7JKDbwGin/8prHKxxQ==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz",
"integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/node-fetch": {
"version": "2.6.15",
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.2.tgz",
"integrity": "sha512-MXRbk4wpwhWl9IN6rIY1mR8uZCCG4MZAEji942ve6nMwIqnBgBnZhZlON6zTTs6fgveMnoCILpZv1+K91jN+ow==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz",
"integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14",
"@types/phoenix": "^1.5.4",
"@types/ws": "^8.5.10",
"ws": "^8.18.0"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
"integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.49.1",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.1.tgz",
"integrity": "sha512-lKaptKQB5/juEF5+jzmBeZlz69MdHZuxf+0f50NwhL+IE//m4ZnOeWlsKRjjsM0fVayZiQKqLvYdBn0RLkhGiQ==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.68.0",
"@supabase/functions-js": "2.4.4",
"@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "1.19.2",
"@supabase/realtime-js": "2.11.2",
"@supabase/storage-js": "2.7.1"
}
},
"node_modules/@swc/core": { "node_modules/@swc/core": {
"version": "1.7.39", "version": "1.7.39",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.39.tgz", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.39.tgz",
@@ -3208,6 +3283,12 @@
"undici-types": "~6.19.2" "undici-types": "~6.19.2"
} }
}, },
"node_modules/@types/phoenix": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
"license": "MIT"
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.13", "version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
@@ -3242,6 +3323,15 @@
"integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.11.0", "version": "8.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
@@ -7631,6 +7721,12 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tree-kill": { "node_modules/tree-kill": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -7926,6 +8022,22 @@
} }
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8039,6 +8151,27 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml2js": { "node_modules/xml2js": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",

View File

@@ -43,6 +43,7 @@
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.4",
"@supabase/supabase-js": "^2.49.1",
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { PlusIcon, X, Coffee, Home, Car } from 'lucide-react'; import { PlusIcon, X, Coffee, Home, Car } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -10,6 +9,8 @@ import { Button } from './ui/button';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group'; import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group';
import { toast } from '@/components/ui/use-toast'; import { toast } from '@/components/ui/use-toast';
import { supabase } from '@/lib/supabase';
import { isSyncEnabled } from '@/utils/syncUtils';
interface ExpenseFormValues { interface ExpenseFormValues {
title: string; title: string;
@@ -17,8 +18,6 @@ interface ExpenseFormValues {
category: string; category: string;
} }
const EXPENSE_CATEGORIES = ['식비', '생활비', '교통비'];
// Define category icons mapping // Define category icons mapping
const categoryIcons: Record<string, React.ReactNode> = { const categoryIcons: Record<string, React.ReactNode> = {
: <Coffee size={18} />, : <Coffee size={18} />,
@@ -28,6 +27,7 @@ const categoryIcons: Record<string, React.ReactNode> = {
const AddTransactionButton = () => { const AddTransactionButton = () => {
const [showExpenseDialog, setShowExpenseDialog] = useState(false); const [showExpenseDialog, setShowExpenseDialog] = useState(false);
const [userId, setUserId] = useState<string | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm<ExpenseFormValues>({ const form = useForm<ExpenseFormValues>({
@@ -38,6 +38,16 @@ const AddTransactionButton = () => {
} }
}); });
useEffect(() => {
// 현재 로그인한 사용자 가져오기
const getUser = async () => {
const { data: { user } } = await supabase.auth.getUser();
setUserId(user?.id || null);
};
getUser();
}, []);
// Format number with commas // Format number with commas
const formatWithCommas = (value: string): string => { const formatWithCommas = (value: string): string => {
// Remove commas first to avoid duplicates when typing // Remove commas first to avoid duplicates when typing
@@ -50,7 +60,7 @@ const AddTransactionButton = () => {
form.setValue('amount', formattedValue); form.setValue('amount', formattedValue);
}; };
const onSubmit = (data: ExpenseFormValues) => { const onSubmit = async (data: ExpenseFormValues) => {
// Remove commas before processing the amount // Remove commas before processing the amount
const numericAmount = data.amount.replace(/,/g, ''); const numericAmount = data.amount.replace(/,/g, '');
@@ -75,6 +85,26 @@ const AddTransactionButton = () => {
existingTransactions = [newExpense, ...existingTransactions]; existingTransactions = [newExpense, ...existingTransactions];
localStorage.setItem('transactions', JSON.stringify(existingTransactions)); localStorage.setItem('transactions', JSON.stringify(existingTransactions));
// 동기화가 활성화되어 있고 사용자가 로그인되어 있다면 Supabase에도 저장
if (isSyncEnabled() && userId) {
try {
const { error } = await supabase.from('transactions').insert({
user_id: userId,
title: data.title,
amount: parseInt(numericAmount),
date: formattedDate,
category: data.category,
type: 'expense',
transaction_id: newExpense.id
});
if (error) throw error;
} catch (error) {
console.error('Supabase에 지출 추가 실패:', error);
// 실패해도 로컬에는 저장되어 있으므로 사용자에게 알리지 않음
}
}
// 폼을 초기화하고 다이얼로그를 닫습니다 // 폼을 초기화하고 다이얼로그를 닫습니다
form.reset(); form.reset();
setShowExpenseDialog(false); setShowExpenseDialog(false);

View File

@@ -0,0 +1,123 @@
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Loader2, CloudSync } from 'lucide-react';
import { isSyncEnabled, setSyncEnabled, syncAllData } from '@/utils/syncUtils';
import { supabase } from '@/lib/supabase';
import { toast } from '@/components/ui/use-toast';
const SyncSettings = () => {
const [syncEnabled, setSyncEnabledState] = useState(isSyncEnabled());
const [loading, setLoading] = useState(false);
const [userId, setUserId] = useState<string | null>(null);
useEffect(() => {
// 현재 로그인한 사용자 가져오기
const getUser = async () => {
const { data: { user } } = await supabase.auth.getUser();
setUserId(user?.id || null);
};
getUser();
}, []);
const handleSyncToggle = (checked: boolean) => {
setSyncEnabledState(checked);
setSyncEnabled(checked);
if (checked) {
toast({
title: "동기화 활성화됨",
description: "데이터가 클라우드에 자동으로 백업됩니다.",
});
} else {
toast({
title: "동기화 비활성화됨",
description: "데이터가 이 기기에만 저장됩니다.",
});
}
};
const handleSyncNow = async () => {
if (!userId) {
toast({
title: "로그인이 필요합니다",
description: "동기화를 사용하려면 먼저 로그인해주세요.",
variant: "destructive"
});
return;
}
setLoading(true);
try {
await syncAllData(userId);
toast({
title: "동기화 완료",
description: "모든 데이터가 성공적으로 동기화되었습니다."
});
} catch (error) {
console.error("동기화 오류:", error);
toast({
title: "동기화 실패",
description: "데이터 동기화 중 오류가 발생했습니다.",
variant: "destructive"
});
} finally {
setLoading(false);
}
};
return (
<div className="neuro-card space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium"> </h3>
<p className="text-sm text-gray-500">
</p>
</div>
<Switch
checked={syncEnabled}
onCheckedChange={handleSyncToggle}
disabled={!userId || loading}
/>
</div>
{syncEnabled && (
<div className="pt-2">
<Button
onClick={handleSyncNow}
disabled={loading || !userId}
className="w-full"
variant="outline"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<CloudSync className="mr-2 h-4 w-4" />
</>
)}
</Button>
<p className="text-xs text-gray-500 mt-2 text-center">
: {new Date().toLocaleString('ko-KR')}
</p>
</div>
)}
{!userId && (
<p className="text-xs text-gray-500 text-center">
</p>
)}
</div>
);
};
export default SyncSettings;

8
src/lib/supabase.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createClient } from '@supabase/supabase-js';
// Supabase URL과 anon key는 실제 프로젝트 값으로 대체해야 합니다
const supabaseUrl = 'YOUR_SUPABASE_URL';
const supabaseAnonKey = 'YOUR_SUPABASE_ANON_KEY';
export const supabase = createClient(supabaseUrl, supabaseAnonKey);

View File

@@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import NavBar from '@/components/NavBar'; import NavBar from '@/components/NavBar';
import SyncSettings from '@/components/SyncSettings';
import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from 'lucide-react'; import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -52,6 +53,12 @@ const Settings = () => {
</div> </div>
</header> </header>
{/* Data Sync Settings */}
<div className="mb-8">
<h2 className="text-sm font-medium text-gray-500 mb-2 px-2"> </h2>
<SyncSettings />
</div>
{/* Settings Options */} {/* Settings Options */}
<div className="space-y-4 mb-8"> <div className="space-y-4 mb-8">
<h2 className="text-sm font-medium text-gray-500 mb-2 px-2"></h2> <h2 className="text-sm font-medium text-gray-500 mb-2 px-2"></h2>

211
src/utils/syncUtils.ts Normal file
View File

@@ -0,0 +1,211 @@
import { supabase } from '@/lib/supabase';
import { Transaction } from '@/components/TransactionCard';
// 동기화 상태 확인
export const isSyncEnabled = (): boolean => {
return localStorage.getItem('syncEnabled') === 'true';
};
// 동기화 설정 변경
export const setSyncEnabled = (enabled: boolean): void => {
localStorage.setItem('syncEnabled', enabled.toString());
};
// 로컬 트랜잭션 데이터를 Supabase에 업로드
export const uploadTransactions = async (userId: string): Promise<void> => {
if (!isSyncEnabled()) return;
try {
const localTransactions = localStorage.getItem('transactions');
if (!localTransactions) return;
const transactions: Transaction[] = JSON.parse(localTransactions);
// 기존 데이터 삭제 후 새로 업로드
await supabase
.from('transactions')
.delete()
.eq('user_id', userId);
// 트랜잭션 배치 처리
const { error } = await supabase.from('transactions').insert(
transactions.map(t => ({
user_id: userId,
title: t.title,
amount: t.amount,
date: t.date,
category: t.category,
type: t.type,
transaction_id: t.id // 로컬 ID 보존
}))
);
if (error) throw error;
console.log('트랜잭션 업로드 완료');
} catch (error) {
console.error('트랜잭션 업로드 실패:', error);
}
};
// Supabase에서 트랜잭션 데이터 다운로드
export const downloadTransactions = async (userId: string): Promise<void> => {
if (!isSyncEnabled()) return;
try {
const { data, error } = await supabase
.from('transactions')
.select('*')
.eq('user_id', userId);
if (error) throw error;
if (data && data.length > 0) {
// Supabase 형식에서 로컬 형식으로 변환
const transactions = data.map(t => ({
id: t.transaction_id || t.id,
title: t.title,
amount: t.amount,
date: t.date,
category: t.category,
type: t.type
}));
localStorage.setItem('transactions', JSON.stringify(transactions));
console.log('트랜잭션 다운로드 완료');
}
} catch (error) {
console.error('트랜잭션 다운로드 실패:', error);
}
};
// 예산 데이터 업로드
export const uploadBudgets = async (userId: string): Promise<void> => {
if (!isSyncEnabled()) return;
try {
const budgetData = localStorage.getItem('budgetData');
const categoryBudgets = localStorage.getItem('categoryBudgets');
if (budgetData) {
const parsedBudgetData = JSON.parse(budgetData);
// 기존 예산 데이터 삭제
await supabase
.from('budgets')
.delete()
.eq('user_id', userId);
// 새 예산 데이터 삽입
const { error } = await supabase.from('budgets').insert({
user_id: userId,
daily_target: parsedBudgetData.daily.targetAmount,
weekly_target: parsedBudgetData.weekly.targetAmount,
monthly_target: parsedBudgetData.monthly.targetAmount
});
if (error) throw error;
}
if (categoryBudgets) {
const parsedCategoryBudgets = JSON.parse(categoryBudgets);
// 기존 카테고리 예산 삭제
await supabase
.from('category_budgets')
.delete()
.eq('user_id', userId);
// 카테고리별 예산 데이터 변환 및 삽입
const categoryEntries = Object.entries(parsedCategoryBudgets).map(
([category, amount]) => ({
user_id: userId,
category,
amount
})
);
const { error } = await supabase
.from('category_budgets')
.insert(categoryEntries);
if (error) throw error;
}
console.log('예산 데이터 업로드 완료');
} catch (error) {
console.error('예산 데이터 업로드 실패:', error);
}
};
// 예산 데이터 다운로드
export const downloadBudgets = async (userId: string): Promise<void> => {
if (!isSyncEnabled()) return;
try {
// 예산 데이터 가져오기
const { data: budgetData, error: budgetError } = await supabase
.from('budgets')
.select('*')
.eq('user_id', userId)
.single();
if (budgetError && budgetError.code !== 'PGRST116') throw budgetError;
// 카테고리 예산 가져오기
const { data: categoryData, error: categoryError } = await supabase
.from('category_budgets')
.select('*')
.eq('user_id', userId);
if (categoryError) throw categoryError;
if (budgetData) {
// 예산 데이터 로컬 형식으로 변환
const localBudgetData = {
daily: {
targetAmount: budgetData.daily_target,
spentAmount: 0, // 지출액은 로컬에서 계산
remainingAmount: budgetData.daily_target
},
weekly: {
targetAmount: budgetData.weekly_target,
spentAmount: 0,
remainingAmount: budgetData.weekly_target
},
monthly: {
targetAmount: budgetData.monthly_target,
spentAmount: 0,
remainingAmount: budgetData.monthly_target
}
};
localStorage.setItem('budgetData', JSON.stringify(localBudgetData));
}
if (categoryData && categoryData.length > 0) {
// 카테고리 예산 로컬 형식으로 변환
const localCategoryBudgets = categoryData.reduce((acc, curr) => {
acc[curr.category] = curr.amount;
return acc;
}, {} as Record<string, number>);
localStorage.setItem('categoryBudgets', JSON.stringify(localCategoryBudgets));
}
console.log('예산 데이터 다운로드 완료');
} catch (error) {
console.error('예산 데이터 다운로드 실패:', error);
}
};
// 전체 데이터 동기화
export const syncAllData = async (userId: string): Promise<void> => {
if (!userId || !isSyncEnabled()) return;
await downloadTransactions(userId);
await downloadBudgets(userId);
await uploadTransactions(userId);
await uploadBudgets(userId);
};