# 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: 3378/dev/account-recovery.spec.ts >> [3378][dev] 비스킷페이 계정 복구 화면 권한별 복구 검증 >> [BO] role >> 해지 상태 사용자는 계정 복구를 시도해도 정상 상태로 변경되지 않고 로그인되지 않는지 확인
- Location: src/qa/scenarios/accountRecovery.ts:88:11

# Error details

```
Error: BLOCKED: BO 해지 계정 복구 화면은 등록 휴대폰으로 발송된 6자리 인증번호가 필요합니다. QA_3378_BO_TERMINATED_SMS_OTP에 6자리 SMS 인증번호를 설정해야 합니다. 제공된 2FA/PIN 값만으로는 복구를 완료할 수 없습니다.
```

# Test source

```ts
  369 |       target.twoFactorKey ? `${target.twoFactorKey}: ${getTargetTwoFactorCode(role, status, target) ? "설정됨" : "없음"}` : "",
  370 |       `로그인 결과: ${loginError ? "차단됨" : "성공"}`
  371 |     ].filter(Boolean));
  372 |     await attachPageScreenshot(page, testInfo, "비스킷페이 로그인", role, options.suffix);
  373 | 
  374 |     if (options.shouldSucceed) {
  375 |       if (loginError) {
  376 |         throw new Error(loginError);
  377 |       }
  378 |       await expect(page.locator("body"), "새 비밀번호 로그인 후 로그인 화면에 머물면 안 됩니다.").not.toContainText(/로그인|아이디를 입력/);
  379 |       return;
  380 |     }
  381 | 
  382 |     if (!loginError && !(await isLoginPage(page, loginEnv))) {
  383 |       throw new Error("차단되어야 할 비밀번호로 로그인이 성공했습니다.");
  384 |     }
  385 |   } finally {
  386 |     await context.close();
  387 |   }
  388 | }
  389 | 
  390 | function getTargetTwoFactorCode(role: string, status: "LOCKED" | "TERMINATED", target: RecoveryTarget): string | undefined {
  391 |   const rolePrefix = `QA_3378_${role}_${status}`;
  392 |   return (
  393 |     target.twoFactorCode ??
  394 |     process.env[`${rolePrefix}_2FA`] ??
  395 |     process.env[`${rolePrefix}_LOGIN_OTP`] ??
  396 |     process.env[`QA_3378_${role}_2FA`] ??
  397 |     process.env.QA_3378_2FA ??
  398 |     target.otp
  399 |   );
  400 | }
  401 | 
  402 | async function withFreshPage(browser: Browser, callback: (page: Page) => Promise<void>): Promise<void> {
  403 |   const context = await browser.newContext();
  404 |   const page = await context.newPage();
  405 |   try {
  406 |     await callback(page);
  407 |   } finally {
  408 |     await context.close();
  409 |   }
  410 | }
  411 | 
  412 | function ensurePriorRecoveryCompleted(completedRoles: Set<string>, role: string): void {
  413 |   if (!completedRoles.has(role)) {
  414 |     blocked(`${role} 잠금 사용자 계정 복구 완료 검증이 먼저 통과해야 로그인 검증을 진행할 수 있습니다.`);
  415 |   }
  416 | }
  417 | 
  418 | function formatError(error: unknown): string {
  419 |   return error instanceof Error ? error.message : String(error);
  420 | }
  421 | 
  422 | async function bodyTextOf(page: Page): Promise<string> {
  423 |   return page.locator("body").innerText({ timeout: 5_000 });
  424 | }
  425 | 
  426 | function requireBaseUrl(runtimeEnv: RuntimeQaEnv): string {
  427 |   if (!runtimeEnv.baseUrl) {
  428 |     throw new Error("store dev base URL 설정이 필요합니다.");
  429 |   }
  430 |   return runtimeEnv.baseUrl;
  431 | }
  432 | 
  433 | async function attachEvidenceLog(
  434 |   testInfo: TestInfo,
  435 |   pageName: string,
  436 |   role: string,
  437 |   suffix: string,
  438 |   lines: string[]
  439 | ): Promise<void> {
  440 |   await testInfo.attach(`${safeFilename(pageName)}-${role}-${suffix}.md`, {
  441 |     body: lines.join("\n"),
  442 |     contentType: "text/markdown"
  443 |   });
  444 | }
  445 | 
  446 | async function attachPageScreenshot(
  447 |   page: Page,
  448 |   testInfo: TestInfo,
  449 |   pageName: string,
  450 |   role: string,
  451 |   suffix: string
  452 | ): Promise<void> {
  453 |   const screenshotPath = testInfo.outputPath(`${safeFilename(pageName)}-${role}-${suffix}.png`);
  454 |   await page.screenshot({ path: screenshotPath, fullPage: true });
  455 |   await testInfo.attach(`${safeFilename(pageName)}-${role}-${suffix}.png`, {
  456 |     path: screenshotPath,
  457 |     contentType: "image/png"
  458 |   });
  459 | }
  460 | 
  461 | function safeFilename(value: string): string {
  462 |   return value
  463 |     .replace(/[^a-zA-Z0-9가-힣_.-]+/g, "-")
  464 |     .replace(/^-+|-+$/g, "")
  465 |     .slice(0, 80);
  466 | }
  467 | 
  468 | function blocked(message: string): never {
> 469 |   throw new Error(`BLOCKED: ${message}`);
      |         ^ Error: BLOCKED: BO 해지 계정 복구 화면은 등록 휴대폰으로 발송된 6자리 인증번호가 필요합니다. QA_3378_BO_TERMINATED_SMS_OTP에 6자리 SMS 인증번호를 설정해야 합니다. 제공된 2FA/PIN 값만으로는 복구를 완료할 수 없습니다.
  470 | }
  471 | 
```