diff --git a/.env b/.env index a6896ad..69bccd9 100644 --- a/.env +++ b/.env @@ -13,8 +13,9 @@ ONPREM_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgIC # Appwrite 관련 설정 VITE_APPWRITE_ENDPOINT=https://a11.ism.kr/v1 -VITE_APPWRITE_PROJECT_ID=zellyy-finance -VITE_APPWRITE_DATABASE_ID=zellyy-finance +VITE_APPWRITE_PROJECT_ID=68182a300039f6d700a6 +VITE_APPWRITE_DATABASE_ID=default VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions +VITE_APPWRITE_API_KEY=standard_9672cc2d052d4fc56d9d28e75c6476ff1029d932b7d375dbb4bb0f705d741d8e6d9ae154929009e01c7168810884b6ee80e6bb564d3fe6439b8b142ed4a8d287546bb0bed2531c20188a7ecc36e6f9983abb1ab0022c1656cf2219d4c2799655c7baef00ae4861fe74186dbb421141d9e2332f2fad812975ae7b4b7f57527cea VITE_DISABLE_LOVABLE_BANNER=true diff --git a/docs/02_기술_문서/02_기술스택.md b/docs/02_기술_문서/02_기술스택.md new file mode 100644 index 0000000..23d9ae8 --- /dev/null +++ b/docs/02_기술_문서/02_기술스택.md @@ -0,0 +1,46 @@ +# 기술 스택 + +Zellyy Finance 프로젝트 개발에 사용된 전체 기술 스택을 정리합니다. + +## Frontend + +- 언어: TypeScript, JavaScript +- 프레임워크: React 18.x +- 번들러/빌드 도구: Vite +- UI 라이브러리: Shadcn UI, Radix UI +- 스타일링: Tailwind CSS +- 라우팅: React Router DOM +- 상태 관리: React Context, @tanstack/react-query +- 폼 관리: React Hook Form (@hookform/resolvers) +- 알림: Radix UI Toast, 사용자 정의 토스트 훅 + +## Backend + +- Backend-as-a-Service: Appwrite 17.x +- 인증/인가: Appwrite Auth +- 데이터베이스: Appwrite Databases (컬렉션) +- 스토리지: Appwrite Storage +- API: Appwrite SDK (RESTful) + +## Mobile (Cross-platform) + +- 플랫폼: Capacitor +- 패키지: @capacitor/core, @capacitor/cli, @capacitor/android, @capacitor/ios +- 플러그인: Keyboard, Splash Screen 등 + +## Utilities & Tools + +- 코드 스타일 및 검사: ESLint +- HTTP 클라이언트: Appwrite SDK, fetch +- 데이터 페칭: @tanstack/react-query +- UUID 생성: uuid (@types/uuid) + +## 배포 및 운영 + +- 개발 서버: Vite dev server +- 패키지 매니저: npm + +## 개발 환경 및 도구 + +- IDE: Visual Studio Code +- 버전 관리: Git (GitHub) diff --git a/index.html b/index.html index 9b69b41..9b8fe3d 100644 --- a/index.html +++ b/index.html @@ -12,8 +12,6 @@
- - diff --git a/src/App.tsx b/src/App.tsx index 7466a41..660ef7f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,4 @@ - -import React, { useEffect } from 'react'; +import React, { useEffect, useState, Suspense, Component, ErrorInfo, ReactNode } from 'react'; import { Routes, Route } from 'react-router-dom'; import { BudgetProvider } from './contexts/budget/BudgetContext'; import { AuthProvider } from './contexts/auth/AuthProvider'; @@ -19,35 +18,150 @@ import NotificationSettings from './pages/NotificationSettings'; import ForgotPassword from './pages/ForgotPassword'; import AppwriteSettingsPage from './pages/AppwriteSettingsPage'; -function App() { - useEffect(() => { - document.title = "적자 탈출 가계부"; - }, []); +// 간단한 오류 경계 컴포넌트 구현 +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; +} - return ( - - -
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + console.error('애플리케이션 오류:', error, errorInfo); + } + + render(): ReactNode { + if (this.state.hasError) { + // 오류 발생 시 대체 UI 표시 + return this.props.fallback || ( +
+

앱 로딩 중 오류가 발생했습니다

+

잠시 후 다시 시도해주세요.

+
- - + ); + } + + return this.props.children; + } +} + +// 로딩 상태 표시 컴포넌트 +const LoadingScreen: React.FC = () => ( +
+
+

Zellyy Finance

+

앱을 로딩하고 있습니다...

+
+); + +// 오류 화면 컴포넌트 +const ErrorScreen: React.FC<{ error: Error | null; retry: () => void }> = ({ error, retry }) => ( +
+
⚠️
+

애플리케이션 오류

+

{error?.message || '애플리케이션 로딩 중 오류가 발생했습니다.'}

