Files
zellyy-finance/src/App.tsx
hansoo 3463c836e7 debug: BasicApp으로 전환하여 Vercel 배포 문제 디버깅
- App.tsx 대신 BasicApp.tsx로 전환
- 환경 변수 로깅 강화
- Vercel에서 발생하는 공백 페이지 문제 해결 시도
2025-07-15 05:22:03 +09:00

453 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, {
useEffect,
useState,
Component,
ErrorInfo,
ReactNode,
Suspense,
} from "react";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { logger } from "@/utils/logger";
import { Routes, Route, useLocation } from "react-router-dom";
import { initializeStores } from "./stores/storeInitializer";
import { queryClient, isDevMode } from "./lib/query/queryClient";
import { Toaster } from "./components/ui/toaster";
import { SentryErrorBoundary, captureError, trackPageView } from "./lib/sentry";
import { initializePWA } from "./utils/pwa";
import { EnvTest } from "./components/debug/EnvTest";
// import { setupChunkErrorHandler, resetRetryCount } from "./utils/chunkErrorHandler"; // 임시 비활성화
import { createLazyComponent } from "./utils/lazyWithRetry";
import {
isChunkLoadError,
isClerkChunkError,
handleChunkLoadError,
} from "./utils/chunkErrorProtection";
import {
ClerkProvider,
ClerkDebugInfo,
} from "./components/providers/ClerkProvider";
import { BudgetProvider } from "./contexts/budget/BudgetContext";
// 페이지 컴포넌트들을 개선된 레이지 로딩으로 변경 (ChunkLoadError 재시도 포함)
const Index = createLazyComponent(() => import("./pages/Index"));
const Login = createLazyComponent(() => import("./pages/Login"));
const Register = createLazyComponent(() => import("./pages/Register"));
const Settings = createLazyComponent(() => import("./pages/Settings"));
const Transactions = createLazyComponent(() => import("./pages/Transactions"));
const Analytics = createLazyComponent(() => import("./pages/Analytics"));
const ProfileManagement = createLazyComponent(
() => import("./pages/ProfileManagement")
);
const NotFound = createLazyComponent(() => import("./pages/NotFound"));
const PaymentMethods = createLazyComponent(
() => import("./pages/PaymentMethods")
);
const HelpSupport = createLazyComponent(() => import("./pages/HelpSupport"));
const SecurityPrivacySettings = createLazyComponent(
() => import("./pages/SecurityPrivacySettings")
);
const NotificationSettings = createLazyComponent(
() => import("./pages/NotificationSettings")
);
const ForgotPassword = createLazyComponent(
() => import("./pages/ForgotPassword")
);
const PWADebugPage = createLazyComponent(() => import("./pages/PWADebugPage"));
// 중요하지 않은 컴포넌트들도 개선된 레이지 로딩 적용
const BackgroundSync = createLazyComponent(
() => import("./components/sync/BackgroundSync")
);
const QueryCacheManager = createLazyComponent(
() => import("./components/query/QueryCacheManager")
);
const OfflineManager = createLazyComponent(
() => import("./components/offline/OfflineManager")
);
const SentryTestButton = createLazyComponent(
() => import("./components/SentryTestButton")
);
const ClerkDebugControl = createLazyComponent(
() => import("./components/debug/ClerkDebugControl")
);
// Clerk 인증 컴포넌트 (다시 활성화)
const SignIn = createLazyComponent(() =>
import("./components/auth/SignIn").then((module) => ({
default: module.SignIn,
}))
);
const SignUp = createLazyComponent(() =>
import("./components/auth/SignUp").then((module) => ({
default: module.SignUp,
}))
);
// 간단한 오류 경계 컴포넌트 구현
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 {
logger.error("애플리케이션 오류:", {
error: error.message,
componentStack: errorInfo.componentStack,
});
// ChunkLoadError 처리
if (isChunkLoadError(error)) {
if (isClerkChunkError(error)) {
logger.warn("Error Boundary에서 Clerk 청크 오류 감지. 자동 복구 시도");
// Clerk 자동 비활성화
sessionStorage.setItem("disableClerk", "true");
// 3초 후 새로고침
setTimeout(() => {
const url = new URL(window.location.href);
url.searchParams.set("noClerk", "true");
url.searchParams.set("_t", Date.now().toString());
window.location.href = url.toString();
}, 3000);
return;
} else {
// 일반 청크 오류 처리
handleChunkLoadError(error);
return;
}
}
// Sentry에 에러 리포팅 (청크 오류가 아닌 경우만)
captureError(error, { errorInfo });
}
render(): ReactNode {
if (this.state.hasError) {
// ChunkLoadError인 경우 특별한 UI 표시
if (this.state.error && isChunkLoadError(this.state.error)) {
const isClerkError = isClerkChunkError(this.state.error);
return (
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center">
<div className={`text-4xl mb-4 ${isClerkError ? "🔧" : "⚠️"}`}>
{isClerkError ? "🔧" : "⚠️"}
</div>
<h2 className="text-xl font-bold mb-4">
{isClerkError ? "Clerk 로딩 오류" : "앱 로딩 오류"}
</h2>
<p className="mb-4 text-gray-600">
{isClerkError
? "Supabase 인증으로 자동 전환 중입니다. 잠시만 기다려주세요..."
: "앱을 복구하고 있습니다. 잠시만 기다려주세요..."}
</p>
{!isClerkError && (
<button
onClick={() => {
sessionStorage.clear();
window.location.reload();
}}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
)}
</div>
);
}
// 일반 오류 처리
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 className="mt-4 text-xs text-gray-500">
: {import.meta.env.MODE} | Clerk:{" "}
{import.meta.env.VITE_CLERK_PUBLISHABLE_KEY ? "✓" : "✗"} | Supabase:{" "}
{import.meta.env.VITE_SUPABASE_URL ? "✓" : "✗"}
</div>
</div>
);
// 페이지 로딩 컴포넌트 (코드 스플리팅용)
const PageLoadingSpinner: React.FC = () => (
<div className="flex flex-col items-center justify-center min-h-[50vh] p-4 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500 mb-2"></div>
<p className="text-gray-600 text-sm"> ...</p>
</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": "도움말",
};
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,
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);
useEffect(() => {
document.title = "Zellyy Finance";
// eslint-disable-next-line no-console
console.log("🚀 App useEffect 실행됨");
// 프로덕션 환경에서 간단한 초기화 테스트
const simpleInitialize = async () => {
try {
// eslint-disable-next-line no-console
console.log("🔧 간단한 초기화 시작");
// eslint-disable-next-line no-console
console.log("환경:", import.meta.env.MODE);
// eslint-disable-next-line no-console
console.log("모든 환경변수:", import.meta.env);
// eslint-disable-next-line no-console
console.log(
"VITE_CLERK_PUBLISHABLE_KEY:",
import.meta.env.VITE_CLERK_PUBLISHABLE_KEY || "없음"
);
// eslint-disable-next-line no-console
console.log(
"VITE_SUPABASE_URL:",
import.meta.env.VITE_SUPABASE_URL || "없음"
);
// 매우 간단한 초기화만 수행
await new Promise((resolve) => setTimeout(resolve, 100));
// eslint-disable-next-line no-console
console.log("✅ 간단한 초기화 완료 - ready 상태로 변경");
setAppState("ready");
} catch (error) {
console.error("❌ 간단한 초기화 실패:", error);
setError(error instanceof Error ? error : new Error("초기화 실패"));
setAppState("error");
}
};
simpleInitialize();
// 컴포넌트 언마운트 시 정리
return () => {
// eslint-disable-next-line no-console
console.log("🧹 App 컴포넌트 정리");
};
}, []);
// 재시도 기능
const handleRetry = async () => {
setAppState("loading");
setError(null);
try {
// 재시도 시 PWA 및 스토어 재초기화
await initializePWA();
await initializeStores();
setAppState("ready");
} catch (error) {
logger.error(
"재시도 실패",
error instanceof Error
? { message: error.message, stack: error.stack }
: String(error)
);
setError(error instanceof Error ? error : new Error("재시도 실패"));
setAppState("error");
}
};
// 로딩 상태 표시
if (appState === "loading") {
return (
<ErrorBoundary
fallback={<ErrorScreen error={error} retry={handleRetry} />}
>
<LoadingScreen />
</ErrorBoundary>
);
}
// 오류 상태 표시
if (appState === "error") {
return <ErrorScreen error={error} retry={handleRetry} />;
}
return (
<ClerkProvider>
<QueryClientProvider client={queryClient}>
<SentryErrorBoundary
fallback={<ErrorScreen error={error} retry={handleRetry} />}
showDialog={false}
>
<BudgetProvider>
<BasicLayout>
<PageTracker />
<Suspense fallback={<PageLoadingSpinner />}>
<Routes>
<Route path="/" element={<Index />} />
{/* Clerk 라우트 다시 활성화 */}
<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="/pwa-debug" element={<PWADebugPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
{/* React Query 캐시 관리 */}
<Suspense fallback={null}>
<QueryCacheManager
cleanupIntervalMinutes={30}
enableOfflineCache={true}
enableCacheAnalysis={isDevMode}
/>
</Suspense>
{/* 오프라인 상태 관리 */}
<Suspense fallback={null}>
<OfflineManager
showOfflineToast={true}
autoSyncOnReconnect={true}
/>
</Suspense>
{/* 백그라운드 자동 동기화 - 성능 최적화로 30초 간격으로 조정 */}
<Suspense fallback={null}>
<BackgroundSync
intervalMinutes={0.5}
syncOnFocus={true}
syncOnOnline={true}
/>
</Suspense>
{/* 개발환경에서 Sentry 테스트 버튼 */}
<Suspense fallback={null}>
<SentryTestButton />
</Suspense>
{/* 개발환경에서 Clerk 상태 디버깅 */}
<ClerkDebugInfo />
{/* 개발환경에서 환경 변수 테스트 */}
{isDevMode && <EnvTest />}
{/* Clerk 디버그 및 제어 */}
<Suspense fallback={null}>
<ClerkDebugControl />
</Suspense>
</BasicLayout>
</BudgetProvider>
{isDevMode && <ReactQueryDevtools initialIsOpen={false} />}
</SentryErrorBoundary>
</QueryClientProvider>
</ClerkProvider>
);
}
export default App;