# 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: 3583/dev/firm-bank-transfer-secondary-auth.spec.ts >> [3583][dev] [QA] 펌뱅킹/통합계좌 이체 2차 인증 및 계정별 펌뱅크 송금 권한 노출 >> [secret-redacted] 펌뱅크 송금 권한이 있는 계정으로 로그인했을 때 두 화면의 이체 컬럼/버튼이 보이는지 확인
- Location: src/qa/scenarios/firmBankTransferSecondaryAuth.ts:178:5

# Error details

```
Error: 통합계좌 관리 이체 컬럼 또는 버튼은 ON 상태에서 보여야 합니다.

expect(received).toBe(expected) // Object.is equality

Expected: true
Received: false
```

# Test source

```ts
  99  |   firmBankType: string;
  100 | }
  101 |
  102 | interface PreparedTransfer {
  103 |   source: TransferSource;
  104 |   swalText: string;
  105 | }
  106 |
  107 | interface TransferResponseEvidence {
  108 |   verifyStatus: number;
  109 |   verifyText: string;
  110 |   transferStatus: number;
  111 |   transferText: string;
  112 |   successText: string;
  113 | }
  114 |
  115 | interface TransferState {
  116 |   issueId: string;
  117 |   environment: QaEnvironment;
  118 |   sourceFirmBankId: string;
  119 |   sourceFirmBankName: string;
  120 |   amount: number;
  121 |   receiverBankName: string;
  122 |   receiverAccountNumber: string;
  123 |   receiverAccountHolder: string;
  124 |   completedAt: string;
  125 |   verifyStatus: number;
  126 |   transferStatus: number;
  127 | }
  128 |
  129 | const ADMIN_ROLE = "ADMIN";
  130 | const TARGET_PRESET: TargetPreset = { role: ADMIN_ROLE, name: "USE_SYSTEM" };
  131 | const REQUESTED_PERMISSION_ID = "FIRM_BANK_TRANSFER_REQUEST";
  132 | const PRESET_MUTATION_GUARD = "QA_3583_ALLOW_PRESET_MUTATION";
  133 | const TRANSFER_ALLOWED_ROLE = "ADMIN";
  134 | const TRANSFER_DENIED_ROLE = "ADMIN_NO_TRANSFER";
  135 | const TRANSFER_EXECUTION_GUARD = "QA_3583_ALLOW_FIRM_BANK_TRANSFER";
  136 | const TRANSFER_AMOUNT = Number(process.env.QA_3583_TRANSFER_AMOUNT ?? "1000");
  137 | const RECEIVER_BANK_NAME = process.env.QA_3583_RECEIVER_BANK_NAME ?? "카카오뱅크";
  138 | const RECEIVER_BANK_CODE = process.env.QA_3583_RECEIVER_BANK_CODE ?? "090";
  139 | const RECEIVER_ACCOUNT_NUMBER = process.env.QA_3583_RECEIVER_ACCOUNT_NUMBER ?? "3333088854750";
  140 | const RECEIVER_ACCOUNT_HOLDER = process.env.QA_3583_RECEIVER_ACCOUNT_HOLDER ?? "이의현";
  141 | const RECEIVER_CONTENT = "QA3583";
  142 | const TRANSFER_NOTE = "QA3583";
  143 |
  144 | export function runFirmBankTransferSecondaryAuthSuite(options: ScenarioOptions): void {
  145 |   const metadata = loadIssueMetadata(options.issueId);
  146 |   const checklist = loadChecklist(options.issueId, options.environment);
  147 |   const integratedPage = requirePage(checklist.pages, "통합계좌 관리");
  148 |   const firmBankPage = requirePage(checklist.pages, "펌뱅킹 등록");
  149 |   const transferAllowedRuntime = getRuntimeQaEnv(options.environment, firmBankPage.application ?? "admin", TRANSFER_ALLOWED_ROLE);
  150 |   const transferDeniedRuntime = getRuntimeQaEnv(options.environment, firmBankPage.application ?? "admin", TRANSFER_DENIED_ROLE);
  151 |
  152 |   test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
  153 |     test.setTimeout(Number(process.env.QA_TEST_TIMEOUT_MS ?? 120_000));
  154 |
  155 |     test(checklist.checklist[0] ?? "[secret-redacted2] 펌뱅크 송금 권한 미보유 계정 이체 컬럼/버튼 미노출", async ({ browser }, testInfo) => {
  156 |       const snapshots: PageTransferSnapshot[] = [];
  157 |       await withRolePage(browser, transferDeniedRuntime, async (page) => {
  158 |         snapshots.push(await collectTransferSnapshot(page, transferDeniedRuntime, integratedPage));
  159 |         await attachPageScreenshot(page, testInfo, integratedPage.name, TRANSFER_DENIED_ROLE, "permission-denied");
  160 |       });
  161 |       await withRolePage(browser, transferDeniedRuntime, async (page) => {
  162 |         snapshots.push(await collectTransferSnapshot(page, transferDeniedRuntime, firmBankPage));
  163 |         await attachPageScreenshot(page, testInfo, firmBankPage.name, TRANSFER_DENIED_ROLE, "permission-denied");
  164 |       });
  165 |
  166 |       await attachEvidenceLog(testInfo, firmBankPage.name, TRANSFER_DENIED_ROLE, "permission-denied-summary", [
  167 |         "secret-redacted2 계정으로 펌뱅크 송금 권한 미보유 상태의 이체 컬럼/버튼 노출 여부를 확인했습니다.",
  168 |         ...snapshots.flatMap(formatSnapshotLines),
  169 |         "기대값: 통합계좌 관리와 펌뱅킹 등록 화면 모두 이체 컬럼/버튼 미노출"
  170 |       ]);
  171 |
  172 |       for (const snapshot of snapshots) {
  173 |         expect(snapshot.transferColumnVisible, `${snapshot.pageName} 이체 컬럼은 OFF 상태에서 보이면 안 됩니다.`).toBe(false);
  174 |         expect(snapshot.transferButtonCount, `${snapshot.pageName} 이체 버튼은 OFF 상태에서 보이면 안 됩니다.`).toBe(0);
  175 |       }
  176 |     });
  177 |
  178 |     test(checklist.checklist[1] ?? "[secret-redacted] 펌뱅크 송금 권한 보유 계정 이체 컬럼/버튼 노출", async ({ browser }, testInfo) => {
  179 |       const snapshots: PageTransferSnapshot[] = [];
  180 |       await withRolePage(browser, transferAllowedRuntime, async (page) => {
  181 |         snapshots.push(await collectTransferSnapshot(page, transferAllowedRuntime, integratedPage));
  182 |         await attachPageScreenshot(page, testInfo, integratedPage.name, TRANSFER_ALLOWED_ROLE, "permission-allowed");
  183 |       });
  184 |       await withRolePage(browser, transferAllowedRuntime, async (page) => {
  185 |         snapshots.push(await collectTransferSnapshot(page, transferAllowedRuntime, firmBankPage));
  186 |         await attachPageScreenshot(page, testInfo, firmBankPage.name, TRANSFER_ALLOWED_ROLE, "permission-allowed");
  187 |       });
  188 |
  189 |       await attachEvidenceLog(testInfo, firmBankPage.name, TRANSFER_ALLOWED_ROLE, "permission-allowed-summary", [
  190 |         "secret-redacted 계정으로 펌뱅크 송금 권한 보유 상태의 이체 컬럼/버튼 노출 여부를 확인했습니다.",
  191 |         ...snapshots.flatMap(formatSnapshotLines),
  192 |         "기대값: 통합계좌 관리와 펌뱅킹 등록 화면 모두 이체 컬럼/버튼 노출"
  193 |       ]);
  194 |
  195 |       for (const snapshot of snapshots) {
  196 |         expect(
  197 |           snapshot.transferColumnVisible || snapshot.transferButtonCount > 0,
  198 |           `${snapshot.pageName} 이체 컬럼 또는 버튼은 ON 상태에서 보여야 합니다.`
> 199 |         ).toBe(true);
      |           ^ Error: 통합계좌 관리 이체 컬럼 또는 버튼은 ON 상태에서 보여야 합니다.
  200 |       }
  201 |     });
  202 |
  203 |     test(checklist.checklist[2] ?? "[secret-redacted] 올바른 2차 인증번호 입력 후 이체 진행 확인", async ({ browser }, testInfo) => {
  204 |       if (process.env[TRANSFER_EXECUTION_GUARD] !== "1") {
  205 |         test.skip(true, `BLOCKED: 실제 펌뱅킹 이체 실행은 ${TRANSFER_EXECUTION_GUARD}=1일 때만 수행합니다.`);
  206 |       }
  207 |
  208 |       await withRolePage(browser, transferAllowedRuntime, async (page) => {
  209 |         const existingState = readTransferState(options);
  210 |         if (existingState) {
  211 |           await openTargetPage(page, transferAllowedRuntime, firmBankPage);
  212 |           await attachEvidenceLog(testInfo, firmBankPage.name, TRANSFER_ALLOWED_ROLE, "secondary-auth-success-reused", [
  213 |             "동일 실행 내 실제 이체 성공 state가 있어 이체를 재실행하지 않았습니다.",
  214 |             `기록된 펌뱅킹: ${existingState.sourceFirmBankName} / id=${existingState.sourceFirmBankId}`,
  215 |             `기록된 금액: ${formatNumber(existingState.amount)}원`,
  216 |             `verify status=${existingState.verifyStatus}, transfer status=${existingState.transferStatus}`,
  217 |             `completedAt=${existingState.completedAt}`
  218 |           ]);
  219 |           await attachPageScreenshot(page, testInfo, firmBankPage.name, TRANSFER_ALLOWED_ROLE, "secondary-auth-success-reused");
  220 |           expect(existingState.verifyStatus, "기존 2차 인증 verify 응답은 성공 status여야 합니다.").toBeGreaterThanOrEqual(200);
  221 |           expect(existingState.verifyStatus, "기존 2차 인증 verify 응답은 성공 status여야 합니다.").toBeLessThan(300);
  222 |           expect(existingState.transferStatus, "기존 이체 요청 응답은 성공 status여야 합니다.").toBeGreaterThanOrEqual(200);
  223 |           expect(existingState.transferStatus, "기존 이체 요청 응답은 성공 status여야 합니다.").toBeLessThan(300);
  224 |           return;
  225 |         }
  226 |
  227 |         const authCode = process.env.QA_3583_SECONDARY_AUTH_CODE ?? transferAllowedRuntime.twoFactorCode;
  228 |         if (!authCode) {
  229 |           test.skip(true, "BLOCKED: 2차 인증번호 입력에 사용할 QA_3583_SECONDARY_AUTH_CODE 또는 ADMIN 2FA 설정이 없습니다.");
  230 |         }
  231 |
  232 |         const prepared = await prepareSingleFirmBankTransferConfirmation(page, transferAllowedRuntime, firmBankPage);
  233 |         const authText = await confirmSwalAndWaitForSecondaryAuth(page);
  234 |         const transferEvidence = await submitSecondaryAuthAndWaitForTransfer(page, authCode!);
  235 |
  236 |         const state: TransferState = {
  237 |           issueId: options.issueId,
  238 |           environment: options.environment,
  239 |           sourceFirmBankId: prepared.source.id,
  240 |           sourceFirmBankName: prepared.source.name,
  241 |           amount: TRANSFER_AMOUNT,
  242 |           receiverBankName: RECEIVER_BANK_NAME,
  243 |           receiverAccountNumber: RECEIVER_ACCOUNT_NUMBER,
  244 |           receiverAccountHolder: RECEIVER_ACCOUNT_HOLDER,
  245 |           completedAt: new Date().toISOString(),
  246 |           verifyStatus: transferEvidence.verifyStatus,
  247 |           transferStatus: transferEvidence.transferStatus
  248 |         };
  249 |         writeTransferState(options, state);
  250 |
  251 |         await attachEvidenceLog(testInfo, firmBankPage.name, TRANSFER_ALLOWED_ROLE, "secondary-auth-success", [
  252 |           `실제 이체 실행 guard(${TRANSFER_EXECUTION_GUARD}): enabled`,
  253 |           `선택 펌뱅킹: ${prepared.source.name} / id=${prepared.source.id} / balance=${formatNumber(prepared.source.balance)}원`,
  254 |           `확인창 텍스트: ${prepared.swalText}`,
  255 |           `2차 인증창 텍스트: ${authText}`,
  256 |           `verify 응답: status=${transferEvidence.verifyStatus} body=${snippet(transferEvidence.verifyText, 800)}`,
  257 |           `이체 응답: status=${transferEvidence.transferStatus} body=${snippet(transferEvidence.transferText, 1200)}`,
  258 |           `완료 안내: ${transferEvidence.successText}`
  259 |         ]);
  260 |         await attachPageScreenshot(page, testInfo, firmBankPage.name, TRANSFER_ALLOWED_ROLE, "secondary-auth-success");
  261 |
  262 |         expect(authText, "2차 인증 입력창이 표시되어야 합니다.").toMatch(/PIN\s*인증|OTP\s*인증|SMS\s*인증|이메일\s*인증|2차\s*인증/);
  263 |         expect(transferEvidence.verifyStatus, "2차 인증 verify API가 성공해야 합니다.").toBeGreaterThanOrEqual(200);
  264 |         expect(transferEvidence.verifyStatus, "2차 인증 verify API가 성공해야 합니다.").toBeLessThan(300);
  265 |         expect(transferEvidence.transferStatus, "이체 요청 API가 성공해야 합니다.").toBeGreaterThanOrEqual(200);
  266 |         expect(transferEvidence.transferStatus, "이체 요청 API가 성공해야 합니다.").toBeLessThan(300);
  267 |         expect(transferEvidence.successText, "이체 완료 안내가 표시되어야 합니다.").toMatch(/정상적으로\s*처리|완료|성공/);
  268 |       });
  269 |     });
  270 |
  271 |     test(checklist.checklist[3] ?? "[secret-redacted] 2차 인증 취소 시 이체 미진행 확인", async ({ browser }, testInfo) => {
  272 |       await withRolePage(browser, transferAllowedRuntime, async (page) => {
  273 |         const transferEvents: string[] = [];
  274 |         page.on("request", (request) => {
  275 |           if (isFirmBankTransferMutationUrl(request.url())) {
  276 |             transferEvents.push(`REQ ${request.method()} ${sanitizeUrl(request.url())}`);
  277 |           }
  278 |         });
  279 |         page.on("response", (response) => {
  280 |           if (isFirmBankTransferMutationUrl(response.url())) {
  281 |             transferEvents.push(`RES ${response.status()} ${sanitizeUrl(response.url())}`);
  282 |           }
  283 |         });
  284 |
  285 |         const prepared = await prepareSingleFirmBankTransferConfirmation(page, transferAllowedRuntime, firmBankPage);
  286 |         const authText = await confirmSwalAndWaitForSecondaryAuth(page);
  287 |         const authInput = page.locator("input[autocomplete='one-time-code']").last();
  288 |         await authInput.waitFor({ state: "visible", timeout: 10_000 });
  289 |         const authDialog = authInput.locator("xpath=ancestor::*[.//button[normalize-space()='취소']][1]");
  290 |         await authDialog.getByRole("button", { name: "취소" }).click();
  291 |         await expect(authInput, "2차 인증 취소 후 인증 입력창은 닫혀야 합니다.").toBeHidden({ timeout: 5_000 });
  292 |         const authStillVisible = await page.locator("input[autocomplete='one-time-code']:visible").count();
  293 |         const verifyOrTransferEvents = transferEvents.filter((event) => /\/transfer\/verify|\/firm-banks\/[^/]+\/transfer(?:\?|$)/.test(event));
  294 |
  295 |         await attachEvidenceLog(testInfo, firmBankPage.name, TRANSFER_ALLOWED_ROLE, "secondary-auth-cancel", [
  296 |           `선택 펌뱅킹: ${prepared.source.name} / id=${prepared.source.id}`,
  297 |           `확인창 텍스트: ${prepared.swalText}`,
  298 |           `2차 인증창 텍스트: ${authText}`,
  299 |           `취소 후 인증창 표시 수: ${authStillVisible}`,
```