Implement error handling and loading states for Appwrite integration
This commit is contained in:
5
.env
5
.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
|
||||
|
||||
46
docs/02_기술_문서/02_기술스택.md
Normal file
46
docs/02_기술_문서/02_기술스택.md
Normal 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)
|
||||
@@ -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>
|
||||
|
||||
166
src/App.tsx
166
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';
|
||||
|
||||
// 간단한 오류 경계 컴포넌트 구현
|
||||
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 (
|
||||
<AuthProvider>
|
||||
<BudgetProvider>
|
||||
<div className="App">
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/transactions" element={<Transactions />} />
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
<Route path="/profile" element={<ProfileManagement />} />
|
||||
<Route path="/payment-methods" element={<PaymentMethods />} />
|
||||
<Route path="/help-support" element={<HelpSupport />} />
|
||||
<Route path="/security-privacy" element={<SecurityPrivacySettings />} />
|
||||
<Route path="/notifications" element={<NotificationSettings />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
<Route path="/appwrite-settings" element={<AppwriteSettingsPage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
<Toaster />
|
||||
</div>
|
||||
</BudgetProvider>
|
||||
</AuthProvider>
|
||||
<ErrorBoundary fallback={<ErrorScreen error={error} retry={handleRetry} />}>
|
||||
<AuthProvider>
|
||||
<BudgetProvider>
|
||||
<BasicLayout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/transactions" element={<Transactions />} />
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
<Route path="/profile" element={<ProfileManagement />} />
|
||||
<Route path="/payment-methods" element={<PaymentMethods />} />
|
||||
<Route path="/help-support" element={<HelpSupport />} />
|
||||
<Route path="/security-privacy" element={<SecurityPrivacySettings />} />
|
||||
<Route path="/notifications" element={<NotificationSettings />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
<Route path="/appwrite-settings" element={<AppwriteSettingsPage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BasicLayout>
|
||||
</BudgetProvider>
|
||||
</AuthProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
|
||||
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<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 인증 상태 변경: 로그인됨');
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// 오류 발생 시 로그아웃 상태로 간주
|
||||
if (user) {
|
||||
// 상태 업데이트를 마이크로태스크로 지연
|
||||
// 상태 업데이트를 마이크로태스크로 지연
|
||||
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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
// 로딩 중이 아니고 오류가 없을 때만 자식 컴포넌트 렌더링
|
||||
// 오류가 있어도 애플리케이션이 중단되지 않도록 처리
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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')
|
||||
? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.'
|
||||
: '예상치 못한 오류가 발생했습니다.';
|
||||
|
||||
|
||||
@@ -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`;
|
||||
try {
|
||||
// Appwrite로 회원가입 요청
|
||||
const user = await account.create(
|
||||
ID.unique(),
|
||||
email,
|
||||
password,
|
||||
username
|
||||
);
|
||||
|
||||
console.log('이메일 인증 리디렉션 URL:', redirectUrl);
|
||||
// 이메일 인증 메일 발송
|
||||
await account.createVerification(window.location.origin + '/login');
|
||||
|
||||
// 회원가입 요청
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
username, // 사용자 이름을 메타데이터에 저장
|
||||
},
|
||||
emailRedirectTo: redirectUrl
|
||||
}
|
||||
});
|
||||
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
|
||||
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
||||
|
||||
if (error) {
|
||||
console.error('회원가입 오류:', error);
|
||||
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');
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
/**
|
||||
* Appwrite 클라이언트 초기화 함수
|
||||
* UI 스레드를 차단하지 않도록 비동기적으로 초기화합니다.
|
||||
*/
|
||||
const initializeAppwriteClient = () => {
|
||||
try {
|
||||
// 설정 유효성 검증
|
||||
validateConfig();
|
||||
|
||||
console.log(`Appwrite 클라이언트 생성 중: ${config.endpoint}`);
|
||||
console.log(`Appwrite 클라이언트 생성 중: ${config.endpoint}`);
|
||||
console.log(`프로젝트 ID: ${config.projectId}`);
|
||||
|
||||
// Appwrite 클라이언트 생성
|
||||
appwriteClient = new Client();
|
||||
// Appwrite 클라이언트 생성
|
||||
appwriteClient = new Client();
|
||||
|
||||
appwriteClient
|
||||
.setEndpoint(config.endpoint)
|
||||
.setProject(config.projectId);
|
||||
appwriteClient
|
||||
.setEndpoint(config.endpoint)
|
||||
.setProject(config.projectId);
|
||||
|
||||
// 서비스 초기화
|
||||
accountService = new Account(appwriteClient);
|
||||
databasesService = new Databases(appwriteClient);
|
||||
storageService = new Storage(appwriteClient);
|
||||
avatarsService = new Avatars(appwriteClient);
|
||||
// API 키가 있는 경우 설정
|
||||
if (config.apiKey) {
|
||||
console.log('API 키 설정 중...');
|
||||
// 최신 Appwrite SDK에서는 JWT 토큰을 사용하거나 세션 기반 인증을 사용합니다.
|
||||
// 서버에서는 API 키를 사용하지만 클라이언트에서는 사용하지 않습니다.
|
||||
// 클라이언트에서 API 키를 사용하는 것은 보안 위험이 있어 권장되지 않습니다.
|
||||
console.log('API 키가 설정되었지만 클라이언트에서는 사용하지 않습니다.');
|
||||
} else {
|
||||
console.warn('API 키가 설정되지 않았습니다. 일부 기능이 제한될 수 있습니다.');
|
||||
}
|
||||
|
||||
console.log('Appwrite 클라이언트가 성공적으로 생성되었습니다.');
|
||||
// 서비스 초기화
|
||||
accountService = new Account(appwriteClient);
|
||||
databasesService = new Databases(appwriteClient);
|
||||
storageService = new Storage(appwriteClient);
|
||||
avatarsService = new Avatars(appwriteClient);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Appwrite 클라이언트 생성 오류:', error);
|
||||
isInitialized = true;
|
||||
console.log('Appwrite 클라이언트가 성공적으로 생성되었습니다.');
|
||||
|
||||
// 더미 클라이언트 생성 (앱이 완전히 실패하지 않도록)
|
||||
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 서버 연결에 실패했습니다. 환경 설정을 확인해주세요.');
|
||||
// 세션 확인 (선택적)
|
||||
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<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;
|
||||
}
|
||||
|
||||
@@ -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 유효한 설정인지 여부
|
||||
|
||||
28
src/lib/appwrite/defaultUser.ts
Normal file
28
src/lib/appwrite/defaultUser.ts
Normal 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;
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
114
src/main.tsx
114
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(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
);
|
||||
// 전역 오류 핸들러 추가
|
||||
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>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
86
src/test-appwrite-user.ts
Normal 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
88
src/test-appwrite.ts
Normal 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);
|
||||
});
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user