feat: Clerk + Supabase 통합 시스템 구현 완료
주요 변경사항: • Clerk 인증 시스템 통합 및 설정 • Supabase 데이터베이스 스키마 설계 및 적용 • JWT 기반 Row Level Security (RLS) 정책 구현 • 기존 Appwrite 인증을 Clerk로 완전 교체 기술적 개선: • 무한 로딩 문제 해결 - Index.tsx 인증 로직 수정 • React root 마운팅 오류 수정 - main.tsx 개선 • CORS 설정 추가 - vite.config.ts 수정 • Sentry 에러 모니터링 통합 추가된 컴포넌트: • AuthGuard: 인증 보호 컴포넌트 • SignIn/SignUp: Clerk 기반 인증 UI • ClerkProvider: Clerk 설정 래퍼 • EnvTest: 개발환경 디버깅 도구 데이터베이스: • user_profiles, transactions, budgets, category_budgets 테이블 • Clerk JWT 토큰 기반 RLS 정책 • 자동 사용자 프로필 생성 및 동기화 Task Master: • Task 11.1, 11.2, 11.4 완료 • 프로젝트 관리 시스템 업데이트 Note: ESLint 정리는 별도 커밋에서 진행 예정 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
186
src/App.tsx
186
src/App.tsx
@@ -10,13 +10,26 @@ import React, {
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { Routes, Route, useLocation } from "react-router-dom";
|
||||
import { initializeStores, cleanupStores } from "./stores/storeInitializer";
|
||||
import { queryClient, isDevMode } from "./lib/query/queryClient";
|
||||
import { Toaster } from "./components/ui/toaster";
|
||||
import BackgroundSync from "./components/sync/BackgroundSync";
|
||||
import QueryCacheManager from "./components/query/QueryCacheManager";
|
||||
import OfflineManager from "./components/offline/OfflineManager";
|
||||
import SentryTestButton from "./components/SentryTestButton";
|
||||
import {
|
||||
initSentry,
|
||||
SentryErrorBoundary,
|
||||
captureError,
|
||||
initWebVitals,
|
||||
trackPageView,
|
||||
} from "./lib/sentry";
|
||||
import {
|
||||
ClerkProvider,
|
||||
ClerkDebugInfo,
|
||||
} from "./components/providers/ClerkProvider";
|
||||
import { EnvTest } from "./components/debug/EnvTest";
|
||||
|
||||
// 페이지 컴포넌트들을 레이지 로딩으로 변경
|
||||
const Index = lazy(() => import("./pages/Index"));
|
||||
@@ -34,7 +47,19 @@ const SecurityPrivacySettings = lazy(
|
||||
);
|
||||
const NotificationSettings = lazy(() => import("./pages/NotificationSettings"));
|
||||
const ForgotPassword = lazy(() => import("./pages/ForgotPassword"));
|
||||
const AppwriteSettingsPage = lazy(() => import("./pages/AppwriteSettingsPage"));
|
||||
// const AppwriteSettingsPage = lazy(() => import("./pages/AppwriteSettingsPage")); // 제거됨 - Supabase로 이전
|
||||
|
||||
// Clerk 인증 컴포넌트
|
||||
const SignIn = lazy(() =>
|
||||
import("./components/auth/SignIn").then((module) => ({
|
||||
default: module.SignIn,
|
||||
}))
|
||||
);
|
||||
const SignUp = lazy(() =>
|
||||
import("./components/auth/SignUp").then((module) => ({
|
||||
default: module.SignUp,
|
||||
}))
|
||||
);
|
||||
|
||||
// 간단한 오류 경계 컴포넌트 구현
|
||||
interface ErrorBoundaryProps {
|
||||
@@ -59,6 +84,8 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
logger.error("애플리케이션 오류:", error, errorInfo);
|
||||
// Sentry에 에러 리포팅
|
||||
captureError(error, { errorInfo });
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
@@ -103,6 +130,35 @@ const PageLoadingSpinner: React.FC = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
// 페이지 전환 추적 컴포넌트
|
||||
const PageTracker = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
// 페이지 이름 매핑
|
||||
const getPageName = (pathname: string) => {
|
||||
const pageMap: Record<string, string> = {
|
||||
"/": "홈",
|
||||
"/login": "로그인",
|
||||
"/register": "회원가입",
|
||||
"/forgot-password": "비밀번호 찾기",
|
||||
"/analytics": "분석",
|
||||
"/transactions": "거래내역",
|
||||
"/settings": "설정",
|
||||
"/profile": "프로필",
|
||||
"/help": "도움말",
|
||||
"/appwrite-settings": "백엔드 설정",
|
||||
};
|
||||
return pageMap[pathname] || pathname;
|
||||
};
|
||||
|
||||
const pageName = getPageName(location.pathname);
|
||||
trackPageView(pageName, location.pathname + location.search);
|
||||
}, [location]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 오류 화면 컴포넌트
|
||||
const ErrorScreen: React.FC<{ error: Error | null; retry: () => void }> = ({
|
||||
error,
|
||||
@@ -142,6 +198,12 @@ function App() {
|
||||
useEffect(() => {
|
||||
document.title = "Zellyy Finance";
|
||||
|
||||
// Sentry 초기화
|
||||
initSentry();
|
||||
|
||||
// Web Vitals 측정 초기화
|
||||
initWebVitals();
|
||||
|
||||
// Zustand 스토어 초기화
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
@@ -149,7 +211,10 @@ function App() {
|
||||
setAppState("ready");
|
||||
} catch (error) {
|
||||
logger.error("앱 초기화 실패", error);
|
||||
setError(error instanceof Error ? error : new Error("앱 초기화 실패"));
|
||||
const appError =
|
||||
error instanceof Error ? error : new Error("앱 초기화 실패");
|
||||
captureError(appError, { context: "앱 초기화" });
|
||||
setError(appError);
|
||||
setAppState("error");
|
||||
}
|
||||
};
|
||||
@@ -199,55 +264,76 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ErrorBoundary
|
||||
fallback={<ErrorScreen error={error} retry={handleRetry} />}
|
||||
>
|
||||
<BasicLayout>
|
||||
<Suspense fallback={<PageLoadingSpinner />}>
|
||||
<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>
|
||||
</Suspense>
|
||||
{/* React Query 캐시 관리 */}
|
||||
<QueryCacheManager
|
||||
cleanupIntervalMinutes={30}
|
||||
enableOfflineCache={true}
|
||||
enableCacheAnalysis={isDevMode}
|
||||
/>
|
||||
<ClerkProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SentryErrorBoundary
|
||||
fallback={<ErrorScreen error={error} retry={handleRetry} />}
|
||||
showDialog={false}
|
||||
>
|
||||
<BasicLayout>
|
||||
<PageTracker />
|
||||
<Suspense fallback={<PageLoadingSpinner />}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/sign-in/*" element={<SignIn />} />
|
||||
<Route path="/sign-up/*" element={<SignUp />} />
|
||||
<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={<div>Supabase Settings (구현 예정)</div>}
|
||||
/>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
{/* React Query 캐시 관리 */}
|
||||
<QueryCacheManager
|
||||
cleanupIntervalMinutes={30}
|
||||
enableOfflineCache={true}
|
||||
enableCacheAnalysis={isDevMode}
|
||||
/>
|
||||
|
||||
{/* 오프라인 상태 관리 */}
|
||||
<OfflineManager showOfflineToast={true} autoSyncOnReconnect={true} />
|
||||
{/* 오프라인 상태 관리 */}
|
||||
<OfflineManager
|
||||
showOfflineToast={true}
|
||||
autoSyncOnReconnect={true}
|
||||
/>
|
||||
|
||||
{/* 백그라운드 자동 동기화 - 성능 최적화로 30초 간격으로 조정 */}
|
||||
<BackgroundSync
|
||||
intervalMinutes={0.5}
|
||||
syncOnFocus={true}
|
||||
syncOnOnline={true}
|
||||
/>
|
||||
</BasicLayout>
|
||||
{isDevMode && <ReactQueryDevtools initialIsOpen={false} />}
|
||||
</ErrorBoundary>
|
||||
</QueryClientProvider>
|
||||
{/* 백그라운드 자동 동기화 - 성능 최적화로 30초 간격으로 조정 */}
|
||||
<BackgroundSync
|
||||
intervalMinutes={0.5}
|
||||
syncOnFocus={true}
|
||||
syncOnOnline={true}
|
||||
/>
|
||||
|
||||
{/* 개발환경에서 Sentry 테스트 버튼 */}
|
||||
<SentryTestButton />
|
||||
|
||||
{/* 개발환경에서 Clerk 상태 디버깅 */}
|
||||
<ClerkDebugInfo />
|
||||
|
||||
{/* 개발환경에서 환경 변수 테스트 */}
|
||||
{isDevMode && <EnvTest />}
|
||||
</BasicLayout>
|
||||
{isDevMode && <ReactQueryDevtools initialIsOpen={false} />}
|
||||
</SentryErrorBoundary>
|
||||
</QueryClientProvider>
|
||||
</ClerkProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user