G0~G8 성과·동맹 측정 OS 작업 일괄 고정
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 산출물은 커밋에서 제외했다.
This commit is contained in:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
124
apps/web/src/pages/session-review/deliberatePracticeApi.ts
Normal file
124
apps/web/src/pages/session-review/deliberatePracticeApi.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
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 },
|
||||
),
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue