모바일 톱바 조작면 보강

This commit is contained in:
Yun Chan 2026-08-12 14:34:00 +09:00
parent 2605661f0c
commit 9687f18669
3 changed files with 122 additions and 8 deletions

View file

@ -119,20 +119,105 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
// checklist: shell-appshell-role-theming
// 역할별 셸 진입 시 body[data-role]이 learner/instructor/admin으로 설정된다.
test("applies body[data-role] theming for each role shell", async ({ page }) => {
await signInAsLearner(page);
let currentUser = meResponse();
await page.route("**/api/auth/me", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(currentUser),
});
});
await page.goto("/learn");
await expect(page.locator(".lh-root")).toBeVisible({ timeout: 15_000 });
await expect(page.locator("body")).toHaveAttribute("data-role", "learner");
await signInAsTeacher(page);
currentUser = meResponse({
role: "teacher",
email: "shell.teacher@hs.ac.kr",
display_name: "Shell Teacher",
});
await page.goto("/teach");
await expect(page.locator(".vg-topbar")).toBeVisible({ timeout: 15_000 });
await expect(page.locator("body")).toHaveAttribute("data-role", "instructor");
await signInAsAdmin(page);
currentUser = meResponse({
role: "admin",
email: "shell.admin@twentyoz.kr",
display_name: "Shell Admin",
admin_access: true,
super_admin: true,
});
await page.goto("/admin");
await expect(page.locator(".vg-topbar")).toBeVisible({ timeout: 15_000 });
await expect(page.locator("body")).toHaveAttribute("data-role", "admin");
for (const viewport of [
{ width: 390, height: 844 },
{ width: 320, height: 568 },
]) {
await page.setViewportSize(viewport);
await page.evaluate(() => new Promise(requestAnimationFrame));
const topbar = await page.locator(".vg-topbar").evaluate((element) => {
const topbarRect = element.getBoundingClientRect();
const controls = Array.from(
element.querySelectorAll<HTMLElement>(
".vg-topbar__brand, .vg-topbar__switch-link, .vg-iconbtn",
),
).map((control) => {
const rect = control.getBoundingClientRect();
return {
label:
control.getAttribute("aria-label") ??
control.getAttribute("title") ??
control.textContent?.trim() ??
control.className,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
};
});
return {
controls,
topbarLeft: topbarRect.left,
topbarRight: topbarRect.right,
topbarTop: topbarRect.top,
topbarBottom: topbarRect.bottom,
viewportWidth: window.innerWidth,
scrollWidth: document.documentElement.scrollWidth,
};
});
const labels = topbar.controls.map((control) => control.label);
expect(labels.slice(0, 4)).toEqual([
"Vignette",
"학습자 공간",
"교수 콘솔",
"운영 콘솔",
]);
expect(labels[4]).toMatch(/^(라이트|다크) 모드로$/);
expect(labels[5]).toBe("로그아웃");
for (const control of topbar.controls) {
expect(control.width, `${control.label} ${viewport.width}px width`).toBeGreaterThanOrEqual(44);
expect(control.height, `${control.label} ${viewport.width}px height`).toBeGreaterThanOrEqual(44);
expect(control.left, `${control.label} ${viewport.width}px left`).toBeGreaterThanOrEqual(
topbar.topbarLeft,
);
expect(control.right, `${control.label} ${viewport.width}px right`).toBeLessThanOrEqual(
topbar.topbarRight,
);
expect(control.top, `${control.label} ${viewport.width}px top`).toBeGreaterThanOrEqual(
topbar.topbarTop,
);
expect(control.bottom, `${control.label} ${viewport.width}px bottom`).toBeLessThanOrEqual(
topbar.topbarBottom,
);
}
expect(topbar.scrollWidth).toBe(topbar.viewportWidth);
}
});
// checklist: shell-auth-bootstrap-loading

View file

@ -369,20 +369,48 @@
display: none;
}
.vg-topbar {
padding: 0 var(--sp-4);
gap: var(--sp-3);
padding: 0 4px;
gap: 4px;
}
.vg-topbar__brand {
width: 44px;
height: 44px;
flex: 0 0 44px;
justify-content: center;
}
.vg-topbar__wm,
.vg-topbar__user {
max-width: 42px;
padding-right: 6px;
display: none;
}
.vg-topbar__actions {
flex: 1 1 auto;
gap: 4px;
}
.vg-topbar__switch {
flex: 1 1 auto;
height: 44px;
padding: 0;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
}
.vg-topbar__switch::-webkit-scrollbar {
display: none;
}
.vg-topbar__switch-link span {
display: none;
}
.vg-topbar__switch-link {
width: 28px;
width: 44px;
height: 44px;
flex: 0 0 44px;
padding: 0;
}
.vg-iconbtn {
width: 44px;
height: 44px;
flex: 0 0 44px;
}
.vg-main {
padding: var(--sp-5) var(--sp-4) var(--sp-7);
}