8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본 폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을 git 이력으로 고정하는 것이 목적이다. - contracts/routes/services: measurement, outcome_trajectory, rupture_repair, deliberate_practice, calibration_transfer, supervision_research, multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트 - infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행) - apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가 - docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG - scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트 engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
124 lines
4.4 KiB
TypeScript
124 lines
4.4 KiB
TypeScript
import {
|
|
api,
|
|
ApiError,
|
|
apiFetch,
|
|
apiUrl,
|
|
supplementalApi,
|
|
} from "../../lib/api";
|
|
import type { components, paths } from "../../lib/api.gen";
|
|
|
|
type ApiSchema<Name extends keyof components["schemas"]> =
|
|
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<DeliberatePracticeReadModel>(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<DeliberatePracticeReadModel>(
|
|
learnerPracticePath(session.learner_id),
|
|
{ signal },
|
|
);
|
|
},
|
|
|
|
submitAttempt: (
|
|
prescriptionId: string,
|
|
body: PracticeAttemptSubmissionRequest,
|
|
) =>
|
|
api.post<PracticeAttemptSubmissionResponse>(
|
|
`/practice/${encodeURIComponent(prescriptionId)}/attempts`,
|
|
body,
|
|
),
|
|
|
|
observeCompletedSession: (
|
|
prescriptionId: ObserveCompletedPracticeSessionPath["prescription_id"],
|
|
practiceSessionId: ObserveCompletedPracticeSessionPath["practice_session_id"],
|
|
) =>
|
|
api.post<ObserveCompletedPracticeSessionResponse>(
|
|
`/practice/${encodeURIComponent(prescriptionId)}/attempts/from-session/${encodeURIComponent(practiceSessionId)}`,
|
|
),
|
|
|
|
appendCorrection: (
|
|
attemptRecordId: string,
|
|
body: PracticeTeacherCorrectionRequest,
|
|
) =>
|
|
apiFetch<PracticeTeacherCorrectionResponse>(
|
|
`/practice/attempts/${encodeURIComponent(attemptRecordId)}/correction`,
|
|
{ method: "PATCH", body },
|
|
),
|
|
};
|