운영 화면과 회기 리뷰 UI 갱신

This commit is contained in:
Yun Chan 2026-06-28 20:13:21 +09:00
parent e7ebb38177
commit 3a9f70a97b
18 changed files with 2210 additions and 137 deletions

View file

@ -1,7 +1,12 @@
import type { Page } from "@playwright/test";
import type { SessionReviewResponse } from "../src/lib/api";
import type {
SessionReviewResponse,
UserPrepostMeasureItem,
UserPrepostMeasuresResponse,
} from "../src/lib/api";
export const FILLED_REVIEW_SESSION_ID = "filled-review-visual";
export const EMPTY_REVIEW_SESSION_ID = "empty-review-visual";
export function filledReviewResponse(
sessionId: string = FILLED_REVIEW_SESSION_ID,
@ -67,7 +72,10 @@ export function filledReviewResponse(
{ kind: "reflect", label: "감정 반영" },
{ kind: "explore", label: "개방 질문" },
],
nonverbal: [{ kind: "pace", label: "말 속도", detail: "안정" }],
nonverbal: [
{ kind: "pace", label: "말 속도", detail: "안정" },
{ kind: "paralinguistic", label: "음성 단서", detail: "한숨 감지 · 신뢰도 82%" },
],
note: {
author: "AI 슈퍼바이저",
tone: "ai",
@ -239,3 +247,176 @@ export async function routeFilledSessionReview(
});
});
}
export function prepostMeasuresResponse(
measures: UserPrepostMeasureItem[] = [
{
measure_id: "prepost-self-efficacy-pre",
pilot_id: "phase3-pilot-draft",
measure_name: "self_efficacy",
timepoint: "pre",
raw_score: 3,
min_score: 1,
max_score: 5,
normalized_score: 0.5,
instrument_version: "pilot-prepost-scaffold-2026-06-28",
item_count: 1,
collected_at: 1782662400,
updated_at: 1782662400,
},
{
measure_id: "prepost-self-efficacy-post",
pilot_id: "phase3-pilot-draft",
measure_name: "self_efficacy",
timepoint: "post",
raw_score: 4,
min_score: 1,
max_score: 5,
normalized_score: 0.75,
instrument_version: "pilot-prepost-scaffold-2026-06-28",
item_count: 1,
collected_at: 1782662400,
updated_at: 1782662400,
},
],
): UserPrepostMeasuresResponse {
const completePairs = new Set<string>();
for (const measureName of ["self_efficacy", "skill_proficiency", "training_satisfaction"]) {
const hasPre = measures.some(
(item) => item.measure_name === measureName && item.timepoint === "pre",
);
const hasPost = measures.some(
(item) => item.measure_name === measureName && item.timepoint === "post",
);
if (hasPre && hasPost) completePairs.add(measureName);
}
return {
source: "database",
durable: true,
pilot_id: "phase3-pilot-draft",
required_measure_names: [
"self_efficacy",
"skill_proficiency",
"training_satisfaction",
],
required_timepoints: ["pre", "post"],
complete_measure_pairs: completePairs.size,
measures,
generated_at: 1782662400,
};
}
export async function routePrepostMeasures(page: Page) {
let measures = prepostMeasuresResponse().measures;
await page.route("**/api/users/me/prepost-measures**", async (route) => {
const request = route.request();
if (request.method() === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(prepostMeasuresResponse(measures)),
});
return;
}
if (request.method() === "PUT") {
const body = request.postDataJSON() as {
measure_name: UserPrepostMeasureItem["measure_name"];
timepoint: UserPrepostMeasureItem["timepoint"];
raw_score: number;
min_score?: number;
max_score?: number;
instrument_version?: string;
item_count?: number;
pilot_id?: string;
};
const now = 1782666000;
const next: UserPrepostMeasureItem = {
measure_id: `prepost-${body.measure_name}-${body.timepoint}`,
pilot_id: body.pilot_id ?? "phase3-pilot-draft",
measure_name: body.measure_name,
timepoint: body.timepoint,
raw_score: body.raw_score,
min_score: body.min_score ?? 1,
max_score: body.max_score ?? 5,
normalized_score:
(body.raw_score - (body.min_score ?? 1)) / ((body.max_score ?? 5) - (body.min_score ?? 1)),
instrument_version: body.instrument_version ?? "pilot-prepost-scaffold-2026-06-28",
item_count: body.item_count ?? 1,
collected_at: now,
updated_at: now,
};
measures = measures
.filter(
(item) =>
!(
item.measure_name === next.measure_name &&
item.timepoint === next.timepoint &&
item.instrument_version === next.instrument_version
),
)
.concat(next);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(next),
});
return;
}
await route.fallback();
});
}
export function emptyReviewResponse(
sessionId: string = EMPTY_REVIEW_SESSION_ID,
): SessionReviewResponse {
const response = filledReviewResponse(sessionId);
return {
...response,
date: "2026-06-28",
durationLabel: "0분",
durationSeconds: 0,
reachedPhase: "라포",
sessionSignal: "종료됨",
supervisorState: "평가 대기",
summary:
"축어록이 아직 저장되지 않아 평가 근거를 생성하지 않았습니다. 빈 상태에서는 임의의 강점이나 개선점을 표시하지 않습니다.",
phases: [],
phaseAxis: [],
valenceAxis: [],
clientValence: [],
counselorBaseline: [],
turns: [],
rubric: [],
goodMoments: [],
growthPoints: [],
caseWorksheet: {
status: "empty",
generatedBy: "rule-based transcript extractor",
savedAt: null,
sections: [],
limitations: ["축어록이 없어 사례개념화 워크시트를 생성하지 않았습니다."],
},
nextLine: null,
clientFeedback: null,
audioUrl: null,
pdfExportUrl: null,
degraded: false,
reviewReady: false,
};
}
export async function routeEmptySessionReview(
page: Page,
sessionId: string = EMPTY_REVIEW_SESSION_ID,
) {
await page.route(`**/api/sessions/${sessionId}/review`, async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(emptyReviewResponse(sessionId)),
});
});
}