동의 게이트와 런타임 안정화

This commit is contained in:
Yun Chan 2026-06-27 17:22:38 +09:00
parent 0eb7d925ed
commit 0ec266a761
34 changed files with 1186 additions and 158 deletions

View file

@ -156,6 +156,30 @@ export interface paths {
patch?: never;
trace?: never;
};
"/auth/consent": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Accept Consent
* @description Record the current learner's practice-session consent receipt.
*/
post: operations["accept_consent_auth_consent_post"];
/**
* Withdraw Consent
* @description Withdraw practice-session consent until the learner accepts again.
*/
delete: operations["withdraw_consent_auth_consent_delete"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/auth/dev-login": {
parameters: {
query?: never;
@ -1072,6 +1096,19 @@ export interface components {
/** Rationale */
rationale?: string | null;
};
/** ConsentRequest */
ConsentRequest: {
/**
* Accepted
* @default true
*/
accepted: boolean;
};
/** ConsentResponse */
ConsentResponse: {
/** Consent At */
consent_at?: number | null;
};
/** CrisisResourceResponse */
CrisisResourceResponse: {
/** Message */
@ -1294,6 +1331,8 @@ export interface components {
MeResponse: {
/** Cohort Ids */
cohort_ids: string[];
/** Consent At */
consent_at?: number | null;
/** Display Name */
display_name: string;
/** Email */
@ -2586,6 +2625,74 @@ export interface operations {
};
};
};
accept_consent_auth_consent_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: {
"__Host-vignette_sid"?: string | null;
vignette_sid?: string | null;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["ConsentRequest"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ConsentResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
withdraw_consent_auth_consent_delete: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: {
"__Host-vignette_sid"?: string | null;
vignette_sid?: string | null;
};
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ConsentResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
dev_login_auth_dev_login_post: {
parameters: {
query?: never;

View file

@ -161,6 +161,11 @@ export interface MeResponse {
display_name: string;
role: string; // "learner" | "teacher" | "admin"
cohort_ids: string[];
consent_at: number | null;
}
export interface ConsentResponse {
consent_at: number | null;
}
export interface AuthConfigResponse {
@ -179,6 +184,8 @@ export interface AuthConfigResponse {
export const authApi = {
config: () => api.get<AuthConfigResponse>("/auth/config"),
acceptConsent: () => api.post<ConsentResponse>("/auth/consent", { accepted: true }),
withdrawConsent: () => api.del<ConsentResponse>("/auth/consent"),
};
export type SessionStage = "라포" | "탐색" | "개입" | "정리";

View file

@ -7,7 +7,7 @@ import {
useState,
type ReactNode,
} from "react";
import { api, type MeResponse } from "./api";
import { api, authApi, type MeResponse } from "./api";
export type Role = "learner" | "teacher" | "admin";
export type DesignRole = "learner" | "instructor" | "admin";
@ -18,6 +18,7 @@ export interface AuthUser {
name: string;
role: Role;
cohortIds: string[];
consentAt: number | null;
}
export interface AuthContextValue {
@ -26,6 +27,8 @@ export interface AuthContextValue {
loading: boolean;
login: (role: Role, opts?: { email?: string; displayName?: string }) => Promise<AuthUser>;
logout: () => Promise<void>;
acceptConsent: () => Promise<void>;
withdrawConsent: () => Promise<void>;
}
export function designRoleOf(role: Role): DesignRole {
@ -75,6 +78,7 @@ function userFromMe(me: MeResponse): AuthUser {
name: me.display_name || me.email || me.user_id,
role: (me.role as Role) ?? "learner",
cohortIds: me.cohort_ids ?? [],
consentAt: me.consent_at ?? null,
};
}
@ -124,9 +128,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
}, []);
const acceptConsent = useCallback<AuthContextValue["acceptConsent"]>(async () => {
const response = await authApi.acceptConsent();
setUser((current) =>
current ? { ...current, consentAt: response.consent_at ?? null } : current,
);
}, []);
const withdrawConsent = useCallback<AuthContextValue["withdrawConsent"]>(async () => {
await authApi.withdrawConsent();
setUser((current) => (current ? { ...current, consentAt: null } : current));
}, []);
const value = useMemo<AuthContextValue>(
() => ({ user, role: user?.role ?? null, loading, login, logout }),
[user, loading, login, logout],
() => ({ user, role: user?.role ?? null, loading, login, logout, acceptConsent, withdrawConsent }),
[user, loading, login, logout, acceptConsent, withdrawConsent],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;