# 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: 3936/dev/settlement-segment-column.spec.ts >> [3936][dev] [QA] 정산내역 구간 컬럼·필터·엑셀 검증 >> [ADMIN] 정산내역 >> 정산내역 목록에 구간 컬럼과 값이 표시되는지 확인
- Location: src/qa/scenarios/settlementSegmentColumn.ts:97:9

# Error details

```
Error: 정산내역 목록에 구간 컬럼이 추가되어야 합니다.

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

Expected: true
Received: false
```

# Test source

```ts
  20  | interface ScenarioOptions {
  21  |   issueId: string;
  22  |   environment: QaEnvironment;
  23  | }
  24  |
  25  | interface ParsedExcel {
  26  |   fileName: string;
  27  |   rows: string[][];
  28  | }
  29  |
  30  | interface FilterCapture {
  31  |   segmentControlCount: number;
  32  |   segmentControlVisible: boolean;
  33  |   segmentInitialValue: string;
  34  |   segmentOptions: string[];
  35  |   segmentSelectedValue: string;
  36  |   salesLineControlLabel: string;
  37  |   salesLineControlVisible: boolean;
  38  |   salesLinePickerOpened: boolean;
  39  |   salesLineOptions: string[];
  40  |   settlementRequests: string[];
  41  | }
  42  |
  43  | const REQUIRED_ROLES = ["ADMIN", "TA", "CO", "BO"];
  44  |
  45  | export function runSettlementSegmentColumnSuite(options: ScenarioOptions): void {
  46  |   const metadata = loadIssueMetadata(options.issueId);
  47  |   const checklist = loadChecklist(options.issueId, options.environment);
  48  |   const settlementPage = findPageDefinition(checklist.pages, "정산내역");
  49  |
  50  |   test.describe(`[${options.issueId}][${options.environment}] ${metadata.subject}`, () => {
  51  |     for (const role of resolveChecklistExecutionRoles(checklist)) {
  52  |       if (!REQUIRED_ROLES.includes(role) || !pageRunsForRole(checklist, settlementPage, role)) {
  53  |         continue;
  54  |       }
  55  |
  56  |       const runtime = getRuntimeQaEnv(options.environment, settlementPage.application ?? "admin", role);
  57  |       const blockReason = getRuntimeBlockReason(runtime);
  58  |
  59  |       test.describe(`[${role}] ${settlementPage.name}`, () => {
  60  |         test.skip(!!blockReason, blockReason);
  61  |
  62  |         test("영업라인 및 구간 검색 필터 선택 가능 여부 확인", async ({ browser }, testInfo) => {
  63  |           test.setTimeout(120_000);
  64  |           await withRolePage(browser, runtime, async (page) => {
  65  |             await openSettlementPage(page, runtime, settlementPage);
  66  |             await openAdvancedSearch(page);
  67  |             await attachPageScreenshot(page, testInfo, settlementPage, role, "filter-selection-before");
  68  |             const capture = await captureAndUseFilters(page, settlementPage);
  69  |
  70  |             await attachEvidenceLog(testInfo, settlementPage, role, "filter-selection", [
  71  |               `${role} 계정으로 정산내역 검색 필터를 확인했습니다.`,
  72  |               `진입 URL: ${redactUrl(page.url())}`,
  73  |               `구간 컨트롤 수/노출: ${capture.segmentControlCount}/${capture.segmentControlVisible ? "예" : "아니오"}`,
  74  |               `구간 초기값: ${capture.segmentInitialValue || "-"}`,
  75  |               `구간 옵션: ${capture.segmentOptions.join(", ") || "없음"}`,
  76  |               `구간 선택 후 값: ${capture.segmentSelectedValue || "-"}`,
  77  |               `영업라인 필터 매핑 라벨: ${capture.salesLineControlLabel || "없음"}`,
  78  |               `영업라인 필터 노출: ${capture.salesLineControlVisible ? "예" : "아니오"}`,
  79  |               `영업라인 선택기 오픈: ${capture.salesLinePickerOpened ? "예" : "아니오"}`,
  80  |               `영업라인 선택 후보: ${capture.salesLineOptions.join(" / ") || "없음"}`,
  81  |               `검색 후 정산 API 요청: ${capture.settlementRequests.join(" / ") || "없음"}`
  82  |             ]);
  83  |
  84  |             expect(capture.segmentControlCount, "구간 검색 필터 컨트롤이 정확히 1개 노출되어야 합니다.").toBe(1);
  85  |             expect(capture.segmentControlVisible, "구간 검색 필터가 보여야 합니다.").toBe(true);
  86  |             expect(capture.segmentOptions, "구간 필터에 신고/비신고 선택지가 있어야 합니다.").toEqual(
  87  |               expect.arrayContaining(["신고", "비신고"])
  88  |             );
  89  |             expect(capture.segmentSelectedValue, "구간 필터에서 신고를 선택한 값이 유지되어야 합니다.").toContain("신고");
  90  |             expect(capture.salesLineControlVisible, "영업라인 검색 필터에 해당하는 정산대상 선택 컨트롤이 보여야 합니다.").toBe(true);
  91  |             expect(capture.salesLinePickerOpened, "영업라인 검색 필터를 클릭했을 때 선택기가 열려야 합니다.").toBe(true);
  92  |             expect(capture.salesLineOptions.length, "영업라인 검색 필터에서 선택 후보가 보여야 합니다.").toBeGreaterThan(0);
  93  |             expect(capture.settlementRequests.length, "필터 검색 후 정산내역 조회 요청이 발생해야 합니다.").toBeGreaterThan(0);
  94  |           });
  95  |         });
  96  |
  97  |         test("정산내역 목록에 구간 컬럼과 값이 표시되는지 확인", async ({ browser }, testInfo) => {
  98  |           test.setTimeout(90_000);
  99  |           await withRolePage(browser, runtime, async (page) => {
  100 |             await openSettlementPage(page, runtime, settlementPage);
  101 |             const headers = await visibleTableHeaders(page);
  102 |             const fields = await tableDataFields(page);
  103 |             const bodyText = normalizeText(await page.locator("body").innerText().catch(() => ""));
  104 |             const hasSegmentColumn = headers.some((header) => header === "구간") || fields.some((field) => /segment|구간/i.test(field));
  105 |             const segmentValues = await collectSegmentValues(page);
  106 |             const dataRowCount = await visibleDataRowCount(page);
  107 |
  108 |             await attachEvidenceLog(testInfo, settlementPage, role, "segment-column", [
  109 |               `${role} 계정으로 정산내역 목록 컬럼을 확인했습니다.`,
  110 |               `현재 URL: ${redactUrl(page.url())}`,
  111 |               `표시 컬럼: ${headers.join(" | ") || "없음"}`,
  112 |               `테이블 data-field: ${fields.join(", ") || "없음"}`,
  113 |               `구간 컬럼 감지: ${hasSegmentColumn ? "예" : "아니오"}`,
  114 |               `구간 값 샘플: ${segmentValues.join(", ") || "없음"}`,
  115 |               `조회 데이터 행 수: ${dataRowCount}`,
  116 |               `화면 본문 일부: ${bodyText.slice(0, 800)}`
  117 |             ]);
  118 |             await attachPageScreenshot(page, testInfo, settlementPage, role, "segment-column");
  119 |
> 120 |             expect(hasSegmentColumn, "정산내역 목록에 구간 컬럼이 추가되어야 합니다.").toBe(true);
      |                                                                    ^ Error: 정산내역 목록에 구간 컬럼이 추가되어야 합니다.
  121 |             if (dataRowCount > 0) {
  122 |               expect(segmentValues.length, "조회 데이터가 있을 때 정산내역 목록의 구간 컬럼에 값이 표시되어야 합니다.").toBeGreaterThan(0);
  123 |             }
  124 |           });
  125 |         });
  126 |
  127 |         test("정산내역 엑셀에 구간 컬럼이 포함되는지 확인", async ({ browser }, testInfo) => {
  128 |           test.setTimeout(240_000);
  129 |           await withRolePage(browser, runtime, async (page) => {
  130 |             await openSettlementPage(page, runtime, settlementPage);
  131 |             const parsedExcel = await requestAndDownloadSettlementExcel(page, settlementPage, runtime, testInfo, role);
  132 |             const header = findHeaderRow(parsedExcel.rows);
  133 |             const normalizedHeaders = (header ?? []).map(normalizeText);
  134 |             const segmentIndex = normalizedHeaders.findIndex((value) => value === "구간");
  135 |             const segmentValues = segmentIndex >= 0
  136 |               ? parsedExcel.rows
  137 |                   .slice(parsedExcel.rows.indexOf(header!) + 1)
  138 |                   .map((row) => normalizeText(row[segmentIndex] ?? ""))
  139 |                   .filter(Boolean)
  140 |                   .slice(0, 20)
  141 |               : [];
  142 |             const excelDataRows = header
  143 |               ? parsedExcel.rows.slice(parsedExcel.rows.indexOf(header) + 1).filter((row) => row.some((cell) => normalizeText(cell)))
  144 |               : [];
  145 |
  146 |             await attachEvidenceLog(testInfo, settlementPage, role, "excel-segment-column", [
  147 |               `${role} 계정으로 정산내역 엑셀을 생성하고 다운로드 파일을 파싱했습니다.`,
  148 |               `파일명: ${maskSensitive(parsedExcel.fileName)}`,
  149 |               `파싱 행 수: ${parsedExcel.rows.length}`,
  150 |               `엑셀 헤더: ${header?.join(" | ") || "없음"}`,
  151 |               `구간 헤더 위치: ${segmentIndex >= 0 ? segmentIndex + 1 : "없음"}`,
  152 |               `구간 값 샘플: ${segmentValues.join(", ") || "없음"}`,
  153 |               `엑셀 데이터 행 수: ${excelDataRows.length}`
  154 |             ]);
  155 |             await attachPageScreenshot(page, testInfo, settlementPage, role, "excel-segment-column");
  156 |
  157 |             expect(header, "정산내역 엑셀 헤더 행을 찾을 수 있어야 합니다.").toBeDefined();
  158 |             expect(segmentIndex, "정산내역 엑셀에 구간 컬럼이 포함되어야 합니다.").toBeGreaterThanOrEqual(0);
  159 |             if (excelDataRows.length > 0) {
  160 |               expect(segmentValues.length, "데이터 행이 있을 때 정산내역 엑셀 구간 컬럼에 값이 표시되어야 합니다.").toBeGreaterThan(0);
  161 |             }
  162 |           });
  163 |         });
  164 |       });
  165 |     }
  166 |   });
  167 | }
  168 |
  169 | function findPageDefinition(pages: QaPageDefinition[], keyword: string): QaPageDefinition {
  170 |   const pageDefinition = pages.find((page) => page.name === keyword || page.name.includes(keyword));
  171 |   if (!pageDefinition) {
  172 |     throw new Error(`checklist.json pages에서 '${keyword}' 페이지 정의를 찾지 못했습니다.`);
  173 |   }
  174 |   return pageDefinition;
  175 | }
  176 |
  177 | function getRuntimeBlockReason(runtimeEnv: RuntimeQaEnv): string | undefined {
  178 |   const appKey = runtimeEnv.application.toUpperCase();
  179 |   const envKey = runtimeEnv.environment.toUpperCase();
  180 |   if (!runtimeEnv.baseUrl) {
  181 |     return `BIX_${appKey}_${envKey}_BASE_URL 또는 BIX_${appKey}_${envKey}_LOGIN_URL이 필요합니다.`;
  182 |   }
  183 |   if (!runtimeEnv.tenantId) {
  184 |     return `BIX_${appKey}_${envKey}_TENANT_ID가 필요합니다.`;
  185 |   }
  186 |   if (!runtimeEnv.storageState && (!runtimeEnv.username || !runtimeEnv.password || !runtimeEnv.loginUrl)) {
  187 |     return `${runtimeEnv.application}/${runtimeEnv.environment}/${runtimeEnv.role} role의 storageState 또는 로그인 계정이 필요합니다.`;
  188 |   }
  189 |   return undefined;
  190 | }
  191 |
  192 | async function withRolePage(
  193 |   browser: Browser,
  194 |   runtime: RuntimeQaEnv,
  195 |   callback: (page: Page) => Promise<void>
  196 | ): Promise<void> {
  197 |   const context = await browser.newContext(
  198 |     runtime.storageState ? { storageState: runtime.storageState, acceptDownloads: true } : { acceptDownloads: true }
  199 |   );
  200 |   const page = await context.newPage();
  201 |   try {
  202 |     await callback(page);
  203 |   } finally {
  204 |     await context.close();
  205 |   }
  206 | }
  207 |
  208 | async function openSettlementPage(page: Page, runtime: RuntimeQaEnv, pageDefinition: QaPageDefinition): Promise<void> {
  209 |   if (!runtime.storageState) {
  210 |     await loginWithCredentials(page, runtime);
  211 |   }
  212 |
  213 |   const targetUrl = buildPageUrl(runtime.baseUrl!, pageDefinition, runtime.tenantId!);
  214 |   await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 60_000 });
  215 |   await waitForNavigationToSettle(page);
  216 |   if (await isLoginPage(page, runtime)) {
  217 |     await loginWithCredentials(page, runtime);
  218 |     await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 60_000 });
  219 |     await waitForNavigationToSettle(page);
  220 |   }
```