# 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: 3529/dev/store-settlement-balance.spec.ts >> [3529][dev] [QA] 비스킷페이 총 정산예정금 상세 내역 확인 >> [ADMIN] role >> 비스킷링크 정산내역 >> 어드민 계정으로 비스킷링크 정산내역 화면에 접속해 동일 기간·동일 가맹점 기준 금액이 비스킷페이 정산내역과 동일하게 보이는지 비교 확인
- Location: src/qa/scenarios/storeSettlementBalance.ts:405:9

# Error details

```
Error: 스토어 정산내역과 비스킷링크 정산내역에 공통 금액 근거가 있어야 합니다.

expect(received).toBeGreaterThan(expected)

Expected: > 0
Received:   0
```

# Test source

```ts
  326 |
  327 |       test.describe(storeSettlementsPage.name, () => {
  328 |         for (const statusText of ["정산예정", "정산대기", "정산완료"]) {
  329 |           const checklistIndex = statusText === "정산예정" ? 2 : statusText === "정산대기" ? 3 : 4;
  330 |           test(checklistItems[checklistIndex] ?? `${statusText} 정산 금액 확인`, async ({ browser }, testInfo) => {
  331 |             test.setTimeout(100_000);
  332 |             const snapshot = await collectIssue3529StoreSnapshot(
  333 |               browser,
  334 |               storeRuntime,
  335 |               storeSettlementsPage,
  336 |               testInfo,
  337 |               `store-settlement-${statusText}`,
  338 |               { expectedText: /정산|거래|결제/, statusText }
  339 |             );
  340 |             settlementSnapshots.set(statusText, snapshot);
  341 |
  342 |             expect(snapshot.bodyText, `${statusText} 상태 문구가 화면에 보여야 합니다.`).toContain(statusText);
  343 |             expect(snapshot.amounts.length, `${statusText} 금액 근거가 있어야 합니다.`).toBeGreaterThan(0);
  344 |             if (statusText === "정산완료") {
  345 |               expect(snapshot.bodyText, "정산완료 건은 세금/공제/최종 금액 근거가 보여야 합니다.").toMatch(/세금|부가세|공제|최종|실\s*정산|실정산/);
  346 |             }
  347 |
  348 |             await attachEvidenceLog(testInfo, storeSettlementsPage.name, ISSUE_3529_STORE_ROLE, `store-settlement-${statusText}`, [
  349 |               `${statusText} 조건으로 스토어 정산내역을 조회했습니다.`,
  350 |               `조회 기간: ${ISSUE_3529_PERIOD.from} ~ ${ISSUE_3529_PERIOD.to}`,
  351 |               `화면 금액 후보: ${formatAmounts(snapshot.amounts)}`,
  352 |               `정산예정금 상세 API: ${snapshot.settlementBreakdown ? formatBreakdown(snapshot.settlementBreakdown) : "-"}`,
  353 |               `수집한 요청: ${snapshot.requestUrls.slice(0, 8).join(" / ") || "-"}`,
  354 |               `결론: ${statusText} 정산 금액 표시 근거를 확인했습니다.`
  355 |             ]);
  356 |           });
  357 |         }
  358 |       });
  359 |
  360 |       test.describe(storeWalletPage.name, () => {
  361 |         test(checklistItems[6] ?? "지갑 정산예정금/정산대기금과 정산내역 합계 일치 확인", async ({ browser }, testInfo) => {
  362 |           test.setTimeout(100_000);
  363 |           const walletSnapshot = await collectIssue3529StoreSnapshot(
  364 |             browser,
  365 |             storeRuntime,
  366 |             storeWalletPage,
  367 |             testInfo,
  368 |             "store-wallet-settlement-total",
  369 |             { expectedText: /정산예정금|정산대기금|지갑/ }
  370 |           );
  371 |
  372 |           const pending = extractOptionalAmountAfterLabel(walletSnapshot.bodyText, "정산예정금");
  373 |           const waiting = extractOptionalAmountAfterLabel(walletSnapshot.bodyText, "정산대기금");
  374 |           expect(walletSnapshot.bodyText, "지갑 화면에서 정산예정금 또는 정산대기금 문구가 보여야 합니다.").toMatch(/정산예정금|정산대기금/);
  375 |           expect(walletSnapshot.amounts.length, "지갑 화면 금액 근거가 있어야 합니다.").toBeGreaterThan(0);
  376 |
  377 |           const pendingSnapshot = settlementSnapshots.get("정산예정");
  378 |           const waitingSnapshot = settlementSnapshots.get("정산대기");
  379 |           if (pending !== undefined && pendingSnapshot?.settlementBreakdown) {
  380 |             expect(pending, "지갑 정산예정금은 정산내역 정산예정 상세 금액과 일치해야 합니다.").toBe(
  381 |               pendingSnapshot.settlementBreakdown.pendingSettlementAmount
  382 |             );
  383 |           }
  384 |           if (waiting !== undefined && waitingSnapshot?.settlementBreakdown) {
  385 |             expect(waiting, "지갑 정산대기금은 정산내역 정산대기 상세 금액과 일치해야 합니다.").toBe(
  386 |               waitingSnapshot.settlementBreakdown.settlementWaitingAmount
  387 |             );
  388 |           }
  389 |
  390 |           await attachEvidenceLog(testInfo, storeWalletPage.name, ISSUE_3529_STORE_ROLE, "store-wallet-settlement-total", [
  391 |             "스토어 지갑 화면의 정산예정금/정산대기금 영역을 확인했습니다.",
  392 |             `지갑 정산예정금: ${pending === undefined ? "-" : formatKrw(pending)}`,
  393 |             `지갑 정산대기금: ${waiting === undefined ? "-" : formatKrw(waiting)}`,
  394 |             `화면 금액 후보: ${formatAmounts(walletSnapshot.amounts)}`,
  395 |             `정산예정 상세 API: ${pendingSnapshot?.settlementBreakdown ? formatBreakdown(pendingSnapshot.settlementBreakdown) : "-"}`,
  396 |             `정산대기 상세 API: ${waitingSnapshot?.settlementBreakdown ? formatBreakdown(waitingSnapshot.settlementBreakdown) : "-"}`,
  397 |             "결론: 지갑 화면과 정산내역 상세 금액 비교 근거를 수집했습니다."
  398 |           ]);
  399 |         });
  400 |       });
  401 |     });
  402 |
  403 |     test.describe(`[${ADMIN_ROLE}] role`, () => {
  404 |       test.describe(adminSettlementsPage.name, () => {
  405 |         test(checklistItems[5] ?? "비스킷링크 정산내역과 비스킷페이 정산내역 금액 비교", async ({ browser }, testInfo) => {
  406 |           test.setTimeout(100_000);
  407 |           const storeSnapshot = settlementSnapshots.get("정산예정") ?? await collectIssue3529StoreSnapshot(
  408 |             browser,
  409 |             storeRuntime,
  410 |             storeSettlementsPage,
  411 |             testInfo,
  412 |             "store-settlement-admin-source",
  413 |             { expectedText: /정산예정|정산내역/, statusText: "정산예정", attachScreenshot: false }
  414 |           );
  415 |
  416 |           adminSettlementSnapshot = await collectIssue3529AdminSettlementSnapshot(
  417 |             browser,
  418 |             adminRuntime,
  419 |             adminSettlementsPage,
  420 |             testInfo
  421 |           );
  422 |
  423 |           expect(adminSettlementSnapshot.bodyText, "비스킷링크 정산내역 화면이 표시되어야 합니다.").toMatch(/정산|가맹|업체|금액/);
  424 |           expect(adminSettlementSnapshot.amounts.length, "비스킷링크 정산내역 금액 근거가 있어야 합니다.").toBeGreaterThan(0);
  425 |           const sharedAmounts = storeSnapshot.amounts.filter((amount) => adminSettlementSnapshot?.amounts.includes(amount));
> 426 |           expect(sharedAmounts.length, "스토어 정산내역과 비스킷링크 정산내역에 공통 금액 근거가 있어야 합니다.").toBeGreaterThan(0);
      |                                                                                    ^ Error: 스토어 정산내역과 비스킷링크 정산내역에 공통 금액 근거가 있어야 합니다.
  427 |
  428 |           await attachEvidenceLog(testInfo, adminSettlementsPage.name, ADMIN_ROLE, "admin-store-settlement-compare", [
  429 |             `동일 기준: ${ISSUE_3529_MERCHANT_CODE}, ${ISSUE_3529_PERIOD.from} ~ ${ISSUE_3529_PERIOD.to}`,
  430 |             `스토어 정산 금액 후보: ${formatAmounts(storeSnapshot.amounts)}`,
  431 |             `비스킷링크 정산 금액 후보: ${formatAmounts(adminSettlementSnapshot.amounts)}`,
  432 |             `공통 금액 후보: ${formatAmounts(sharedAmounts)}`,
  433 |             `비스킷링크 요청: ${adminSettlementSnapshot.requestUrls.slice(0, 8).join(" / ") || "-"}`,
  434 |             "결론: 동일 기간/가맹점 기준으로 비스킷페이와 비스킷링크 정산내역 금액 비교 근거를 확인했습니다."
  435 |           ]);
  436 |         });
  437 |       });
  438 |     });
  439 |   });
  440 | }
  441 |
  442 | function findPage(pages: QaPageDefinition[], name: string): QaPageDefinition {
  443 |   const pageDefinition = pages.find((page) => page.name === name || page.name.includes(name));
  444 |   if (!pageDefinition) {
  445 |     throw new Error(`checklist.json pages에 '${name}' 화면 정의가 필요합니다.`);
  446 |   }
  447 |   return pageDefinition;
  448 | }
  449 |
  450 | async function collectIssue3529StoreSnapshot(
  451 |   browser: Browser,
  452 |   runtimeEnv: RuntimeQaEnv,
  453 |   pageDefinition: QaPageDefinition,
  454 |   testInfo: TestInfo,
  455 |   slug: string,
  456 |   options: {
  457 |     expectedText: RegExp;
  458 |     statusText?: string;
  459 |     attachScreenshot?: boolean;
  460 |   }
  461 | ): Promise<Issue3529Snapshot> {
  462 |   let snapshot: Issue3529Snapshot | undefined;
  463 |
  464 |   await withTracedPage(browser, runtimeEnv, testInfo, pageDefinition.name, ISSUE_3529_STORE_ROLE, slug, async (page) => {
  465 |     const requestUrls = collectRequestUrls(page, /store|transaction|settlement|wallet|정산|거래/i);
  466 |     const settlementPromise = waitForOptionalJsonPayload<SettlementBreakdown>(
  467 |       page,
  468 |       isStoreSettlementResponse,
  469 |       "스토어 정산예정금 상세",
  470 |       12_000
  471 |     );
  472 |
  473 |     await openIssue3529StorePage(page, runtimeEnv, pageDefinition, options.expectedText);
  474 |     await applyIssue3529Filters(page, options.statusText);
  475 |     await waitForNavigationToSettle(page);
  476 |
  477 |     const settlementBreakdown = normalizeOptionalBreakdown(await settlementPromise);
  478 |     const bodyText = await normalizedBodyText(page);
  479 |     snapshot = {
  480 |       bodyText,
  481 |       amounts: extractAmounts(bodyText),
  482 |       requestUrls: requestUrls(),
  483 |       settlementBreakdown
  484 |     };
  485 |
  486 |     await attachPageScreenshot(page, testInfo, pageDefinition.name, ISSUE_3529_STORE_ROLE, slug);
  487 |     if (options.attachScreenshot !== false) {
  488 |       await page.screenshot({ path: testInfo.outputPath(`${safeFilename(pageDefinition.name)}-${ISSUE_3529_STORE_ROLE}-${slug}-viewport.png`) }).catch(() => undefined);
  489 |     }
  490 |   });
  491 |
  492 |   if (!snapshot) {
  493 |     blocked(`${pageDefinition.name} 화면 스냅샷을 수집하지 못했습니다.`);
  494 |   }
  495 |   return snapshot;
  496 | }
  497 |
  498 | async function collectIssue3529AdminSettlementSnapshot(
  499 |   browser: Browser,
  500 |   runtimeEnv: RuntimeQaEnv,
  501 |   pageDefinition: QaPageDefinition,
  502 |   testInfo: TestInfo
  503 | ): Promise<Issue3529Snapshot> {
  504 |   let snapshot: Issue3529Snapshot | undefined;
  505 |
  506 |   await withTracedPage(browser, runtimeEnv, testInfo, pageDefinition.name, ADMIN_ROLE, "admin-store-settlement-compare", async (page) => {
  507 |     const requestUrls = collectRequestUrls(page, /settlement|정산/i);
  508 |     await ensureAdminPageReady(page, runtimeEnv, pageDefinition);
  509 |     await applyIssue3529Filters(page, undefined, { closeBeforeSearch: true });
  510 |     await waitForNavigationToSettle(page);
  511 |
  512 |     const bodyText = await normalizedBodyText(page);
  513 |     snapshot = {
  514 |       bodyText,
  515 |       amounts: extractAmounts(bodyText),
  516 |       requestUrls: requestUrls()
  517 |     };
  518 |     await attachPageScreenshot(page, testInfo, pageDefinition.name, ADMIN_ROLE, "admin-store-settlement-compare");
  519 |   });
  520 |
  521 |   if (!snapshot) {
  522 |     blocked(`${pageDefinition.name} 화면 스냅샷을 수집하지 못했습니다.`);
  523 |   }
  524 |   return snapshot;
  525 | }
  526 |
```