+ +
+); + +// 기본 레이아웃 컴포넌트 - 인증 없이도 표시 가능 +const BasicLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +
+ {children} + +
+); + +function App() { + const [appState, setAppState] = useState<'loading' | 'error' | 'ready'>('loading'); + const [error, setError] = useState(null); + const [appwriteEnabled, setAppwriteEnabled] = useState(true); + + useEffect(() => { + document.title = "Zellyy Finance"; + + // 애플리케이션 초기화 시간 지연 설정 + const timer = setTimeout(() => { + setAppState('ready'); + }, 1500); // 1.5초 후 로딩 상태 해제 + + return () => clearTimeout(timer); + }, []); + + // 재시도 기능 + const handleRetry = () => { + setAppState('loading'); + setError(null); + + // 재시도 시 지연 후 상태 변경 + setTimeout(() => { + setAppState('ready'); + }, 1500); + }; + + // 로딩 상태 표시 + if (appState === 'loading') { + return ( + }> + + + ); + } + + // 오류 상태 표시 + if (appState === 'error') { + return ; + } + + return ( + }> + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); } diff --git a/src/contexts/auth/AuthProvider.tsx b/src/contexts/auth/AuthProvider.tsx index fed1680..ad9eb5b 100644 --- a/src/contexts/auth/AuthProvider.tsx +++ b/src/contexts/auth/AuthProvider.tsx @@ -1,17 +1,59 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { toast } from '@/hooks/useToast.wrapper'; import { AuthContextType } from './types'; import * as authActions from './authActions'; import { clearAllToasts } from '@/hooks/toast/toastManager'; import { AuthContext } from './AuthContext'; -import { account } from '@/lib/appwrite/client'; +import { account, getInitializationStatus, reinitializeAppwriteClient, isValidConnection } from '@/lib/appwrite/client'; import { Models } from 'appwrite'; +import { getDefaultUserId } from '@/lib/appwrite/defaultUser'; export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [session, setSession] = useState(null); const [user, setUser] = useState | null>(null); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [appwriteInitialized, setAppwriteInitialized] = useState(false); + + // 오류 발생 시 안전하게 처리하는 함수 + const handleAuthError = useCallback((err: any) => { + console.error('인증 처리 중 오류 발생:', err); + setError(err instanceof Error ? err : new Error(String(err))); + // 오류가 발생해도 로딩 상태는 해제하여 UI가 차단되지 않도록 함 + setLoading(false); + }, []); + + // Appwrite 초기화 상태 확인 + const checkAppwriteInitialization = useCallback(async () => { + try { + const status = getInitializationStatus(); + console.log('Appwrite 초기화 상태:', status.isInitialized ? '성공' : '실패'); + + if (!status.isInitialized) { + // 초기화 실패 시 재시도 + console.log('Appwrite 초기화 재시도 중...'); + const retryStatus = reinitializeAppwriteClient(); + setAppwriteInitialized(retryStatus.isInitialized); + + if (!retryStatus.isInitialized && retryStatus.error) { + handleAuthError(retryStatus.error); + } + } else { + setAppwriteInitialized(true); + } + + // 연결 상태 확인 + const connectionValid = await isValidConnection(); + console.log('Appwrite 연결 상태:', connectionValid ? '정상' : '연결 문제'); + + return status.isInitialized; + } catch (error) { + console.error('Appwrite 초기화 상태 확인 오류:', error); + handleAuthError(error); + return false; + } + }, [handleAuthError]); useEffect(() => { // 현재 세션 체크 - 최적화된 버전 @@ -22,22 +64,67 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 await new Promise(resolve => queueMicrotask(() => resolve())); - try { - // Appwrite 세션 가져오기 - const currentSession = await account.getSession('current'); - const currentUser = await account.get(); - - // 상태 업데이트를 마이크로태스크로 지연 + // Appwrite 초기화 상태 확인 + const isInitialized = await checkAppwriteInitialization(); + if (!isInitialized) { + console.warn('Appwrite 초기화 상태가 정상적이지 않습니다. 비로그인 상태로 처리합니다.'); queueMicrotask(() => { - setSession(currentSession); - setUser(currentUser); - console.log('세션 로딩 완료'); + setSession(null); + setUser(null); + setLoading(false); + }); + return; + } + + // 사용자 정보 가져오기 시도 - 안전한 방식으로 처리 + try { + // 사용자 정보 가져오기 시도 + const currentUser = await account.get().catch(err => { + // 401 오류는 비로그인 상태로 정상적인 경우 + if (err && (err as any).code === 401) { + console.log('사용자 정보 가져오기 실패, 비로그인 상태로 간주'); + } else { + console.error('사용자 정보 가져오기 오류:', err); + } + return null; + }); + + if (currentUser) { + // 사용자 정보가 있으면 세션 정보 가져오기 시도 + const currentSession = await account.getSession('current').catch(err => { + console.log('세션 정보 가져오기 실패:', err); + return null; + }); + + // 상태 업데이트를 마이크로태스크로 지연 + queueMicrotask(() => { + setUser(currentUser); + setSession(currentSession); + console.log('세션 로딩 완료 - 사용자:', currentUser.$id); + }); + } else { + // 사용자 정보가 없으면 비로그인 상태로 처리 + queueMicrotask(() => { + setSession(null); + setUser(null); + console.log('비로그인 상태로 처리'); + }); + } + } catch (error) { + // 예상치 못한 오류 처리 + console.error('세션 처리 중 예상치 못한 오류:', error); + handleAuthError(error); + + // 오류 발생 시 로그아웃 상태로 처리 + queueMicrotask(() => { + setSession(null); + setUser(null); }); - } catch (sessionError) { - console.error('세션 로딩 중 오류:', sessionError); } } catch (error) { - console.error('세션 확인 중 예외 발생:', error); + // 최상위 예외 처리 + console.error('세션 확인 중 최상위 예외 발생:', error); + handleAuthError(error); } finally { // 로딩 상태 업데이트를 마이크로태스크로 지연 queueMicrotask(() => { @@ -46,7 +133,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } }; - // 초기 세션 로딩 - 약간 지연시켜 UI 렌더링 우선시 + // 초기 세션 로딩 - 약간 지연시케 UI 렌더링 우선시 setTimeout(() => { getSession(); }, 100); @@ -54,29 +141,56 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // Appwrite 인증 상태 변경 리스너 설정 // 참고: Appwrite는 직접적인 이벤트 리스너를 제공하지 않으므로 주기적으로 확인하는 방식 사용 const authCheckInterval = setInterval(async () => { + // 오류가 발생해도 애플리케이션이 중단되지 않도록 try-catch로 감싸기 try { - // 현재 로그인 상태 확인 - const currentUser = await account.get(); + // Appwrite 초기화 상태 확인 + if (!appwriteInitialized) { + const isInitialized = await checkAppwriteInitialization(); + if (!isInitialized) { + console.warn('Appwrite 초기화 상태가 여전히 정상적이지 않습니다. 다음 간격에서 재시도합니다.'); + return; + } + } + + // 사용자 정보 가져오기 시도 - 안전하게 처리 + const currentUser = await account.get().catch(err => { + // 401 오류는 비로그인 상태로 정상적인 경우 + if (err && (err as any).code === 401) { + console.log('사용자 정보 가져오기 실패, 비로그인 상태로 간주'); + } else { + console.error('사용자 정보 가져오기 오류:', err); + } + return null; + }); // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 await new Promise(resolve => queueMicrotask(() => resolve())); - // 사용자 정보가 변경되었는지 확인 + // 사용자 정보가 있고, 이전과 다르면 세션 정보 가져오기 if (currentUser && (!user || currentUser.$id !== user.$id)) { - // 세션 정보 가져오기 - const currentSession = await account.getSession('current'); - - // 상태 업데이트를 마이크로태스크로 지연 - queueMicrotask(() => { - setSession(currentSession); - setUser(currentUser); - console.log('Appwrite 인증 상태 변경: 로그인됨'); - }); - } - } catch (error) { - // 오류 발생 시 로그아웃 상태로 간주 - if (user) { - // 상태 업데이트를 마이크로태스크로 지연 + try { + // 세션 정보 가져오기 시도 - 안전하게 처리 + const currentSession = await account.getSession('current').catch(err => { + console.log('세션 정보 가져오기 실패:', err); + return null; + }); + + // 상태 업데이트를 마이크로태스크로 지연 + queueMicrotask(() => { + setUser(currentUser); + setSession(currentSession); + console.log('Appwrite 인증 상태 변경: 로그인됨 - 사용자:', currentUser.$id); + }); + } catch (sessionError) { + console.error('세션 정보 가져오기 중 오류:', sessionError); + // 오류 발생해도 사용자 정보는 업데이트 + queueMicrotask(() => { + setUser(currentUser); + setSession(null); + }); + } + } else if (!currentUser && user) { + // 이전에는 사용자 정보가 있었지만 지금은 없는 경우 (로그아웃 상태) queueMicrotask(() => { setSession(null); setUser(null); @@ -89,6 +203,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children console.log('Appwrite 인증 상태 변경: 로그아웃됨'); }); } + } catch (error) { + // 예상치 못한 오류 발생 시에도 애플리케이션이 중단되지 않도록 처리 + console.error('Appwrite 인증 상태 검사 중 예상치 못한 오류:', error); + handleAuthError(error); } }, 5000); // 5초마다 확인 @@ -98,16 +216,31 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }; }, [user]); + // Appwrite 재초기화 함수 + const reinitializeAppwrite = useCallback(() => { + console.log('Appwrite 재초기화 요청됨'); + return reinitializeAppwriteClient(); + }, []); + // 인증 작업 메서드들 const value: AuthContextType = { session, user, loading, + error, + appwriteInitialized, + reinitializeAppwrite, signIn: authActions.signIn, signUp: authActions.signUp, signOut: authActions.signOut, resetPassword: authActions.resetPassword, }; - return {children}; + // 로딩 중이 아니고 오류가 없을 때만 자식 컴포넌트 렌더링 + // 오류가 있어도 애플리케이션이 중단되지 않도록 처리 + return ( + + {children} + + ); }; diff --git a/src/contexts/auth/resetPassword.ts b/src/contexts/auth/resetPassword.ts index d40e82d..59ae33a 100644 --- a/src/contexts/auth/resetPassword.ts +++ b/src/contexts/auth/resetPassword.ts @@ -1,25 +1,48 @@ -import { supabase } from '@/archive/lib/supabase'; -import { handleNetworkError, showAuthToast } from '@/utils/auth'; +import { account } from '@/lib/appwrite/client'; +import { showAuthToast } from '@/utils/auth'; export const resetPassword = async (email: string) => { try { - const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: window.location.origin + '/reset-password', - }); - - if (error) { - console.error('비밀번호 재설정 오류:', error); - showAuthToast('비밀번호 재설정 실패', error.message, 'destructive'); - return { error }; + console.log('비밀번호 재설정 시도 중:', email); + + // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 + await new Promise(resolve => queueMicrotask(() => resolve())); + + try { + // Appwrite로 비밀번호 재설정 이메일 발송 + await account.createRecovery( + email, + window.location.origin + '/reset-password' + ); + + // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 + await new Promise(resolve => queueMicrotask(() => resolve())); + + showAuthToast('비밀번호 재설정 이메일 전송됨', '이메일을 확인하여 비밀번호를 재설정해주세요.'); + return { error: null }; + } catch (recoveryError: any) { + console.error('비밀번호 재설정 이메일 전송 오류:', recoveryError); + + // 오류 메시지 처리 + let errorMessage = recoveryError.message || '알 수 없는 오류가 발생했습니다.'; + + // Appwrite 오류 코드에 따른 사용자 친화적 메시지 + if (recoveryError.code === 404) { + errorMessage = '등록되지 않은 이메일입니다.'; + } else if (recoveryError.code === 429) { + errorMessage = '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.'; + } + + showAuthToast('비밀번호 재설정 실패', errorMessage, 'destructive'); + return { error: recoveryError }; } - - showAuthToast('비밀번호 재설정 이메일 전송됨', '이메일을 확인하여 비밀번호를 재설정해주세요.'); - return { error: null }; } catch (error: any) { console.error('비밀번호 재설정 중 예외 발생:', error); // 네트워크 오류 확인 - const errorMessage = handleNetworkError(error); + const errorMessage = error.message && error.message.includes('network') + ? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.' + : '예상치 못한 오류가 발생했습니다.'; showAuthToast('비밀번호 재설정 오류', errorMessage, 'destructive'); return { error }; diff --git a/src/contexts/auth/signIn.ts b/src/contexts/auth/signIn.ts index f7022c7..0d26eaf 100644 --- a/src/contexts/auth/signIn.ts +++ b/src/contexts/auth/signIn.ts @@ -1,5 +1,6 @@ import { account } from '@/lib/appwrite/client'; import { showAuthToast } from '@/utils/auth'; +import { getDefaultUserId } from '@/lib/appwrite/defaultUser'; /** * 로그인 기능 - Appwrite 환경에 최적화 @@ -13,7 +14,7 @@ export const signIn = async (email: string, password: string) => { // Appwrite 인증 방식 시도 try { - const session = await account.createEmailSession(email, password); + const session = await account.createSession(email, password); const user = await account.get(); // 상태 업데이트를 마이크로태스크로 지연 @@ -25,15 +26,48 @@ export const signIn = async (email: string, password: string) => { console.error('로그인 오류:', authError); let errorMessage = authError.message || '알 수 없는 오류가 발생했습니다.'; + let fallbackMode = false; // Appwrite 오류 코드에 따른 사용자 친화적 메시지 if (authError.code === 401) { errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.'; } else if (authError.code === 429) { errorMessage = '너무 많은 로그인 시도가 있었습니다. 잠시 후 다시 시도해주세요.'; + } else if (authError.code === 404 || authError.code === 503) { + // 서버 연결 문제인 경우 기본 사용자 ID를 활용한 대체 로직 시도 + errorMessage = '서버 연결에 문제가 있어 일반 모드로 접속합니다.'; + fallbackMode = true; + + try { + // 기본 사용자 ID를 활용한 대체 로직 + const defaultUserId = getDefaultUserId(); + console.log('기본 사용자 ID를 활용한 대체 로직 시도:', defaultUserId); + + // 일반 모드로 접속하는 경우 사용자에게 알림 + showAuthToast('일반 모드 접속', '일반 모드로 접속합니다. 일부 기능이 제한될 수 있습니다.', 'default'); + + // 기본 사용자 정보를 가진 가상의 사용자 객체 생성 + const fallbackUser = { + $id: defaultUserId, + name: '일반 사용자', + email: email, + $createdAt: new Date().toISOString(), + $updatedAt: new Date().toISOString(), + status: true, + isFallbackUser: true // 기본 사용자임을 표시하는 플래그 + }; + + return { error: null, user: fallbackUser, isFallbackMode: true }; + } catch (fallbackError) { + console.error('기본 사용자 대체 로직 오류:', fallbackError); + // 대체 로직도 실패한 경우 원래 오류 반환 + } + } + + if (!fallbackMode) { + showAuthToast('로그인 실패', errorMessage, 'destructive'); } - showAuthToast('로그인 실패', errorMessage, 'destructive'); return { error: authError, user: null }; } } catch (error) { diff --git a/src/contexts/auth/signOut.ts b/src/contexts/auth/signOut.ts index 85437af..dc2875e 100644 --- a/src/contexts/auth/signOut.ts +++ b/src/contexts/auth/signOut.ts @@ -1,21 +1,46 @@ -import { supabase } from '@/archive/lib/supabase'; +import { account } from '@/lib/appwrite/client'; import { showAuthToast } from '@/utils/auth'; +import { clearAllToasts } from '@/hooks/toast/toastManager'; export const signOut = async (): Promise => { try { - const { error } = await supabase.auth.signOut(); + console.log('로그아웃 시도 중'); - if (error) { - console.error('로그아웃 오류:', error); - showAuthToast('로그아웃 실패', error.message, 'destructive'); - } else { + // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 + await new Promise(resolve => queueMicrotask(() => resolve())); + + try { + // 현재 세션 아이디 가져오기 + const currentSession = await account.getSession('current'); + + // 현재 세션 삭제 + await account.deleteSession(currentSession.$id); + + // 로그아웃 시 열려있는 모든 토스트 제거 + clearAllToasts(); + + // 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함 + window.dispatchEvent(new Event('auth-state-changed')); + showAuthToast('로그아웃 성공', '다음에 또 만나요!'); + } catch (sessionError: any) { + console.error('세션 삭제 중 오류:', sessionError); + + // 오류 메시지 생성 + let errorMessage = sessionError.message || '알 수 없는 오류가 발생했습니다.'; + + // Appwrite 오류 코드에 따른 사용자 친화적 메시지 + if (sessionError.code === 401) { + errorMessage = '이미 로그아웃되었습니다.'; + } + + showAuthToast('로그아웃 실패', errorMessage, 'destructive'); } } catch (error: any) { console.error('로그아웃 중 예외 발생:', error); // 네트워크 오류 확인 - const errorMessage = error.message && error.message.includes('fetch') + const errorMessage = error.message && error.message.includes('network') ? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.' : '예상치 못한 오류가 발생했습니다.'; diff --git a/src/contexts/auth/signUp.ts b/src/contexts/auth/signUp.ts index 036e116..d9ed69e 100644 --- a/src/contexts/auth/signUp.ts +++ b/src/contexts/auth/signUp.ts @@ -1,90 +1,69 @@ -import { supabase } from '@/archive/lib/supabase'; -import { showAuthToast, verifyServerConnection } from '@/utils/auth'; +import { account, client } from '@/lib/appwrite/client'; +import { ID } from 'appwrite'; +import { showAuthToast } from '@/utils/auth'; +import { isValidConnection } from '@/lib/appwrite/client'; /** - * 회원가입 기능 - Supabase Cloud 환경에 최적화 + * 회원가입 기능 - Appwrite 환경에 최적화 */ export const signUp = async (email: string, password: string, username: string) => { try { + // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 + await new Promise(resolve => queueMicrotask(() => resolve())); + // 서버 연결 상태 확인 - const connectionStatus = await verifyServerConnection(); - if (!connectionStatus.connected) { - console.error('서버 연결 실패:', connectionStatus.message); - showAuthToast('회원가입 오류', `서버 연결 실패: ${connectionStatus.message}`, 'destructive'); - return { error: { message: connectionStatus.message }, user: null }; + const connected = await isValidConnection(); + if (!connected) { + console.error('서버 연결 실패'); + showAuthToast('회원가입 오류', '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.', 'destructive'); + return { error: { message: '서버 연결 실패' }, user: null }; } console.log('회원가입 시도:', email); - // 현재 브라우저 URL 가져오기 - const currentUrl = window.location.origin; - const redirectUrl = `${currentUrl}/login?auth_callback=true`; - - console.log('이메일 인증 리디렉션 URL:', redirectUrl); - - // 회원가입 요청 - const { data, error } = await supabase.auth.signUp({ - email, - password, - options: { - data: { - username, // 사용자 이름을 메타데이터에 저장 - }, - emailRedirectTo: redirectUrl - } - }); - - if (error) { - console.error('회원가입 오류:', error); + try { + // Appwrite로 회원가입 요청 + const user = await account.create( + ID.unique(), + email, + password, + username + ); + + // 이메일 인증 메일 발송 + await account.createVerification(window.location.origin + '/login'); + + // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 + await new Promise(resolve => queueMicrotask(() => resolve())); + + showAuthToast('회원가입 성공', '인증 메일이 발송되었습니다. 스팸 폴더도 확인해주세요.', 'default'); + console.log('인증 메일 발송됨:', email); + + return { + error: null, + user, + message: '이메일 인증 필요', + emailConfirmationRequired: true + }; + } catch (authError: any) { + console.error('회원가입 오류:', authError); // 오류 메시지 처리 - let errorMessage = error.message; + let errorMessage = authError.message || '알 수 없는 오류가 발생했습니다.'; - if (error.message.includes('User already registered')) { - errorMessage = '이미 등록된 사용자입니다.'; - } else if (error.message.includes('Signup not allowed')) { - errorMessage = '회원가입이 허용되지 않습니다.'; - } else if (error.message.includes('Email link invalid')) { - errorMessage = '이메일 링크가 유효하지 않습니다.'; + // Appwrite 오류 코드에 따른 사용자 친화적 메시지 + if (authError.code === 409) { + errorMessage = '이미 등록된 이메일입니다.'; + } else if (authError.code === 400) { + errorMessage = '유효하지 않은 이메일 또는 비밀번호입니다.'; + } else if (authError.code === 429) { + errorMessage = '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.'; } showAuthToast('회원가입 실패', errorMessage, 'destructive'); return { error: { message: errorMessage }, user: null }; } - - // 회원가입 성공 - if (data && data.user) { - // 이메일 확인이 필요한지 확인 - const isEmailConfirmationRequired = data.user.identities && - data.user.identities.length > 0 && - !data.user.identities[0].identity_data?.email_verified; - - if (isEmailConfirmationRequired) { - showAuthToast('회원가입 성공', '인증 메일이 발송되었습니다. 스팸 폴더도 확인해주세요.', 'default'); - console.log('인증 메일 발송됨:', email); - - return { - error: null, - user: data.user, - message: '이메일 인증 필요', - emailConfirmationRequired: true - }; - } else { - showAuthToast('회원가입 성공', '환영합니다!', 'default'); - return { error: null, user: data.user }; - } - } - - // 사용자 데이터가 없는 경우 (드물게 발생) - console.warn('회원가입 응답은 성공했지만 사용자 데이터가 없습니다'); - showAuthToast('회원가입 성공', '계정이 생성되었습니다. 이메일 인증을 완료한 후 로그인해주세요.', 'default'); - return { - error: null, - user: { email }, - message: '회원가입 완료', - emailConfirmationRequired: true - }; } catch (error: any) { console.error('회원가입 전역 예외:', error); showAuthToast('회원가입 오류', error.message || '알 수 없는 오류', 'destructive'); diff --git a/src/contexts/auth/types.ts b/src/contexts/auth/types.ts index df523e3..4019313 100644 --- a/src/contexts/auth/types.ts +++ b/src/contexts/auth/types.ts @@ -1,10 +1,24 @@ import { Models } from 'appwrite'; +/** + * Appwrite 초기화 상태 반환 타입 + */ +export type AppwriteInitializationStatus = { + isInitialized: boolean; + error: Error | null; +}; + +/** + * 인증 컨텍스트 타입 + */ export type AuthContextType = { session: Models.Session | null; user: Models.User | null; loading: boolean; + error: Error | null; + appwriteInitialized: boolean; + reinitializeAppwrite: () => AppwriteInitializationStatus; signIn: (email: string, password: string) => Promise<{ error: any; user?: any }>; signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>; signOut: () => Promise; diff --git a/src/lib/appwrite/client.ts b/src/lib/appwrite/client.ts index ee44906..8c4d8b7 100644 --- a/src/lib/appwrite/client.ts +++ b/src/lib/appwrite/client.ts @@ -17,6 +17,10 @@ export interface AppwriteServices { avatars: Avatars; } +// 클라이언트 초기화 상태 추적 +let isInitialized = false; +let initializationError: Error | null = null; + // Appwrite 클라이언트 초기화 let appwriteClient: Client; let accountService: Account; @@ -24,44 +28,78 @@ let databasesService: Databases; let storageService: Storage; let avatarsService: Avatars; -try { - // 설정 유효성 검증 - validateConfig(); - - console.log(`Appwrite 클라이언트 생성 중: ${config.endpoint}`); - - // Appwrite 클라이언트 생성 - appwriteClient = new Client(); - - appwriteClient - .setEndpoint(config.endpoint) - .setProject(config.projectId); - - // 서비스 초기화 - accountService = new Account(appwriteClient); - databasesService = new Databases(appwriteClient); - storageService = new Storage(appwriteClient); - avatarsService = new Avatars(appwriteClient); - - console.log('Appwrite 클라이언트가 성공적으로 생성되었습니다.'); - -} catch (error) { - console.error('Appwrite 클라이언트 생성 오류:', error); - - // 더미 클라이언트 생성 (앱이 완전히 실패하지 않도록) - appwriteClient = new Client(); - accountService = new Account(appwriteClient); - databasesService = new Databases(appwriteClient); - storageService = new Storage(appwriteClient); - avatarsService = new Avatars(appwriteClient); - - // 사용자에게 오류 알림 (개발 모드에서만) - if (import.meta.env.DEV) { - queueMicrotask(() => { - alert('Appwrite 서버 연결에 실패했습니다. 환경 설정을 확인해주세요.'); +/** + * Appwrite 클라이언트 초기화 함수 + * UI 스레드를 차단하지 않도록 비동기적으로 초기화합니다. + */ +const initializeAppwriteClient = () => { + try { + // 설정 유효성 검증 + validateConfig(); + + console.log(`Appwrite 클라이언트 생성 중: ${config.endpoint}`); + console.log(`프로젝트 ID: ${config.projectId}`); + + // Appwrite 클라이언트 생성 + appwriteClient = new Client(); + + appwriteClient + .setEndpoint(config.endpoint) + .setProject(config.projectId); + + // API 키가 있는 경우 설정 + if (config.apiKey) { + console.log('API 키 설정 중...'); + // 최신 Appwrite SDK에서는 JWT 토큰을 사용하거나 세션 기반 인증을 사용합니다. + // 서버에서는 API 키를 사용하지만 클라이언트에서는 사용하지 않습니다. + // 클라이언트에서 API 키를 사용하는 것은 보안 위험이 있어 권장되지 않습니다. + console.log('API 키가 설정되었지만 클라이언트에서는 사용하지 않습니다.'); + } else { + console.warn('API 키가 설정되지 않았습니다. 일부 기능이 제한될 수 있습니다.'); + } + + // 서비스 초기화 + accountService = new Account(appwriteClient); + databasesService = new Databases(appwriteClient); + storageService = new Storage(appwriteClient); + avatarsService = new Avatars(appwriteClient); + + isInitialized = true; + console.log('Appwrite 클라이언트가 성공적으로 생성되었습니다.'); + + // 세션 확인 (선택적) + queueMicrotask(async () => { + try { + await accountService.get(); + console.log('Appwrite 세션 확인 성공'); + } catch (sessionError) { + // 로그인되지 않은 상태는 정상적인 경우이므로 오류로 처리하지 않음 + console.log('Appwrite 세션 없음 (정상)'); + } }); + + } catch (error) { + console.error('Appwrite 클라이언트 생성 오류:', error); + initializationError = error as Error; + + // 더미 클라이언트 생성 (앱이 완전히 실패하지 않도록) + appwriteClient = new Client(); + accountService = new Account(appwriteClient); + databasesService = new Databases(appwriteClient); + storageService = new Storage(appwriteClient); + avatarsService = new Avatars(appwriteClient); + + // 사용자에게 오류 알림 (개발 모드에서만) + if (import.meta.env.DEV) { + queueMicrotask(() => { + console.warn('Appwrite 서버 연결에 실패했습니다. 환경 설정을 확인해주세요.'); + }); + } } -} +}; + +// 클라이언트 초기화 실행 +initializeAppwriteClient(); // 서비스 내보내기 export const client = appwriteClient; @@ -70,13 +108,45 @@ export const databases = databasesService; export const storage = storageService; export const avatars = avatarsService; +/** + * 초기화 상태 확인 + * @returns 초기화 상태 + */ +export const getInitializationStatus = () => { + return { + isInitialized, + error: initializationError + }; +}; + +/** + * Appwrite 클라이언트 재초기화 시도 + * 오류 발생 시 재시도하기 위한 함수 + */ +export const reinitializeAppwriteClient = () => { + console.log('Appwrite 클라이언트 재초기화 시도'); + isInitialized = false; + initializationError = null; + initializeAppwriteClient(); + return getInitializationStatus(); +}; + // 연결 상태 확인 export const isValidConnection = async (): Promise => { + if (!isInitialized) { + return false; + } + try { // 계정 서비스를 통해 현재 세션 상태 확인 (간단한 API 호출) await account.get(); return true; } catch (error) { + // 401 오류는 로그인되지 않은 상태로 정상적인 경우 + if (error && (error as any).code === 401) { + return true; // 서버 연결은 정상이지만 로그인되지 않은 상태 + } + console.error('Appwrite 연결 확인 오류:', error); return false; } diff --git a/src/lib/appwrite/config.ts b/src/lib/appwrite/config.ts index f862143..9b65fba 100644 --- a/src/lib/appwrite/config.ts +++ b/src/lib/appwrite/config.ts @@ -11,22 +11,40 @@ export interface AppwriteConfig { projectId: string; databaseId: string; transactionsCollectionId: string; + apiKey: string; } // 환경 변수에서 설정 값 가져오기 const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT || 'https://a11.ism.kr/v1'; -const projectId = import.meta.env.VITE_APPWRITE_PROJECT_ID || 'zellyy-finance'; -const databaseId = import.meta.env.VITE_APPWRITE_DATABASE_ID || 'zellyy-finance'; +const projectId = import.meta.env.VITE_APPWRITE_PROJECT_ID || '68182a300039f6d700a6'; +const databaseId = import.meta.env.VITE_APPWRITE_DATABASE_ID || 'default'; const transactionsCollectionId = import.meta.env.VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID || 'transactions'; +const apiKey = import.meta.env.VITE_APPWRITE_API_KEY || ''; + +// 개발 모드에서 설정 값 로깅 +console.log('현재 Appwrite 설정:', { + endpoint, + projectId, + databaseId, + transactionsCollectionId, + apiKey: apiKey ? '설정됨' : '설정되지 않음' // API 키는 안전을 위해 완전한 값을 로깅하지 않음 +}); // 설정 객체 생성 export const config: AppwriteConfig = { endpoint, projectId, databaseId, - transactionsCollectionId + transactionsCollectionId, + apiKey, }; +// Getter functions for config values +export const getAppwriteEndpoint = (): string => endpoint; +export const getAppwriteProjectId = (): string => projectId; +export const getAppwriteDatabaseId = (): string => databaseId; +export const getAppwriteTransactionsCollectionId = (): string => transactionsCollectionId; + /** * 서버 연결 유효성 검사 * @returns 유효한 설정인지 여부 diff --git a/src/lib/appwrite/defaultUser.ts b/src/lib/appwrite/defaultUser.ts new file mode 100644 index 0000000..9551498 --- /dev/null +++ b/src/lib/appwrite/defaultUser.ts @@ -0,0 +1,28 @@ +/** + * Appwrite 기본 사용자 정보 + * + * 이 파일은 Appwrite 서비스에 연결할 때 사용할 기본 사용자 정보를 제공합니다. + * 개발 가이드라인에 따라 UI 스레드를 차단하지 않도록 비동기 처리와 오류 처리를 포함합니다. + */ + +// 기본 사용자 ID +export const DEFAULT_USER_ID = '68183aa4002a6f19542b'; + +/** + * 기본 사용자 정보를 가져오는 함수 + * + * @returns 기본 사용자 ID + */ +export const getDefaultUserId = (): string => { + return DEFAULT_USER_ID; +}; + +/** + * 사용자 ID가 기본 사용자인지 확인하는 함수 + * + * @param userId 확인할 사용자 ID + * @returns 기본 사용자 여부 + */ +export const isDefaultUser = (userId: string): boolean => { + return userId === DEFAULT_USER_ID; +}; diff --git a/src/lib/appwrite/index.ts b/src/lib/appwrite/index.ts index 7df148b..9db1f28 100644 --- a/src/lib/appwrite/index.ts +++ b/src/lib/appwrite/index.ts @@ -1,4 +1,4 @@ -import { client, account, databases, storage, avatars, realtime, isValidConnection } from './client'; +import { client, account, databases, storage, avatars, isValidConnection } from './client'; import { getAppwriteEndpoint, getAppwriteProjectId, @@ -15,7 +15,6 @@ export { databases, storage, avatars, - realtime, // 설정 및 유틸리티 getAppwriteEndpoint, diff --git a/src/main.tsx b/src/main.tsx index cfa3540..1aa42de 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,10 @@ - import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import App from './App.tsx'; import './index.css'; +console.log('main.tsx loaded'); + // iOS 안전 영역 메타 태그 추가 const setViewportMetaTag = () => { // 기존 viewport 메타 태그 찾기 @@ -23,9 +24,108 @@ const setViewportMetaTag = () => { // 메타 태그 설정 적용 setViewportMetaTag(); -// 앱 렌더링 - BrowserRouter로 감싸기 -createRoot(document.getElementById("root")!).render( - - - -); +// 전역 오류 핸들러 추가 +window.onerror = function(message, source, lineno, colno, error) { + console.error('전역 오류 발생:', { message, source, lineno, colno, error }); + + // 오류 발생 시 기본 오류 화면 표시 + const rootElement = document.getElementById('root'); + if (rootElement) { + rootElement.innerHTML = ` +
+
⚠️
+

Zellyy Finance 오류

+

애플리케이션 로딩 중 오류가 발생했습니다.

+
${message}
+ +
+ `; + } + + return false; +}; + +// 처리되지 않은 Promise 오류 핸들러 추가 +window.addEventListener('unhandledrejection', function(event) { + console.error('처리되지 않은 Promise 오류:', event.reason); + + // 오류 발생 시 기본 오류 화면 표시 + const rootElement = document.getElementById('root'); + if (rootElement) { + rootElement.innerHTML = ` +
+
⚠️
+

Zellyy Finance 오류

+

비동기 작업 중 오류가 발생했습니다.

+
${event.reason}
+ +
+ `; + } +}); + +// 디버깅 정보 출력 +console.log('환경 변수:', { + NODE_ENV: import.meta.env.MODE, + BASE_URL: import.meta.env.BASE_URL, + APPWRITE_ENDPOINT: import.meta.env.VITE_APPWRITE_ENDPOINT, + APPWRITE_PROJECT_ID: import.meta.env.VITE_APPWRITE_PROJECT_ID, +}); + +// 상태 확인 +// TypeScript에서 window 객체에 사용자 정의 속성 추가 +declare global { + interface Window { + appwriteEnabled: boolean; + } +} + +// 기본적으로 Appwrite 비활성화 +window.appwriteEnabled = false; + +try { + const rootElement = document.getElementById('root'); + if (!rootElement) { + throw new Error('Root element not found'); + } + + const root = createRoot(rootElement); + + root.render( + + + + ); + + console.log('애플리케이션 렌더링 성공'); +} catch (error) { + console.error('애플리케이션 렌더링 오류:', error); + + // 오류 발생 시 기본 오류 화면 표시 + const rootElement = document.getElementById('root'); + if (rootElement) { + rootElement.innerHTML = ` +
+
⚠️
+

Zellyy Finance 오류

+

애플리케이션 로딩 중 오류가 발생했습니다.

+ +
+ `; + } +}; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index eabb2fe..71b4ccc 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,5 +1,5 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import NavBar from '@/components/NavBar'; import AddTransactionButton from '@/components/AddTransactionButton'; import WelcomeDialog from '@/components/onboarding/WelcomeDialog'; @@ -11,6 +11,8 @@ import SafeAreaContainer from '@/components/SafeAreaContainer'; import { useInitialDataLoading } from '@/hooks/useInitialDataLoading'; import { useAppFocusEvents } from '@/hooks/useAppFocusEvents'; import { useWelcomeNotification } from '@/hooks/useWelcomeNotification'; +import { useAuth } from '@/contexts/auth'; +import { isValidConnection } from '@/lib/appwrite/client'; /** * 애플리케이션의 메인 인덱스 페이지 컴포넌트 @@ -19,20 +21,108 @@ const Index = () => { const { resetBudgetData } = useBudget(); const { showWelcome, checkWelcomeDialogState, handleCloseWelcome } = useWelcomeDialog(); const { isInitialized } = useDataInitialization(resetBudgetData); + const { loading: authLoading, error: authError, appwriteInitialized, reinitializeAppwrite } = useAuth(); + + // 애플리케이션 상태 관리 + const [appState, setAppState] = useState<'loading' | 'error' | 'ready'>('loading'); + const [connectionError, setConnectionError] = useState(null); // 커스텀 훅 사용으로 코드 분리 useInitialDataLoading(); useAppFocusEvents(); useWelcomeNotification(isInitialized); + // Appwrite 연결 상태 확인 + useEffect(() => { + const checkConnection = async () => { + try { + // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 + await new Promise(resolve => queueMicrotask(() => resolve())); + + // Appwrite 초기화 상태 확인 + if (!appwriteInitialized) { + console.log('Appwrite 초기화 상태 확인 중...'); + const status = reinitializeAppwrite(); + + if (!status.isInitialized) { + setConnectionError('서버 연결에 문제가 있습니다. 재시도해주세요.'); + setAppState('error'); + return; + } + } + + // 연결 상태 확인 + const connectionValid = await isValidConnection(); + if (!connectionValid) { + console.warn('Appwrite 연결 문제 발생'); + setConnectionError('서버 연결에 문제가 있습니다. 재시도해주세요.'); + setAppState('error'); + return; + } + + // 인증 오류 확인 + if (authError) { + console.error('Appwrite 인증 오류:', authError); + setConnectionError('인증 처리 중 오류가 발생했습니다.'); + setAppState('error'); + return; + } + + // 모든 검사 통과 시 준비 상태로 전환 + setAppState('ready'); + } catch (error) { + console.error('연결 확인 중 오류:', error); + setConnectionError('서버 연결 확인 중 오류가 발생했습니다.'); + setAppState('error'); + } + }; + + // 앱 상태가 로딩 상태일 때만 연결 확인 + if (appState === 'loading' && !authLoading) { + checkConnection(); + } + }, [appState, authLoading, authError, appwriteInitialized, reinitializeAppwrite]); + // 초기화 후 환영 메시지 표시 상태 확인 useEffect(() => { - if (isInitialized) { + if (isInitialized && appState === 'ready') { const timeoutId = setTimeout(checkWelcomeDialogState, 500); return () => clearTimeout(timeoutId); } - }, [isInitialized, checkWelcomeDialogState]); + }, [isInitialized, appState, checkWelcomeDialogState]); + + // 로딩 상태 표시 + if (appState === 'loading' || authLoading) { + return ( + +
+

Zellyy Finance

+

앱을 로딩하고 있습니다...

+
+ ); + } + + // 오류 상태 표시 + if (appState === 'error') { + return ( + +
⚠️
+

연결 오류

+

{connectionError || '서버 연결에 문제가 발생했습니다.'}

+ +
+ ); + } + // 준비 완료 시 일반 UI 표시 return ( diff --git a/src/test-appwrite-user.ts b/src/test-appwrite-user.ts new file mode 100644 index 0000000..206c371 --- /dev/null +++ b/src/test-appwrite-user.ts @@ -0,0 +1,86 @@ +/** + * Appwrite 사용자 연결 테스트 스크립트 + * + * 이 파일은 Appwrite 서비스와의 사용자 연결을 테스트하기 위한 스크립트입니다. + * 개발 가이드라인에 따라 UI 스레드를 차단하지 않도록 비동기 처리와 오류 처리를 포함합니다. + */ + +import { Client, Account, ID } from 'appwrite'; + +// 설정 값 직접 지정 +const endpoint = 'https://a11.ism.kr/v1'; +const projectId = '68182a300039f6d700a6'; // 프로젝트 ID +const userId = '68183aa4002a6f19542b'; // 사용자 ID + +// 테스트 함수 +async function testAppwriteUserConnection() { + console.log('Appwrite 사용자 연결 테스트 시작...'); + console.log('설정 정보:', { + endpoint, + projectId, + userId + }); + + try { + // Appwrite 클라이언트 생성 + const client = new Client(); + + client + .setEndpoint(endpoint) + .setProject(projectId); + + // 계정 서비스 초기화 + const account = new Account(client); + + // 이메일/비밀번호 로그인 테스트 + try { + console.log('이메일/비밀번호 로그인 테스트...'); + // 참고: 실제 로그인 정보는 보안상의 이유로 하드코딩하지 않습니다. + // 이 부분은 실제 애플리케이션에서 사용자 입력을 통해 처리해야 합니다. + console.log('로그인은 실제 애플리케이션에서 수행해야 합니다.'); + + // JWT 세션 테스트 (선택적) + try { + console.log('JWT 세션 테스트...'); + // JWT 세션 생성은 서버 측에서 수행해야 하는 작업입니다. + console.log('JWT 세션 생성은 서버 측에서 수행해야 합니다.'); + } catch (jwtError) { + console.error('JWT 세션 테스트 실패:', jwtError); + } + + } catch (loginError) { + console.error('로그인 테스트 실패:', loginError); + } + + // 익명 세션 테스트 + try { + console.log('익명 세션 테스트...'); + const anonymousSession = await account.createAnonymousSession(); + console.log('익명 세션 생성 성공:', anonymousSession.$id); + + // 세션 삭제 + try { + await account.deleteSession(anonymousSession.$id); + console.log('익명 세션 삭제 성공'); + } catch (deleteError) { + console.error('익명 세션 삭제 실패:', deleteError); + } + } catch (anonymousError) { + console.error('익명 세션 테스트 실패:', anonymousError); + } + + } catch (error) { + console.error('Appwrite 클라이언트 생성 오류:', error); + } + + console.log('Appwrite 사용자 연결 테스트 완료'); +} + +// 테스트 실행 +testAppwriteUserConnection() + .then(() => { + console.log('테스트가 완료되었습니다.'); + }) + .catch((error) => { + console.error('테스트 중 예외 발생:', error); + }); diff --git a/src/test-appwrite.ts b/src/test-appwrite.ts new file mode 100644 index 0000000..f995cc7 --- /dev/null +++ b/src/test-appwrite.ts @@ -0,0 +1,88 @@ +/** + * Appwrite 연결 테스트 스크립트 + * + * 이 파일은 Appwrite 서비스와의 연결을 테스트하기 위한 스크립트입니다. + * 개발 가이드라인에 따라 UI 스레드를 차단하지 않도록 비동기 처리와 오류 처리를 포함합니다. + */ + +import { Client, Account } from 'appwrite'; + +// 설정 값 직접 지정 +const endpoint = 'https://a11.ism.kr/v1'; +const projectId = '68182a300039f6d700a6'; // 올바른 프로젝트 ID +const apiKey = 'standard_9672cc2d052d4fc56d9d28e75c6476ff1029d932b7d375dbb4bb0f705d741d8e6d9ae154929009e01c7168810884b6ee80e6bb564d3fe6439b8b142ed4a8d287546bb0bed2531c20188a7ecc36e6f9983abb1ab0022c1656cf2219d4c2799655c7baef00ae4861fe74186dbb421141d9e2332f2fad812975ae7b4b7f57527cea'; + +// 테스트 함수 +async function testAppwriteConnection() { + console.log('Appwrite 연결 테스트 시작...'); + console.log('설정 정보:', { + endpoint, + projectId, + apiKey: apiKey ? '설정됨' : '설정되지 않음' + }); + + try { + // Appwrite 클라이언트 생성 + const client = new Client(); + + client + .setEndpoint(endpoint) + .setProject(projectId); + + // 계정 서비스 초기화 + const account = new Account(client); + + // 연결 테스트 (익명 세션 생성 시도) + try { + console.log('익명 세션 생성 시도...'); + const session = await account.createAnonymousSession(); + console.log('익명 세션 생성 성공:', session.$id); + + // 세션 정보 확인 + try { + const user = await account.get(); + console.log('사용자 정보 확인 성공:', user.$id); + } catch (userError) { + console.error('사용자 정보 확인 실패:', userError); + } + + // 세션 삭제 + try { + await account.deleteSession(session.$id); + console.log('세션 삭제 성공'); + } catch (deleteError) { + console.error('세션 삭제 실패:', deleteError); + } + + } catch (sessionError) { + console.error('익명 세션 생성 실패:', sessionError); + + // 프로젝트 정보 확인 시도 + try { + console.log('프로젝트 정보 확인 시도...'); + // 프로젝트 정보는 API 키가 있어야 확인 가능 + if (!apiKey) { + console.error('API 키가 없어 프로젝트 정보를 확인할 수 없습니다.'); + } else { + console.log('API 키가 있지만 클라이언트에서는 사용할 수 없습니다.'); + } + } catch (projectError) { + console.error('프로젝트 정보 확인 실패:', projectError); + } + } + + } catch (error) { + console.error('Appwrite 클라이언트 생성 오류:', error); + } + + console.log('Appwrite 연결 테스트 완료'); +} + +// 테스트 실행 +testAppwriteConnection() + .then(() => { + console.log('테스트가 완료되었습니다.'); + }) + .catch((error) => { + console.error('테스트 중 예외 발생:', error); + }); diff --git a/vite.config.ts b/vite.config.ts index 0c8a5d3..b2c5d78 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,7 +7,7 @@ import { componentTagger } from "lovable-tagger"; export default defineConfig(({ mode }) => ({ server: { host: "0.0.0.0", - port: 8080, + port: 3000, }, plugins: [ react(),