import { api, ApiError, apiFetch, apiUrl, supplementalApi, } from "../../lib/api"; import type { components, paths } from "../../lib/api.gen"; type ApiSchema = components["schemas"][Name]; export type DeliberatePracticeReadModel = ApiSchema<"DeliberatePracticeReadModelResponse">; export type PracticePrescriptionItem = ApiSchema<"PracticePrescriptionItem">; export type PracticeEpisodeItem = ApiSchema<"PracticeEpisodeItem">; export type PracticeAttemptItem = ApiSchema<"PracticeAttemptItem">; export type PracticeEvidenceRef = ApiSchema<"PracticeEvidenceRef">; export type PracticeAttemptSubmissionRequest = ApiSchema<"PracticeAttemptSubmissionRequest">; export type PracticeAttemptSubmissionResponse = ApiSchema<"PracticeAttemptSubmissionResponse">; type ObserveCompletedPracticeSessionOperation = NonNullable< paths["/practice/{prescription_id}/attempts/from-session/{practice_session_id}"]["post"] >; type ObserveCompletedPracticeSessionPath = ObserveCompletedPracticeSessionOperation["parameters"]["path"]; export type ObserveCompletedPracticeSessionResponse = ObserveCompletedPracticeSessionOperation["responses"][201]["content"]["application/json"]; export type PracticeTeacherCorrectionRequest = ApiSchema<"PracticeTeacherCorrectionRequest">; export type PracticeTeacherCorrectionResponse = ApiSchema<"PracticeTeacherCorrectionResponse">; export type TeacherDashboardResponse = ApiSchema<"TeacherDashboardResponse">; function learnerPracticePath(learnerId?: string): string { return learnerId ? `/practice/learners/${encodeURIComponent(learnerId)}` : "/practice/learners/me"; } function dashboardSessions(dashboard: TeacherDashboardResponse) { return [ ...(dashboard.pending_reviews ?? []), ...(dashboard.recent_sessions ?? []), ]; } async function getTeacherDashboard(signal?: AbortSignal) { const response = await fetch(apiUrl("/teacher/dashboard"), { method: "GET", credentials: "include", headers: { Accept: "application/json" }, signal, }); if (!response.ok) { let body: unknown; let detail = response.statusText || "교수자 학습자 맥락을 불러오지 못했습니다."; try { body = await response.json(); const candidate = (body as { detail?: unknown }).detail; if (typeof candidate === "string") detail = candidate; } catch { body = undefined; } // 이 보조 조회의 실패는 전체 회기 리뷰의 인증 성공을 뒤집지 않는다. // 후속 역할 보호 API는 공통 client를 사용하므로 실제 세션 만료는 그대로 전파된다. throw new ApiError(response.status, detail, body); } return (await response.json()) as TeacherDashboardResponse; } export class PracticeLearnerContextError extends Error { constructor() { super("이 회기의 학습자 연습 원장을 찾지 못했습니다."); this.name = "PracticeLearnerContextError"; } } export const deliberatePracticeApi = { getForLearner: (signal?: AbortSignal) => supplementalApi.get(learnerPracticePath(), { signal, }), getForTeacherSession: async (sessionId: string, signal?: AbortSignal) => { const dashboard = await getTeacherDashboard(signal); const session = dashboardSessions(dashboard).find( (item) => item.session_id === sessionId, ); if (!session) throw new PracticeLearnerContextError(); return supplementalApi.get( learnerPracticePath(session.learner_id), { signal }, ); }, submitAttempt: ( prescriptionId: string, body: PracticeAttemptSubmissionRequest, ) => api.post( `/practice/${encodeURIComponent(prescriptionId)}/attempts`, body, ), observeCompletedSession: ( prescriptionId: ObserveCompletedPracticeSessionPath["prescription_id"], practiceSessionId: ObserveCompletedPracticeSessionPath["practice_session_id"], ) => api.post( `/practice/${encodeURIComponent(prescriptionId)}/attempts/from-session/${encodeURIComponent(practiceSessionId)}`, ), appendCorrection: ( attemptRecordId: string, body: PracticeTeacherCorrectionRequest, ) => apiFetch( `/practice/attempts/${encodeURIComponent(attemptRecordId)}/correction`, { method: "PATCH", body }, ), };