관리자 화면 스크롤 복원
This commit is contained in:
parent
d939253b3f
commit
a5a2e2ea78
4 changed files with 47 additions and 1 deletions
|
|
@ -629,6 +629,36 @@ test.describe("admin route guards", () => {
|
|||
.poll(() => Array.from(seenAdminEndpoints).sort())
|
||||
.toEqual(["health", "tickets", "uptime", "usage", "users"]);
|
||||
});
|
||||
|
||||
test("resets scroll when moving from long admin pages to access policy", async ({ page }) => {
|
||||
await mockAdminSession(page, {
|
||||
user_id: "scroll-admin",
|
||||
email: "scroll-admin@twentyoz.kr",
|
||||
display_name: "Scroll Admin",
|
||||
role: "admin",
|
||||
admin_access: true,
|
||||
super_admin: true,
|
||||
onboarding_completed_at: 1_782_900_000,
|
||||
});
|
||||
|
||||
await page.goto("/admin/users");
|
||||
await expect(page.getByRole("heading", { name: "가입 승인과 권한 관리" })).toBeVisible();
|
||||
const scrolled = await page.evaluate(() => {
|
||||
document.documentElement.style.minHeight = "2200px";
|
||||
document.body.style.minHeight = "2200px";
|
||||
window.scrollTo(0, 900);
|
||||
return window.scrollY;
|
||||
});
|
||||
expect(scrolled).toBeGreaterThan(0);
|
||||
|
||||
await page.locator(".vg-nav").getByRole("link", { name: "권한" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/admin\/access$/);
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.scrollY))
|
||||
.toBe(0);
|
||||
await expect(page.getByRole("heading", { name: "역할, 그룹, 접근 범위" })).toBeInViewport();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("admin route", () => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
보호 라우트는 AuthContext 기반 간단 가드. 과설계 금지.
|
||||
===================================================================== */
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { AuthProvider, canAccessRole, initialPathForUser, useAuth, roleHomePath, type AuthUser, type Role } from "./lib/auth";
|
||||
|
||||
|
|
@ -129,6 +129,18 @@ function RootRedirect() {
|
|||
return <Navigate to={user ? initialPathForUser(user) : "/login"} replace />;
|
||||
}
|
||||
|
||||
function ScrollToTopOnPathChange() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
||||
document.documentElement.scrollTop = 0;
|
||||
document.body.scrollTop = 0;
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function AppRoutes() {
|
||||
return (
|
||||
<PendingApprovalGate>
|
||||
|
|
@ -298,6 +310,7 @@ export default function App() {
|
|||
return (
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<ScrollToTopOnPathChange />
|
||||
<AppRoutes />
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
|
|
|
|||
|
|
@ -962,6 +962,7 @@
|
|||
<tr><td>P1/H4 masking gate</td><td><code>app/test_pii_masking_eval.py app/test_orchestrator_masking.py app/test_evaluation_persistence.py app/test_session_turn_persistence.py</code> / <code>PLAYWRIGHT_PORT=5244 npx playwright test e2e/session-persistence.spec.ts --project=chromium-single-run --workers=1 --grep "Korean PII"</code></td><td>47 passed; 최신 보정 회귀 59 passed + DB-backed browser PII E2E 1 passed. phone/email/RRN 및 한국어 NAME/ORG raw 값이 generate/stream/evaluator payload와 client <code>text_masked</code>에 남지 않음. Optional ko recognizer fake span은 <code>[NAME]</code>/<code>[ORG]</code>로 마스킹되고 같은 문장의 phone은 후단 regex가 처리하며, adapter 실패 시에도 regex fallback이 유지된다. Synthetic ko fixture 16/16 pass, 자연 발화형 이름 라벨·자기소개와 negative control 포함, input/report schema validation, summary-only report(<code>evidence_text_included=false</code>), entity recall 1.0, forbidden substring removal 1.0, unexpected entity violations 0. 최신 브라우저 stream E2E는 한국어 이름·기관·전화번호 발화가 DB-backed <code>GET /sessions/{id}</code> 상세와 <code>/review</code> 모두에서 raw 값 없이 <code>[NAME]</code>/<code>[ORG]</code>/<code>[PHONE]</code>으로 남는지 확인한다.</td></tr>
|
||||
<tr><td>P2a RBAC/audit/visibility</td><td><code>app.test_rbac_idor</code></td><td>11 tests OK; other learner 403, read_session audit, evaluator-only hidden, teacher session review read allowed, super_admin learner review principal is promoted to admin for evaluation/worksheet/review-status loaders, admin_access-only learner remains blocked, and learner worksheet write remains 403</td></tr>
|
||||
<tr><td>Admin access delegation</td><td><code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_admin_ops.py app/test_auth_providers.py::AuthProviderScaffoldTest::test_admin_role_can_enter_admin_console_even_with_stale_access_flag app/test_auth_providers.py::AuthProviderScaffoldTest::test_admin_role_can_enter_all_role_spaces_without_super_admin app/test_auth_providers.py::AuthProviderScaffoldTest::test_admin_access_flag_without_admin_role_does_not_enter_learner_space -q</code> / <code>npx playwright test e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --grep "approved admin without onboarding"</code> / <code>npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "admin route guards"</code> / <code>npm run typecheck</code></td><td>13 backend focused passed + admin onboarding guard E2E 2 passed + latest admin route guards 2 passed + web typecheck/build passed. 실제 <code>admin</code> role은 <code>admin_access=false</code> 세션이어도 <code>can_access_role(Role.ADMIN)</code>으로 관리자 API를 열고, 승인된 관리자 또는 <code>admin_access</code> 계정은 <code>/admin*</code>에서 학습자 온보딩으로 우회하지 않는다. 최초 진입 경로도 <code>initialPathForUser</code>가 소유해 primary role이 learner/teacher인 관리자 권한 계정이 <code>/</code> 또는 로그인 완료 후 <code>/admin</code>으로 들어간다. 관리자 sidebar와 기존 workspace 전환 계약은 유지된다.</td></tr>
|
||||
<tr><td>SPA route scroll reset</td><td><code>npm run typecheck</code> / <code>npx playwright test e2e/admin.spec.ts --project=chromium-desktop --workers=1 --grep "resets scroll"</code></td><td>Typecheck passed and focused E2E 1 passed. <code>App.tsx</code> resets document scroll to top on <code>pathname</code> changes, so moving from a long admin page to short <code>/admin/access</code> no longer preserves stale <code>scrollY</code> and shows only the sticky topbar/sidebar over an empty main area. Probe proof: previous <code>scrollY=1000</code>, after clicking <code>권한</code> <code>scrollY=0</code>, heading <code>역할, 그룹, 접근 범위</code> in viewport.</td></tr>
|
||||
<tr><td>OAuth/SAML admin landing</td><td><code>C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -X utf8 -m pytest -p no:cacheprovider app/test_auth_providers.py -q</code> / public runtime restart</td><td>36 passed; OAuth/SAML callback now normalizes generic saved <code>next</code> paths such as <code>/learn</code> and <code>/teach</code> to <code>/admin</code> for admin-entitled users while preserving deep links such as <code>/learn/session/...</code>. Live proof after commits <code>998acb44</code>/<code>6b6241f4</code>: local/public <code>/health</code> returned <code>prod</code>, <code>db=true</code>, <code>engine=true</code>; custom domain now serves <code>assets/index-COxjbe7E.js</code>/<code>assets/index-liGoZae_.css</code>; unauthenticated <code>/personas</code> remains <code>401</code>.</td></tr>
|
||||
<tr><td>Session read-model DB readiness</td><td><code>python scripts\check-deploy-preflight.py --skip-db --env-file infra\.env.example --allow-placeholder-secrets</code> / <code>https://api-vignette.chanpaca.net/health</code></td><td>2026-06-29 prod 503 원인은 운영 DB의 <code>app.turns.provider_events</code> 컬럼 누락이었다. 운영 DB hotfix 후 <code>/teacher/dashboard</code> code path는 <code>source=database</code>로 복구됐다. 현재 <code>db.healthcheck()</code>, runtime table readiness, deploy preflight DB mode는 <code>app.turns</code> 음성 메타 컬럼 5개(<code>audio_ref</code>, <code>silence_ms</code>, <code>speech_rate</code>, <code>barge_in</code>, <code>provider_events</code>), <code>app.session_review_status</code> worksheet 컬럼, <code>app.safety_events</code> 필수 컬럼을 함께 검증한다. 2026-06-30에는 Docker Desktop/DB 중단으로 public API 530/error code 1033이 재발했지만, Docker Desktop/DB 재기동 뒤 public health가 <code>environment=prod</code>, <code>db=true</code>, <code>engine=true</code>로 복구됐다.</td></tr>
|
||||
<tr><td>Admin usage persistence</td><td><code>python -m pytest app/test_admin_ops.py app/test_runtime_policy.py -q</code> / authenticated local public-API smoke</td><td>26 passed; <code>/admin/usage</code> returns 200 with <code>source=database</code>, <code>durable=true</code>. Cloudflare blocked raw Python public smoke with 1010, so app-level HTTP was verified against <code>127.0.0.1:8001</code> using the same prod process.</td></tr>
|
||||
|
|
|
|||
|
|
@ -724,6 +724,8 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
|
|||
- 프론트의 최초 진입 경로(`initialPathForUser`)는 pending이면 `/pending`, 온보딩 미완료 일반 사용자는
|
||||
`/onboarding`, 관리자 콘솔 접근권이 있는 사용자는 기본 역할이 learner/teacher여도 `/admin`을 우선한다.
|
||||
역할 전환용 `roleHomePath`는 그대로 역할별 홈(`/learn`, `/teach`, `/admin`)만 소유한다.
|
||||
- SPA 라우트 전환은 `App.tsx`가 `pathname` 변경마다 문서 스크롤을 맨 위로 복원한다. 긴 관리자 목록에서
|
||||
짧은 `/admin/access` 정책 화면으로 이동할 때 이전 `scrollY`가 유지되어 sticky 셸만 보이는 빈 화면을 막기 위한 계약이다.
|
||||
- OAuth/SAML callback은 저장된 `next`가 일반 진입 경로(`/`, `/learn`, `/teach`, `/login`, `/onboarding`)이고
|
||||
로그인 사용자가 관리자 콘솔 접근권을 가지면 `/admin`으로 정규화한다. 단, `/learn/session/...` 같은 깊은
|
||||
링크는 사용자가 의도적으로 연 URL일 수 있으므로 보존한다.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue