# 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: 3399/dev/payletter-mid-receipt-key.spec.ts >> [3399][dev] [QA] 페이레터 단말기 MID 영수증 조회 키 검증 >> [TA] role >> PG MID 관리 >> ta 권한에서 페이레터 단말기 MID 등록 시 영수증 조회 키를 입력하고 저장되는지 확인
- Location: src/qa/scenarios/payletterMidReceiptKey.ts:137:11

# Error details

```
Error: PG MID 등록 버튼이 비활성화되어 저장 요청이 발생하지 않았습니다.
```

# Test source

```ts
  384 |   expect(await isLoginPage(page, runtimeEnv), `${pageName} 업무 화면 진입 전 로그인 화면을 벗어나야 합니다.`).toBe(false);
  385 | }
  386 | 
  387 | async function waitForNavigationToSettle(page: Page): Promise<void> {
  388 |   await page.waitForLoadState("domcontentloaded");
  389 |   await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
  390 |   await page.waitForTimeout(500);
  391 | }
  392 | 
  393 | async function openCreatePage(page: Page, runtimeEnv: RuntimeQaEnv, midType: PgMidType): Promise<void> {
  394 |   const url = buildPgMidUrl(runtimeEnv, "/business/pg-mid-manage/create");
  395 |   url.searchParams.set("midType", midType);
  396 |   await openAuthenticatedUrl(page, url.toString(), runtimeEnv, `PG MID 생성 ${midType}`);
  397 |   await expect(page.locator("body"), "PG MID 생성 화면이 표시되어야 합니다.").toContainText(/PG MID\s*생성/);
  398 | }
  399 | 
  400 | async function openUpdatePage(page: Page, runtimeEnv: RuntimeQaEnv, id: string): Promise<void> {
  401 |   const url = buildPgMidUrl(runtimeEnv, `/business/pg-mid-manage/update/${id}`);
  402 |   await openAuthenticatedUrl(page, url.toString(), runtimeEnv, "PG MID 수정");
  403 |   await expect(page.locator("body"), "PG MID 수정 화면이 표시되어야 합니다.").toContainText(/PG MID\s*수정/);
  404 | }
  405 | 
  406 | async function createPayletterTerminalMidViaUi(
  407 |   page: Page,
  408 |   runtimeEnv: RuntimeQaEnv,
  409 |   role: string,
  410 |   testInfo: TestInfo,
  411 |   pageDefinition: QaPageDefinition
  412 | ): Promise<CreatedMid> {
  413 |   await openCreatePage(page, runtimeEnv, "TERMINAL");
  414 |   await selectContract(page, PAYLETTER_CONTRACT_LABEL);
  415 |   await expect(page.locator("#searchKey"), "페이레터 단말기 등록 화면에 영수증 조회 키 입력이 보여야 합니다.").toBeVisible();
  416 | 
  417 |   const mid = makeQaMid(role === "TA" ? "TA" : "AD");
  418 |   const searchKey = `SEARCH-${mid}`;
  419 |   await fillTextInput(page, "#mid", mid);
  420 |   await fillTextInput(page, "#rootMid", mid);
  421 |   await fillTextInput(page, "#memo", `QA #3399 ${role} ${mid}`);
  422 |   await fillTextInput(page, "#searchKey", searchKey);
  423 |   await attachPageScreenshot(page, testInfo, pageDefinition, role, "payletter-terminal-create-filled");
  424 | 
  425 |   const { responseJson, requestBody } = await submitCreateForm(page);
  426 |   expect(requestBody.contractId, "등록 요청 계약은 페이레터 계약이어야 합니다.").toBe(PAYLETTER_CONTRACT_ID);
  427 |   expect(requestBody.midType, "등록 요청 MID 타입은 단말기여야 합니다.").toBe("TERMINAL");
  428 |   expect(getRequestMetadataValue(requestBody, "searchKey"), "등록 요청 payload에 영수증 조회 키가 포함되어야 합니다.").toBe(searchKey);
  429 | 
  430 |   if (!responseJson.payload) {
  431 |     throw new Error("페이레터 단말기 MID 생성 응답에 ID가 없습니다.");
  432 |   }
  433 | 
  434 |   return {
  435 |     id: responseJson.payload,
  436 |     mid,
  437 |     searchKey
  438 |   };
  439 | }
  440 | 
  441 | async function createCommonMetadataMidViaUi(
  442 |   page: Page,
  443 |   runtimeEnv: RuntimeQaEnv,
  444 |   midType: "KEYIN" | "ISP",
  445 |   testInfo: TestInfo,
  446 |   pageDefinition: QaPageDefinition
  447 | ): Promise<CreatedMid> {
  448 |   await openCreatePage(page, runtimeEnv, midType);
  449 |   await selectContract(page, COMMON_METADATA_CONTRACT_LABEL);
  450 |   await expect(page.locator("#apiKey"), `${midType} 등록 화면에 결제 공통 metadata API KEY 입력이 보여야 합니다.`).toBeVisible();
  451 | 
  452 |   const mid = makeQaMid(midType === "KEYIN" ? "K" : "I");
  453 |   const apiKey = `API-${mid}`;
  454 |   await fillTextInput(page, "#mid", mid);
  455 |   await fillTextInput(page, "#rootMid", mid);
  456 |   await fillTextInput(page, "#memo", `QA #3399 ${midType} ${mid}`);
  457 |   await fillTextInput(page, "#apiKey", apiKey);
  458 |   await attachPageScreenshot(page, testInfo, pageDefinition, "ADMIN", `common-metadata-${midType.toLowerCase()}-filled`);
  459 | 
  460 |   const { responseJson, requestBody } = await submitCreateForm(page);
  461 |   expect(requestBody.contractId, `${midType} 등록 요청 계약은 기존 루시페이먼츠 계약이어야 합니다.`).toBe(COMMON_METADATA_CONTRACT_ID);
  462 |   expect(requestBody.midType, `${midType} 등록 요청 MID 타입이 유지되어야 합니다.`).toBe(midType);
  463 |   expect(getRequestMetadataValue(requestBody, "apiKey"), `${midType} 등록 요청 payload에 API KEY metadata가 포함되어야 합니다.`,).toBe(apiKey);
  464 | 
  465 |   if (!responseJson.payload) {
  466 |     throw new Error(`${midType} MID 생성 응답에 ID가 없습니다.`);
  467 |   }
  468 | 
  469 |   await attachPageScreenshot(page, testInfo, pageDefinition, "ADMIN", `common-metadata-${midType.toLowerCase()}-saved`);
  470 |   return {
  471 |     id: responseJson.payload,
  472 |     mid,
  473 |     apiKey
  474 |   };
  475 | }
  476 | 
  477 | async function submitCreateForm(page: Page): Promise<{
  478 |   responseJson: PayloadResponse<string>;
  479 |   requestBody: Record<string, unknown>;
  480 | }> {
  481 |   const submitButton = page.getByRole("button", { name: "등록" }).last();
  482 |   await expect(submitButton, "PG MID 등록 버튼이 보여야 합니다.").toBeVisible({ timeout: 5_000 });
  483 |   if (!await submitButton.isEnabled()) {
> 484 |     throw new Error("PG MID 등록 버튼이 비활성화되어 저장 요청이 발생하지 않았습니다.");
      |           ^ Error: PG MID 등록 버튼이 비활성화되어 저장 요청이 발생하지 않았습니다.
  485 |   }
  486 | 
  487 |   const requestPromise = page.waitForRequest(
  488 |     (request) => request.method() === "POST" && /\/api\/v1\/pgmids$/.test(request.url()),
  489 |     { timeout: 15_000 }
  490 |   );
  491 |   const responsePromise = page.waitForResponse(
  492 |     (response) => response.request().method() === "POST" && /\/api\/v1\/pgmids$/.test(response.url()),
  493 |     { timeout: 15_000 }
  494 |   );
  495 | 
  496 |   await submitButton.click({ force: true });
  497 |   const [request, response] = await Promise.all([requestPromise, responsePromise]);
  498 |   const responseJson = await parseResponseJson<PayloadResponse<string>>(response);
  499 |   expect(response.ok(), formatHttpFailure("PG MID 등록", response.status(), responseJson)).toBe(true);
  500 | 
  501 |   return {
  502 |     responseJson,
  503 |     requestBody: parseRequestBody(request.postData())
  504 |   };
  505 | }
  506 | 
  507 | async function updateSearchKeyViaUi(
  508 |   page: Page,
  509 |   runtimeEnv: RuntimeQaEnv,
  510 |   id: string,
  511 |   updatedSearchKey: string
  512 | ): Promise<Record<string, unknown>> {
  513 |   if (!page.url().includes(`/business/pg-mid-manage/update/${id}`)) {
  514 |     await openUpdatePage(page, runtimeEnv, id);
  515 |   }
  516 | 
  517 |   await fillTextInput(page, "#searchKey", updatedSearchKey);
  518 |   await page.getByRole("button", { name: "수정" }).click({ force: true });
  519 |   const reasonInput = page.locator("#swal2-textarea");
  520 |   await expect(reasonInput, "수정 저장 전 변경사유 입력창이 표시되어야 합니다.").toBeVisible({ timeout: 5_000 });
  521 |   await reasonInput.fill("QA #3399 영수증 조회 키 수정 검증");
  522 | 
  523 |   const requestPromise = page.waitForRequest(
  524 |     (request) => request.method() === "PATCH" && request.url().includes(`/api/v1/pgmids/${id}`),
  525 |     { timeout: 15_000 }
  526 |   );
  527 |   const responsePromise = page.waitForResponse(
  528 |     (response) => response.request().method() === "PATCH" && response.url().includes(`/api/v1/pgmids/${id}`),
  529 |     { timeout: 15_000 }
  530 |   );
  531 |   await page.getByRole("button", { name: "확인" }).last().click({ force: true });
  532 |   const [request, response] = await Promise.all([requestPromise, responsePromise]);
  533 |   const responseJson = await parseResponseJson<PayloadResponse<unknown>>(response);
  534 |   expect(response.ok(), formatHttpFailure("PG MID 수정", response.status(), responseJson)).toBe(true);
  535 | 
  536 |   const requestBody = parseRequestBody(request.postData());
  537 |   expect(getRequestMetadataValue(requestBody, "searchKey"), "수정 요청 payload에 변경한 영수증 조회 키가 포함되어야 합니다.").toBe(updatedSearchKey);
  538 |   await expect(page.locator("body"), "수정 성공 안내가 표시되어야 합니다.").toContainText(/성공적으로 MID가 수정되었습니다|성공/);
  539 |   return requestBody;
  540 | }
  541 | 
  542 | async function selectContract(page: Page, label: string): Promise<void> {
  543 |   const control = page
  544 |     .locator("div.relative")
  545 |     .filter({ hasText: /계약을 선택하세요|선택된 계약|PG계약/ })
  546 |     .first();
  547 |   await expect(control, "PG 계약 선택 컨트롤이 보여야 합니다.").toBeVisible({ timeout: 10_000 });
  548 |   await control.click({ position: { x: 300, y: 20 } });
  549 |   await expect(page.getByText(label, { exact: false }).first(), `테스트 PG 계약 옵션이 보여야 합니다: ${label}`).toBeVisible({
  550 |     timeout: 10_000
  551 |   });
  552 |   await page.getByText(label, { exact: false }).first().click({ force: true });
  553 |   await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => undefined);
  554 |   await page.waitForTimeout(500);
  555 | }
  556 | 
  557 | async function fillTextInput(page: Page, selector: string, value: string): Promise<void> {
  558 |   const input = page.locator(selector).first();
  559 |   await expect(input, `${selector} 입력창이 보여야 합니다.`).toBeVisible({ timeout: 5_000 });
  560 |   await input.fill(value);
  561 | }
  562 | 
  563 | async function loadMetadataSettings(
  564 |   page: Page,
  565 |   runtimeEnv: RuntimeQaEnv,
  566 |   providerId: string,
  567 |   midType: PgMidType
  568 | ): Promise<MetadataSettingsPage> {
  569 |   const result = await callApi<PayloadResponse<MetadataSettingsPage>>(
  570 |     page,
  571 |     runtimeEnv,
  572 |     `/v1/pgproviders/${providerId}/metadata-settings?midType=${midType}`
  573 |   );
  574 |   expect(result.ok, formatApiFailure("PG provider metadata 설정 조회", result)).toBe(true);
  575 |   return result.json.payload ?? {};
  576 | }
  577 | 
  578 | async function loadMidDetail(page: Page, runtimeEnv: RuntimeQaEnv, id: string): Promise<PgMid> {
  579 |   const result = await callApi<PayloadResponse<PgMid>>(page, runtimeEnv, `/v1/pgmids/${id}`);
  580 |   expect(result.ok, formatApiFailure("PG MID 상세 조회", result)).toBe(true);
  581 |   if (!result.json.payload) {
  582 |     throw new Error(`PG MID 상세 응답에 payload가 없습니다: ${id}`);
  583 |   }
  584 |   return result.json.payload;
```