전 저장소 리팩터링과 SSOT 정비
This commit is contained in:
parent
14ecbd4e7d
commit
3dfddcac6f
173 changed files with 19679 additions and 6952 deletions
|
|
@ -15,29 +15,52 @@
|
|||
보호 라우트는 AuthContext 기반 간단 가드. 과설계 금지.
|
||||
===================================================================== */
|
||||
|
||||
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";
|
||||
import {
|
||||
Component,
|
||||
Suspense,
|
||||
lazy,
|
||||
useLayoutEffect,
|
||||
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";
|
||||
import { runtimeAssetLabel } from "./lib/runtimeDiagnostics";
|
||||
|
||||
import Login from "./pages/Login";
|
||||
import Onboarding from "./pages/Onboarding";
|
||||
import PendingApproval from "./pages/PendingApproval";
|
||||
import LearnerHome from "./pages/LearnerHome";
|
||||
import AvatarExpressionLab from "./pages/AvatarExpressionLab";
|
||||
import AvatarPreview from "./pages/AvatarPreview";
|
||||
import Session from "./pages/Session";
|
||||
import SessionReview from "./pages/SessionReview";
|
||||
import Professor from "./pages/Professor";
|
||||
import PersonaStudio from "./pages/PersonaStudio";
|
||||
import Admin from "./pages/Admin";
|
||||
import Settings from "./pages/Settings";
|
||||
const Login = lazy(() => import("./pages/Login"));
|
||||
const Onboarding = lazy(() => import("./pages/Onboarding"));
|
||||
const PendingApproval = lazy(() => import("./pages/PendingApproval"));
|
||||
const LearnerHome = lazy(() => import("./pages/LearnerHome"));
|
||||
const AvatarExpressionLab = lazy(() => import("./pages/AvatarExpressionLab"));
|
||||
const AvatarPreview = lazy(() => import("./pages/AvatarPreview"));
|
||||
const Session = lazy(() => import("./pages/Session"));
|
||||
const SessionReview = lazy(() => import("./pages/SessionReview"));
|
||||
const Professor = lazy(() => import("./pages/Professor"));
|
||||
const PersonaStudio = lazy(() => import("./pages/PersonaStudio"));
|
||||
const Admin = lazy(() => import("./pages/Admin"));
|
||||
const AdminAi = lazy(() => import("./pages/AdminAi"));
|
||||
const Settings = lazy(() => import("./pages/Settings"));
|
||||
|
||||
/** 부트스트랩 로딩 동안 깜빡임 최소화용 중립 화면. */
|
||||
function BootScreen() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
minHeight: "100dvh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
background: "var(--bg-app)",
|
||||
|
|
@ -95,7 +118,11 @@ function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
}
|
||||
return <Navigate to="/onboarding" replace state={{ from: path }} />;
|
||||
}
|
||||
if (user && user.onboardingCompletedAt != null && (path === "/login" || path === "/onboarding")) {
|
||||
if (
|
||||
user &&
|
||||
user.onboardingCompletedAt != null &&
|
||||
(path === "/login" || path === "/onboarding")
|
||||
) {
|
||||
return <Navigate to={initialPathForUser(user)} replace />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
|
|
@ -129,27 +156,35 @@ function RootRedirect() {
|
|||
return <Navigate to={user ? initialPathForUser(user) : "/login"} replace />;
|
||||
}
|
||||
|
||||
function resetDocumentScroll() {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
||||
document.documentElement.scrollTop = 0;
|
||||
document.body.scrollTop = 0;
|
||||
}
|
||||
|
||||
function ScrollToTopOnPathChange() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
||||
document.documentElement.scrollTop = 0;
|
||||
document.body.scrollTop = 0;
|
||||
useLayoutEffect(() => {
|
||||
const previousRestoration = window.history.scrollRestoration;
|
||||
window.history.scrollRestoration = "manual";
|
||||
const handlePageShow = () => resetDocumentScroll();
|
||||
window.addEventListener("pageshow", handlePageShow);
|
||||
return () => {
|
||||
window.removeEventListener("pageshow", handlePageShow);
|
||||
window.history.scrollRestoration = previousRestoration;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
resetDocumentScroll();
|
||||
const frame = window.requestAnimationFrame(resetDocumentScroll);
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [pathname]);
|
||||
|
||||
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,
|
||||
|
|
@ -161,7 +196,7 @@ function RouteErrorFallback({
|
|||
<main
|
||||
role="alert"
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
minHeight: "100dvh",
|
||||
padding: 32,
|
||||
background: "var(--bg-app)",
|
||||
color: "var(--text-strong)",
|
||||
|
|
@ -192,8 +227,15 @@ function RouteErrorFallback({
|
|||
<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
|
||||
style={{
|
||||
margin: "6px 0 0",
|
||||
color: "var(--text-body)",
|
||||
lineHeight: 1.55,
|
||||
}}
|
||||
>
|
||||
React 라우트 렌더 중 예외가 발생했습니다. 빈 화면 대신 원인 정보를
|
||||
표시합니다.
|
||||
</p>
|
||||
</div>
|
||||
<dl
|
||||
|
|
@ -209,10 +251,18 @@ function RouteErrorFallback({
|
|||
}}
|
||||
>
|
||||
{[
|
||||
["path", typeof window === "undefined" ? "unknown" : window.location.pathname],
|
||||
[
|
||||
"path",
|
||||
typeof window === "undefined"
|
||||
? "unknown"
|
||||
: window.location.pathname,
|
||||
],
|
||||
["error", error.message || error.name],
|
||||
["asset", runtimeAssetLabel()],
|
||||
["componentStack", errorInfo?.componentStack?.trim() || "not captured"],
|
||||
[
|
||||
"componentStack",
|
||||
errorInfo?.componentStack?.trim() || "not captured",
|
||||
],
|
||||
].map(([key, value]) => (
|
||||
<div key={key} style={{ display: "contents" }}>
|
||||
<dt
|
||||
|
|
@ -270,7 +320,12 @@ class RouteErrorBoundary extends Component<
|
|||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <RouteErrorFallback error={this.state.error} errorInfo={this.state.errorInfo} />;
|
||||
return (
|
||||
<RouteErrorFallback
|
||||
error={this.state.error}
|
||||
errorInfo={this.state.errorInfo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
|
|
@ -280,7 +335,9 @@ function AppRoutesWithBoundary() {
|
|||
const { pathname } = useLocation();
|
||||
return (
|
||||
<RouteErrorBoundary resetKey={pathname}>
|
||||
<AppRoutes />
|
||||
<Suspense fallback={<BootScreen />}>
|
||||
<AppRoutes />
|
||||
</Suspense>
|
||||
</RouteErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
@ -406,6 +463,14 @@ function AppRoutes() {
|
|||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/ai"
|
||||
element={
|
||||
<RequireAuth roles={["admin"]}>
|
||||
<AdminAi />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/users"
|
||||
element={
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue