# 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: 4077/dev/pg-mid-linked-vendors.spec.ts >> [4077][dev] [QA] TID/MID 수기·ISP 연동가맹점 표기 검증 >> [BO] TID/MID 관리 (결제망 연동 PG MID) >> [BO] role / TID/MID 관리 (결제망 연동 PG MID) / 수기, ISP 유형 데이터의 연동가맹점 표기 확인
- Location: src/qa/scenarios/pgMidLinkedVendors.ts:111:9

# Error details

```
Error: BLOCKED: BO 조회 범위에서 수기(API 403)·ISP(API 403) 연동가맹점 행 표기를 확인할 수 없습니다.
```

# Test source

```ts
  377 | }
  378 |
  379 | async function attachEvidenceLog(
  380 |   testInfo: TestInfo,
  381 |   pageDefinition: QaPageDefinition,
  382 |   role: string,
  383 |   action: string,
  384 |   lines: string[]
  385 | ): Promise<void> {
  386 |   await testInfo.attach(`${safeFilename(pageDefinition.name)}-${safeFilename(role)}-${safeFilename(action)}.md`, {
  387 |     body: `${lines.join("\n")}\n`,
  388 |     contentType: "text/markdown"
  389 |   });
  390 | }
  391 |
  392 | function resolveRuntimeRole(checklist: QaChecklist, role: string): string {
  393 |   return checklist.subjectAccounts?.find((account) => account.role.toUpperCase() === role)?.envRole ?? role;
  394 | }
  395 |
  396 | function resolveAccountLabel(checklist: QaChecklist, role: string): string {
  397 |   return checklist.subjectAccounts?.find((account) => account.role.toUpperCase() === role)?.label ?? role;
  398 | }
  399 |
  400 | function findPageDefinition(pages: QaPageDefinition[], name: string): QaPageDefinition {
  401 |   const page = pages.find((candidate) => candidate.name.includes(name));
  402 |   if (!page) {
  403 |     throw new Error(`checklist.json pages에 '${name}' 페이지 정의가 필요합니다.`);
  404 |   }
  405 |   return page;
  406 | }
  407 |
  408 | function getRuntimeBlockReason(runtime: RuntimeQaEnv): string | undefined {
  409 |   const appKey = runtime.application.toUpperCase();
  410 |   const envKey = runtime.environment.toUpperCase();
  411 |   if (!runtime.baseUrl) {
  412 |     return `BIX_${appKey}_${envKey}_BASE_URL 또는 BIX_${appKey}_${envKey}_LOGIN_URL이 필요합니다.`;
  413 |   }
  414 |   if (!runtime.tenantId) {
  415 |     return `BIX_${appKey}_${envKey}_TENANT_ID가 필요합니다.`;
  416 |   }
  417 |   if (!runtime.storageState && (!runtime.username || !runtime.password || !runtime.loginUrl)) {
  418 |     return `${runtime.application}/${runtime.environment}/${runtime.role} role의 storageState 또는 로그인 계정이 필요합니다.`;
  419 |   }
  420 |   return undefined;
  421 | }
  422 |
  423 | function requestHeaders(runtime: RuntimeQaEnv): Record<string, string> {
  424 |   return {
  425 |     Accept: "application/json",
  426 |     ...(runtime.tenantId ? { "X-TENANT-ID": runtime.tenantId } : {})
  427 |   };
  428 | }
  429 |
  430 | function resolveApiBase(runtime: RuntimeQaEnv): string {
  431 |   const baseUrl = new URL(runtime.baseUrl!);
  432 |   baseUrl.hostname = baseUrl.hostname.replace(/^admin-/, "api-").replace(/^store-/, "api-");
  433 |   baseUrl.pathname = "/api";
  434 |   baseUrl.search = "";
  435 |   baseUrl.hash = "";
  436 |   return baseUrl.toString().replace(/\/$/, "");
  437 | }
  438 |
  439 | function isPgMidListUrl(rawUrl: string): boolean {
  440 |   return /\/api\/v1\/pgmids\/manage(?:\?|$)/.test(rawUrl);
  441 | }
  442 |
  443 | function midTypeValue(value: OptionValue | string | undefined): string {
  444 |   return typeof value === "string" ? value : value?.value ?? "";
  445 | }
  446 |
  447 | function formatVendors(vendors: LinkedVendor[]): string {
  448 |   return vendors.map((vendor) => `${vendor.code ?? "-"} ${vendor.name ?? "-"}`).join(" / ");
  449 | }
  450 |
  451 | function requireText(value: string | undefined, label: string): string {
  452 |   if (!value) {
  453 |     blocked(`${label} 값이 없습니다.`);
  454 |   }
  455 |   return value;
  456 | }
  457 |
  458 | async function waitForNavigationToSettle(page: Page): Promise<void> {
  459 |   await page.waitForLoadState("domcontentloaded").catch(() => undefined);
  460 |   await page.waitForLoadState("networkidle", { timeout: 8_000 }).catch(() => undefined);
  461 |   await page.waitForTimeout(700);
  462 | }
  463 |
  464 | function redactUrl(value: string): string {
  465 |   return value.replace(/tenantId=redacted&]+/g, "tenantId=<redacted>");
  466 | }
  467 |
  468 | function normalizeText(value: string): string {
  469 |   return value.replace(/\u200b/g, "").replace(/\s+/g, " ").trim();
  470 | }
  471 |
  472 | function safeFilename(value: string): string {
  473 |   return value.replace(/[^a-zA-Z0-9가-힣_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
  474 | }
  475 |
  476 | function blocked(message: string): never {
> 477 |   throw new Error(`BLOCKED: ${message}`);
      |         ^ Error: BLOCKED: BO 조회 범위에서 수기(API 403)·ISP(API 403) 연동가맹점 행 표기를 확인할 수 없습니다.
  478 | }
  479 |
```