feat: 운영 안정성과 세션 음성 경험 개선

This commit is contained in:
Yun Chan 2026-07-31 00:13:08 +09:00
parent facc4ad2d9
commit c788343467
95 changed files with 8431 additions and 1785 deletions

View file

@ -19,6 +19,7 @@ import {
Component,
Suspense,
lazy,
useEffect,
useLayoutEffect,
type ErrorInfo,
type ReactNode,
@ -40,6 +41,7 @@ import {
type Role,
} from "./lib/auth";
import { runtimeAssetLabel } from "./lib/runtimeDiagnostics";
import { clearChunkRecoveryMarker } from "./lib/chunkRecovery";
const Login = lazy(() => import("./pages/Login"));
const Onboarding = lazy(() => import("./pages/Onboarding"));
@ -73,6 +75,68 @@ function BootScreen() {
);
}
function AuthRestoreGate({ children }: { children: ReactNode }) {
const { loading, restoreState, retryRestore } = useAuth();
if (loading && restoreState === "loading") return <BootScreen />;
if (loading && restoreState === "retrying") {
return (
<main
role="status"
className="vg-route-error"
data-testid="auth-restore-retrying"
>
<div className="vg-route-error__panel">
<p className="vg-route-error__eyebrow"> </p>
<h1> .</h1>
<p>
.
.
</p>
</div>
</main>
);
}
if (restoreState === "failed") {
return (
<main
role="alert"
className="vg-route-error"
data-testid="auth-restore-failed"
>
<div className="vg-route-error__panel">
<p className="vg-route-error__eyebrow"> </p>
<h1> .</h1>
<p>
.
.
</p>
<button
type="button"
data-testid="auth-restore-retry"
onClick={() => void retryRestore()}
style={{
minHeight: 42,
padding: "0 16px",
border: "1px solid var(--accent)",
borderRadius: "var(--radius-sm)",
background: "var(--accent)",
color: "var(--text-on-accent)",
font: "inherit",
fontWeight: 700,
cursor: "pointer",
}}
>
</button>
</div>
</main>
);
}
return <>{children}</>;
}
/**
* RequireAuth /login . () roles .
* ( / ).
@ -192,6 +256,11 @@ function RouteErrorFallback({
error: Error;
errorInfo: ErrorInfo | null;
}) {
const reloadLatestVersion = () => {
clearChunkRecoveryMarker(window.location.pathname);
window.location.reload();
};
return (
<main
role="alert"
@ -292,6 +361,42 @@ function RouteErrorFallback({
</div>
))}
</dl>
<div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
<button
type="button"
data-testid="route-error-reload"
onClick={reloadLatestVersion}
style={{
minHeight: 42,
padding: "0 16px",
border: "1px solid var(--accent)",
borderRadius: "var(--radius-sm)",
background: "var(--accent)",
color: "var(--text-on-accent)",
font: "inherit",
fontWeight: 700,
cursor: "pointer",
}}
>
</button>
<a
href="/"
style={{
minHeight: 42,
display: "inline-flex",
alignItems: "center",
padding: "0 16px",
border: "1px solid var(--hair)",
borderRadius: "var(--radius-sm)",
color: "var(--text-body)",
textDecoration: "none",
fontWeight: 700,
}}
>
</a>
</div>
</div>
</main>
);
@ -336,16 +441,26 @@ function AppRoutesWithBoundary() {
return (
<RouteErrorBoundary resetKey={pathname}>
<Suspense fallback={<BootScreen />}>
<ChunkRecoveryReady pathname={pathname} />
<AppRoutes />
</Suspense>
</RouteErrorBoundary>
);
}
function ChunkRecoveryReady({ pathname }: { pathname: string }) {
useEffect(() => {
clearChunkRecoveryMarker(pathname);
}, [pathname]);
return null;
}
function AppRoutes() {
return (
<PendingApprovalGate>
<OnboardingGate>
<AuthRestoreGate>
<PendingApprovalGate>
<OnboardingGate>
<Routes>
<Route path="/login" element={<Login />} />
@ -510,8 +625,9 @@ function AppRoutes() {
{/* 미정의 경로 → 루트로 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</OnboardingGate>
</PendingApprovalGate>
</OnboardingGate>
</PendingApprovalGate>
</AuthRestoreGate>
);
}