Refactor form component
Refactor the form component to use a hybrid approach.
This commit is contained in:
133
package-lock.json
generated
133
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
123
src/components/SyncSettings.tsx
Normal file
123
src/components/SyncSettings.tsx
Normal 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
8
src/lib/supabase.ts
Normal 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);
|
||||||
@@ -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
211
src/utils/syncUtils.ts
Normal 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);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user