diff --git a/package-lock.json b/package-lock.json index da48d6b..b672b68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "@radix-ui/react-tooltip": "^1.1.4", "@supabase/supabase-js": "^2.49.1", "@tanstack/react-query": "^5.56.2", + "@types/react-helmet": "^6.1.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -53,6 +54,7 @@ "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-helmet": "^6.1.0", "react-hook-form": "^7.53.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", @@ -3293,14 +3295,12 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3317,6 +3317,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", @@ -6836,6 +6845,27 @@ "react": "^18.3.1" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-hook-form": { "version": "7.53.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz", @@ -6947,6 +6977,15 @@ "react-dom": ">=16.8" } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-smooth": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", diff --git a/package.json b/package.json index 13e2ce5..93e8717 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@radix-ui/react-tooltip": "^1.1.4", "@supabase/supabase-js": "^2.49.1", "@tanstack/react-query": "^5.56.2", + "@types/react-helmet": "^6.1.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -56,6 +57,7 @@ "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-helmet": "^6.1.0", "react-hook-form": "^7.53.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", diff --git a/src/components/SupabaseTestPanel.tsx b/src/components/SupabaseTestPanel.tsx new file mode 100644 index 0000000..e680612 --- /dev/null +++ b/src/components/SupabaseTestPanel.tsx @@ -0,0 +1,151 @@ + +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { CircleCheck, CircleAlert, RefreshCw, Database } from 'lucide-react'; +import { + testSupabaseConnection, + checkSupabaseEnvironment, + checkSupabaseTables +} from '@/utils/supabaseTest'; + +const SupabaseTestPanel = () => { + const [loading, setLoading] = useState(false); + const [connectionStatus, setConnectionStatus] = useState<{ + tested: boolean; + success?: boolean; + message?: string; + }>({ tested: false }); + + const [envStatus] = useState(() => checkSupabaseEnvironment()); + const [tablesStatus, setTablesStatus] = useState<{ + checked: boolean; + exists?: boolean; + tables?: string[]; + }>({ checked: false }); + + const runConnectionTest = async () => { + setLoading(true); + try { + const result = await testSupabaseConnection(); + setConnectionStatus({ + tested: true, + success: result.success, + message: result.message + }); + + // 연결 성공 시 테이블도 확인 + if (result.success) { + const tablesResult = await checkSupabaseTables(); + setTablesStatus({ + checked: true, + exists: tablesResult.exists, + tables: tablesResult.tables + }); + } + } catch (error) { + setConnectionStatus({ + tested: true, + success: false, + message: error instanceof Error ? error.message : '알 수 없는 오류' + }); + } finally { + setLoading(false); + } + }; + + return ( + + + + + Supabase 연결 테스트 + + + + {/* 환경 변수 상태 */} +
+ 환경 변수 설정 + + {envStatus.valid ? ( + + ) : ( + + )} + {envStatus.valid ? '설정됨' : '오류'} + +
+ + {!envStatus.valid && ( +

{envStatus.message}

+ )} + + {/* 연결 테스트 */} +
+ 서버 연결 상태 + {connectionStatus.tested ? ( + + {connectionStatus.success ? ( + + ) : ( + + )} + {connectionStatus.success ? '연결됨' : '연결 실패'} + + ) : ( + 테스트 필요 + )} +
+ + {connectionStatus.tested && ( +

{connectionStatus.message}

+ )} + + {/* 테이블 확인 */} + {tablesStatus.checked && ( + <> +
+ 테이블 구조 + + {tablesStatus.exists ? '완료' : '미설정'} + +
+ {tablesStatus.tables && tablesStatus.tables.length > 0 && ( +
+

확인된 테이블:

+
+ {tablesStatus.tables.map(table => ( + + {table} + + ))} +
+
+ )} + + )} + + +
+
+ ); +}; + +export default SupabaseTestPanel; diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 1955b91..054635f 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1,89 +1,65 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import NavBar from '@/components/NavBar'; -import SyncSettings from '@/components/SyncSettings'; -import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from 'lucide-react'; -import { cn } from '@/lib/utils'; - -const SettingsOption = ({ - icon: Icon, - label, - description, - onClick, - color = "text-neuro-income" -}: { - icon: React.ElementType; - label: string; - description?: string; - onClick?: () => void; - color?: string; -}) => { - return
-
-
- -
-
-

{label}

- {description &&

{description}

} -
- -
-
; -}; +import { Helmet } from 'react-helmet'; +import NavBar from "@/components/NavBar"; +import Header from "@/components/Header"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import SyncSettings from "@/components/SyncSettings"; +import SupabaseTestPanel from "@/components/SupabaseTestPanel"; const Settings = () => { - const navigate = useNavigate(); - return
-
- {/* Header */} -
-

설정

+ return ( +
+ + 설정 | 예산 관리 + +
+ +
+ + + 일반 설정 + 고급 설정 + - {/* User Profile */} -
-
- -
-
-

홍길동

-

honggildong@example.com

-
-
-
- - {/* Data Sync Settings */} -
-

데이터 동기화

- -
- - {/* Settings Options */} -
-

계정

- navigate('/profile-management')} /> - navigate('/payment-methods')} /> - navigate('/notification-settings')} /> -
- -
-

