# 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: 3983/dev/url-search.spec.ts >> [3983][dev] 가맹점 동일카드 결제 제한시간 선택·직접입력·엑셀·결제 집계 QA >> 한결데테스트 가맹점 수정 화면의 동일카드 제한시간에서 제한 없음, 30분, 1시간, 3시간, 6시간, 12시간, 24시간, 48시간, 72시간, 1주일, 1개월을 선택해 저장할 수 있는지 확인
- Location: src/qa/scenarios/merchantSameCardTimeLimit.ts:169:5

# Error details

```
Error: 요청된 고정 옵션명이 모두 그대로 노출되어야 합니다. 실제 화면의 1주 옵션은 1주일과 다릅니다.

expect(received).toEqual(expected) // deep equality

- Expected  - 1
+ Received  + 3

- Array []
+ Array [
+   "1주일",
+ ]
```

# Test source

```ts
  117 |   const metadata = loadIssueMetadata(options.issueId);
  118 |   const checklist = loadChecklist(options.issueId, options.environment);
  119 |   const merchantPage = findPageDefinition(checklist.pages, "가맹점관리");
  120 |   const paymentPage = findPageDefinition(checklist.pages, "수기결제");
  121 |   const adminEnv = getRuntimeQaEnv(options.environment, "admin", ADMIN_ROLE);
  122 |   const storeEnv = getRuntimeQaEnv(options.environment, "store", STORE_RUNTIME_ROLE);
  123 |   const paymentConfig = getPaymentConfig();
  124 |
  125 |   test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
  126 |     test.beforeAll(async ({ browser }) => {
  127 |       ensureRuntime(adminEnv, "ADMIN");
  128 |       ensureRuntime(storeEnv, "한결데테스트 VE");
  129 |       const detail = await withApiPage(browser, adminEnv, (page) => loadVendorDetail(page, adminEnv));
  130 |       expect(detail.code, "QA 대상 가맹점 고유코드가 BP100060이어야 합니다.").toBe(TARGET_VENDOR_CODE);
  131 |       expect(detail.businessInfo.name, "QA 대상 가맹점명이 요청 데이터와 일치해야 합니다.").toBe(TARGET_VENDOR_NAME);
  132 |       baselinePolicy = structuredClone(detail.paymentPolicy);
  133 |       baselineTerminalCount = detail.terminalCount;
  134 |     });
  135 |
  136 |     test.afterEach(async ({ browser }, testInfo) => {
  137 |       const cleanupLines: string[] = [];
  138 |       if (baselinePolicy) {
  139 |         const restored = await patchVendorPolicy(
  140 |           browser,
  141 |           adminEnv,
  142 |           baselinePolicy,
  143 |           baselineTerminalCount,
  144 |           `QA #${options.issueId} 테스트 후 동일카드 정책 원복`
  145 |         ).catch((error) => ({ ok: false, status: 0, text: errorMessage(error) }));
  146 |         cleanupLines.push(`정책 원복: status=${restored.status}, ok=${restored.ok}, body=${snippet(restored.text, 500)}`);
  147 |       }
  148 |
  149 |       cleanupLines.push(...(await cancelOutstandingPayments(browser, adminEnv, options.issueId)));
  150 |       await attachText(testInfo, "cleanup.md", cleanupLines);
  151 |     });
  152 |
  153 |     test.afterAll(async ({ browser }) => {
  154 |       if (baselinePolicy) {
  155 |         await patchVendorPolicy(
  156 |           browser,
  157 |           adminEnv,
  158 |           baselinePolicy,
  159 |           baselineTerminalCount,
  160 |           `QA #${options.issueId} 최종 동일카드 정책 원복`
  161 |         );
  162 |       }
  163 |       const remaining = await cancelOutstandingPayments(browser, adminEnv, options.issueId);
  164 |       if (remaining.some((line) => /ok=false|취소 실패/.test(line))) {
  165 |         throw new Error(`QA 생성 결제 최종 취소 실패: ${remaining.join(" / ")}`);
  166 |       }
  167 |     });
  168 |
  169 |     test(checklist.checklist[0], async ({ browser }, testInfo) => {
  170 |       test.setTimeout(420_000);
  171 |       await withTracedPage(browser, adminEnv, testInfo, "fixed-options", async (page) => {
  172 |         const cases: Array<{ requested: string; fallback?: string; value: SameCardTimeLimit }> = [
  173 |           { requested: "제한 없음", value: "UNLIMITED" },
  174 |           { requested: "30분", value: "MIN30" },
  175 |           { requested: "1시간", value: "H1" },
  176 |           { requested: "3시간", value: "H3" },
  177 |           { requested: "6시간", value: "H6" },
  178 |           { requested: "12시간", value: "H12" },
  179 |           { requested: "24시간", value: "H24" },
  180 |           { requested: "48시간", value: "H48" },
  181 |           { requested: "72시간", value: "H72" },
  182 |           { requested: "1주일", fallback: "1주", value: "W1" },
  183 |           { requested: "1개월", value: "M1" }
  184 |         ];
  185 |         const evidence: string[] = [];
  186 |         const missingLabels: string[] = [];
  187 |
  188 |         for (const entry of cases) {
  189 |           const modal = await openVendorEditModal(page, adminEnv, merchantPage);
  190 |           const available = await openSameCardTimeOptions(page, modal);
  191 |           const selectedLabel = available.includes(entry.requested)
  192 |             ? entry.requested
  193 |             : entry.fallback && available.includes(entry.fallback)
  194 |               ? entry.fallback
  195 |               : undefined;
  196 |           if (!available.includes(entry.requested)) {
  197 |             missingLabels.push(entry.requested);
  198 |           }
  199 |           if (!selectedLabel) {
  200 |             evidence.push(`${entry.requested}: 선택 옵션 없음 / 실제 옵션=${available.join(", ")}`);
  201 |             await closeModal(page, modal);
  202 |             continue;
  203 |           }
  204 |
  205 |           await chooseOpenOption(page, selectedLabel);
  206 |           await saveVendorModal(page, modal, `QA #3983 고정 제한시간 ${entry.requested} 저장`);
  207 |           const saved = await loadVendorDetail(page, adminEnv);
  208 |           evidence.push(`${entry.requested}: 화면 선택='${selectedLabel}', API=${saved.paymentPolicy.sameCardTimeLimit}`);
  209 |           expect(saved.paymentPolicy.sameCardTimeLimit, `${entry.requested} 저장값`).toBe(entry.value);
  210 |           expect(saved.paymentPolicy.sameCardTimeLimitMinutes, `${entry.requested} 고정값은 직접입력 분이 없어야 합니다.`).toBeNull();
  211 |         }
  212 |
  213 |         const finalModal = await openVendorEditModal(page, adminEnv, merchantPage);
  214 |         await attachScreenshot(page, testInfo, "fixed-options-final.png");
  215 |         await attachText(testInfo, "fixed-options.md", evidence);
  216 |         await closeModal(page, finalModal);
> 217 |         expect(missingLabels, `요청된 고정 옵션명이 모두 그대로 노출되어야 합니다. 실제 화면의 1주 옵션은 1주일과 다릅니다.`).toEqual([]);
      |                                                                                         ^ Error: 요청된 고정 옵션명이 모두 그대로 노출되어야 합니다. 실제 화면의 1주 옵션은 1주일과 다릅니다.
  218 |       });
  219 |     });
  220 |
  221 |     test(checklist.checklist[1], async ({ browser }, testInfo) => {
  222 |       test.setTimeout(120_000);
  223 |       await withTracedPage(browser, adminEnv, testInfo, "custom-valid-range", async (page) => {
  224 |         const evidence: string[] = [];
  225 |         for (const minutes of [525_600, 1]) {
  226 |           const modal = await openVendorEditModal(page, adminEnv, merchantPage);
  227 |           await selectCustomMinutes(page, modal, minutes);
  228 |           await saveVendorModal(page, modal, `QA #3983 직접입력 ${minutes}분 저장`);
  229 |           const saved = await loadVendorDetail(page, adminEnv);
  230 |           evidence.push(`${minutes}분 저장: type=${saved.paymentPolicy.sameCardTimeLimit}, minutes=${saved.paymentPolicy.sameCardTimeLimitMinutes}`);
  231 |           expect(saved.paymentPolicy.sameCardTimeLimit).toBe("CUSTOM");
  232 |           expect(saved.paymentPolicy.sameCardTimeLimitMinutes).toBe(minutes);
  233 |         }
  234 |         const modal = await openVendorEditModal(page, adminEnv, merchantPage);
  235 |         await expect(modal.locator("input[name='sameCardTimeLimitMinutes']"), "직접입력 분 입력란이 표시되어야 합니다.").toBeVisible();
  236 |         await attachScreenshot(page, testInfo, "custom-valid-range.png");
  237 |         await attachText(testInfo, "custom-valid-range.md", evidence);
  238 |       });
  239 |     });
  240 |
  241 |     test(checklist.checklist[2], async ({ browser }, testInfo) => {
  242 |       test.setTimeout(90_000);
  243 |       await withTracedPage(browser, adminEnv, testInfo, "custom-invalid-values", async (page) => {
  244 |         const modal = await openVendorEditModal(page, adminEnv, merchantPage);
  245 |         await selectCustomMinutes(page, modal, 1);
  246 |         const input = modal.locator("input[name='sameCardTimeLimitMinutes']");
  247 |         const cases = [
  248 |           { raw: "   ", expected: /직접입력 동일카드 제한시간은 1분 이상이어야 합니다\./ },
  249 |           { raw: "1.5", expected: /직접입력 동일카드 제한시간은 1분 이상이어야 합니다\./ },
  250 |           { raw: "0", expected: /직접입력 동일카드 제한시간은 1분 이상이어야 합니다\./ },
  251 |           { raw: "-1", expected: /직접입력 동일카드 제한시간은 1분 이상이어야 합니다\./ },
  252 |           { raw: "525601", expected: /직접입력 동일카드 제한시간은 최대 1년\(525,600분\)입니다\./ }
  253 |         ];
  254 |         const violations: string[] = [];
  255 |         const evidence: string[] = [];
  256 |
  257 |         for (const entry of cases) {
  258 |           await input.fill(entry.raw);
  259 |           await page.waitForTimeout(500);
  260 |           const actualValue = await input.inputValue();
  261 |           const surroundingText = await modal.innerText();
  262 |           const hasGuidance = entry.expected.test(surroundingText);
  263 |           evidence.push(`입력 '${entry.raw.replace(/ /g, "<space>")}' -> 실제 '${actualValue}', 안내='${snippet(surroundingText, 240)}'`);
  264 |           if (!hasGuidance) {
  265 |             violations.push(`'${entry.raw.replace(/ /g, "<space>")}' 입력에 안내가 없음(실제 입력값 '${actualValue}')`);
  266 |           }
  267 |         }
  268 |
  269 |         const unchanged = await loadVendorDetail(page, adminEnv);
  270 |         expect(unchanged.paymentPolicy.sameCardTimeLimit, "유효하지 않은 값 검증 중 저장 요청이 없어야 합니다.").toBe(baselinePolicy?.sameCardTimeLimit);
  271 |         expect(unchanged.paymentPolicy.sameCardTimeLimitMinutes).toBe(baselinePolicy?.sameCardTimeLimitMinutes);
  272 |         await attachScreenshot(page, testInfo, "custom-invalid-values.png");
  273 |         await attachText(testInfo, "custom-invalid-values.md", evidence);
  274 |         expect(violations, "공백·소수·0·음수·상한 초과 입력은 저장 가능한 값으로 조용히 변환되지 않고 안내가 보여야 합니다.").toEqual([]);
  275 |       });
  276 |     });
  277 |
  278 |     test(checklist.checklist[3], async ({ browser }, testInfo) => {
  279 |       test.setTimeout(90_000);
  280 |       await withTracedPage(browser, adminEnv, testInfo, "custom-persistence", async (page) => {
  281 |         let modal = await openVendorEditModal(page, adminEnv, merchantPage);
  282 |         await selectCustomMinutes(page, modal, 90);
  283 |         await saveVendorModal(page, modal, "QA #3983 직접입력 90분 재진입 유지 검증");
  284 |
  285 |         modal = await openVendorEditModal(page, adminEnv, merchantPage);
  286 |         const typeValue = await modal.locator("input[name='sameCardTimeLimit']").inputValue();
  287 |         const minutesValue = numericInputValue(await modal.locator("input[name='sameCardTimeLimitMinutes']").inputValue());
  288 |         const detail = await loadVendorDetail(page, adminEnv);
  289 |         await attachScreenshot(page, testInfo, "custom-persistence.png");
  290 |         await attachText(testInfo, "custom-persistence.md", [
  291 |           `재진입 화면 type=${typeValue}, minutes=${minutesValue}`,
  292 |           `상세 API type=${detail.paymentPolicy.sameCardTimeLimit}, minutes=${detail.paymentPolicy.sameCardTimeLimitMinutes}`
  293 |         ]);
  294 |         expect(typeValue).toBe("CUSTOM");
  295 |         expect(minutesValue).toBe(90);
  296 |         expect(detail.paymentPolicy.sameCardTimeLimit).toBe("CUSTOM");
  297 |         expect(detail.paymentPolicy.sameCardTimeLimitMinutes).toBe(90);
  298 |       });
  299 |     });
  300 |
  301 |     test(checklist.checklist[4], async ({ browser }, testInfo) => {
  302 |       test.setTimeout(180_000);
  303 |       assertPaymentMutationReady(paymentConfig);
  304 |       await setPolicy(browser, adminEnv, { sameCardTimeLimit: "CUSTOM", sameCardTimeLimitMinutes: 1, sameCardCountLimit: 1, sameCardLimit: 5_000_000 }, "1분 1회");
  305 |       await withTracedPage(browser, storeEnv, testInfo, "count-limit-one", async (page) => {
  306 |         const attempts = await runPaymentAttempts(page, storeEnv, paymentPage, paymentConfig, options.issueId, 2);
  307 |         await attachPaymentEvidence(testInfo, "count-limit-one", attempts);
  308 |         expect(attempts.map((attempt) => attempt.isSuccess), "1회까지 성공하고 두 번째는 차단되어야 합니다.").toEqual([true, false]);
  309 |         expect(attempts[1].message, "두 번째 결제에 동일카드 횟수 초과 사유가 보여야 합니다.").toMatch(/동일\s*카드.*횟수.*초과|횟수.*초과/);
  310 |       });
  311 |     });
  312 |
  313 |     test(checklist.checklist[5], async ({ browser }, testInfo) => {
  314 |       test.setTimeout(240_000);
  315 |       assertPaymentMutationReady(paymentConfig);
  316 |       await setPolicy(browser, adminEnv, { sameCardTimeLimit: "CUSTOM", sameCardTimeLimitMinutes: 1, sameCardCountLimit: 3, sameCardLimit: 5_000_000 }, "1분 3회");
  317 |       await withTracedPage(browser, storeEnv, testInfo, "count-limit-three", async (page) => {
```