전 저장소 리팩터링과 SSOT 정비

This commit is contained in:
Yun Chan 2026-07-15 21:31:30 +09:00
parent 14ecbd4e7d
commit 3dfddcac6f
173 changed files with 19679 additions and 6952 deletions

View file

@ -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={