From 471eb9621af89cfac95d050c5c9e6c3275b93407 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sat, 29 Aug 2026 06:54:11 +0900 Subject: [PATCH] =?UTF-8?q?Google=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=9D=84?= =?UTF-8?q?=20=EB=8B=A8=EC=9D=BC=20=EB=B2=84=ED=8A=BC=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/app/test_auth_providers.py | 2 ++ apps/web/e2e/auth.spec.ts | 13 ++++--- apps/web/e2e/full-sweep-auth.spec.ts | 18 +++++----- apps/web/e2e/full-sweep-shell.spec.ts | 14 ++++---- apps/web/e2e/learner.spec.ts | 4 +-- apps/web/e2e/uc-a11y-mobile.spec.ts | 35 ++++++++++++++++--- .../web/e2e/uc-error-resilience-shell.spec.ts | 2 +- apps/web/src/pages/Login.tsx | 10 ++---- apps/web/src/pages/login/LoginPanel.tsx | 32 ++++------------- apps/web/src/pages/login/login.css | 12 ------- docs/TODO.md | 2 +- docs/dev_dashboard.html | 1 + docs/ops/e2e-full-sweep-2026-07-27.md | 7 ++-- ...inical-technical-preflight-2026-08-28.json | 4 +-- docs/ops/pilot-usage-guide-draft.md | 7 ++-- 15 files changed, 78 insertions(+), 85 deletions(-) diff --git a/apps/api/app/test_auth_providers.py b/apps/api/app/test_auth_providers.py index 672056e..6472cea 100644 --- a/apps/api/app/test_auth_providers.py +++ b/apps/api/app/test_auth_providers.py @@ -1094,6 +1094,8 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase): self.assertEqual(query["client_id"], ["google-client"]) self.assertEqual(query["redirect_uri"], ["https://api-vignette.test/auth/callback"]) self.assertEqual(query["response_type"], ["code"]) + self.assertEqual(query["prompt"], ["select_account"]) + self.assertNotIn("hd", query) self.assertEqual(query["code_challenge_method"], ["S256"]) self.assertEqual( query["code_challenge"], diff --git a/apps/web/e2e/auth.spec.ts b/apps/web/e2e/auth.spec.ts index 28cf521..6663b5b 100644 --- a/apps/web/e2e/auth.spec.ts +++ b/apps/web/e2e/auth.spec.ts @@ -67,13 +67,17 @@ test.describe("auth domain policy", () => { await page.goto("/login"); const googleButtons = page.locator(".lg-obtn"); - await expect(googleButtons).toHaveCount(2); + await expect(googleButtons).toHaveCount(1); + await expect(page.getByRole("button", { name: /^Google 계정으로 로그인/ })).toHaveCount(1); + await expect(page.getByRole("button", { name: /다른 Google 계정 선택/ })).toHaveCount(0); expect(config.allowed_email_domains).toEqual([]); - await expect(page.locator(".lg-policy b")).toHaveText("모든 Google 계정"); const redirectHost = new URL(config.redirect_uri).hostname; const devOAuthUnavailable = config.dev_login_enabled && !isLocalHostname(redirectHost); + await expect(page.locator(".lg-policy b")).toHaveText( + config.google_oauth_configured && !devOAuthUnavailable ? "모든 Google 계정" : "설정 필요", + ); if (config.dev_login_enabled) { await expect(page.locator(".lg-dev")).toBeVisible(); } else { @@ -85,7 +89,6 @@ test.describe("auth domain policy", () => { await expect(page.locator(".lg-config")).toHaveCount(0); } else { await expect(googleButtons.first()).toBeDisabled(); - await expect(googleButtons.nth(1)).toBeDisabled(); await expect(page.locator(".lg-config")).toBeVisible(); await expect(page).toHaveURL(/\/login$/); await expect(page.locator("body")).not.toContainText("Google OAuth is not configured"); @@ -239,7 +242,9 @@ test.describe("auth domain policy", () => { await page.goto(`${publicBase}/login`, { waitUntil: "domcontentloaded" }); await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => undefined); - await expect(page.locator(".lg-obtn")).toHaveCount(2); + await expect(page.locator(".lg-obtn")).toHaveCount(1); + await expect(page.getByRole("button", { name: /^Google 계정으로 로그인/ })).toHaveCount(1); + await expect(page.getByRole("button", { name: /다른 Google 계정 선택/ })).toHaveCount(0); await expect(page.locator(".lg-dev")).toHaveCount(0); await expect(page.getByText("로컬 테스트")).toHaveCount(0); await expect(page.getByText("로컬 테스트 계정으로 계속")).toHaveCount(0); diff --git a/apps/web/e2e/full-sweep-auth.spec.ts b/apps/web/e2e/full-sweep-auth.spec.ts index d121841..a06fffe 100644 --- a/apps/web/e2e/full-sweep-auth.spec.ts +++ b/apps/web/e2e/full-sweep-auth.spec.ts @@ -24,7 +24,7 @@ import { completeOnboarding, expectNoHorizontalOverflow, useRealApi } from "./su const CONFIG_PATTERN = "**/api/auth/config"; const ME_PATTERN = "**/api/auth/me"; -/** 로컬 redirect_uri라서 두 OAuth 버튼이 모두 활성화되는 구성 fixture. */ +/** 로컬 redirect_uri라서 단일 Google OAuth 버튼이 활성화되는 구성 fixture. */ const READY_AUTH_CONFIG = { google_oauth_configured: true, saml_configured: false, @@ -153,7 +153,7 @@ test.describe("full sweep auth", () => { }); // 검증 checklist: auth-login-generic-google-button - test("both Google buttons advertise unrestricted accounts and start the same OAuth flow", async ({ + test("single Google button advertises unrestricted accounts and starts the account chooser flow", async ({ page, }) => { await mockUnauthenticatedMe(page); @@ -173,16 +173,16 @@ test.describe("full sweep auth", () => { }); await expectNoHorizontalOverflow(page); - const secondary = page.locator(".lg-obtn.secondary"); - await expect(secondary).toBeEnabled(); - await expect(secondary).toContainText("다른 Google 계정 선택"); - await expect(secondary.locator(".sub")).toHaveText("이메일 확인 후 바로 시작"); - await expect(page.locator(".lg-obtn.primary .sub")).toHaveText("모든 Google 계정"); + const googleLogin = page.getByRole("button", { name: /^Google 계정으로 로그인/ }); + await expect(googleLogin).toHaveCount(1); + await expect(googleLogin).toBeEnabled(); + await expect(googleLogin.locator(".sub")).toHaveText("모든 Google 계정"); + await expect(page.getByRole("button", { name: /다른 Google 계정 선택/ })).toHaveCount(0); - // 클릭 시 primary와 동일한 provider=google 시작 URL로 이동한다. + // 단일 CTA가 provider=google OAuth 시작 URL로 이동한다. await Promise.all([ page.waitForURL(/\/api\/auth\/login\?provider=google&next=%2F/), - secondary.click(), + googleLogin.click(), ]); }); diff --git a/apps/web/e2e/full-sweep-shell.spec.ts b/apps/web/e2e/full-sweep-shell.spec.ts index 7fac8dc..e8ecef7 100644 --- a/apps/web/e2e/full-sweep-shell.spec.ts +++ b/apps/web/e2e/full-sweep-shell.spec.ts @@ -81,7 +81,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => { await page.getByRole("button", { name: "로그아웃" }).click(); await expect(page).toHaveURL(/\/login$/); - await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible(); // 서버 세션도 무효화되었는지 실 API로 확인 const me = await page.request.get("/api/auth/me"); @@ -112,7 +112,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => { await page.getByRole("button", { name: "로그아웃" }).click(); await expect(page).toHaveURL(/\/login$/); - await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible(); expect(logoutCalled).toBeTruthy(); }); @@ -239,7 +239,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => { releaseMe(); await expect(page).toHaveURL(/\/login$/); - await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible(); }); // checklist: shell-suspense-fallback @@ -269,7 +269,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => { releaseChunk(); - await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible({ + await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible({ timeout: 10_000, }); await expect(page.getByText(BOOT_SCREEN_TEXT)).toHaveCount(0); @@ -287,7 +287,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => { }); await page.goto("/login"); - await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible(); expect(documentRequests).toBe(1); const firstPrevented = await page.evaluate(() => { @@ -300,7 +300,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => { expect(firstPrevented).toBeTruthy(); await expect.poll(() => documentRequests).toBe(2); - await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible(); await expect .poll(() => page.evaluate(() => sessionStorage.getItem("vignette:chunk-recovery:/login")), @@ -358,7 +358,7 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => { test("redirects unknown routes to the role-aware root destination", async ({ page }) => { await page.goto("/definitely/not-a-route"); await expect(page).toHaveURL(/\/login$/); - await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible(); await signInAsLearner(page); await page.goto("/no-such-page"); diff --git a/apps/web/e2e/learner.spec.ts b/apps/web/e2e/learner.spec.ts index 24cb17c..b33805d 100644 --- a/apps/web/e2e/learner.spec.ts +++ b/apps/web/e2e/learner.spec.ts @@ -144,7 +144,7 @@ test.describe("learner app shell and session launcher", () => { await page.goto("/learn/practice"); await expect(page).toHaveURL(/\/login$/); - await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible(); expect(await page.evaluate(() => window.localStorage.getItem("vignette.dev-auth"))).toBeNull(); }); @@ -182,7 +182,7 @@ test.describe("learner app shell and session launcher", () => { await page.goto("/learn/practice"); await expect(page).toHaveURL(/\/login$/); - await expect(page.getByRole("button", { name: /학교 Google 계정으로 계속/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible(); await expect(page.getByText("내담자 목록을 불러오지 못했습니다.")).toHaveCount(0); }); diff --git a/apps/web/e2e/uc-a11y-mobile.spec.ts b/apps/web/e2e/uc-a11y-mobile.spec.ts index de25ad0..00efbd2 100644 --- a/apps/web/e2e/uc-a11y-mobile.spec.ts +++ b/apps/web/e2e/uc-a11y-mobile.spec.ts @@ -905,7 +905,7 @@ async function openLogin(page: Page) { await routeLoginScreen(page); await page.goto("/login"); await expect(page.locator(".lg-root, .lg-enter").first()).toBeVisible({ timeout: 15_000 }); - await expect(page.getByRole("button", { name: /^Google 계정으로 계속/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /^Google 계정으로 로그인/ })).toBeVisible(); } async function openLearnerHome(page: Page) { @@ -970,16 +970,41 @@ test.describe("uc a11y-mobile — 주요 6화면 접근성·모바일 감사", ( // usecase: 키보드 사용자가 Tab 으로 로그인 버튼에 도달하고 포커스 위치를 눈으로 확인한다 test("로그인 화면에서 Tab 순회 시 포커스 표시가 눈에 보인다", async ({ page }) => { await openLogin(page); - await expectFocusVisibleOnTab(page, "login", 5); + const googleLogin = page.getByRole("button", { name: /^Google 계정으로 로그인/ }); + await expect(googleLogin).toHaveCount(1); + await page.keyboard.press("Tab"); + await expect(googleLogin).toBeFocused(); + const focusReport = await googleLogin.evaluate((element) => { + const target = element as HTMLElement; + const snapshot = () => { + const style = window.getComputedStyle(target); + return [ + style.outlineStyle, + style.outlineWidth, + style.outlineColor, + style.boxShadow, + style.borderColor, + style.backgroundColor, + ].join("|"); + }; + const focusVisible = target.matches(":focus-visible"); + const focusedStyle = snapshot(); + target.blur(); + const blurredStyle = snapshot(); + target.focus(); + return { focusVisible, styleChanged: focusedStyle !== blurredStyle }; + }); + expect(focusReport.focusVisible).toBeTruthy(); + expect(focusReport.styleChanged).toBeTruthy(); }); // usecase: 저시력 사용자가 주 로그인 버튼의 문구를 읽는다 - test("로그인 주 CTA(Google 계정으로 계속)의 색 대비가 4.5:1 이상이다", async ({ page }) => { + test("로그인 주 CTA(Google 계정으로 로그인)의 색 대비가 4.5:1 이상이다", async ({ page }) => { await openLogin(page); await expectCtaContrast( - page.getByRole("button", { name: /^Google 계정으로 계속/ }), + page.getByRole("button", { name: /^Google 계정으로 로그인/ }), "login", - "Google 계정으로 계속", + "Google 계정으로 로그인", ); }); diff --git a/apps/web/e2e/uc-error-resilience-shell.spec.ts b/apps/web/e2e/uc-error-resilience-shell.spec.ts index 9faf98f..260272a 100644 --- a/apps/web/e2e/uc-error-resilience-shell.spec.ts +++ b/apps/web/e2e/uc-error-resilience-shell.spec.ts @@ -500,7 +500,7 @@ test.describe("uc error-resilience-shell", () => { await expect(page).toHaveURL(/\/login$/); await expect( - page.getByRole("button", { name: /Google 계정으로 계속/ }), + page.getByRole("button", { name: /Google 계정으로 로그인/ }), ).toBeVisible(); }); diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx index f839aee..a6a35a9 100644 --- a/apps/web/src/pages/Login.tsx +++ b/apps/web/src/pages/Login.tsx @@ -131,16 +131,11 @@ export default function Login() { !isLocalRedirectUri(authConfig.redirect_uri); const oauthReady = authConfig?.google_oauth_configured === true && !devOAuthUnavailable; - const primaryAccountLabel = devOAuthUnavailable + const accountLabel = devOAuthUnavailable ? "로컬은 테스트 계정 사용" : oauthChecking ? "도메인 확인 중" : "모든 Google 계정"; - const secondaryAccountLabel = devOAuthUnavailable - ? "공개 주소에서 사용" - : oauthChecking - ? "계정 정책 확인 중" - : "이메일 확인 후 바로 시작"; const oauthStatusMessage = devOAuthUnavailable ? LOCAL_OAUTH_UNAVAILABLE_MESSAGE : oauthChecking @@ -182,8 +177,7 @@ export default function Login() { ; statusMessage: string; onStart: () => void; @@ -38,33 +37,27 @@ interface LoginPanelProps { error: LoginErrorView; } -function LoginProviderButton({ - variant, - icon, - label, +function GoogleLoginButton({ subLabel, disabled, onClick, }: { - variant: "primary" | "secondary"; - icon: Extract; - label: string; subLabel: string; disabled: boolean; onClick: () => void; }) { return (