# 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: 3485/dev/company-profit-aggregate.spec.ts >> [3485][dev] [QA] 업체별 수익집계 마감/수수료 계산 검증 >> 플랫폼 수수료율 입력 항목에 음수값이 입력되지 않는지 확인
- Location: src/qa/scenarios/companyProfitAggregate.ts:513:5

# Error details

```
Error: 재마감 원장 확인 상태에서 플랫폼 수수료율 입력 항목을 찾을 수 있어야 합니다.

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

Expected: true
Received: false
```

# Test source

```ts
  420 |           "2026년 5월 빅스페이먼츠 기준 이체수수료를 이체내역 API 합계와 대조했습니다.",
  421 |           `업체별 수익집계 실제 이체수수료: ${formatNumber(profitTransferFee)}원`,
  422 |           `이체내역 이체 수수료 합계: ${formatNumber(transferTotal.commissionAmount)}원`,
  423 |           "결론: 이체수수료가 같은 기간/업체 이체내역 수수료 합계와 일치합니다."
  424 |         ]);
  425 |       });
  426 |     });
  427 |
  428 |     test(checklist.checklist[15], async ({ browser }, testInfo) => {
  429 |       test.setTimeout(80_000);
  430 |       await withTracedPage(browser, runtimeEnv, testInfo, profitPage.name, ROLE, "may-completed-transfer-count", async (page) => {
  431 |         await openTargetPage(page, runtimeEnv, profitPage);
  432 |         const snapshot = await loadProfitSnapshot(page, runtimeEnv, CLOSED_MONTH);
  433 |         const bigs = requireTenantItem(reportForDisplay(snapshot), TARGET_TENANT_NAME);
  434 |         const completedCount = computeItemTotals(bigs).completedTransferCount;
  435 |         const transferTotal = await loadMayTransferTotal(page, runtimeEnv);
  436 |
  437 |         expect(transferTotal.completedCount, "이체내역 완료 건수가 업체별 수익집계 완료 이체건수와 일치해야 합니다.").toBe(completedCount);
  438 |
  439 |         await attachEvidenceLog(testInfo, profitPage.name, ROLE, "may-completed-transfer-count", [
  440 |           "2026년 5월 빅스페이먼츠 기준 완료 이체건수를 이체내역 API와 대조했습니다.",
  441 |           `업체별 수익집계 완료 이체건수: ${completedCount}건`,
  442 |           `이체내역 완료 상태 건수: ${transferTotal.completedCount}건`,
  443 |           "결론: 완료 이체건수가 같은 기간/업체 이체내역 완료 상태 건수와 일치합니다."
  444 |         ]);
  445 |       });
  446 |     });
  447 |
  448 |     test(checklist.checklist[16], async ({ browser }, testInfo) => {
  449 |       test.setTimeout(70_000);
  450 |       await withTracedPage(browser, runtimeEnv, testInfo, profitPage.name, ROLE, "transfer-base-calculation", async (page) => {
  451 |         const snapshot = await loadProfitSnapshotFromOpenedPage(page, runtimeEnv, profitPage, CLOSED_MONTH);
  452 |         const bigs = requireTenantItem(reportForDisplay(snapshot), TARGET_TENANT_NAME);
  453 |         const totals = computeItemTotals(bigs);
  454 |         const expectedBase = totals.completedTransferCount * DEFAULT_TRANSFER_FEE_UNIT_PRICE;
  455 |
  456 |         expect(totals.transferFeeBaseAmount, "이체수수료 기준금액은 완료 이체건수 x 업체별 기준 단가여야 합니다.").toBe(expectedBase);
  457 |
  458 |         await attachEvidenceLog(testInfo, profitPage.name, ROLE, "transfer-base-calculation", [
  459 |           "2026년 5월 빅스페이먼츠 기준 이체수수료 기준금액 계산식을 확인했습니다.",
  460 |           `완료 이체건수: ${totals.completedTransferCount}건`,
  461 |           `기준 단가: ${formatNumber(DEFAULT_TRANSFER_FEE_UNIT_PRICE)}원`,
  462 |           `계산값: ${formatNumber(expectedBase)}원`,
  463 |           `화면/API 기준금액: ${formatNumber(totals.transferFeeBaseAmount)}원`,
  464 |           "결론: 이체수수료 기준금액은 완료 이체건수 * 업체별 이체수수료 기준 단가로 계산됩니다."
  465 |         ]);
  466 |       });
  467 |     });
  468 |
  469 |     test(checklist.checklist[17], async ({ browser }, testInfo) => {
  470 |       test.setTimeout(70_000);
  471 |       await withTracedPage(browser, runtimeEnv, testInfo, profitPage.name, ROLE, "transfer-additional-charge", async (page) => {
  472 |         const snapshot = await loadProfitSnapshotFromOpenedPage(page, runtimeEnv, profitPage, CLOSED_MONTH);
  473 |         const bigs = requireTenantItem(reportForDisplay(snapshot), TARGET_TENANT_NAME);
  474 |         const totals = computeItemTotals(bigs);
  475 |         const expectedAdditionalCharge = totals.transferFeeBaseAmount - totals.transferFeeAmount;
  476 |
  477 |         expect(totals.transferFeeAdditionalChargeAmount, "이체수수료 추가청구액은 기준금액 - 실제 이체수수료여야 합니다.").toBe(expectedAdditionalCharge);
  478 |
  479 |         await attachEvidenceLog(testInfo, profitPage.name, ROLE, "transfer-additional-charge", [
  480 |           "2026년 5월 빅스페이먼츠 기준 이체수수료 추가청구액 계산식을 확인했습니다.",
  481 |           `기준금액: ${formatNumber(totals.transferFeeBaseAmount)}원`,
  482 |           `실제 이체수수료: ${formatNumber(totals.transferFeeAmount)}원`,
  483 |           `계산값: ${formatNumber(expectedAdditionalCharge)}원`,
  484 |           `화면/API 추가청구액: ${formatNumber(totals.transferFeeAdditionalChargeAmount)}원`,
  485 |           "결론: 이체수수료 추가청구액은 기준금액 - 실제 이체수수료로 계산됩니다."
  486 |         ]);
  487 |       });
  488 |     });
  489 |
  490 |     test(checklist.checklist[18], async ({ browser }, testInfo) => {
  491 |       test.setTimeout(70_000);
  492 |       await withTracedPage(browser, runtimeEnv, testInfo, profitPage.name, ROLE, "platform-fee-calculation", async (page) => {
  493 |         const snapshot = await loadProfitSnapshotFromOpenedPage(page, runtimeEnv, profitPage, CLOSED_MONTH);
  494 |         const report = reportForDisplay(snapshot);
  495 |         const invalidRows = collectRows(report).filter((row) => row.salesAmount !== 0 && row.platformFeeAmount === undefined);
  496 |         const negativeRows = collectRows(report).filter((row) => Number(row.platformFeeAmount ?? 0) < 0);
  497 |         const totals = computeReportTotals(report);
  498 |
  499 |         expect(invalidRows, "매출금이 있는 행에는 플랫폼 수수료 계산 결과가 있어야 합니다.").toHaveLength(0);
  500 |         expect(negativeRows, "플랫폼 수수료 계산 결과는 음수로 노출되면 안 됩니다.").toHaveLength(0);
  501 |         expect(totals.platformFeeAmount, "2026년 5월 플랫폼 수수료 합계가 있어야 합니다.").toBeGreaterThan(0);
  502 |
  503 |         await attachEvidenceLog(testInfo, profitPage.name, ROLE, "platform-fee-calculation", [
  504 |           "2026년 5월 마감 데이터에서 매출금과 플랫폼 수수료 계산 결과를 확인했습니다.",
  505 |           `플랫폼 수수료 합계: ${formatNumber(totals.platformFeeAmount)}원`,
  506 |           `플랫폼 수수료 미계산 행 수: ${invalidRows.length}`,
  507 |           `음수 표시 행 수: ${negativeRows.length}`,
  508 |           "결론: 플랫폼 수수료는 매출금과 정책 기준 계산 결과로 저장/표시되며 음수로 노출되지 않습니다."
  509 |         ]);
  510 |       });
  511 |     });
  512 |
  513 |     test(checklist.checklist[19], async ({ browser }, testInfo) => {
  514 |       test.setTimeout(90_000);
  515 |       await withTracedPage(browser, runtimeEnv, testInfo, profitPage.name, ROLE, "negative-rate-blocked", async (page) => {
  516 |         await openTargetPage(page, runtimeEnv, profitPage);
  517 |         await clickReclose(page);
  518 |         const result = await tryInputNegativePlatformRate(page);
  519 |
> 520 |         expect(result.found, "재마감 원장 확인 상태에서 플랫폼 수수료율 입력 항목을 찾을 수 있어야 합니다.").toBe(true);
      |                                                                              ^ Error: 재마감 원장 확인 상태에서 플랫폼 수수료율 입력 항목을 찾을 수 있어야 합니다.
  521 |         expect(result.acceptedNegative, "플랫폼 수수료율 입력 항목은 음수값을 그대로 허용하면 안 됩니다.").toBe(false);
  522 |
  523 |         await attachEvidenceLog(testInfo, profitPage.name, ROLE, "negative-rate-blocked", [
  524 |           "재마감 원장 확인 상태에서 플랫폼 수수료율 입력 항목에 음수 입력을 시도했습니다.",
  525 |           `입력 대상 발견: ${result.found ? "yes" : "no"}`,
  526 |           `음수값 유지 여부: ${result.acceptedNegative ? "yes" : "no"}`,
  527 |           `입력 후 값: ${result.valueAfter ?? "-"}`,
  528 |           "결론: 플랫폼 수수료율 입력 항목에 음수값이 유지되지 않습니다."
  529 |         ]);
  530 |         await attachPageScreenshot(page, testInfo, profitPage.name, ROLE, "negative-rate-blocked");
  531 |       });
  532 |     });
  533 |
  534 |     test(checklist.checklist[20], async ({ browser }, testInfo) => {
  535 |       test.setTimeout(70_000);
  536 |       await withTracedPage(browser, runtimeEnv, testInfo, profitPage.name, ROLE, "negative-platform-hidden", async (page) => {
  537 |         const snapshot = await loadProfitSnapshotFromOpenedPage(page, runtimeEnv, profitPage, CLOSED_MONTH);
  538 |         const negativeRows = collectRows(reportForDisplay(snapshot)).filter((row) => Number(row.platformFeeAmount ?? 0) < 0);
  539 |
  540 |         expect(negativeRows, "플랫폼 수수료 계산 결과가 음수인 행이 화면/API에 노출되면 안 됩니다.").toHaveLength(0);
  541 |
  542 |         await attachEvidenceLog(testInfo, profitPage.name, ROLE, "negative-platform-hidden", [
  543 |           "2026년 5월 마감 데이터에서 음수 플랫폼 수수료 표시 여부를 확인했습니다.",
  544 |           `음수 플랫폼 수수료 행 수: ${negativeRows.length}`,
  545 |           "결론: 플랫폼 수수료 계산 결과가 음수인 경우 화면/API에는 0 이상으로 표시됩니다."
  546 |         ]);
  547 |       });
  548 |     });
  549 |
  550 |     test(checklist.checklist[21], async ({ browser }, testInfo) => {
  551 |       test.setTimeout(120_000);
  552 |       await withTracedPage(browser, runtimeEnv, testInfo, profitPage.name, ROLE, "closing-save", async (page) => {
  553 |         await openTargetPage(page, runtimeEnv, profitPage);
  554 |         const before = await loadProfitSnapshot(page, runtimeEnv, CURRENT_CLOSED_MONTH);
  555 |         const beforeTotals = computeReportTotals(closingReport(before));
  556 |         await clickReclose(page);
  557 |         await clickClosingSave(page);
  558 |         const after = await loadProfitSnapshot(page, runtimeEnv, CURRENT_CLOSED_MONTH);
  559 |         const afterTotals = computeReportTotals(closingReport(after));
  560 |
  561 |         expect(after.status.payload?.closed, "마감 저장 후에도 2026년 6월은 마감완료 상태여야 합니다.").toBe(true);
  562 |         expect(after.closing.payload?.transferFeePayableUnitAmount, "마감 저장 후 입력한 이체수수료 단가가 저장되어야 합니다.").toBe(220);
  563 |         expect(afterTotals.platformFeeAmount, "마감 저장 후 플랫폼 수수료 합계가 저장되어야 합니다.").toBeGreaterThan(0);
  564 |
  565 |         await attachEvidenceLog(testInfo, profitPage.name, ROLE, "closing-save", [
  566 |           "2026년 6월 마감완료 상태에서 재마감 후 마감 저장을 실행했습니다.",
  567 |           `저장 전 합계: ${formatTotals(beforeTotals)}`,
  568 |           `저장 후 합계: ${formatTotals(afterTotals)}`,
  569 |           `저장 후 이체수수료 낼금액 단가: ${after.closing.payload?.transferFeePayableUnitAmount ?? "-"}원`,
  570 |           `저장 후 예금주조회 단가: ${after.closing.payload?.accountInquiryPayableUnitAmount ?? "-"}원`,
  571 |           "결론: 마감 저장 시 입력한 단가와 플랫폼 수수료 기준으로 마감 데이터가 저장됩니다."
  572 |         ]);
  573 |         await attachPageScreenshot(page, testInfo, profitPage.name, ROLE, "closing-save");
  574 |       });
  575 |     });
  576 |
  577 |     test(checklist.checklist[22], async ({ browser }, testInfo) => {
  578 |       test.setTimeout(70_000);
  579 |       await withTracedPage(browser, runtimeEnv, testInfo, profitPage.name, ROLE, "closed-revisit", async (page) => {
  580 |         const first = await loadProfitSnapshotFromOpenedPage(page, runtimeEnv, profitPage, CURRENT_CLOSED_MONTH);
  581 |         const firstTotals = computeReportTotals(closingReport(first));
  582 |         await openTargetPage(page, runtimeEnv, profitPage);
  583 |         const second = await loadProfitSnapshot(page, runtimeEnv, CURRENT_CLOSED_MONTH);
  584 |         const secondTotals = computeReportTotals(closingReport(second));
  585 |
  586 |         expect(second.status.payload?.closed, "재진입 후에도 마감완료 상태여야 합니다.").toBe(true);
  587 |         expect(secondTotals, "재진입 후 저장된 마감 데이터가 동일해야 합니다.").toEqual(firstTotals);
  588 |
  589 |         await attachEvidenceLog(testInfo, profitPage.name, ROLE, "closed-revisit", [
  590 |           "마감완료 상태에서 화면/API를 다시 조회해 저장된 마감 데이터가 유지되는지 확인했습니다.",
  591 |           `첫 조회 합계: ${formatTotals(firstTotals)}`,
  592 |           `재조회 합계: ${formatTotals(secondTotals)}`,
  593 |           "결론: 마감완료 상태에서 다시 화면 진입 시 저장된 마감 데이터가 동일하게 표시됩니다."
  594 |         ]);
  595 |       });
  596 |     });
  597 |
  598 |     test(checklist.checklist[23], async ({ browser }, testInfo) => {
  599 |       test.setTimeout(90_000);
  600 |       await withTracedPage(browser, runtimeEnv, testInfo, profitPage.name, ROLE, "reclose-latest-ledger", async (page) => {
  601 |         await openTargetPage(page, runtimeEnv, profitPage);
  602 |         const ledger = await loadProfitSnapshot(page, runtimeEnv, CURRENT_CLOSED_MONTH);
  603 |         await clickReclose(page);
  604 |         const bodyText = await visibleText(page);
  605 |
  606 |         expect(ledger.ledger.payload?.items?.length ?? 0, "최신 원장 데이터가 있어야 합니다.").toBeGreaterThan(0);
  607 |         expect(bodyText, "재마감 후 원장 확인 상태 문구가 보여야 합니다.").toMatch(/원장 기준|마감 버튼/);
  608 |
  609 |         await attachProfitEvidence(testInfo, profitPage.name, ROLE, "reclose-latest-ledger", ledger, [
  610 |           "재마감 클릭 후 최신 원장 데이터 기준으로 다시 조회되는지 확인했습니다.",
  611 |           `재마감 후 화면 일부: ${bodyText.slice(0, 1600)}`,
  612 |           "결론: 재마감 클릭 시 최신 원장 데이터 기준으로 다시 조회됩니다."
  613 |         ]);
  614 |         await attachPageScreenshot(page, testInfo, profitPage.name, ROLE, "reclose-latest-ledger");
  615 |       });
  616 |     });
  617 |
  618 |     test(checklist.checklist[24], async ({ browser }, testInfo) => {
  619 |       test.setTimeout(90_000);
  620 |       await withTracedPage(browser, runtimeEnv, testInfo, profitPage.name, ROLE, "tenant-name-search", async (page) => {
```