Implement error handling and loading states for Appwrite integration

This commit is contained in:
hansoo
2025-05-05 15:41:19 +09:00
parent f83bb384af
commit 5305c98970
19 changed files with 1055 additions and 209 deletions

5
.env
View File

@@ -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

View File

@@ -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)

View File

@@ -12,8 +12,6 @@
<body>
<div id="root"></div>
<!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
<script src="https://cdn.gpteng.co/gptengineer.js" type="module"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -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,15 +18,130 @@ import NotificationSettings from './pages/NotificationSettings';
import ForgotPassword from './pages/ForgotPassword';
import AppwriteSettingsPage from './pages/AppwriteSettingsPage';
// 간단한 오류 경계 컴포넌트 구현
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
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 || (
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center">
<h2 className="text-xl font-bold mb-4"> </h2>
<p className="mb-4"> .</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
</div>
);
}
return this.props.children;
}
}
// 로딩 상태 표시 컴포넌트
const LoadingScreen: React.FC = () => (
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center bg-neuro-background">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
<h2 className="text-xl font-bold mb-2">Zellyy Finance</h2>
<p className="text-gray-600"> ...</p>
</div>
);
// 오류 화면 컴포넌트
const ErrorScreen: React.FC<{ error: Error | null; retry: () => void }> = ({ error, retry }) => (
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center bg-neuro-background">
<div className="text-red-500 text-5xl mb-4"></div>
<h2 className="text-xl font-bold mb-4"> </h2>
<p className="text-center mb-6">{error?.message || '애플리케이션 로딩 중 오류가 발생했습니다.'}</p>
<button
onClick={retry}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
</div>
);
// 기본 레이아웃 컴포넌트 - 인증 없이도 표시 가능
const BasicLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="App">
{children}
<Toaster />
</div>
);
function App() {
const [appState, setAppState] = useState<'loading' | 'error' | 'ready'>('loading');
const [error, setError] = useState<Error | null>(null);
const [appwriteEnabled, setAppwriteEnabled] = useState(true);
useEffect(() => {
document.title = "적자 탈출 가계부";
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 (
<ErrorBoundary fallback={<ErrorScreen error={error} retry={handleRetry} />}>
<LoadingScreen />
</ErrorBoundary>
);
}
// 오류 상태 표시
if (appState === 'error') {
return <ErrorScreen error={error} retry={handleRetry} />;
}
return (
<ErrorBoundary fallback={<ErrorScreen error={error} retry={handleRetry} />}>
<AuthProvider>
<BudgetProvider>
<div className="App">
<BasicLayout>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/login" element={<Login />} />
@@ -44,10 +158,10 @@ function App() {
<Route path="/appwrite-settings" element={<AppwriteSettingsPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Toaster />
</div>
</BasicLayout>
</BudgetProvider>
</AuthProvider>
</ErrorBoundary>
);
}

View File

@@ -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<Models.Session | null>(null);
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [appwriteInitialized, setAppwriteInitialized] = useState<boolean>(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<void>(resolve => queueMicrotask(() => resolve()));
// Appwrite 초기화 상태 확인
const isInitialized = await checkAppwriteInitialization();
if (!isInitialized) {
console.warn('Appwrite 초기화 상태가 정상적이지 않습니다. 비로그인 상태로 처리합니다.');
queueMicrotask(() => {
setSession(null);
setUser(null);
setLoading(false);
});
return;
}
// 사용자 정보 가져오기 시도 - 안전한 방식으로 처리
try {
// Appwrite 세션 가져오기
const currentSession = await account.getSession('current');
const currentUser = await account.get();
// 사용자 정보 가져오기 시도
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(() => {
setSession(currentSession);
setUser(currentUser);
console.log('세션 로딩 완료');
setSession(currentSession);
console.log('세션 로딩 완료 - 사용자:', currentUser.$id);
});
} else {
// 사용자 정보가 없으면 비로그인 상태로 처리
queueMicrotask(() => {
setSession(null);
setUser(null);
console.log('비로그인 상태로 처리');
});
} catch (sessionError) {
console.error('세션 로딩 중 오류:', sessionError);
}
} catch (error) {
console.error('세션 확인 중 예외 발생:', error);
// 예상치 못한 오류 처리
console.error('세션 처리 중 예상치 못한 오류:', error);
handleAuthError(error);
// 오류 발생 시 로그아웃 상태로 처리
queueMicrotask(() => {
setSession(null);
setUser(null);
});
}
} catch (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<void>(resolve => queueMicrotask(() => resolve()));
// 사용자 정보가 변경되었는지 확인
// 사용자 정보가 있고, 이전과 다르면 세션 정보 가져오기
if (currentUser && (!user || currentUser.$id !== user.$id)) {
// 세션 정보 가져오기
const currentSession = await account.getSession('current');
try {
// 세션 정보 가져오기 시도 - 안전하게 처리
const currentSession = await account.getSession('current').catch(err => {
console.log('세션 정보 가져오기 실패:', err);
return null;
});
// 상태 업데이트를 마이크로태스크로 지연
queueMicrotask(() => {
setSession(currentSession);
setUser(currentUser);
console.log('Appwrite 인증 상태 변경: 로그인됨');
setSession(currentSession);
console.log('Appwrite 인증 상태 변경: 로그인됨 - 사용자:', currentUser.$id);
});
} catch (sessionError) {
console.error('세션 정보 가져오기 중 오류:', sessionError);
// 오류 발생해도 사용자 정보는 업데이트
queueMicrotask(() => {
setUser(currentUser);
setSession(null);
});
}
} catch (error) {
// 오류 발생 시 로그아웃 상태로 간주
if (user) {
// 상태 업데이트를 마이크로태스크로 지연
} 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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
// 로딩 중이 아니고 오류가 없을 때만 자식 컴포넌트 렌더링
// 오류가 있어도 애플리케이션이 중단되지 않도록 처리
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};

View File

@@ -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',
});
console.log('비밀번호 재설정 시도 중:', email);
if (error) {
console.error('비밀번호 재설정 오류:', error);
showAuthToast('비밀번호 재설정 실패', error.message, 'destructive');
return { error };
}
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
try {
// Appwrite로 비밀번호 재설정 이메일 발송
await account.createRecovery(
email,
window.location.origin + '/reset-password'
);
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(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 };
}
} catch (error: any) {
console.error('비밀번호 재설정 중 예외 발생:', error);
// 네트워크 오류 확인
const errorMessage = handleNetworkError(error);
const errorMessage = error.message && error.message.includes('network')
? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.'
: '예상치 못한 오류가 발생했습니다.';
showAuthToast('비밀번호 재설정 오류', errorMessage, 'destructive');
return { error };

View File

@@ -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');
}
return { error: authError, user: null };
}
} catch (error) {

View File

@@ -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<void> => {
try {
const { error } = await supabase.auth.signOut();
console.log('로그아웃 시도 중');
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
try {
// 현재 세션 아이디 가져오기
const currentSession = await account.getSession('current');
// 현재 세션 삭제
await account.deleteSession(currentSession.$id);
// 로그아웃 시 열려있는 모든 토스트 제거
clearAllToasts();
// 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함
window.dispatchEvent(new Event('auth-state-changed'));
if (error) {
console.error('로그아웃 오류:', error);
showAuthToast('로그아웃 실패', error.message, 'destructive');
} else {
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')
? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.'
: '예상치 못한 오류가 발생했습니다.';

View File

@@ -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<void>(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({
try {
// Appwrite로 회원가입 요청
const user = await account.create(
ID.unique(),
email,
password,
options: {
data: {
username, // 사용자 이름을 메타데이터에 저장
},
emailRedirectTo: redirectUrl
}
});
username
);
if (error) {
console.error('회원가입 오류:', error);
// 이메일 인증 메일 발송
await account.createVerification(window.location.origin + '/login');
// 오류 메시지 처리
let errorMessage = error.message;
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
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 = '이메일 링크가 유효하지 않습니다.';
}
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,
user,
message: '이메일 인증 필요',
emailConfirmationRequired: true
};
} else {
showAuthToast('회원가입 성공', '환영합니다!', 'default');
return { error: null, user: data.user };
}
} catch (authError: any) {
console.error('회원가입 오류:', authError);
// 오류 메시지 처리
let errorMessage = authError.message || '알 수 없는 오류가 발생했습니다.';
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
if (authError.code === 409) {
errorMessage = '이미 등록된 이메일입니다.';
} else if (authError.code === 400) {
errorMessage = '유효하지 않은 이메일 또는 비밀번호입니다.';
} else if (authError.code === 429) {
errorMessage = '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.';
}
// 사용자 데이터가 없는 경우 (드물게 발생)
console.warn('회원가입 응답은 성공했지만 사용자 데이터가 없습니다');
showAuthToast('회원가입 성공', '계정이 생성되었습니다. 이메일 인증을 완료한 후 로그인해주세요.', 'default');
return {
error: null,
user: { email },
message: '회원가입 완료',
emailConfirmationRequired: true
};
showAuthToast('회원가입 실패', errorMessage, 'destructive');
return { error: { message: errorMessage }, user: null };
}
} catch (error: any) {
console.error('회원가입 전역 예외:', error);
showAuthToast('회원가입 오류', error.message || '알 수 없는 오류', 'destructive');

View File

@@ -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<Models.Preferences> | 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<void>;

View File

@@ -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,11 +28,17 @@ let databasesService: Databases;
let storageService: Storage;
let avatarsService: Avatars;
try {
/**
* Appwrite 클라이언트 초기화 함수
* UI 스레드를 차단하지 않도록 비동기적으로 초기화합니다.
*/
const initializeAppwriteClient = () => {
try {
// 설정 유효성 검증
validateConfig();
console.log(`Appwrite 클라이언트 생성 중: ${config.endpoint}`);
console.log(`프로젝트 ID: ${config.projectId}`);
// Appwrite 클라이언트 생성
appwriteClient = new Client();
@@ -37,16 +47,40 @@ try {
.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 클라이언트가 성공적으로 생성되었습니다.');
} catch (error) {
// 세션 확인 (선택적)
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();
@@ -58,10 +92,14 @@ try {
// 사용자에게 오류 알림 (개발 모드에서만)
if (import.meta.env.DEV) {
queueMicrotask(() => {
alert('Appwrite 서버 연결에 실패했습니다. 환경 설정을 확인해주세요.');
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<boolean> => {
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;
}

View File

@@ -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 유효한 설정인지 여부

View File

@@ -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;
};

View File

@@ -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,

View File

@@ -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 = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: Arial, sans-serif;">
<div style="color: red; font-size: 48px; margin-bottom: 20px;">⚠️</div>
<h1 style="margin-bottom: 20px;">Zellyy Finance 오류</h1>
<p style="margin-bottom: 20px; text-align: center;">애플리케이션 로딩 중 오류가 발생했습니다.</p>
<pre style="max-width: 80%; overflow: auto; background: #f5f5f5; padding: 10px; border-radius: 4px; margin-bottom: 20px;">${message}</pre>
<button
onclick="window.location.reload()"
style="padding: 10px 20px; background-color: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;"
>
새로고침
</button>
</div>
`;
}
return false;
};
// 처리되지 않은 Promise 오류 핸들러 추가
window.addEventListener('unhandledrejection', function(event) {
console.error('처리되지 않은 Promise 오류:', event.reason);
// 오류 발생 시 기본 오류 화면 표시
const rootElement = document.getElementById('root');
if (rootElement) {
rootElement.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: Arial, sans-serif;">
<div style="color: red; font-size: 48px; margin-bottom: 20px;">⚠️</div>
<h1 style="margin-bottom: 20px;">Zellyy Finance 오류</h1>
<p style="margin-bottom: 20px; text-align: center;">비동기 작업 중 오류가 발생했습니다.</p>
<pre style="max-width: 80%; overflow: auto; background: #f5f5f5; padding: 10px; border-radius: 4px; margin-bottom: 20px;">${event.reason}</pre>
<button
onclick="window.location.reload()"
style="padding: 10px 20px; background-color: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;"
>
새로고침
</button>
</div>
`;
}
});
// 디버깅 정보 출력
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(
<BrowserRouter>
<App />
</BrowserRouter>
);
);
console.log('애플리케이션 렌더링 성공');
} catch (error) {
console.error('애플리케이션 렌더링 오류:', error);
// 오류 발생 시 기본 오류 화면 표시
const rootElement = document.getElementById('root');
if (rootElement) {
rootElement.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: Arial, sans-serif;">
<div style="color: red; font-size: 48px; margin-bottom: 20px;">⚠️</div>
<h1 style="margin-bottom: 20px;">Zellyy Finance 오류</h1>
<p style="margin-bottom: 20px; text-align: center;">애플리케이션 로딩 중 오류가 발생했습니다.</p>
<button
onclick="window.location.reload()"
style="padding: 10px 20px; background-color: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;"
>
새로고침
</button>
</div>
`;
}
};

View File

@@ -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<string | null>(null);
// 커스텀 훅 사용으로 코드 분리
useInitialDataLoading();
useAppFocusEvents();
useWelcomeNotification(isInitialized);
// Appwrite 연결 상태 확인
useEffect(() => {
const checkConnection = async () => {
try {
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(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 (
<SafeAreaContainer className="min-h-screen bg-neuro-background flex flex-col items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
<h2 className="text-xl font-bold mb-2">Zellyy Finance</h2>
<p className="text-gray-600"> ...</p>
</SafeAreaContainer>
);
}
// 오류 상태 표시
if (appState === 'error') {
return (
<SafeAreaContainer className="min-h-screen bg-neuro-background flex flex-col items-center justify-center p-4">
<div className="text-red-500 text-5xl mb-4"></div>
<h2 className="text-xl font-bold mb-4"> </h2>
<p className="text-center mb-6">{connectionError || '서버 연결에 문제가 발생했습니다.'}</p>
<button
onClick={() => {
setAppState('loading');
reinitializeAppwrite();
}}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
</SafeAreaContainer>
);
}
// 준비 완료 시 일반 UI 표시
return (
<SafeAreaContainer className="min-h-screen bg-neuro-background pb-24" extraBottomPadding={true}>
<IndexContent />

86
src/test-appwrite-user.ts Normal file
View File

@@ -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);
});

88
src/test-appwrite.ts Normal file
View File

@@ -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);
});

View File

@@ -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(),