diff --git a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md index 57f3eb09..57bcb7ca 100644 --- a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md +++ b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md @@ -18,6 +18,7 @@ - 휴대폰 화면에서 쇼핑몰 환경설정 화면이 좌우로 넘쳐 가로 스크롤이 생기던 문제를 수정했습니다 (이메일 입력 줄, 쇼핑몰 이름·종목 입력칸, 탭 영역). - 휴대폰 화면에서 쿠폰 발급기간 입력칸과 엑셀 다운로드 기간 검색 입력칸이 화면 밖으로 넘치던 문제를 수정했습니다. - 은행 관리·도서산간 배송비 팝업의 표가 좁은 화면에서 가로로 스크롤되도록 명시해, 앞으로 표가 잘려 보이지 않도록 했습니다. +- 관리자 주문상세의 상품 목록에서 구매수량·적립예정만 가운데로 놓여 다른 금액 열과 세로줄이 어긋나 보이던 문제를 수정했습니다. 이제 여섯 개 숫자 열이 모두 오른쪽 끝에 맞춰 정렬됩니다. 휴대폰 화면의 카드 형태에서도 동일하게 적용됩니다. ## [1.0.1] - 2026-07-08 diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_order_info.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_order_info.json index b6b8232d..7b07e718 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_order_info.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_order_info.json @@ -430,7 +430,7 @@ "type": "basic", "name": "Div", "props": { - "className": "flex flex-col items-center gap-1 whitespace-nowrap" + "className": "flex flex-col items-end gap-1 whitespace-nowrap" }, "children": [ { @@ -683,7 +683,7 @@ "type": "basic", "name": "Div", "props": { - "className": "flex flex-col items-center gap-1 whitespace-nowrap" + "className": "flex flex-col items-end gap-1 whitespace-nowrap" }, "children": [ { diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/fixtures/ecommerce-auth.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/fixtures/ecommerce-auth.ts index bbd43629..e77f97cc 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/fixtures/ecommerce-auth.ts +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/fixtures/ecommerce-auth.ts @@ -27,6 +27,12 @@ type EcommerceAuthFixtures = { mileageReadOnlyToken: string; /** 상품 조회 + 수정 권한 보유 토큰 (상품폼 등록/수정 검증용) */ productManageToken: string; + /** 주문 조회(`orders.read`) 권한 보유 토큰 (주문 상세 표시 검증용) */ + ordersReadToken: string; + /** 일반 쇼핑 사용자 토큰 (관리자 권한 없음 — 유저 화면 검증용) */ + userToken: string; + /** 구매 고객 토큰 (장바구니/추가옵션 등 쇼핑 플로우 검증용) */ + customerToken: string; }; export const test = base.extend({ @@ -49,6 +55,18 @@ export const test = base.extend({ productManageToken: async ({}, use) => { await use(issueToken('sirsoft-ecommerce.products.read', 'sirsoft-ecommerce.products.update')); }, + ordersReadToken: async ({}, use) => { + await use(issueToken('sirsoft-ecommerce.orders.read')); + }, + // userToken / customerToken 은 현재 둘 다 "관리자 권한 없는 인증 사용자" 로 동일하다. + // 유저 화면(마이페이지/헤더)과 구매 플로우(장바구니/주문서)는 요구 권한이 갈릴 수 있어 + // 호출부의 의도를 이름으로 남긴다 — 한쪽에 권한이 붙어도 다른 쪽이 영향받지 않는다. + userToken: async ({}, use) => { + await use(issueToken()); + }, + customerToken: async ({}, use) => { + await use(issueToken()); + }, }); export { authenticatePage }; diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/mp06-coupon-and-mileage-timing.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/mp06-coupon-and-mileage-timing.spec.ts index d4e07367..36fe89da 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/mp06-coupon-and-mileage-timing.spec.ts +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/mp06-coupon-and-mileage-timing.spec.ts @@ -13,8 +13,8 @@ * - 무통장 1회 제한 쿠폰은 주문 생성 즉시 USED 차감되어 입금 전까지 재사용 불가(공개#57). * * Select 렌더/disabled 바인딩은 레이아웃 렌더 테스트(adminEcommerceSettingsOrder.test.tsx)가, - * 차감 시점 분기·쿠폰 검증·선차감/반환은 PHPUnit(OrderCalculation*/CouponUse*/CouponRestore*/ - * EcommerceSettingsMileageTiming/MigrateMileageDeductionTiming)가 커버한다. 본 spec 은 브라우저에서 + * 차감 시점 분기·쿠폰 검증·선차감/반환은 PHPUnit(OrderCalculation·CouponUse·CouponRestore· + * EcommerceSettingsMileageTiming·MigrateMileageDeductionTiming)가 커버한다. 본 spec 은 브라우저에서 * 설정 disabled 연동 + 주문서 위반 안내 노출만 왕복 검증한다. * * 활성화 전 사전 작업(data-testid 보강): diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/order-detail-numeric-alignment.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/order-detail-numeric-alignment.spec.ts new file mode 100644 index 00000000..e6631e8e --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/order-detail-numeric-alignment.spec.ts @@ -0,0 +1,132 @@ +/** + * 주문 상세 — 상품 목록 숫자 열 정렬 일치 회귀. + * + * 배경: 상품 목록의 숫자 열 6개 중 `정가/판매가`·`할인가격`·`실 구매가격`·`소계` 는 + * `text-right` 로 우측 정렬인데, `구매수량`·`적립예정` 만 `flex flex-col items-center` + * 로 가운데 정렬이라 값이 서로 어긋나 보였다. 표(≥768px)·카드(<768px) 양쪽에서 발생. + * + * 측정 규율 (이 spec 을 고칠 사람에게): + * - `items-center` 는 `text-align` 이 아니라 `align-items` 로 정렬하므로, + * `textAlign === 'right'` 만 단언하면 두 셀을 영영 못 잡는다. 실제 **콘텐츠의 + * 우측 여백(rightGap)** 이 열끼리 같은지를 본다. + * - 절대 좌표/임계값(`toBeGreaterThan(N)`)은 레이아웃이 바뀌면 거짓 통과한다. + * 같은 행 안에서 **열끼리 서로 같은가**(집합 크기 1)를 단언한다. + * + * @scenario order_detail_numeric_alignment viewport=1280|390 + * @effects numeric_columns_right_aligned_consistently + */ +import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth'; +import type { Page } from '@playwright/test'; + +/** + * 숫자 값을 담는 열의 수 (정가/판매가·구매수량·할인가격·실 구매가격·적립예정·소계). + * + * 헤더 텍스트로 찾지 않는다 — E2E 세션 로케일이 en 이면 `Quantity` 로 렌더돼 + * 한글 문자열 단언이 통째로 새기 때문(실측 확인). 대신 "우측 정렬로 선언된 셀" 을 + * 숫자 열로 식별한다. 회귀(= `items-center` 복귀)가 나면 그 셀이 목록에서 빠지므로 + * 개수 단언이 먼저 깨진다. + */ +const NUMERIC_COLUMN_COUNT = 6; + +/** + * 첫 주문의 상세 URL 을 얻는다 (주문번호 하드코딩 회피). + * + * 목록 화면은 좁은 뷰포트에서 표가 아니라 카드로 렌더되므로, 진입은 항상 데스크톱 폭에서 + * 한다. 상세 진입은 `` 가 아니라 주문번호 `` 클릭(DataGrid onRowClick 계열). + * 상세 URL 은 숫자 id 가 아니라 주문번호(예: `20260709-0144414102`) 다. + */ +async function firstOrderDetailUrl(page: Page): Promise { + await page.setViewportSize({ width: 1280, height: 900 }); + await page.goto('/admin/ecommerce/orders'); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const orderNumber = page.locator('table tbody tr span.cursor-pointer').first(); + await orderNumber.waitFor({ state: 'visible', timeout: 30_000 }); + await orderNumber.click(); + + await page.waitForURL(/\/admin\/ecommerce\/orders\/[\w-]+$/, { timeout: 30_000 }); + return new URL(page.url()).pathname; +} + +/** + * 우측 정렬로 선언된 셀들의 "콘텐츠 우측 여백" 을 잰다. + * + * 셀 경계와 실제 콘텐츠 우측 끝의 거리. 모든 숫자 열이 같은 방식으로 우측 정렬돼 있으면 + * 이 값이 열끼리 같다. `구매수량`·`적립예정` 이 `items-center` 로 돌아가면 그 셀은 + * 우측 정렬 목록에서 빠지므로 개수가 줄고, 설령 잡히더라도 여백이 달라진다. + * + * `text-right`(block) 와 `items-end`(flex) 둘 다 우측 정렬이므로 함께 센다. + */ +async function rightGaps(page: Page): Promise { + return page.evaluate(() => { + const isRightAligned = (el: Element): boolean => { + const cs = getComputedStyle(el); + return cs.textAlign === 'right' || cs.alignItems === 'flex-end'; + }; + const gapOf = (container: Element, inner: Element): number => { + const content = inner.firstElementChild ?? inner; + return Math.round(container.getBoundingClientRect().right - content.getBoundingClientRect().right); + }; + + // 표 뷰(≥768px): thead 가 있는 상품 목록 table + const table = [...document.querySelectorAll('table')].find((t) => t.querySelector('tbody tr td')); + if (table) { + const row = table.querySelector('tbody tr'); + if (!row) return []; + return [...row.querySelectorAll('td')] + .map((td) => { + const inner = td.firstElementChild; + return inner && isRightAligned(inner) ? gapOf(td, inner) : null; + }) + .filter((v): v is number => v !== null); + } + + // 카드 뷰(<768px): DataGrid 가 컬럼을 grid 셀로 편다 + const grid = [...document.querySelectorAll('[class*="grid-cols-"]')].find((g) => g.querySelector('.min-w-0')); + if (!grid) return []; + return [...grid.children] + .map((cell) => { + const label = cell.querySelector('span'); + const value = [...cell.children].find((c) => c !== label); + const inner = value?.firstElementChild; + return inner && isRightAligned(inner) ? gapOf(cell, inner) : null; + }) + .filter((v): v is number => v !== null); + }); +} + +test.describe('@sirsoft-ecommerce 주문 상세 상품 목록 정렬', () => { + // @scenario order_detail_numeric_alignment:desktop_table + // @effects numeric_columns_right_aligned_consistently + test('1280px 표 뷰: 숫자 열이 모두 같은 우측 여백으로 정렬된다', async ({ page, ordersReadToken }) => { + await authenticatePage(page, ordersReadToken); + await firstOrderDetailUrl(page); // 진입만 하면 이미 1280px 표 뷰 + + // 회귀 시 `구매수량`·`적립예정` 이 items-center 로 돌아가 우측 정렬 셀이 4개로 줄어든다. + await expect + .poll(async () => (await rightGaps(page)).length, { timeout: 20_000 }) + .toBe(NUMERIC_COLUMN_COUNT); + + const gaps = await rightGaps(page); + expect(new Set(gaps).size, `열별 우측 여백: ${JSON.stringify(gaps)}`).toBe(1); + }); + + // @scenario order_detail_numeric_alignment:mobile_card + // @effects numeric_columns_right_aligned_consistently + test('390px 카드 뷰: 숫자 열이 모두 같은 우측 여백으로 정렬된다', async ({ page, ordersReadToken }) => { + await authenticatePage(page, ordersReadToken); + // 목록은 데스크톱 폭에서 진입(좁은 폭에선 목록도 카드라 주문번호 span 이 없다) + const detailPath = await firstOrderDetailUrl(page); + + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto(detailPath); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + await expect + .poll(async () => (await rightGaps(page)).length, { timeout: 20_000 }) + .toBe(NUMERIC_COLUMN_COUNT); + + const gaps = await rightGaps(page); + expect(new Set(gaps).size, `열별 우측 여백: ${JSON.stringify(gaps)}`).toBe(1); + }); +});