세션 평가와 교수자 분석 보강
This commit is contained in:
parent
5c4ac04e06
commit
fe2796f05a
51 changed files with 4928 additions and 240 deletions
|
|
@ -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) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue