# 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: 4147/dev/privacy-masking-regression.spec.ts >> [4147][dev] [재QA] 개인정보 마스킹 및 원문 조회 역할별 회귀 검증 >> [CO] 거래내역 / 상세 마스킹 및 PIN 원문 조회
- Location: src/qa/scenarios/privacyMaskingRegression.ts:84:9

# Error details

```
Error: BLOCKED: CO 거래내역 상세 데이터에 마스킹 검증이 가능한 개인정보 값이 없습니다.
```

# Test source

```ts
  912  |
  913  | async function setInputValue(input: Locator, value: string): Promise<void> {
  914  |   await input.evaluate((element, nextValue) => {
  915  |     const target = element as HTMLInputElement;
  916  |     const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
  917  |     setter?.call(target, nextValue);
  918  |     target.dispatchEvent(new Event("input", { bubbles: true }));
  919  |     target.dispatchEvent(new Event("change", { bubbles: true }));
  920  |   }, value);
  921  | }
  922  |
  923  | async function firstVisible(locator: Locator, timeoutMs = 1_000): Promise<Locator | undefined> {
  924  |   const deadline = Date.now() + timeoutMs;
  925  |   do {
  926  |     const count = await locator.count().catch(() => 0);
  927  |     for (let index = 0; index < count; index += 1) {
  928  |       const candidate = locator.nth(index);
  929  |       const visible = await candidate.isVisible().catch(() => false);
  930  |       if (!visible) continue;
  931  |       const active = await candidate.evaluate((element) => {
  932  |         const hiddenAncestor = element.closest("[inert], [aria-hidden='true']");
  933  |         return !hiddenAncestor;
  934  |       }).catch(() => false);
  935  |       if (active) return candidate;
  936  |     }
  937  |     if (Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 100));
  938  |   } while (Date.now() < deadline);
  939  |   return undefined;
  940  | }
  941  |
  942  | async function domClick(locator: Locator): Promise<void> {
  943  |   await locator.evaluate((element) => (element as HTMLElement).click());
  944  | }
  945  |
  946  | async function isActiveElement(locator: Locator): Promise<boolean> {
  947  |   return locator.evaluate((element) => !element.closest("[inert], [aria-hidden='true']")).catch(() => false);
  948  | }
  949  |
  950  | async function settle(page: Page): Promise<void> {
  951  |   await page.waitForLoadState("domcontentloaded").catch(() => undefined);
  952  |   await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
  953  |   await page.waitForTimeout(500);
  954  | }
  955  |
  956  | async function attachEvidence(
  957  |   testInfo: TestInfo,
  958  |   pageName: string,
  959  |   role: string,
  960  |   suffix: string,
  961  |   lines: string[]
  962  | ): Promise<void> {
  963  |   await testInfo.attach(`${safe(pageName)}-${safe(role)}-${safe(suffix)}.md`, {
  964  |     body: `${lines.join("\n")}\n`,
  965  |     contentType: "text/markdown"
  966  |   });
  967  | }
  968  |
  969  | function assertRuntime(runtime: RuntimeQaEnv): void {
  970  |   if (!runtime.baseUrl) blocked(`${runtime.application}/${runtime.environment}/${runtime.role} baseUrl이 없습니다.`);
  971  |   if (!runtime.storageState && (!runtime.loginUrl || !runtime.username || !runtime.password)) {
  972  |     blocked(`${runtime.application}/${runtime.environment}/${runtime.role} 로그인 상태 또는 계정 정보가 없습니다.`);
  973  |   }
  974  | }
  975  |
  976  | function countMaskCharacters(value: string): number {
  977  |   return (value.match(/[＊*•]/g) ?? []).length;
  978  | }
  979  |
  980  | function normalizeMode(value: string | undefined): string {
  981  |   return String(value ?? "").trim().toUpperCase();
  982  | }
  983  |
  984  | function normalize(value: string): string {
  985  |   return value.replace(/\u200b/g, "").replace(/\s+/g, " ").trim();
  986  | }
  987  |
  988  | function ensureTrailingSlash(value: string): string {
  989  |   return value.endsWith("/") ? value : `${value}/`;
  990  | }
  991  |
  992  | function apiBase(runtime: RuntimeQaEnv): string {
  993  |   return new URL(runtime.baseUrl!).origin.replace("admin-", "api-").replace("store-", "api-");
  994  | }
  995  |
  996  | function apiHeaders(runtime: RuntimeQaEnv): Record<string, string> {
  997  |   return {
  998  |     accept: "application/json",
  999  |     ...(runtime.tenantId ? { "x-tenant-id": runtime.tenantId } : {})
  1000 |   };
  1001 | }
  1002 |
  1003 | function safe(value: string): string {
  1004 |   return value.replace(/[^a-zA-Z0-9가-힣._-]+/g, "-").slice(0, 120);
  1005 | }
  1006 |
  1007 | function escapeRegExp(value: string): string {
  1008 |   return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  1009 | }
  1010 |
  1011 | function blocked(message: string): never {
> 1012 |   throw new Error(`BLOCKED: ${message}`);
       |         ^ Error: BLOCKED: CO 거래내역 상세 데이터에 마스킹 검증이 가능한 개인정보 값이 없습니다.
  1013 | }
  1014 |
```