feat: 운영 안정성과 세션 음성 경험 개선

This commit is contained in:
Yun Chan 2026-07-31 00:13:08 +09:00
parent facc4ad2d9
commit c788343467
95 changed files with 8431 additions and 1785 deletions

View file

@ -377,12 +377,12 @@ test.describe("admin console full-sweep (fixtures)", () => {
await page.goto("/admin");
await expect(page.getByRole("heading", { name: "현재 서비스 상태" })).toBeVisible();
const inlineError = page.locator(".ad-error");
const inlineError = page.locator(".vgops-error");
await expect(inlineError).toBeVisible();
await expect(inlineError).toHaveAttribute("role", "alert");
await expect(inlineError).toContainText("헬스 저장소 접근 불가");
// 다른 데이터 소스는 정상이므로 화면 나머지는 렌더된다.
await expect(page.locator(".ad-kpis").first()).toBeVisible();
await expect(page.locator(".vgops-kpis").first()).toBeVisible();
});
// 검증 checklist: admin-users-search-input
@ -417,15 +417,15 @@ test.describe("admin console full-sweep (fixtures)", () => {
await page.goto("/admin/users");
await page.getByRole("tab", { name: "사용자 목록" }).click();
const rows = page.locator(".ad-user-table tbody tr");
const rows = page.locator(".vgops-user-table tbody tr");
await expect(rows).toHaveCount(3);
await expect(page.locator(".ad-users-toolbar")).toContainText("3 / 3명");
await expect(page.locator(".vgops-users-toolbar")).toContainText("3 / 3명");
const search = page.getByLabel("사용자 검색");
await search.fill("교수자");
await expect(rows).toHaveCount(1);
await expect(rows.first()).toContainText("bob@hs.ac.kr");
await expect(page.locator(".ad-users-toolbar")).toContainText("1 / 3명");
await expect(page.locator(".vgops-users-toolbar")).toContainText("1 / 3명");
await search.fill("cohort-alpha");
await expect(rows).toHaveCount(1);
@ -470,7 +470,7 @@ test.describe("admin console full-sweep (fixtures)", () => {
await page.goto("/admin/users");
await page.getByRole("tab", { name: "사용자 목록" }).click();
const rows = page.locator(".ad-user-table tbody tr");
const rows = page.locator(".vgops-user-table tbody tr");
await expect(rows).toHaveCount(3);
// 기본 정렬: last_seen_at 내림차순.
@ -515,8 +515,8 @@ test.describe("admin console full-sweep (fixtures)", () => {
await page.goto("/admin/users");
await page.getByRole("tab", { name: "사용자 목록" }).click();
await expect(page.locator(".ad-user-table tbody tr")).toHaveCount(40);
await expect(page.locator(".ad-users-toolbar")).toContainText("45 / 45명");
await expect(page.locator(".vgops-user-table tbody tr")).toHaveCount(40);
await expect(page.locator(".vgops-users-toolbar")).toContainText("45 / 45명");
await expect(page.getByText(/나머지\s*5명을 더 좁혀 볼 수/)).toBeVisible();
});
@ -553,7 +553,7 @@ test.describe("admin console full-sweep (fixtures)", () => {
// 토글 → 저장 시 admin_access가 PATCH로 반영된다.
await normalToggle.check();
const normalRow = page.locator(".ad-user-table tbody tr").filter({ hasText: "normal@hs.ac.kr" });
const normalRow = page.locator(".vgops-user-table tbody tr").filter({ hasText: "normal@hs.ac.kr" });
const saveButton = normalRow.getByRole("button", { name: "저장" });
await expect(saveButton).toBeEnabled();
const patchPromise = page.waitForResponse(isUserPatchResponse("normal"));
@ -604,7 +604,7 @@ test.describe("admin console full-sweep (fixtures)", () => {
await expect(page.getByRole("heading", { name: "역할 분포" })).toBeVisible();
await expect(page.getByRole("heading", { name: "최근 활동" })).toBeVisible();
const meters = page.locator(".ad-role-meter");
const meters = page.locator(".vgops-role-meter");
await expect(meters).toHaveCount(3);
await expect(meters.nth(0)).toContainText("학습자");
await expect(meters.nth(0)).toContainText("5명");
@ -613,10 +613,10 @@ test.describe("admin console full-sweep (fixtures)", () => {
await expect(meters.nth(2)).toContainText("관리자");
await expect(meters.nth(2)).toContainText("2명");
const activityRows = page.locator(".ad-activity-table > div");
const activityRows = page.locator(".vgops-activity-table > div");
await expect(activityRows).toHaveCount(8);
await expect(activityRows.first()).toContainText("Activity User 1");
await expect(page.locator(".ad-activity-table")).not.toContainText("Activity User 9");
await expect(page.locator(".vgops-activity-table")).not.toContainText("Activity User 9");
});
// 검증 checklist: admin-users-states
@ -636,9 +636,9 @@ test.describe("admin console full-sweep (fixtures)", () => {
await page.goto("/admin/users");
// 로딩 스켈레톤(승인 큐)이 먼저 보인다.
await expect(page.locator(".ad-user--skeleton").first()).toBeVisible();
await expect(page.locator(".vgops-user--skeleton").first()).toBeVisible();
// 실패 후 InlineError가 표시된다.
await expect(page.locator(".ad-error")).toContainText("사용자 저장소 중단");
await expect(page.locator(".vgops-error")).toContainText("사용자 저장소 중단");
// 앱 결함: 이 클릭에서 페이지가 무한 렌더 루프로 프리즈되어 테스트가 타임아웃된다.
await page.getByRole("tab", { name: "사용자 목록" }).click();
await expect(
@ -657,8 +657,8 @@ test.describe("admin console full-sweep (fixtures)", () => {
// 로딩 스켈레톤 → 실패 InlineError (클릭 없이 표시만 검증).
mock.overrides.users = { delayMs: 1_500, status: 500, detail: "사용자 저장소 중단" };
await page.goto("/admin/users");
await expect(page.locator(".ad-user--skeleton").first()).toBeVisible();
await expect(page.locator(".ad-error")).toContainText("사용자 저장소 중단");
await expect(page.locator(".vgops-user--skeleton").first()).toBeVisible();
await expect(page.locator(".vgops-error")).toContainText("사용자 저장소 중단");
// 등록 0건 빈 상태.
delete mock.overrides.users;
@ -673,7 +673,7 @@ test.describe("admin console full-sweep (fixtures)", () => {
];
await page.reload();
await page.getByRole("tab", { name: "사용자 목록" }).click();
await expect(page.locator(".ad-user-table tbody tr")).toHaveCount(1);
await expect(page.locator(".vgops-user-table tbody tr")).toHaveCount(1);
await page.getByLabel("사용자 검색").fill("no-match-full-sweep");
await expect(page.getByText("검색 조건에 맞는 사용자가 없습니다.")).toBeVisible();
await expect(page.getByText("아직 등록된 사용자가 없습니다.")).toHaveCount(0);
@ -737,7 +737,7 @@ test.describe("admin console full-sweep (fixtures)", () => {
params.get("assigned_group") === "ops",
),
);
await page.locator(".ad-ticket-filter__check input").check();
await page.locator(".vgops-ticket-filter__check input").check();
await staleRequest;
await expect(clearButton).toBeEnabled();
@ -760,7 +760,7 @@ test.describe("admin console full-sweep (fixtures)", () => {
await expect(page.getByLabel("카테고리 필터")).toHaveValue("all");
await expect(page.getByLabel("우선순위 필터")).toHaveValue("all");
await expect(page.getByLabel("담당 그룹 필터")).toHaveValue("");
await expect(page.locator(".ad-ticket-filter__check input")).not.toBeChecked();
await expect(page.locator(".vgops-ticket-filter__check input")).not.toBeChecked();
await expect(clearButton).toBeDisabled();
});
@ -776,7 +776,7 @@ test.describe("admin console full-sweep (fixtures)", () => {
];
await page.goto("/admin/tickets");
const chips = page.locator(".ad-ticket-queues button");
const chips = page.locator(".vgops-ticket-queues button");
await expect(chips).toHaveCount(2);
// 건수 내림차순: 안전(2) → 기타(1).
await expect(chips.nth(0)).toContainText("안전");
@ -835,7 +835,7 @@ test.describe("admin console full-sweep (fixtures)", () => {
mock.overrides.tickets = { status: 500, detail: "티켓 저장소 오류" };
await page.getByRole("button", { name: "새로고침" }).click();
const inlineError = page.locator(".ad-error");
const inlineError = page.locator(".vgops-error");
await expect(inlineError).toBeVisible();
await expect(inlineError).toHaveAttribute("role", "alert");
await expect(inlineError).toContainText("티켓 저장소 오류");
@ -876,7 +876,7 @@ test.describe("admin console full-sweep (real API)", () => {
await page.goto("/admin/users");
expect((await usersResponsePromise).ok()).toBeTruthy();
const approvalCard = page.locator(".ad-approval").filter({ hasText: email });
const approvalCard = page.locator(".vgops-approval").filter({ hasText: email });
await expect(approvalCard).toBeVisible();
await expect(approvalCard).toContainText("승인 대기");
@ -941,7 +941,7 @@ test.describe("admin console full-sweep (real API)", () => {
await page.getByRole("tab", { name: "사용자 목록" }).click();
await page.getByLabel("사용자 검색").fill(email);
const row = page.locator(".ad-user-table tbody tr").filter({ hasText: email });
const row = page.locator(".vgops-user-table tbody tr").filter({ hasText: email });
await expect(row).toBeVisible();
const affiliationInput = row.getByLabel(`${email} 소속`);
@ -1019,7 +1019,7 @@ test.describe("admin console full-sweep (real API)", () => {
await page.getByRole("tab", { name: "사용자 목록" }).click();
await page.getByLabel("사용자 검색").fill(email);
const row = page.locator(".ad-user-table tbody tr").filter({ hasText: email });
const row = page.locator(".vgops-user-table tbody tr").filter({ hasText: email });
await expect(row).toBeVisible();
const cohortInput = row.getByLabel(`${email} 코호트`);
await cohortInput.fill("co-a, co-b");
@ -1083,7 +1083,7 @@ test.describe("admin console full-sweep (real API)", () => {
await page.goto("/admin/tickets");
await ticketsResponsePromise;
const card = page.locator(".ad-ticket").filter({ hasText: subject });
const card = page.locator(".vgops-ticket").filter({ hasText: subject });
await expect(card).toBeVisible();
let patchCount = 0;
@ -1171,7 +1171,7 @@ test.describe("admin console full-sweep (real API)", () => {
await ticketsResponsePromise;
const linkedCard = page
.locator(".ad-ticket")
.locator(".vgops-ticket")
.filter({ hasText: subject })
.filter({ has: page.getByRole("button", { name: "해제" }) });
await expect(linkedCard).toBeVisible();