feat: P1 풀빌드 — React 프론트 7화면 + 백엔드 상담루프·평가·음성·RAG

web (Vite+React19+TS, Cloudflare Pages 배포):
- 디자인토큰(세이지틸/테라코타 SSOT), 앱셸, 공통 UI 프리미티브
- 7화면: 로그인/학습자홈/상담세션/회기리뷰/교수자/관리자/설정
- ClientAvatar: SVG 반구상 흉상 4상태 + RMS 립싱크 + 6파라미터 정서
- 회기리뷰는 외부 레퍼런스 디자인을 Vignette 토큰으로 리스킨

api (FastAPI):
- 게이트웨이 /v1/generate·/v1/stream 어댑터(상주풀/EngineSession 보존)
- services: 페르소나 L0~L6 빌더 / 결정론 상태머신 / 가드레일 /
  턴 오케스트레이터 / 회기간 메모리 / 평가AI / 음성 / RAG
- store: DB off 폴백(in-memory), sessions 실구현

검증:
- web: node22 tsc+vite build 통과(node23 segfault 회피), Pages 배포 200
- api: app.main import 통과
- 핫픽스: Topbar initials undefined-safe (undefined.trim 크래시)
- E2E: 서연(P1) 상담 1턴 — 좋은/나쁜 상담에 차등 반응 실증
This commit is contained in:
Yun Chan 2026-06-25 23:37:22 +09:00
parent 859ab26314
commit 24b1b7a6e1
84 changed files with 19645 additions and 107 deletions

146
apps/web/src/App.tsx Normal file
View file

@ -0,0 +1,146 @@
/* =====================================================================
App . AuthProvider (RequireAuth) .
(task ):
/login -> Login
/learn -> LearnerHome (learner)
/learn/session/:sessionId -> Session
/learn/session/:sessionId/review -> SessionReview
/teach -> Professor (teacher data-role=instructor)
/admin -> Admin (admin)
/settings -> Settings
/ -> Navigate( , /login)
AuthContext . .
===================================================================== */
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 Login from "./pages/Login";
import LearnerHome from "./pages/LearnerHome";
import Session from "./pages/Session";
import SessionReview from "./pages/SessionReview";
import Professor from "./pages/Professor";
import Admin from "./pages/Admin";
import Settings from "./pages/Settings";
/** 부트스트랩 로딩 동안 깜빡임 최소화용 중립 화면. */
function BootScreen() {
return (
<div
style={{
minHeight: "100vh",
display: "grid",
placeItems: "center",
background: "var(--bg-app)",
color: "var(--text-muted)",
fontSize: "var(--fs-sm)",
}}
>
</div>
);
}
/**
* RequireAuth /login . () roles .
* ( / ).
*/
function RequireAuth({ children, roles }: { children: ReactNode; roles?: Role[] }) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) return <BootScreen />;
if (!user) {
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
if (roles && !roles.includes(user.role)) {
return <Navigate to={roleHomePath(user.role)} replace />;
}
return <>{children}</>;
}
/** 루트(/) — 인증되면 역할 홈, 아니면 /login. */
function RootRedirect() {
const { user, loading } = useAuth();
if (loading) return <BootScreen />;
return <Navigate to={user ? roleHomePath(user.role) : "/login"} replace />;
}
function AppRoutes() {
return (
<Routes>
<Route path="/login" element={<Login />} />
{/* 학습자 */}
<Route
path="/learn"
element={
<RequireAuth roles={["learner"]}>
<LearnerHome />
</RequireAuth>
}
/>
<Route
path="/learn/session/:sessionId"
element={
<RequireAuth roles={["learner"]}>
<Session />
</RequireAuth>
}
/>
<Route
path="/learn/session/:sessionId/review"
element={
<RequireAuth roles={["learner"]}>
<SessionReview />
</RequireAuth>
}
/>
{/* 교수자 */}
<Route
path="/teach"
element={
<RequireAuth roles={["teacher"]}>
<Professor />
</RequireAuth>
}
/>
{/* 관리자 */}
<Route
path="/admin"
element={
<RequireAuth roles={["admin"]}>
<Admin />
</RequireAuth>
}
/>
{/* 설정 — 3역할 공통 */}
<Route
path="/settings"
element={
<RequireAuth>
<Settings />
</RequireAuth>
}
/>
<Route path="/" element={<RootRedirect />} />
{/* 미정의 경로 → 루트로 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}
export default function App() {
return (
<BrowserRouter>
<AuthProvider>
<AppRoutes />
</AuthProvider>
</BrowserRouter>
);
}