세션 평가와 교수자 분석 보강

This commit is contained in:
Yun Chan 2026-07-01 12:10:52 +09:00
parent 5c4ac04e06
commit fe2796f05a
51 changed files with 4928 additions and 240 deletions

View file

@ -518,6 +518,141 @@ test.describe("teacher console", () => {
await expectNoHorizontalOverflow(page);
});
test("shows selected learner analysis with the full session timeline @single-run", async ({
page,
}) => {
const learnerA = "teacher-analysis-learner-a";
const learnerB = "teacher-analysis-learner-b";
const sessionFor = (learnerId: string, label: string, index: number) => ({
session_id: `${learnerId}-session-${index}`,
learner_id: learnerId,
learner_label: label,
persona_code: index % 2 === 0 ? "P2" : "P1",
persona_name: index % 2 === 0 ? "민재" : "서연",
session_no: index,
status: "ended",
stage: index > 4 ? "개입" : "탐색",
turn_count: 8 + index,
learner_turn_count: 4 + index,
client_turn_count: 4,
started_at: `2026-06-${String(10 + index).padStart(2, "0")}T09:00:00Z`,
ended_at: `2026-06-${String(10 + index).padStart(2, "0")}T09:30:00Z`,
review_status: index === 2 ? "closed" : "pending",
review_note: null,
reviewed_at: index === 2 ? "2026-06-12T10:00:00Z" : null,
});
const pointFor = (learnerId: string, index: number) => ({
session_id: `${learnerId}-session-${index}`,
session_no: index,
persona_code: index % 2 === 0 ? "P2" : "P1",
stage: index > 4 ? "개입" : "탐색",
started_at: `2026-06-${String(10 + index).padStart(2, "0")}T09:00:00Z`,
ended_at: `2026-06-${String(10 + index).padStart(2, "0")}T09:30:00Z`,
score: index >= 5 ? 1 : 0.5,
rapport: Math.min(0.8, index * 0.1),
technique_count: index + 1,
watch_count: index < 5 ? 1 : 0,
});
const analysisFor = (learnerId: string, label: string, count: number) => {
const sessions = Array.from({ length: count }, (_unused, idx) =>
sessionFor(learnerId, label, idx + 1),
);
const points = Array.from({ length: count }, (_unused, idx) =>
pointFor(learnerId, idx + 1),
);
return {
source: "database",
learner_id: learnerId,
learner_label: label,
total_sessions: count,
active_sessions: 0,
ended_sessions: count,
pending_reviews: Math.max(0, count - 1),
closed_reviews: count > 1 ? 1 : 0,
summary: {
learner_id: learnerId,
learner_label: label,
sessions: count,
ended_sessions: count,
latest_at: sessions[count - 1].ended_at ?? "",
first_score: points[0].score,
latest_score: points[count - 1].score,
score_delta: (points[count - 1].score ?? 0) - (points[0].score ?? 0),
avg_score: 0.75,
avg_rapport: 0.4,
trend: count > 3 ? "up" : "flat",
top_techniques: ["reflection", "open question"],
points,
},
points,
stage_breakdown: [
{ stage: "라포", sessions: 0, turns: 0 },
{ stage: "탐색", sessions: Math.min(4, count), turns: 34 },
{ stage: "개입", sessions: Math.max(0, count - 4), turns: 42 },
{ stage: "정리", sessions: 0, turns: 0 },
],
sessions,
message: `${label} 전체 회기 ${count}`,
};
};
const analysisA = analysisFor(learnerA, "사용자 A", 7);
const analysisB = analysisFor(learnerB, "사용자 B", 2);
await page.route("**/api/teacher/dashboard", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
source: "database",
cohort_label: "E2E cohort",
total_learners: 2,
active_sessions: 0,
ended_sessions: 9,
learner_growth: [
{ ...analysisA.summary, points: analysisA.points.slice(-6) },
{ ...analysisB.summary, points: analysisB.points },
],
safety_alerts: [],
pending_reviews: [],
recent_sessions: analysisA.sessions.slice(-2),
message: "사용자별 분석 fixture.",
}),
}),
);
await page.route("**/api/teacher/learners/*/analysis", (route) => {
const path = new URL(route.request().url()).pathname;
const body = path.includes(learnerB) ? analysisB : analysisA;
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(body),
});
});
await page.route("**/api/personas/review", (route) =>
route.fulfill({ status: 200, contentType: "application/json", body: "[]" }),
);
await signInAsTeacher(page);
await page.goto("/teach");
await expect(page.getByRole("link", { name: "학생 분석" })).toBeVisible();
await expect(page.locator('[data-learner-analysis-panel="true"]')).toHaveCount(0);
await page.getByRole("link", { name: "학생 분석" }).click();
await expect(page).toHaveURL(/\/teach\/analysis$/);
const analysisPanel = page.locator('[data-learner-analysis-panel="true"]');
await expect(analysisPanel).toBeVisible();
await expect(analysisPanel.locator('[data-learner-session-row="true"]')).toHaveCount(7);
await expect(analysisPanel.getByText(`${learnerA}-session-7`)).toBeVisible();
await expect(page.getByText("전체 회기 7건")).toBeVisible();
await page.getByRole("button", { name: "사용자 B 사용자별 분석" }).click();
await expect(analysisPanel.locator('[data-learner-session-row="true"]')).toHaveCount(2);
await expect(analysisPanel.getByText(`${learnerB}-session-2`)).toBeVisible();
await expect(analysisPanel.getByText(`${learnerA}-session-7`)).toHaveCount(0);
await expectNoHorizontalOverflow(page);
});
test("opens a pending session review from the teacher queue @single-run", async ({ page }) => {
const sessionId = "teacher-review-fixture-session";
await page.route("**/api/teacher/dashboard", (route) =>