런타임 계약과 학습자 흐름 보강
This commit is contained in:
parent
f456b8997a
commit
206018b088
56 changed files with 4306 additions and 1008 deletions
|
|
@ -30,6 +30,7 @@ function defaultApiBase(): string {
|
|||
|
||||
const configuredApiBase = (import.meta.env.VITE_API_BASE as string | undefined)?.trim();
|
||||
const API_BASE: string = configuredApiBase || defaultApiBase();
|
||||
export const AUTH_EXPIRED_EVENT = "vignette:auth-expired";
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly status: number;
|
||||
|
|
@ -96,6 +97,15 @@ async function parseError(res: Response): Promise<ApiError> {
|
|||
return new ApiError(res.status, detail, body);
|
||||
}
|
||||
|
||||
function notifyAuthExpired(path: string, error: ApiError): void {
|
||||
if (typeof window === "undefined") return;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(AUTH_EXPIRED_EVENT, {
|
||||
detail: { path, status: error.status, detail: error.detail },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON API 호출. 2xx 가 아니면 ApiError throw.
|
||||
* 204/빈 응답은 undefined 반환.
|
||||
|
|
@ -126,7 +136,9 @@ export async function apiFetch<T = unknown>(
|
|||
const res = await fetch(joinUrl(path), init);
|
||||
|
||||
if (!res.ok) {
|
||||
throw await parseError(res);
|
||||
const error = await parseError(res);
|
||||
if (error.status === 401) notifyAuthExpired(path, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { api, apiUrl, authApi, type MeResponse } from "./api";
|
||||
import { AUTH_EXPIRED_EVENT, api, apiUrl, authApi, type MeResponse } from "./api";
|
||||
|
||||
export type Role = "learner" | "teacher" | "admin";
|
||||
export type AccountStatus = "pending" | "approved" | "suspended";
|
||||
|
|
@ -113,6 +113,17 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAuthExpired = () => {
|
||||
setUser(null);
|
||||
setLoading(false);
|
||||
};
|
||||
window.addEventListener(AUTH_EXPIRED_EVENT, handleAuthExpired);
|
||||
return () => {
|
||||
window.removeEventListener(AUTH_EXPIRED_EVENT, handleAuthExpired);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
(async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue