전 저장소 리팩터링과 SSOT 정비

This commit is contained in:
Yun Chan 2026-07-15 21:31:30 +09:00
parent 14ecbd4e7d
commit 3dfddcac6f
173 changed files with 19679 additions and 6952 deletions

View file

@ -184,6 +184,13 @@ function teacherReviewResponse(sessionId: string) {
};
}
/** 리뷰는 모든 폭에서 가로 탭으로 영역을 전환한다. */
async function openReviewTab(page: Page, label: "축어록" | "피드백" | "워크시트") {
const tabs = page.locator(".sr-tabs");
await tabs.waitFor({ state: "attached", timeout: 20_000 });
await tabs.locator("button", { hasText: label }).click();
}
test.describe("teacher console", () => {
test("lets a teacher approve a pending persona review from the console @single-run", async ({
page,
@ -317,7 +324,7 @@ test.describe("teacher console", () => {
});
});
await page.goto("/teach/personas");
await page.goto("/teach/personas?view=personas");
const row = page.locator('[data-approved-persona-row="true"]').filter({ hasText: "P1" });
await expect(row).toBeVisible();
await row.getByRole("button", { name: "수정" }).click();
@ -373,7 +380,7 @@ test.describe("teacher console", () => {
});
});
await page.goto("/teach/personas");
await page.goto("/teach/personas?view=personas&mode=create&step=edit");
await page.getByLabel("표시 이름").fill("항목형 페르소나");
await page.getByRole("tab", { name: "임상" }).click();
@ -387,7 +394,7 @@ test.describe("teacher console", () => {
const forbidden = page.locator(".ps-list-field").filter({ hasText: "상담자 금기" });
await forbidden.getByRole("textbox", { name: "상담자 금기 1", exact: true }).fill("네가 예민한 거라고 단정");
await page.getByRole("button", { name: "초안 저장" }).click();
await page.getByRole("button", { name: "중간 저장" }).click();
await expect(page.getByText("P1 v1 초안을 저장했습니다.")).toBeVisible();
expect(savedPayload).toBeTruthy();
@ -408,7 +415,7 @@ test.describe("teacher console", () => {
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]) });
});
await page.goto("/teach/personas");
await page.goto("/teach/personas?view=personas&mode=create&step=edit");
await page.getByLabel("표시 이름").fill("프롬프트 검토 페르소나");
await page.getByRole("tab", { name: "임상" }).click();
const automaticThoughts = page.locator(".ps-list-field").filter({ hasText: "자동사고" });
@ -484,7 +491,7 @@ test.describe("teacher console", () => {
});
});
await page.goto("/teach/personas");
await page.goto("/teach/personas?view=personas");
const row = page.locator('[data-approved-persona-row="true"]').filter({ hasText: "P7" });
await expect(row).toBeVisible();
await expectVisibleButtonsFit(page, ".ps-approved-row__actions .vg-btn", "approved persona actions");
@ -492,11 +499,104 @@ test.describe("teacher console", () => {
await row.getByRole("button", { name: "삭제" }).click();
await expect(page.getByText("P7 페르소나를 보관 처리했습니다.")).toBeVisible();
await expect(page.getByText("공개 목록 없음")).toBeVisible();
await expect(page.getByText("조건에 맞는 페르소나 없음")).toBeVisible();
expect(archived).toBeTruthy();
await expectNoHorizontalOverflow(page);
});
test("navigates persona dashboard, catalog, table, detail, and review queue @single-run", async ({ page }) => {
const approvedPersona = {
persona_id: "00000000-0000-0000-0000-000000000721",
code: "P1",
version: 3,
status: "approved",
display_name: "서연(가명) · 고2 · 우울/자살사고",
difficulty: "hard",
theory_target: ["humanistic"],
demographics: { age_band: "F-teen" },
presenting_summary: "최근 무기력과 관계 단절을 호소합니다.",
source: "database",
degraded: false,
voice_preset: null,
};
const reviewPersona = {
persona_id: "00000000-0000-0000-0000-000000000722",
code: "P12",
version: 1,
status: "review",
display_name: "직장 적응 훈련 페르소나",
difficulty: "moderate",
theory_target: ["cbt"],
source_provenance: "교수자 작성 초안",
is_synthetic: true,
created_at: "2026-07-15T08:00:00Z",
approved_at: null,
};
await signInAsTeacher(page);
await page.route("**/api/personas", (route) => {
if (route.request().method() !== "GET") return route.fallback();
return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([approvedPersona]) });
});
await page.route("**/api/personas/review", (route) =>
route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([reviewPersona]) }),
);
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: 1,
active_sessions: 1,
ended_sessions: 1,
learner_growth: [{
learner_id: "00000000-0000-0000-0000-000000000723",
learner_label: "학습자 A",
sessions: 2,
ended_sessions: 1,
latest_at: "2026-07-15T08:20:00Z",
avg_score: 0.75,
avg_rapport: 0.4,
trend: "up",
points: [{ session_id: "persona-session-1", session_no: 1, persona_code: "P1", stage: "탐색", started_at: "2026-07-15T08:00:00Z", ended_at: "2026-07-15T08:20:00Z", score: 0.75, rapport: 0.4, technique_count: 3, watch_count: 0 }],
}],
safety_alerts: [],
pending_reviews: [],
recent_sessions: [{ session_id: "persona-session-2", learner_id: "00000000-0000-0000-0000-000000000723", learner_label: "학습자 A", persona_code: "P1", persona_name: approvedPersona.display_name, session_no: 2, status: "active", stage: "탐색", turn_count: 4, learner_turn_count: 2, client_turn_count: 2, started_at: "2026-07-15T08:30:00Z", ended_at: null }],
message: "페르소나 운영 fixture",
}),
}),
);
await page.goto("/teach/personas");
await expect(page.getByRole("heading", { name: "페르소나 운영" })).toBeVisible();
await expect(page.getByText("상담 수행 점수")).toBeVisible();
await page.getByRole("button", { name: /카탈로그/ }).first().click();
await expect(page.getByRole("heading", { name: "페르소나 카탈로그" })).toBeVisible();
await expect(page.getByText("학습자가 회기를 시작할 때 선택하는 승인본 모음입니다.")).toBeVisible();
await page.getByRole("button", { name: /페르소나/ }).first().click();
await expect(page.getByRole("button", { name: "새로운 페르소나 만들기" })).toBeVisible();
const approvedRow = page.locator('[data-approved-persona-row="true"]').filter({ hasText: "P1" });
await expect(approvedRow).toBeVisible();
await approvedRow.getByRole("button", { name: "상세" }).click();
await expect(page.getByRole("tab", { name: "소개·정보" })).toBeVisible();
await page.getByRole("tab", { name: "학습 현황" }).click();
await expect(page.getByText("75%").first()).toBeVisible();
await page.getByRole("button", { name: "목록으로" }).click();
await page.getByRole("tab", { name: /검수 현황/ }).click();
await expect(page.getByText("직장 적응 훈련 페르소나")).toBeVisible();
await page.getByRole("button", { name: "새로운 페르소나 만들기" }).click();
await expect(page.getByRole("navigation", { name: "페르소나 작성 단계" })).toBeVisible();
await page.getByRole("button", { name: /설정 임상·회기·말투 조정/ }).click();
await expect(page.getByRole("tab", { name: "개요" })).toBeVisible();
await page.getByRole("button", { name: "이전" }).click();
await expect(page).toHaveURL(/step=generate/);
await expectNoHorizontalOverflow(page);
});
test("renders real server sessions from server-owned rows", async ({ page }) => {
const sessionId = await createEndedLearnerSession(page);
await signInAsTeacher(page);
@ -768,9 +868,11 @@ test.describe("teacher console", () => {
await expect(page).toHaveURL(new RegExp(`/teach/session/${sessionId}/review$`));
await expect(page.getByText("교수자 검토 화면", { exact: true })).toBeVisible();
await expect(page.getByText("교수자 검토", { exact: true })).toBeVisible();
await openReviewTab(page, "워크시트");
await expect(page.getByText("축어록 자동 초안 읽기 전용")).toBeVisible();
await expect(page.getByText("검토 전용")).toBeVisible();
await openReviewTab(page, "피드백");
await expect(page.getByText("교수자 검토", { exact: true })).toBeVisible();
await page.getByLabel("검토 메모").fill("다음 회기에서 감정 반영을 먼저 확인");
await page.getByRole("button", { name: "검토 완료" }).click();
await expect(page.getByText("완료 시각 2026-06-27T10:00:00Z")).toBeVisible();
@ -953,6 +1055,7 @@ test.describe("teacher console", () => {
await expect(page).toHaveURL(new RegExp(`/teach/session/${activeSession.session_id}/review$`));
await expect(page.getByText("교수자 검토 화면", { exact: true })).toBeVisible();
await expect(page.getByText("진행 중", { exact: true })).toBeVisible();
await openReviewTab(page, "피드백");
await expect(page.getByText("평가 대기", { exact: true })).toBeVisible();
await expectNoHorizontalOverflow(page);
});
@ -1022,6 +1125,7 @@ test.describe("teacher console", () => {
await expect(page).toHaveURL(new RegExp(`/teach/session/${sessionId}/review$`));
await expect(page.getByText("교수자 검토 화면", { exact: true })).toBeVisible();
await expect(page.getByText("진행 중", { exact: true })).toBeVisible();
await openReviewTab(page, "워크시트");
await expect(page.getByText("검토 전용")).toBeVisible();
await expect(page.getByRole("button", { name: /^저장/ })).toHaveCount(0);
await expectNoHorizontalOverflow(page);