273 lines
7.7 KiB
React
273 lines
7.7 KiB
React
import { lazy, Suspense, useState } from "react";
|
|
import {
|
|
BrowserRouter,
|
|
Route,
|
|
Routes,
|
|
useLocation,
|
|
} from "react-router-dom";
|
|
|
|
import { getRoute, routePath } from "../../contracts/routes.js";
|
|
import { RouteBoundary } from "../boundaries/render-error-boundary.jsx";
|
|
import { AppShell } from "../layouts/app-shell.jsx";
|
|
import { PageHeader } from "../components/page-header.jsx";
|
|
import { SessionProvider, useSession } from "../providers/session-provider.jsx";
|
|
import { ThemeProvider } from "../providers/theme-provider.jsx";
|
|
import { decideRouteAccess } from "./navigation-policy.js";
|
|
|
|
const HomePage = lazy(() => import("../pages/home-page.jsx"));
|
|
const UiGalleryPage = lazy(() => import("../examples/ui-gallery-page.jsx"));
|
|
const StateGalleryPage = lazy(
|
|
() => import("../examples/state-gallery-page.jsx"),
|
|
);
|
|
const AuthExamplePage = lazy(
|
|
() => import("../examples/auth-example-page.jsx"),
|
|
);
|
|
const SampleContractPage = lazy(
|
|
() => import("../pages/sample-contract-page.jsx"),
|
|
);
|
|
const NotFoundPage = lazy(() => import("../pages/not-found-page.jsx"));
|
|
|
|
/** @param {{ routeId: string }} props */
|
|
function RouteLoadingSurface({ routeId }) {
|
|
const definition = getRoute(routeId);
|
|
return (
|
|
<section className="ui-page route-loading" aria-live="polite" aria-busy="true">
|
|
<div className="ui-skeleton" aria-hidden="true" />
|
|
<p>화면을 준비하고 있습니다.</p>
|
|
<span className="visually-hidden">{definition.title} 로딩 중</span>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function RouteFailureSurface() {
|
|
return (
|
|
<section className="ui-page">
|
|
<PageHeader
|
|
title="화면을 표시하지 못했습니다."
|
|
description="잠시 후 페이지를 새로고침해 주세요. 문제가 계속되면 운영 지원 참조 정보를 확인하세요."
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* routeId: string,
|
|
* buildId: string,
|
|
* telemetry?: import("../../application/ports/telemetry-port.js").TelemetryPort,
|
|
* children: React.ReactNode
|
|
* }} props
|
|
*/
|
|
function RouteSurface({ routeId, buildId, telemetry, children }) {
|
|
return (
|
|
<RouteBoundary
|
|
routeId={routeId}
|
|
buildId={buildId}
|
|
telemetry={telemetry}
|
|
fallback={<RouteFailureSurface />}
|
|
>
|
|
<Suspense fallback={<RouteLoadingSurface routeId={routeId} />}>
|
|
{children}
|
|
</Suspense>
|
|
</RouteBoundary>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* routeId: string,
|
|
* children: React.ReactNode
|
|
* }} props
|
|
*/
|
|
function ProtectedRoute({ routeId, children }) {
|
|
const location = useLocation();
|
|
const { sessionState, beginSignIn, recover } = useSession();
|
|
const [pending, setPending] = useState(false);
|
|
const [failed, setFailed] = useState(false);
|
|
const decision = decideRouteAccess(routeId, sessionState);
|
|
|
|
async function continueSession() {
|
|
setPending(true);
|
|
setFailed(false);
|
|
try {
|
|
if (decision.action === "wait-for-session") {
|
|
await recover();
|
|
} else {
|
|
await beginSignIn(
|
|
`${location.pathname}${location.search}${location.hash}`,
|
|
);
|
|
}
|
|
} catch {
|
|
setFailed(true);
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
if (decision.allowed) return children;
|
|
|
|
if (sessionState === "integration-failed") {
|
|
return (
|
|
<section className="ui-page">
|
|
<PageHeader
|
|
title="로그인 연동이 필요합니다."
|
|
description="외부 인증 소유자가 런타임에 연결되면 이 보호 라우트를 사용할 수 있습니다."
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const recovering = decision.action === "wait-for-session";
|
|
return (
|
|
<section className="ui-page">
|
|
<PageHeader
|
|
title={recovering ? "세션을 복구하고 있습니다." : "세션이 필요합니다."}
|
|
description={
|
|
recovering
|
|
? "기존 세션 확인을 계속하려면 복구를 실행하세요."
|
|
: "이 화면은 인증 연동 지점을 확인하기 위한 보호 라우트입니다."
|
|
}
|
|
/>
|
|
<div>
|
|
<button
|
|
className="ui-button"
|
|
type="button"
|
|
disabled={pending}
|
|
onClick={() => void continueSession()}
|
|
>
|
|
{pending
|
|
? "처리 중…"
|
|
: recovering
|
|
? "세션 복구"
|
|
: "로그인 시작"}
|
|
</button>
|
|
</div>
|
|
{failed ? (
|
|
<p className="ui-terminal-error" role="alert">
|
|
세션 작업을 완료하지 못했습니다.
|
|
</p>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* routeId: string,
|
|
* buildId: string,
|
|
* telemetry?: import("../../application/ports/telemetry-port.js").TelemetryPort,
|
|
* children: React.ReactNode
|
|
* }} props
|
|
*/
|
|
function PublicRoute({ routeId, buildId, telemetry, children }) {
|
|
return (
|
|
<RouteSurface routeId={routeId} buildId={buildId} telemetry={telemetry}>
|
|
{children}
|
|
</RouteSurface>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* authSession: import("../../application/ports/auth-session-port.js").AuthSessionPort,
|
|
* basename?: string,
|
|
* buildId?: string,
|
|
* storage?: import("../../application/ports/storage-port.js").StoragePort,
|
|
* telemetry?: import("../../application/ports/telemetry-port.js").TelemetryPort
|
|
* }} props
|
|
*/
|
|
export function AppRouter({
|
|
authSession,
|
|
basename = "/",
|
|
buildId = "local-build",
|
|
storage,
|
|
telemetry,
|
|
}) {
|
|
return (
|
|
<BrowserRouter basename={basename}>
|
|
<ThemeProvider storage={storage}>
|
|
<SessionProvider authSession={authSession}>
|
|
<Routes>
|
|
<Route element={<AppShell />}>
|
|
<Route
|
|
index
|
|
element={
|
|
<PublicRoute
|
|
routeId="APP_HOME"
|
|
buildId={buildId}
|
|
telemetry={telemetry}
|
|
>
|
|
<HomePage />
|
|
</PublicRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path={routePath("EXAMPLES_UI")}
|
|
element={
|
|
<PublicRoute
|
|
routeId="EXAMPLES_UI"
|
|
buildId={buildId}
|
|
telemetry={telemetry}
|
|
>
|
|
<UiGalleryPage />
|
|
</PublicRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path={routePath("EXAMPLES_STATES")}
|
|
element={
|
|
<PublicRoute
|
|
routeId="EXAMPLES_STATES"
|
|
buildId={buildId}
|
|
telemetry={telemetry}
|
|
>
|
|
<StateGalleryPage />
|
|
</PublicRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path={routePath("EXAMPLES_AUTH")}
|
|
element={
|
|
<PublicRoute
|
|
routeId="EXAMPLES_AUTH"
|
|
buildId={buildId}
|
|
telemetry={telemetry}
|
|
>
|
|
<AuthExamplePage />
|
|
</PublicRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path={routePath("SAMPLE_RESOURCE_LIST")}
|
|
element={
|
|
<RouteSurface
|
|
routeId="SAMPLE_RESOURCE_LIST"
|
|
buildId={buildId}
|
|
telemetry={telemetry}
|
|
>
|
|
<ProtectedRoute routeId="SAMPLE_RESOURCE_LIST">
|
|
<SampleContractPage />
|
|
</ProtectedRoute>
|
|
</RouteSurface>
|
|
}
|
|
/>
|
|
<Route
|
|
path={routePath("NOT_FOUND")}
|
|
element={
|
|
<PublicRoute
|
|
routeId="NOT_FOUND"
|
|
buildId={buildId}
|
|
telemetry={telemetry}
|
|
>
|
|
<NotFoundPage />
|
|
</PublicRoute>
|
|
}
|
|
/>
|
|
</Route>
|
|
</Routes>
|
|
</SessionProvider>
|
|
</ThemeProvider>
|
|
</BrowserRouter>
|
|
);
|
|
}
|