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:
hansoo
2025-07-13 14:01:27 +09:00
parent e72f9e8d26
commit c231d5be65
59 changed files with 5974 additions and 751 deletions

View File

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