앱 설정

- navigate('/security-privacy-settings')} /> - navigate('/help-support')} /> -
- -
- -
- -
-

앱 버전 0.1

-
-
+ + + + 동기화 설정 + + 클라우드 동기화 및 백업 기능을 관리합니다. + + + + + + + + {/* Supabase 테스트 패널 */} + + + + + + + 고급 설정 + + 앱의 고급 기능 및 최적화 옵션을 설정합니다. + + + +

+ 현재 사용 가능한 고급 설정이 없습니다. +

+
+
+
+ + -
; +
+ ); }; export default Settings; diff --git a/src/utils/supabaseTest.ts b/src/utils/supabaseTest.ts new file mode 100644 index 0000000..661e1c2 --- /dev/null +++ b/src/utils/supabaseTest.ts @@ -0,0 +1,126 @@ + +import { supabase } from '@/lib/supabase'; +import { toast } from '@/components/ui/use-toast'; + +/** + * Supabase 연결 테스트 + * 현재 Supabase 연결 상태를 확인합니다. + */ +export const testSupabaseConnection = async (): Promise<{ success: boolean; message: string }> => { + try { + // 서버에 핑 보내기 + const { data, error } = await supabase.from('_ping').select('*').limit(1); + + if (error) { + console.error('Supabase 연결 테스트 실패:', error); + return { + success: false, + message: `연결 오류: ${error.message}` + }; + } + + // 사용자 세션 확인 + const { data: userData, error: userError } = await supabase.auth.getUser(); + const isLoggedIn = userData?.user !== null; + + return { + success: true, + message: `Supabase 연결 성공! 로그인 상태: ${isLoggedIn ? '로그인됨' : '로그인 필요'}` + }; + } catch (err) { + const error = err as Error; + console.error('Supabase 테스트 중 예외 발생:', error); + return { + success: false, + message: `예외 발생: ${error.message}` + }; + } +}; + +/** + * 사용자 인터페이스에서 Supabase 연결 테스트를 실행 + * 결과를 토스트 메시지로 표시합니다. + */ +export const runSupabaseConnectionTest = async () => { + toast({ + title: "Supabase 연결 테스트", + description: "연결 상태를 확인 중입니다...", + }); + + const result = await testSupabaseConnection(); + + if (result.success) { + toast({ + title: "연결 성공", + description: result.message, + }); + } else { + toast({ + title: "연결 실패", + description: result.message, + variant: "destructive" + }); + } +}; + +/** + * Supabase 테이블 구조 확인 + * 지정된 테이블이 존재하는지 확인합니다. + */ +export const checkSupabaseTables = async (): Promise<{ exists: boolean; tables: string[] }> => { + try { + // 현재 스키마의 테이블 목록 가져오기 + const { data, error } = await supabase + .from('pg_tables') + .select('tablename') + .eq('schemaname', 'public'); + + if (error) { + console.error('테이블 확인 오류:', error); + return { exists: false, tables: [] }; + } + + const tables = data?.map(table => table.tablename) || []; + + // 필요한 테이블이 있는지 확인 + const requiredTables = ['transactions', 'budgets', 'category_budgets']; + const missingTables = requiredTables.filter(table => !tables.includes(table)); + + return { + exists: missingTables.length === 0, + tables + }; + } catch (error) { + console.error('테이블 확인 중 오류 발생:', error); + return { exists: false, tables: [] }; + } +}; + +/** + * Supabase 환경 설정 확인 + * 환경 변수가 올바르게 설정되었는지 확인합니다. + */ +export const checkSupabaseEnvironment = (): { valid: boolean; message: string } => { + const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; + const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseAnonKey) { + return { + valid: false, + message: '환경 변수가 설정되지 않았습니다.' + }; + } + + if (supabaseUrl.includes('YOUR_SUPABASE_URL') || + supabaseAnonKey.includes('YOUR_SUPABASE_ANON_KEY')) { + return { + valid: false, + message: '환경 변수가 기본값으로 설정되어 있습니다.' + }; + } + + return { + valid: true, + message: '환경 변수가 올바르게 설정되었습니다.' + }; +};