관리자 흰 화면 진단 보강

This commit is contained in:
Yun Chan 2026-07-03 22:50:14 +09:00
parent e9efaa8031
commit 287029f2a2
6 changed files with 307 additions and 25 deletions

View file

@ -15,7 +15,7 @@
AuthContext . .
===================================================================== */
import { useEffect, type ReactNode } from "react";
import { Component, useEffect, type ErrorInfo, type ReactNode } from "react";
import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-router-dom";
import { AuthProvider, canAccessRole, initialPathForUser, useAuth, roleHomePath, type AuthUser, type Role } from "./lib/auth";
@ -141,6 +141,150 @@ function ScrollToTopOnPathChange() {
return null;
}
function runtimeAssetLabel(): string {
if (typeof document === "undefined") return "unknown";
const script = Array.from(document.scripts)
.map((item) => item.getAttribute("src") ?? "")
.find((src) => src.includes("/assets/index-") && src.endsWith(".js"));
if (!script) return "unknown";
return script.split("/").pop() ?? script;
}
function RouteErrorFallback({
error,
errorInfo,
}: {
error: Error;
errorInfo: ErrorInfo | null;
}) {
return (
<main
role="alert"
style={{
minHeight: "100vh",
padding: 32,
background: "var(--bg-app)",
color: "var(--text-strong)",
}}
>
<div
style={{
maxWidth: 920,
display: "grid",
gap: 16,
padding: 20,
border: "1px solid var(--hair)",
borderRadius: "var(--radius)",
background: "var(--bg-surface)",
boxShadow: "var(--shadow-sm)",
}}
>
<div>
<span
style={{
color: "var(--text-muted)",
fontSize: 12,
fontWeight: 700,
}}
>
</span>
<h1 style={{ margin: "6px 0 0", fontSize: "var(--fs-h2)" }}>
</h1>
<p style={{ margin: "6px 0 0", color: "var(--text-body)", lineHeight: 1.55 }}>
React . .
</p>
</div>
<dl
style={{
display: "grid",
gridTemplateColumns: "160px minmax(0, 1fr)",
gap: 0,
margin: 0,
border: "1px solid var(--hair)",
borderRadius: "var(--radius)",
overflow: "hidden",
fontSize: 13,
}}
>
{[
["path", typeof window === "undefined" ? "unknown" : window.location.pathname],
["error", error.message || error.name],
["asset", runtimeAssetLabel()],
["componentStack", errorInfo?.componentStack?.trim() || "not captured"],
].map(([key, value]) => (
<div key={key} style={{ display: "contents" }}>
<dt
style={{
padding: "9px 11px",
borderBottom: "1px solid var(--hair)",
background: "var(--bg-surface-2)",
color: "var(--text-muted)",
fontWeight: 700,
}}
>
{key}
</dt>
<dd
style={{
minWidth: 0,
margin: 0,
padding: "9px 11px",
borderBottom: "1px solid var(--hair)",
color: "var(--text-body)",
overflowWrap: "anywhere",
whiteSpace: "pre-wrap",
}}
>
{value}
</dd>
</div>
))}
</dl>
</div>
</main>
);
}
class RouteErrorBoundary extends Component<
{ resetKey: string; children: ReactNode },
{ error: Error | null; errorInfo: ErrorInfo | null }
> {
state = { error: null, errorInfo: null };
static getDerivedStateFromError(error: Error) {
return { error, errorInfo: null };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({ errorInfo });
console.error("[route-render-error]", error, errorInfo.componentStack);
}
componentDidUpdate(prevProps: { resetKey: string; children: ReactNode }) {
if (prevProps.resetKey !== this.props.resetKey && this.state.error) {
this.setState({ error: null, errorInfo: null });
}
}
render() {
if (this.state.error) {
return <RouteErrorFallback error={this.state.error} errorInfo={this.state.errorInfo} />;
}
return this.props.children;
}
}
function AppRoutesWithBoundary() {
const { pathname } = useLocation();
return (
<RouteErrorBoundary resetKey={pathname}>
<AppRoutes />
</RouteErrorBoundary>
);
}
function AppRoutes() {
return (
<PendingApprovalGate>
@ -311,7 +455,7 @@ export default function App() {
<BrowserRouter>
<AuthProvider>
<ScrollToTopOnPathChange />
<AppRoutes />
<AppRoutesWithBoundary />
</AuthProvider>
</BrowserRouter>
);