음성 재생과 운영 배포 정리

This commit is contained in:
Yun Chan 2026-06-28 12:18:20 +09:00
parent 8ed185ce6c
commit ac7db95542
1020 changed files with 46863 additions and 2175 deletions

View file

@ -6,6 +6,8 @@
/learn/session/:sessionId -> Session
/learn/session/:sessionId/review -> SessionReview
/teach -> Professor (teacher data-role=instructor)
/teach/personas -> PersonaStudio (teacher/admin)
/teach/session/:sessionId/review -> SessionReview (teacher read-only)
/admin -> Admin (admin)
/settings -> Settings
/ -> Navigate( , /login)
@ -14,15 +16,18 @@
import type { ReactNode } from "react";
import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-router-dom";
import { AuthProvider, useAuth, roleHomePath, type Role } from "./lib/auth";
import { AuthProvider, canAccessRole, useAuth, roleHomePath, type Role } from "./lib/auth";
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";
@ -48,7 +53,13 @@ function BootScreen() {
* RequireAuth /login . () roles .
* ( / ).
*/
function RequireAuth({ children, roles }: { children: ReactNode; roles?: Role[] }) {
function RequireAuth({
children,
roles,
}: {
children: ReactNode;
roles?: Role[];
}) {
const { user, loading } = useAuth();
const location = useLocation();
@ -56,95 +67,215 @@ function RequireAuth({ children, roles }: { children: ReactNode; roles?: Role[]
if (!user) {
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
if (roles && !roles.includes(user.role)) {
if (roles && !roles.some((role) => canAccessRole(user, role))) {
return <Navigate to={roleHomePath(user.role)} replace />;
}
return <>{children}</>;
}
function OnboardingGate({ children }: { children: ReactNode }) {
const { user, loading } = useAuth();
const location = useLocation();
const path = location.pathname;
if (loading) return <BootScreen />;
if (path === "/pending") return <>{children}</>;
if (user && user.onboardingCompletedAt == null && path !== "/onboarding") {
return <Navigate to="/onboarding" replace state={{ from: path }} />;
}
if (user && user.onboardingCompletedAt != null && (path === "/login" || path === "/onboarding")) {
return <Navigate to={roleHomePath(user.role)} replace />;
}
return <>{children}</>;
}
function PendingApprovalGate({ children }: { children: ReactNode }) {
const { user, loading } = useAuth();
const location = useLocation();
const path = location.pathname;
if (loading) return <BootScreen />;
if (user && user.accountStatus !== "approved" && path !== "/pending") {
return <Navigate to="/pending" replace state={{ from: path }} />;
}
if (user && user.accountStatus === "approved" && path === "/pending") {
return (
<Navigate
to={user.onboardingCompletedAt == null ? "/onboarding" : roleHomePath(user.role)}
replace
/>
);
}
return <>{children}</>;
}
/** 루트(/) — 인증되면 역할 홈, 아니면 /login. */
function RootRedirect() {
const { user, loading } = useAuth();
if (loading) return <BootScreen />;
if (user && user.accountStatus !== "approved") {
return <Navigate to="/pending" replace />;
}
if (user && user.onboardingCompletedAt == null) {
return <Navigate to="/onboarding" replace />;
}
return <Navigate to={user ? roleHomePath(user.role) : "/login"} replace />;
}
function AppRoutes() {
return (
<Routes>
<Route path="/login" element={<Login />} />
<PendingApprovalGate>
<OnboardingGate>
<Routes>
<Route path="/login" element={<Login />} />
{/* dev: 인증 없는 아바타 컴포지션 튜닝 페이지 (실서비스 아님) */}
<Route path="/dev/avatar-preview" element={<AvatarPreview />} />
<Route
path="/pending"
element={
<RequireAuth>
<PendingApproval />
</RequireAuth>
}
/>
{/* 학습자 */}
<Route
path="/learn"
element={
<RequireAuth roles={["learner"]}>
<LearnerHome />
</RequireAuth>
}
/>
<Route
path="/learn/avatar-expressions"
element={
<RequireAuth roles={["learner"]}>
<AvatarExpressionLab />
</RequireAuth>
}
/>
<Route
path="/learn/session/:sessionId"
element={
<RequireAuth roles={["learner"]}>
<Session />
</RequireAuth>
}
/>
<Route
path="/learn/session/:sessionId/review"
element={
<RequireAuth roles={["learner"]}>
<SessionReview />
</RequireAuth>
}
/>
{/* dev: 인증 없는 아바타 컴포지션 튜닝 페이지 (실서비스 아님) */}
<Route path="/dev/avatar-preview" element={<AvatarPreview />} />
{/* 교수자 */}
<Route
path="/teach"
element={
<RequireAuth roles={["teacher"]}>
<Professor />
</RequireAuth>
}
/>
<Route
path="/onboarding"
element={
<RequireAuth>
<Onboarding />
</RequireAuth>
}
/>
{/* 관리자 */}
<Route
path="/admin"
element={
<RequireAuth roles={["admin"]}>
<Admin />
</RequireAuth>
}
/>
{/* 학습자 */}
<Route
path="/learn"
element={
<RequireAuth roles={["learner"]}>
<LearnerHome view="dashboard" />
</RequireAuth>
}
/>
<Route
path="/learn/practice"
element={
<RequireAuth roles={["learner"]}>
<LearnerHome view="practice" />
</RequireAuth>
}
/>
<Route
path="/learn/history"
element={
<RequireAuth roles={["learner"]}>
<LearnerHome view="history" />
</RequireAuth>
}
/>
<Route
path="/learn/avatar-expressions"
element={
<RequireAuth roles={["learner"]}>
<AvatarExpressionLab />
</RequireAuth>
}
/>
<Route
path="/learn/session/:sessionId"
element={
<RequireAuth roles={["learner"]}>
<Session />
</RequireAuth>
}
/>
<Route
path="/learn/session/:sessionId/review"
element={
<RequireAuth roles={["learner"]}>
<SessionReview />
</RequireAuth>
}
/>
{/* 설정 — 3역할 공통 */}
<Route
path="/settings"
element={
<RequireAuth>
<Settings />
</RequireAuth>
}
/>
{/* 교수자 */}
<Route
path="/teach"
element={
<RequireAuth roles={["teacher"]}>
<Professor />
</RequireAuth>
}
/>
<Route
path="/teach/personas"
element={
<RequireAuth roles={["teacher", "admin"]}>
<PersonaStudio />
</RequireAuth>
}
/>
<Route
path="/teach/session/:sessionId/review"
element={
<RequireAuth roles={["teacher"]}>
<SessionReview />
</RequireAuth>
}
/>
<Route path="/" element={<RootRedirect />} />
{/* 미정의 경로 → 루트로 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
{/* 관리자 */}
<Route
path="/admin"
element={
<RequireAuth roles={["admin"]}>
<Admin section="overview" />
</RequireAuth>
}
/>
<Route
path="/admin/users"
element={
<RequireAuth roles={["admin"]}>
<Admin section="users" />
</RequireAuth>
}
/>
<Route
path="/admin/access"
element={
<RequireAuth roles={["admin"]}>
<Admin section="access" />
</RequireAuth>
}
/>
<Route
path="/admin/tickets"
element={
<RequireAuth roles={["admin"]}>
<Admin section="tickets" />
</RequireAuth>
}
/>
{/* 설정 — 3역할 공통 */}
<Route
path="/settings"
element={
<RequireAuth>
<Settings />
</RequireAuth>
}
/>
<Route path="/" element={<RootRedirect />} />
{/* 미정의 경로 → 루트로 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</OnboardingGate>
</PendingApprovalGate>
);
}