feat: 운영 안정성과 세션 음성 경험 개선
This commit is contained in:
parent
facc4ad2d9
commit
c788343467
95 changed files with 8431 additions and 1785 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue