# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: 4104/dev/duplicate-login-second-factor.spec.ts >> [4104][dev] SMS/Email 2차 인증 중복 로그인 강제로그인 모달 검증 >> jjm_sms: SMS 2차 인증으로 일반/중복 로그인을 완료하고 강제로그인 후 현재 세션 활성화와 기존 세션 종료를 확인
- Location: src/qa/scenarios/duplicateLoginSecondFactor.ts:45:7

# Error details

```
Error: BLOCKED: SMS 2차 인증 primary 인증번호를 60초 안에 받지 못했습니다.
```

# Test source

```ts
  195 |   }
  196 |   return value;
  197 | }
  198 |
  199 | async function expectForceModalOnly(page: Page, account: Account): Promise<void> {
  200 |   const surface = await inspectSurface(page, account);
  201 |   expect(surface.forceVisible, `${account.label} 중복 로그인 시 강제로그인 확인 모달이 단독으로 표시되어야 합니다.`).toBe(true);
  202 |   expect(surface.factorVisible, `${account.label} 강제로그인 확인 모달과 2차 인증 모달이 겹치면 안 됩니다.`).toBe(false);
  203 | }
  204 |
  205 | async function clickForceButton(page: Page, name: RegExp): Promise<void> {
  206 |   const modal = forceModal(page);
  207 |   const button = modal.getByRole("button", { name }).last();
  208 |   await expect(button).toBeVisible({ timeout: 5_000 });
  209 |   await button.click();
  210 | }
  211 |
  212 | async function forceModalText(page: Page): Promise<string> {
  213 |   const modal = forceModal(page);
  214 |   await expect(modal).toBeVisible({ timeout: 10_000 });
  215 |   return normalize(await modal.innerText());
  216 | }
  217 |
  218 | function forceModal(page: Page) {
  219 |   return page.locator("[role='dialog'], .fixed.inset-0").filter({ hasText: FORCE_MODAL }).last();
  220 | }
  221 |
  222 | interface LoginSurface {
  223 |   forceVisible: boolean;
  224 |   factorVisible: boolean;
  225 |   path: string;
  226 |   body: string;
  227 | }
  228 |
  229 | async function inspectSurface(page: Page, account: Account): Promise<LoginSurface> {
  230 |   const body = normalize(await page.locator("body").innerText().catch(() => ""));
  231 |   const factorVisible = await secondFactorInput(page, account).isVisible().catch(() => false);
  232 |   return {
  233 |     forceVisible: await forceModal(page).isVisible().catch(() => false),
  234 |     factorVisible,
  235 |     path: new URL(page.url()).pathname,
  236 |     body: body.slice(-700)
  237 |   };
  238 | }
  239 |
  240 | function describeLoginSurface(surface: LoginSurface): string[] {
  241 |   return [
  242 |     `강제로그인 확인 모달: ${surface.forceVisible ? "노출" : "미노출"}`,
  243 |     `2차 인증 입력 모달: ${surface.factorVisible ? "노출" : "미노출"}`,
  244 |     `현재 경로: ${surface.path}`,
  245 |     `화면 일부: ${surface.body || "-"}`
  246 |   ];
  247 | }
  248 |
  249 | async function clickExactButton(page: Page, name: string): Promise<void> {
  250 |   const buttons = page.getByRole("button", { name, exact: true });
  251 |   const count = await buttons.count();
  252 |   for (let index = count - 1; index >= 0; index -= 1) {
  253 |     const button = buttons.nth(index);
  254 |     if (await button.isVisible().catch(() => false)) {
  255 |       await button.click();
  256 |       return;
  257 |     }
  258 |   }
  259 |   await buttons.last().click();
  260 | }
  261 |
  262 | async function clickFirstVisible(locator: ReturnType<Page["locator"]>): Promise<void> {
  263 |   const count = await locator.count();
  264 |   for (let index = 0; index < count; index += 1) {
  265 |     const candidate = locator.nth(index);
  266 |     if (await candidate.isVisible().catch(() => false)) {
  267 |       await candidate.click();
  268 |       return;
  269 |     }
  270 |   }
  271 |   await locator.first().click();
  272 | }
  273 |
  274 | async function attachEvidence(testInfo: TestInfo, page: Page, slug: string, lines: string[]): Promise<void> {
  275 |   await testInfo.attach(`${slug}.md`, { body: `${lines.join("\n")}\n`, contentType: "text/markdown" });
  276 |   const screenshot = testInfo.outputPath(`${slug}.png`);
  277 |   await page.screenshot({ path: screenshot, fullPage: true });
  278 |   await testInfo.attach(`${slug}.png`, { path: screenshot, contentType: "image/png" });
  279 | }
  280 |
  281 | async function closeDuplicateState(state: DuplicateState): Promise<void> {
  282 |   await state.firstContext.close();
  283 |   await state.secondContext.close();
  284 | }
  285 |
  286 | function requiredEnv(key: string): string {
  287 |   const value = process.env[key];
  288 |   if (!value) {
  289 |     blocked(`${key} 런타임 환경변수가 필요합니다. 실제 비밀번호와 인증번호는 저장소 및 QA 산출물에 기록하지 않습니다.`);
  290 |   }
  291 |   return value;
  292 | }
  293 |
  294 | function blocked(message: string): never {
> 295 |   throw new Error(`BLOCKED: ${message}`);
      |         ^ Error: BLOCKED: SMS 2차 인증 primary 인증번호를 60초 안에 받지 못했습니다.
  296 | }
  297 |
  298 | function normalize(value: string): string {
  299 |   return value.replace(/\s+/g, " ").trim();
  300 | }
  301 |
  302 | function positiveNumber(value: string | undefined, fallback: number): number {
  303 |   const parsed = Number(value);
  304 |   return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
  305 | }
  306 |
```