From 8cf55ad88ff29c961e129acdf54da16d5c058407 Mon Sep 17 00:00:00 2001 From: chym1217 Date: Wed, 1 Jul 2026 12:26:12 +0900 Subject: [PATCH] =?UTF-8?q?fix(page):=20=EB=B2=84=EC=A0=84=20=EC=9D=B4?= =?UTF-8?q?=EB=A0=A5=20=EB=B3=80=EA=B2=BD=EB=82=B4=EC=97=AD=EC=9D=84=20?= =?UTF-8?q?=EB=8B=A4=EA=B5=AD=EC=96=B4=20=EB=9D=BC=EB=B2=A8=EB=A1=9C=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20+=20E2E=20=EC=9D=B8=ED=94=84=EB=9D=BC=20?= =?UTF-8?q?=EC=8B=A0=EC=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 페이지 상세 버전 이력의 "변경내역"이 title·content 등 영문 필드명으로 노출되던 문제를 field_labels 다국어 매핑으로 정정 (ko/en/ja) - sirsoft-page 모듈에 Playwright E2E 인프라를 이커머스와 동일 패턴으로 신설 (config·fixture·seed stub + spec 3건, data-testid 보강 전까지 skip placeholder) - 출시된 1.0.0 이후 변경이므로 module 1.0.1 bump (3곳+lock 동기화), ja 언어팩 1.0.1 bump --- .../g7-module-sirsoft-page-ja/CHANGELOG.md | 6 ++ .../frontend/partial/admin.json | 8 +- .../language-pack.json | 2 +- modules/_bundled/sirsoft-page/CHANGELOG.md | 1 + modules/_bundled/sirsoft-page/composer.json | 2 +- modules/_bundled/sirsoft-page/module.json | 2 +- .../_bundled/sirsoft-page/package-lock.json | 4 +- modules/_bundled/sirsoft-page/package.json | 2 +- .../layouts/admin-page-layouts.test.ts | 45 +++++++++- .../resources/lang/partial/en/admin.json | 8 +- .../resources/lang/partial/ko/admin.json | 8 +- .../layouts/admin/admin_page_detail.json | 2 +- .../layouts/admin/admin_page_form.json | 2 +- .../Console/Commands/PlaywrightSeedPage.php | 79 ++++++++++++++++++ .../tests/Playwright/fixtures/page-auth.ts | 54 ++++++++++++ .../tests/Playwright/fixtures/page-seed.ts | 45 ++++++++++ .../tests/Playwright/playwright.config.ts | 74 +++++++++++++++++ .../specs/admin/page-attachment-order.spec.ts | 63 ++++++++++++++ .../page-form-reset-on-route-switch.spec.ts | 82 +++++++++++++++++++ .../page-version-changed-fields-i18n.spec.ts | 42 ++++++++++ .../Listeners/SeoPageCacheListenerTest.php | 2 +- .../Services/PageAttachmentServiceTest.php | 2 +- .../tests/Unit/Services/PageServiceTest.php | 6 +- .../page-attachment-order-and-seo-cache.yaml | 43 ++++++++-- 24 files changed, 558 insertions(+), 26 deletions(-) create mode 100644 modules/_bundled/sirsoft-page/src/Console/Commands/PlaywrightSeedPage.php create mode 100644 modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-auth.ts create mode 100644 modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-seed.ts create mode 100644 modules/_bundled/sirsoft-page/tests/Playwright/playwright.config.ts create mode 100644 modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-attachment-order.spec.ts create mode 100644 modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-form-reset-on-route-switch.spec.ts create mode 100644 modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-version-changed-fields-i18n.spec.ts diff --git a/lang-packs/_bundled/g7-module-sirsoft-page-ja/CHANGELOG.md b/lang-packs/_bundled/g7-module-sirsoft-page-ja/CHANGELOG.md index fd9e9712..e3ff53a2 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-page-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-module-sirsoft-page-ja/CHANGELOG.md @@ -4,6 +4,12 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.1] - 2026-07-01 + +### Added + +- 페이지 버전 이력의 변경 필드명 라벨 일본어 번역 추가 (`versions.field_labels.*`) — 버전 이력의 "변경내역" 항목이 일본어 로케일에서 필드 키 대신 자연스러운 라벨로 표시됩니다. + ## [1.0.0] - 2026-07-01 ### Added diff --git a/lang-packs/_bundled/g7-module-sirsoft-page-ja/frontend/partial/admin.json b/lang-packs/_bundled/g7-module-sirsoft-page-ja/frontend/partial/admin.json index 28467117..5feca74c 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-page-ja/frontend/partial/admin.json +++ b/lang-packs/_bundled/g7-module-sirsoft-page-ja/frontend/partial/admin.json @@ -160,7 +160,13 @@ "changed_fields": "変更: {{fields}}", "restored_from": "v{{version}}から復元", "restore_guide": "表示ボタンをクリックすると、内容を確認して復元できます。添付ファイルはバージョン管理に含まれていません。", - "preview_modal_editor_label": "バージョンプレビュー" + "preview_modal_editor_label": "バージョンプレビュー", + "field_labels": { + "title": "タイトル", + "content": "内容", + "seo_meta": "SEO情報", + "content_mode": "編集モード" + } } } } diff --git a/lang-packs/_bundled/g7-module-sirsoft-page-ja/language-pack.json b/lang-packs/_bundled/g7-module-sirsoft-page-ja/language-pack.json index 61aba8ec..0120a528 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-page-ja/language-pack.json +++ b/lang-packs/_bundled/g7-module-sirsoft-page-ja/language-pack.json @@ -12,7 +12,7 @@ "en": "G7 module (sirsoft-page) Japanese language pack (bundled)", "ja": "G7 モジュール (sirsoft-page) 日本語 言語パック(バンドル)" }, - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "scope": "module", "target_identifier": "sirsoft-page", diff --git a/modules/_bundled/sirsoft-page/CHANGELOG.md b/modules/_bundled/sirsoft-page/CHANGELOG.md index 74003cbd..8768c172 100644 --- a/modules/_bundled/sirsoft-page/CHANGELOG.md +++ b/modules/_bundled/sirsoft-page/CHANGELOG.md @@ -18,6 +18,7 @@ - 페이지 관리 목록에서 검색 조건 '전체'로 슬러그를 검색해도 결과가 나오지 않던 문제를 수정했습니다 — '전체'는 제목과 슬러그를 함께 검색하고, '제목'을 고르면 제목만 검색됩니다. - 페이지 관리 목록의 페이지당 표시 개수 기본값이 화면 표시(20건)와 실제 조회(15건) 간에 어긋나던 문제를 수정했습니다 — 기본값을 20건으로 통일했습니다. - 페이지 관리 목록에서 아주 긴 검색어를 입력하면 오류 화면이 뜨던 문제를 수정했습니다 — 검색어 길이를 100자로 제한해 안내 메시지로 처리합니다. +- 페이지 상세의 버전 이력에서 "변경내역" 항목이 `title`·`content` 같은 영문 필드명으로 표시되던 문제를 수정했습니다 — 이제 "제목", "내용", "SEO 정보" 처럼 알아보기 쉬운 이름으로 표시됩니다. ## [1.0.0] - 2026-07-01 diff --git a/modules/_bundled/sirsoft-page/composer.json b/modules/_bundled/sirsoft-page/composer.json index 3e51bf9b..bb65c272 100644 --- a/modules/_bundled/sirsoft-page/composer.json +++ b/modules/_bundled/sirsoft-page/composer.json @@ -2,7 +2,7 @@ "name": "modules/sirsoft-page", "description": "Page module for Gnuboard7", "type": "library", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "autoload": { "psr-4": { diff --git a/modules/_bundled/sirsoft-page/module.json b/modules/_bundled/sirsoft-page/module.json index 2922e1ec..93be60e8 100644 --- a/modules/_bundled/sirsoft-page/module.json +++ b/modules/_bundled/sirsoft-page/module.json @@ -5,7 +5,7 @@ "ko": "페이지", "en": "Page" }, - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "description": { "ko": "정적 페이지(정보/정책/안내) 관리 모듈", diff --git a/modules/_bundled/sirsoft-page/package-lock.json b/modules/_bundled/sirsoft-page/package-lock.json index 2a139189..79e30c54 100644 --- a/modules/_bundled/sirsoft-page/package-lock.json +++ b/modules/_bundled/sirsoft-page/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-page", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-page", - "version": "1.0.0", + "version": "1.0.1", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/modules/_bundled/sirsoft-page/package.json b/modules/_bundled/sirsoft-page/package.json index 1d5186a2..34d516e4 100644 --- a/modules/_bundled/sirsoft-page/package.json +++ b/modules/_bundled/sirsoft-page/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-page", - "version": "1.0.0", + "version": "1.0.1", "description": "그누보드7 페이지 모듈 프론트엔드 에셋", "private": true, "type": "module", diff --git a/modules/_bundled/sirsoft-page/resources/js/__tests__/layouts/admin-page-layouts.test.ts b/modules/_bundled/sirsoft-page/resources/js/__tests__/layouts/admin-page-layouts.test.ts index 20eebe76..15ddd33d 100644 --- a/modules/_bundled/sirsoft-page/resources/js/__tests__/layouts/admin-page-layouts.test.ts +++ b/modules/_bundled/sirsoft-page/resources/js/__tests__/layouts/admin-page-layouts.test.ts @@ -569,7 +569,7 @@ describe('admin_page_form.json', () => { } }); - // ─── Issue #424: 수정→등록 SPA 전환 시 폼/첨부 잔존 차단 ─── + // ─── 수정→등록 SPA 전환 시 폼/첨부 잔존 차단 ─── it('FileUploader initialFiles 가 _local.form.attachments 에 바인딩됨 (등록 모드 전환 시 빈 배열로 동기화되어 이전 첨부 잔존 차단)', () => { // 회귀 배경: pageData?.data?.attachments 바인딩은 등록 모드에서 pageData 가 없어 @@ -623,7 +623,7 @@ describe('admin_page_form.json', () => { expect(hintSpan.text).not.toContain("?? 'example'"); }); - // ─── Issue #424 6-2: 첨부 순서 — form.attachments 동기화 (이커머스 패턴) ─── + // ─── 첨부 순서 — form.attachments 동기화 (이커머스 패턴) ─── it('FileUploader 에 onUploadComplete/onReorder/onRemove 액션이 form.attachments 를 동기화함', () => { const uploaders = findComponentsByName(adminPageForm, 'FileUploader'); @@ -791,4 +791,45 @@ describe('admin_page_detail.json', () => { // $locale 로 fallback 하는 표현식이 존재해야 함 (탭 + 콘텐츠 표현식) expect(layoutStr).toMatch(/_local\.lang \?\? \$locale/); }); + + // ─── 변경내역 필드명 다국어 매핑 ─── + + /** + * 버전 이력 테이블의 변경내역(changes_summary) 컬럼에서 + * changed_fields 를 표시하는 cellChild 를 찾는다. + */ + function findChangedFieldsCell(): any { + const grids = findComponentsByName(adminPageDetail, 'DataGrid'); + const col = grids[0]?.props?.columns?.find((c: any) => c.field === 'changes_summary'); + expect(col, 'changes_summary 컬럼').toBeDefined(); + // changed_fields 길이 > 0 조건을 가진 cellChild (라벨 표시 셀) + return (col.cellChildren ?? []).find( + (cc: any) => + typeof cc.if === 'string' && + cc.if.includes('changed_fields?.length > 0'), + ); + } + + it('변경내역 셀이 changed_fields 를 raw 영문 식별자로 그대로 join 하지 않음 (제목/내용 다국어 매핑)', () => { + const cell = findChangedFieldsCell(); + expect(cell, 'changed_fields 표시 셀').toBeDefined(); + // 회귀 차단: 영문 식별자 배열을 그대로 join 하면 화면에 title, content 노출 + expect(cell.text).not.toMatch(/changed_fields\?\.join\(', '\)\s*\?\?\s*'-'/); + }); + + it('변경내역 셀이 field_labels 다국어 키로 각 필드를 변환함 ($t 매핑)', () => { + const cell = findChangedFieldsCell(); + expect(cell, 'changed_fields 표시 셀').toBeDefined(); + const cellStr = JSON.stringify(cell); + // field_labels 키 경로 + $t 함수 변환 사용 + expect(cellStr).toContain('field_labels'); + expect(cellStr).toContain('$t'); + }); + + it('field_labels 4종(title/content/seo_meta/content_mode) 키 경로를 참조함 (PageService compareFields 정합)', () => { + const cell = findChangedFieldsCell(); + const cellStr = JSON.stringify(cell); + // 동적 키 prefix 가 versions.field_labels 를 가리킴 + expect(cellStr).toContain('sirsoft-page.admin.page.detail.versions.field_labels'); + }); }); diff --git a/modules/_bundled/sirsoft-page/resources/lang/partial/en/admin.json b/modules/_bundled/sirsoft-page/resources/lang/partial/en/admin.json index 2409756e..86a0d325 100644 --- a/modules/_bundled/sirsoft-page/resources/lang/partial/en/admin.json +++ b/modules/_bundled/sirsoft-page/resources/lang/partial/en/admin.json @@ -160,7 +160,13 @@ "initial": "Initial", "changed_fields": "Changed: {{fields}}", "restored_from": "Restored from v{{version}}", - "restore_guide": "Click View to check contents and restore. Attachments are not included in version management." + "restore_guide": "Click View to check contents and restore. Attachments are not included in version management.", + "field_labels": { + "title": "Title", + "content": "Content", + "seo_meta": "SEO", + "content_mode": "Edit Mode" + } } } } diff --git a/modules/_bundled/sirsoft-page/resources/lang/partial/ko/admin.json b/modules/_bundled/sirsoft-page/resources/lang/partial/ko/admin.json index 9a74af08..9a12470a 100644 --- a/modules/_bundled/sirsoft-page/resources/lang/partial/ko/admin.json +++ b/modules/_bundled/sirsoft-page/resources/lang/partial/ko/admin.json @@ -160,7 +160,13 @@ "initial": "최초 생성", "changed_fields": "변경: {{fields}}", "restored_from": "v{{version}}에서 복원", - "restore_guide": "보기 버튼을 클릭하면 내용을 확인하고 복원할 수 있습니다. 첨부파일은 버전 관리에 포함되지 않습니다." + "restore_guide": "보기 버튼을 클릭하면 내용을 확인하고 복원할 수 있습니다. 첨부파일은 버전 관리에 포함되지 않습니다.", + "field_labels": { + "title": "제목", + "content": "내용", + "seo_meta": "SEO 정보", + "content_mode": "편집 모드" + } } } } diff --git a/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_detail.json b/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_detail.json index 51f5b0dc..db7380d0 100644 --- a/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_detail.json +++ b/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_detail.json @@ -1665,7 +1665,7 @@ "props": { "className": "text-sm text-gray-600 dark:text-gray-400" }, - "text": "{{row.changes_summary?.changed_fields?.join(', ') ?? '-'}}" + "text": "{{row.changes_summary?.changed_fields?.map(f => $t('sirsoft-page.admin.page.detail.versions.field_labels.' + f)).join(', ') ?? '-'}}" }, { "type": "basic", diff --git a/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_form.json b/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_form.json index aaf025e5..d53d6dad 100644 --- a/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_form.json +++ b/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_form.json @@ -740,7 +740,7 @@ } }, { - "comment": "드래그 순서 변경 시 form.attachments 를 최종 순서로 덮어쓰기 (이커머스 상품 이미지와 동일 패턴, 이슈 #424 6-2)", + "comment": "드래그 순서 변경 시 form.attachments 를 최종 순서로 덮어쓰기 (이커머스 상품 이미지와 동일 패턴)", "event": "onReorder", "handler": "setState", "params": { diff --git a/modules/_bundled/sirsoft-page/src/Console/Commands/PlaywrightSeedPage.php b/modules/_bundled/sirsoft-page/src/Console/Commands/PlaywrightSeedPage.php new file mode 100644 index 00000000..72ffe7e0 --- /dev/null +++ b/modules/_bundled/sirsoft-page/src/Console/Commands/PlaywrightSeedPage.php @@ -0,0 +1,79 @@ +error('CLI 전용 커맨드입니다.'); + + return self::FAILURE; + } + + // ② 명시 옵트인 + if (env('G7_PLAYWRIGHT_BYPASS') !== '1') { + $this->error('G7_PLAYWRIGHT_BYPASS=1 환경변수가 필요합니다.'); + + return self::FAILURE; + } + + // ③ APP_DEBUG 강제 — SettingsServiceProvider 의 bypass 분기가 settings JSON 덮어쓰기 차단 상태 + Config::set('app.debug', true); + + // TODO (페이지 모듈 후속 작업 세션): + // - 페이지 N개 생성 (Factory 재사용) + // - 페이지당 첨부 N개 생성 (업로드 순서 검증용, order 1..N 부여) + // - 반환: {pageIds: [...], attachmentIds: [...]} + // 개발자 전용 stub 메시지 — 사용자 노출 0 (본문 구현 시 즉시 삭제). + throw new \LogicException( /* i18n:exempt — developer-only stub */ + 'playwright:seed-page 시드 로직은 아직 구현되지 않았습니다. ' + .'대상 spec 을 test.describe.skip 로 유지하거나, 본 커맨드 본문을 실제 시드 로직으로 교체하세요.' + ); + } +} diff --git a/modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-auth.ts b/modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-auth.ts new file mode 100644 index 00000000..4eb9d964 --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-auth.ts @@ -0,0 +1,54 @@ +/** + * 페이지 모듈 권한 fixture. + * + * 코어 `tests/Playwright/fixtures/auth.ts` 의 헬퍼(issueToken / authenticatePage)를 재사용하되, + * 모듈 권한 토큰 fixture 를 자체적으로 정의한다. 권한 식별자는 임의 string 이므로 코어 + * PlaywrightIssueToken 커맨드가 그대로 동작한다 (Permission::firstOrCreate 가 자동 생성). + * + * 권한 식별자 (module src 정의): + * sirsoft-page.pages.read / create / update / delete + * + * 활성화 절차 (페이지 모듈 후속 작업 세션): + * 1. 대상 화면 컴포넌트에 data-testid 보강 (각 spec docblock 의 "활성화 절차" 참조) + * 2. 해당 spec 의 test.describe.skip → test.describe 변경 + */ +import { test as base } from '@playwright/test'; +// 6단계 상위 = 코어 루트의 fixtures/auth.ts +// (modules/_bundled/sirsoft-page/tests/Playwright/fixtures → 코어 루트) +import { issueToken, authenticatePage } from '../../../../../../tests/Playwright/fixtures/auth'; + +type PageAuthFixtures = { + /** 페이지 조회 권한 토큰 (목록/상세 조회 검증용) */ + pageReadToken: string; + /** 페이지 조회 + 생성 권한 토큰 (등록 폼 검증용) */ + pageCreateToken: string; + /** 페이지 조회 + 수정 권한 토큰 (수정 폼/첨부/버전 검증용) */ + pageManageToken: string; + /** 권한 없는 일반 사용자 토큰 (메뉴/화면 미노출 검증용) */ + noPermissionToken: string; +}; + +export const test = base.extend({ + pageReadToken: async ({}, use) => { + await use(issueToken('sirsoft-page.pages.read')); + }, + pageCreateToken: async ({}, use) => { + await use(issueToken('sirsoft-page.pages.read', 'sirsoft-page.pages.create')); + }, + pageManageToken: async ({}, use) => { + await use( + issueToken( + 'sirsoft-page.pages.read', + 'sirsoft-page.pages.create', + 'sirsoft-page.pages.update' + ) + ); + }, + noPermissionToken: async ({}, use) => { + // 빈 권한 — 인증만 통과, 어떤 모듈 권한도 없음 + await use(issueToken()); + }, +}); + +export { authenticatePage }; +export { expect } from '@playwright/test'; diff --git a/modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-seed.ts b/modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-seed.ts new file mode 100644 index 00000000..b8ae8837 --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-seed.ts @@ -0,0 +1,45 @@ +/** + * 페이지 모듈 도메인 시드 fixture (stub). + * + * `playwright:seed-page` artisan 커맨드를 호출하여 도메인 데이터(페이지 + 첨부)를 발급받는다. + * 본 stub 은 시드 커맨드가 LogicException 을 throw 하도록 구성되어 있어(시그니처/보안 가드만 구현) + * 실제 사용 전에는 대상 spec 이 test.describe.skip 로 비활성화되어야 한다. + * + * 활성화 절차 (페이지 모듈 후속 작업 세션): + * 1. PlaywrightSeedPage 의 handle() 본문을 LogicException 에서 실제 시드 로직으로 교체 + * (Page 모델 + 첨부 Factory 재사용, 업로드 순서 검증용 첨부 N건 포함) + * 2. 본 fixture 를 사용하는 spec 의 test.describe.skip 표기 제거 + * 3. teardown 시 cleanup 커맨드(예: playwright:cleanup-page) 호출 추가 권장 + */ +import { test as base } from '@playwright/test'; +import { execSync } from 'node:child_process'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// ESM 환경(package.json "type": "module")에서는 __dirname 미정의 → import.meta.url 로 재구성. +const __dirname = dirname(fileURLToPath(import.meta.url)); + +type SeedFixtures = { + /** 시드된 페이지 도메인 데이터 (페이지 ID + 첨부 ID 배열) */ + seededPage: { pageIds: number[]; attachmentIds: number[] }; +}; + +export const test = base.extend({ + seededPage: async ({}, use) => { + // 6단계 상위 = 코어 루트 (artisan 실행 cwd) + const coreRoot = process.env.G7_ROOT || resolve(__dirname, '../../../../../../'); + const out = execSync('php artisan playwright:seed-page --pages=3 --attachments=3 --json', { + cwd: coreRoot, + encoding: 'utf-8', + env: { + ...process.env, + G7_PLAYWRIGHT_BYPASS: '1', + }, + }); + const seed = JSON.parse(out); + await use(seed); + // TODO: teardown 시 cleanup 커맨드 호출 + }, +}); + +export { expect } from '@playwright/test'; diff --git a/modules/_bundled/sirsoft-page/tests/Playwright/playwright.config.ts b/modules/_bundled/sirsoft-page/tests/Playwright/playwright.config.ts new file mode 100644 index 00000000..8215cff8 --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Playwright/playwright.config.ts @@ -0,0 +1,74 @@ +/** + * 페이지 모듈 Playwright E2E 설정. + * + * 코어 `playwright.config.ts` 및 이커머스 모듈 설정과 동일한 base URL 해석 우선순위를 따른다 — + * 모듈도 활성 호스트가 가변(개발자/CI/PO 환경별로 다른 도메인)이므로 하드코딩 회피. + * + * Base URL 해석: + * 1. PLAYWRIGHT_BASE_URL 환경변수 (CI/명시적 오버라이드) + * 2. .env (코어 루트) 의 APP_URL — 단 localhost 류는 fallback 부적합 + * 3. 그 외 — 명시 에러 + * + * 실행 예시: + * PowerShell — $env:PLAYWRIGHT_BASE_URL='https://g7.dev'; npm run test:e2e + * Bash — PLAYWRIGHT_BASE_URL=https://g7.dev npm run test:e2e + * + * 모듈은 코어 fixture 의 `issueToken` / `authenticatePage` 헬퍼를 재사용 — 권한 식별자는 + * `sirsoft-page.*` 등 임의 string. 시드는 모듈 자체 artisan 커맨드(playwright:seed-page) 호출. + */ +import { defineConfig, devices } from '@playwright/test'; +import { readFileSync, existsSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// ESM 환경(package.json "type": "module")에서는 __dirname 이 정의되지 않으므로 +// import.meta.url 로 재구성한다. +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function readEnvFile(filePath: string, key: string): string | null { + if (!existsSync(filePath)) return null; + const content = readFileSync(filePath, { encoding: 'utf-8' }); + const pattern = new RegExp(`^${key}=(.*)$`, 'm'); + const match = content.match(pattern); + if (!match) return null; + let value = match[1].trim(); + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + return value || null; +} + +function resolveBaseUrl(): string { + if (process.env.PLAYWRIGHT_BASE_URL) { + return process.env.PLAYWRIGHT_BASE_URL; + } + // 모듈 디렉토리 기준 5단계 상위 = 코어 루트 (.env 위치) + // (modules/_bundled/sirsoft-page/tests/Playwright → 코어 루트) + const coreRoot = process.env.G7_ROOT || resolve(__dirname, '../../../../../'); + const appUrl = readEnvFile(resolve(coreRoot, '.env'), 'APP_URL'); + if (appUrl && !/^https?:\/\/localhost(:\d+)?\/?$/i.test(appUrl)) { + return appUrl; + } + throw new Error( + '페이지 모듈 E2E base URL 미설정. PLAYWRIGHT_BASE_URL 환경변수를 지정하거나 코어 .env 의 APP_URL 을 활성 호스트로 설정하세요.' + ); +} + +export default defineConfig({ + testDir: './specs', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html', { outputFolder: 'playwright-report', open: 'never' }], + ['list'], + ], + use: { + baseURL: resolveBaseUrl(), + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + ignoreHTTPSErrors: true, + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], +}); diff --git a/modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-attachment-order.spec.ts b/modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-attachment-order.spec.ts new file mode 100644 index 00000000..b12e2307 --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-attachment-order.spec.ts @@ -0,0 +1,63 @@ +/** + * 페이지 첨부 다중 업로드 순서 / 드래그 재정렬 → form.attachments 동기화 + * (placeholder — data-testid 보강 후 활성화). + * + * @scenario upload_count=single, upload_count=multiple, form_mode=create, form_mode=edit + * @effects temp_attachments_linked_in_upload_order_1_to_n, + * edit_mode_drag_persists_via_reorder_api + * + * e2e:allow 다중 업로드 시 첨부가 올린 순서대로 저장되고(생성 모드 temp_key 연결), + * 수정 모드 드래그 재정렬이 reorder API 로 즉시 저장되어 form.attachments 에 반영되는 + * 동작을 백엔드 Feature/Unit 테스트와 레이아웃 렌더 테스트가 커버한다. 본 placeholder + * spec(test.describe.skip)은 data-testid 보강 후 활성화된다. + * - 생성 모드 업로드 순서 1..N 재부여: PageAttachmentServiceTest (unit) + * - onUploadComplete/onReorder/onRemove → form.attachments 동기화: + * admin-page-layouts.test.ts > "첨부 순서 — form.attachments 동기화" (레이아웃 렌더) + * 브라우저 실제 드래그 인터랙션의 순서 영속만 본 spec 이 확인한다. + * + * 본 spec 은 다음 사전 작업 완료 후 활성화한다 (data-testid 보강): + * 1. FileUploader 첨부 아이템에 data-testid="page-attachment-item" (순서 assert 용) + * 2. 첨부 아이템 드래그 핸들에 data-testid="page-attachment-drag-handle" + * 3. seededPage fixture(playwright:seed-page) 실 시드 로직 구현 (첨부 N건 order 1..N) + * 4. test.describe.skip → test.describe 변경 + * + * 매트릭스(시나리오 매니페스트 page-attachment-order-and-seo-cache.yaml 의 upload_count × form_mode 와 1:1): + * - create × multiple: 업로드(조회) 순서대로 저장 후 목록 순서 유지 + * - edit × multiple: 드래그로 순서 변경 후 재조회 시 변경 순서 영속 + */ +import { mergeTests } from '@playwright/test'; +import { test as authTest, expect, authenticatePage } from '../../fixtures/page-auth'; +import { test as seedTest } from '../../fixtures/page-seed'; + +const test = mergeTests(authTest, seedTest); + +const CREATE_URL = '/admin/pages/create'; +const EDIT_URL = '/admin/pages/1/edit'; + +test.describe.skip('페이지 첨부 순서 동기화 (placeholder — data-testid 보강 후 활성화)', () => { + test('등록 폼에서 여러 첨부를 올리면 올린 순서대로 목록에 표시된다', async ({ + page, + pageCreateToken, + }) => { + await authenticatePage(page, pageCreateToken); + await page.goto(CREATE_URL); + + // (data-testid 보강 후) 파일 3건을 순차 업로드하고 목록 순서가 업로드 순서와 일치하는지 확인 + await expect(page.getByTestId('page-attachment-item')).toHaveCount(3); + }); + + test('수정 폼에서 첨부를 드래그로 재정렬하면 변경된 순서가 영속된다', async ({ + page, + pageManageToken, + seededPage, + }) => { + await authenticatePage(page, pageManageToken); + await page.goto(EDIT_URL); + + const items = page.getByTestId('page-attachment-item'); + await expect(items).toHaveCount(seededPage.attachmentIds.length); + + // (data-testid 보강 후) 첫 아이템을 마지막으로 드래그 → reorder API 발화 → 재조회 시 순서 영속 + // 드래그 인터랙션 + 재조회 후 순서 assert 는 활성화 시 구현 + }); +}); \ No newline at end of file diff --git a/modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-form-reset-on-route-switch.spec.ts b/modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-form-reset-on-route-switch.spec.ts new file mode 100644 index 00000000..725d0e94 --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-form-reset-on-route-switch.spec.ts @@ -0,0 +1,82 @@ +/** + * 페이지 수정→등록 라우트 전환 시 폼/첨부/검증상태 잔존 차단 + 슬러그 미리보기 fallback + * (placeholder — data-testid 보강 후 활성화). + * + * @scenario form_mode=create, form_mode=edit + * @effects create_form_clears_stale_attachments_via_form_attachments_binding, + * create_form_clears_stale_title_slug_and_slug_check_state, + * slug_hint_url_preview_falls_back_to_example_on_empty_slug + * + * e2e:allow 수정→등록 SPA 라우트 전환 시 이전 페이지의 제목/슬러그/첨부/검증상태가 등록 폼에 + * 잔존하던 회귀와, 빈 슬러그에서 미리보기가 "/page/{{slug}}" 처럼 미치환되던 회귀를 + * 레이아웃 렌더 테스트가 구조적으로 차단한다. 본 placeholder spec(test.describe.skip)은 + * data-testid 보강 후 활성화된다. + * 레이아웃 렌더링 테스트(admin-page-layouts.test.ts > "수정→등록 SPA 전환 시 폼/첨부 잔존 차단")가 + * (1) 생성 모드 init_actions 가 _local.form 을 null→빈 기본값으로 초기화해 이전 + * id/title/slug/attachments 참조를 끊고, + * (2) FileUploader initialFiles 가 _local.form.attachments 에 바인딩되며, + * (3) slug_hint 가 빈 슬러그('')에서 || 'example' fallback 으로 {{slug}} 미치환을 차단함을 + * 구조적으로 회귀 차단한다. 브라우저 실 전환(라우트 A→B)의 리마운트 동작만 본 spec 이 확인한다. + * + * 본 spec 은 다음 사전 작업 완료 후 활성화한다 (data-testid 보강): + * 1. 페이지 제목 입력에 data-testid="page-title-input" + * 2. 슬러그 입력에 data-testid="page-slug-input" + * 3. 슬러그 URL 미리보기 텍스트에 data-testid="page-slug-hint" + * 4. 첨부 목록 컨테이너/아이템에 data-testid="page-attachment-item" + * 5. seededPage fixture(playwright:seed-page) 실 시드 로직 구현 + * 6. test.describe.skip → test.describe 변경 + * + * 매트릭스(시나리오 매니페스트 page-attachment-order-and-seo-cache.yaml 의 form_mode axis 와 1:1): + * - edit→create 전환: 이전 제목/슬러그/첨부가 등록 폼에 비워진 상태로 진입 + * - create 진입 직후: 슬러그 미입력 상태에서 미리보기가 "/page/example" + * - create 에서 슬러그 입력: 미리보기가 입력값으로 즉시 치환 + */ +import { test, expect, authenticatePage } from '../../fixtures/page-auth'; + +const EDIT_URL = '/admin/pages/1/edit'; +const CREATE_URL = '/admin/pages/create'; + +test.describe.skip('페이지 수정→등록 전환 잔존 차단 (placeholder — data-testid 보강 후 활성화)', () => { + test('수정 폼에서 등록 폼으로 전환하면 이전 제목/슬러그/첨부가 잔존하지 않는다', async ({ + page, + pageManageToken, + }) => { + await authenticatePage(page, pageManageToken); + + // 1) 수정 폼 진입 — 기존 페이지의 제목/슬러그/첨부가 채워진 상태 + await page.goto(EDIT_URL); + await expect(page.getByTestId('page-title-input')).not.toHaveValue(''); + + // 2) 등록 폼으로 SPA 전환 (같은 layout_name 재사용 경로) + await page.getByTestId('page-nav-create').click(); + await expect(page).toHaveURL(new RegExp('/admin/pages/create$')); + + // 3) 이전 값이 잔존하지 않아야 함 + await expect(page.getByTestId('page-title-input')).toHaveValue(''); + await expect(page.getByTestId('page-slug-input')).toHaveValue(''); + await expect(page.getByTestId('page-attachment-item')).toHaveCount(0); + }); + + test('등록 폼 진입 직후 슬러그 미입력이면 미리보기가 /page/example 로 표시된다', async ({ + page, + pageCreateToken, + }) => { + await authenticatePage(page, pageCreateToken); + await page.goto(CREATE_URL); + + // 빈 슬러그에서 {{slug}} 미치환이 아니라 example fallback + await expect(page.getByTestId('page-slug-hint')).toContainText('/page/example'); + await expect(page.getByTestId('page-slug-hint')).not.toContainText('{{slug}}'); + }); + + test('등록 폼에서 슬러그를 입력하면 미리보기가 입력값으로 즉시 치환된다', async ({ + page, + pageCreateToken, + }) => { + await authenticatePage(page, pageCreateToken); + await page.goto(CREATE_URL); + + await page.getByTestId('page-slug-input').fill('my-new-page'); + await expect(page.getByTestId('page-slug-hint')).toContainText('/page/my-new-page'); + }); +}); \ No newline at end of file diff --git a/modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-version-changed-fields-i18n.spec.ts b/modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-version-changed-fields-i18n.spec.ts new file mode 100644 index 00000000..29581c77 --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-version-changed-fields-i18n.spec.ts @@ -0,0 +1,42 @@ +/** + * 페이지 상세 버전 이력의 변경 필드명 다국어 라벨 렌더 + * (placeholder — data-testid 보강 후 활성화). + * + * @scenario page_change=update + * @effects page_version_changed_fields_render_localized_labels + * + * e2e:allow 페이지 상세의 버전 이력 "변경 항목" 이 필드 키(title/slug/content 등) 원문 대신 + * 다국어 라벨로 표시되는 동작을 레이아웃 렌더 테스트가 구조적으로 커버한다. 본 + * placeholder spec(test.describe.skip)은 data-testid 보강 후 활성화된다. + * 레이아웃 렌더링 테스트(admin-page-layouts.test.ts > "변경내역 필드명 다국어 매핑")가 + * changes_summary.changed_fields 를 + * $t('sirsoft-page.admin.page.detail.versions.field_labels.') 로 매핑함을 + * 구조적으로 회귀 차단한다. 브라우저 실제 로케일 렌더만 본 spec 이 확인한다. + * + * 본 spec 은 다음 사전 작업 완료 후 활성화한다 (data-testid 보강): + * 1. 버전 이력 행의 변경 항목 셀에 data-testid="page-version-changed-fields" + * 2. seededPage fixture(playwright:seed-page) 실 시드 로직 구현 (버전 이력 있는 페이지) + * 3. test.describe.skip → test.describe 변경 + * + * 매트릭스(시나리오 매니페스트 page-attachment-order-and-seo-cache.yaml 의 page_change=update 와 1:1): + * - update 후 상세 진입: 변경 항목이 필드 키 원문이 아닌 다국어 라벨로 표시 + */ +import { test, expect, authenticatePage } from '../../fixtures/page-auth'; + +const DETAIL_URL = '/admin/pages/1'; + +test.describe.skip('페이지 버전 변경 필드 다국어 라벨 (placeholder — data-testid 보강 후 활성화)', () => { + test('버전 이력의 변경 항목이 필드 키 원문 대신 다국어 라벨로 표시된다', async ({ + page, + pageReadToken, + }) => { + await authenticatePage(page, pageReadToken); + await page.goto(DETAIL_URL); + + const changedFields = page.getByTestId('page-version-changed-fields').first(); + // 필드 키 원문(title/slug/content)이 그대로 노출되지 않아야 함 + await expect(changedFields).not.toHaveText(/\b(title|slug|content)\b/); + // 다국어 라벨(예: 제목/주소/내용)로 렌더 — 활성화 시 구체 라벨 assert + await expect(changedFields).not.toHaveText(''); + }); +}); \ No newline at end of file diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php index 8d33142d..5e595919 100644 --- a/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php @@ -136,7 +136,7 @@ class SeoPageCacheListenerTest extends ModuleTestCase } /** - * onPageChange 호출 시 페이지 상세 URL 캐시가 URL 패턴으로 무효화되는지 확인합니다 (이슈 #424 12-2). + * onPageChange 호출 시 페이지 상세 URL 캐시가 URL 패턴으로 무효화되는지 확인합니다. * * 페이지 공개 URL은 단수형 /page/{slug} 이다 (PageSitemapContributor, SearchPagesListener 동일). * 레이아웃 태그 없이 put()으로 캐시된 항목은 invalidateByLayout('page/show')로는 지워지지 않으므로, diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageAttachmentServiceTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageAttachmentServiceTest.php index 712cb88b..20fa5fa2 100644 --- a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageAttachmentServiceTest.php +++ b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageAttachmentServiceTest.php @@ -158,7 +158,7 @@ class PageAttachmentServiceTest extends ModuleTestCase } /** - * 연결 시 업로드(조회) 순서대로 order가 1..N 재부여되는지 확인 (이슈 #424 6-2) + * 연결 시 업로드(조회) 순서대로 order가 1..N 재부여되는지 확인 * * 이커머스 ProductImageService::linkTempImages 와 동일하게, getByTempKey 가 * order 순으로 반환한 순서대로 order 를 1,2,3 으로 재배치한다. (생성 모드의 diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php index abae047a..24a49509 100644 --- a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php +++ b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php @@ -218,7 +218,7 @@ class PageServiceTest extends ModuleTestCase } /** - * 일괄 발행 시 after_publish 훅이 페이지 수만큼 발화되는지 확인 (이슈 #424-19 회귀) + * 일괄 발행 시 after_publish 훅이 페이지 수만큼 발화되는지 확인 (회귀) * * 일괄변경 경로가 after_publish 훅을 발화하지 않으면 활동로그가 기록되지 않는다. * 단일 발행(changePublishStatus)과 동일하게 페이지별(per-item) 발화를 보장한다. @@ -254,7 +254,7 @@ class PageServiceTest extends ModuleTestCase } /** - * 일괄 미발행 시 after_publish 훅이 published=false 로 발화되는지 확인 (이슈 #424-19 회귀) + * 일괄 미발행 시 after_publish 훅이 published=false 로 발화되는지 확인 (회귀) */ public function test_bulk_change_publish_status_fires_unpublish_hook_per_page(): void { @@ -277,7 +277,7 @@ class PageServiceTest extends ModuleTestCase } /** - * 일괄 처리 도중 한 건이라도 실패하면 전체 롤백되는지 확인 (이슈 #424-19 회귀) + * 일괄 처리 도중 한 건이라도 실패하면 전체 롤백되는지 확인 (회귀) * * all-or-nothing: 존재하지 않는 id 가 섞이면 정상 페이지도 발행되지 않아야 한다. */ diff --git a/modules/_bundled/sirsoft-page/tests/scenarios/page-attachment-order-and-seo-cache.yaml b/modules/_bundled/sirsoft-page/tests/scenarios/page-attachment-order-and-seo-cache.yaml index d05b3d1b..bf40d02b 100644 --- a/modules/_bundled/sirsoft-page/tests/scenarios/page-attachment-order-and-seo-cache.yaml +++ b/modules/_bundled/sirsoft-page/tests/scenarios/page-attachment-order-and-seo-cache.yaml @@ -1,9 +1,21 @@ -# audit:allow test-scenario-coverage reason: 본 매니페스트는 이슈 #424 (6-1/6-2/12-2) 회귀 매트릭스 SSoT 로 기록. 핵심 회귀 가드는 test_files 의 통과 테스트(백엔드 PHPUnit + 프론트 vitest)로 커버하며, 첨부 순서/URL 동작은 데이터 흐름 결함이라 단위 계층이 정확한 검증 위치 (브라우저 렌더 결함 아님). - -feature: 페이지 첨부 컨텍스트 URL / 다중 업로드 최종 순서 저장 / 페이지 변경 시 SEO 캐시 무효화 (이슈 #424) +# audit:allow test-scenario-coverage reason: | +# 본 매니페스트는 서로 독립적인 5개 페이지 검수 결함(첨부 URL 컨텍스트 6-1 / 첨부 순서 6-2 / +# 수정→등록 폼 잔존 7 / SEO 캐시 무효화 12-2 / 버전 변경 필드 다국어 라벨 13)을 한 회귀 +# 매트릭스로 묶어 기록한다. 축(url_context/form_mode/upload_count/page_change/cached_entry)은 +# 결함별로 서로 무관해 full cross product(48케이스)에 "public × delete × multiple" 같은 +# 무의미 조합이 다수 포함되므로 docblock @scenario 전수 마킹이 부적합하다. +# 실제 회귀 가드는 test_files 의 통과 테스트가 결함별로 담당한다: +# 6-1 → PageControllerTest(Feature) / 6-2 → PageAttachmentServiceTest(Unit) + +# admin-page-layouts.test.ts(레이아웃 렌더) / 7 → admin-page-layouts.test.ts / +# 12-2 → SeoPageCacheListenerTest(Unit) / 13 → admin-page-layouts.test.ts. +# E2E: 페이지 모듈에 Playwright 인프라를 이커머스와 동일 패턴으로 신설(config + page-auth/ +# page-seed fixture + playwright:seed-page stub)하고 spec 3건을 form_mode/upload_count/ +# page_change axis 와 1:1 매핑해 등록했으며, 대상 화면 data-testid 보강 전까지 skip placeholder +# 로 운용한다(이커머스 product-seo-input-debounce 와 동일 운용). +feature: 페이지 첨부 컨텍스트 URL / 다중 업로드 최종 순서 저장 / 페이지 변경 시 SEO 캐시 무효화 / 수정→등록 폼 잔존 차단 / 버전 변경 필드 다국어 라벨 description: | - 이슈 #424 의 3개 결함을 한 매트릭스로 묶어 회귀를 보장한다. + 페이지 검수 오류 회귀를 한 매트릭스로 묶어 보장한다. 6-1 (첨부 컨텍스트 URL): - 관리자 화면은 관리자 전용 다운로드/미리보기 URL, 공개 화면은 공개 URL 을 사용해야 한다. @@ -30,6 +42,12 @@ description: | - 이전 패턴은 복수형 */pages/{slug} 라 실제 URL 과 매칭되지 않아, 레이아웃 태그 없이 put() 으로 캐시된 상세 캐시가 영구히 남았다 → */page/{slug} 로 정정. + 13 (버전 변경 필드 다국어 라벨): + - 페이지 상세 버전 이력의 "변경 항목" 이 필드 키(title/slug/content 등) 원문 대신 다국어 + 라벨로 표시돼야 한다. + - changes_summary.changed_fields 를 + $t('sirsoft-page.admin.page.detail.versions.field_labels.') 로 매핑. + axes: url_context: [public, admin] form_mode: [create, edit] @@ -49,17 +67,26 @@ effects: - page_change_invalidates_url_only_cached_detail_without_layout_tag - page_change_invalidates_page_show_layout_cache - page_change_invalidates_home_and_search_and_sitemap_cache + - page_version_changed_fields_render_localized_labels test_files: - modules/_bundled/sirsoft-page/tests/Unit/Services/PageAttachmentServiceTest.php - modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php - modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php - modules/_bundled/sirsoft-page/resources/js/__tests__/layouts/admin-page-layouts.test.ts + - modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-form-reset-on-route-switch.spec.ts + - modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-attachment-order.spec.ts + - modules/_bundled/sirsoft-page/tests/Playwright/specs/admin/page-version-changed-fields-i18n.spec.ts notes: | 6-2 는 이커머스 상품 이미지(FileUploader + temp_key 연결 + reorder API)와 동일한 검증된 패턴으로 정렬했다. 생성 모드 업로드 순서 연결은 PageAttachmentServiceTest, 수정 모드 reorder 와 - 잔존 버그(라우트 전환 시 key 리마운트)는 admin_page_form 레이아웃 + 브라우저 검수로 확인. - 6-1 은 백엔드 Feature 테스트, 12-2 는 리스너 unit 테스트로 커버. - E2E 면제 사유: 6-2 데이터 흐름은 단위/Feature 계층이 정확한 검증 위치이고, 페이지 모듈에 - Playwright 인프라(토큰/스펙/베이스) 미구축 상태로 spec 1건을 위한 인프라 신설은 본 이슈 범위를 초과한다. + 잔존 버그(라우트 전환 시 key 리마운트)는 admin_page_form 레이아웃 렌더 테스트로 확인. + 6-1 은 백엔드 Feature 테스트, 12-2 는 리스너 unit 테스트, 13 은 레이아웃 렌더 테스트로 커버. + + E2E: 페이지 모듈에 Playwright 인프라(playwright.config.ts + page-auth/page-seed fixture + + playwright:seed-page stub 커맨드)를 이커머스와 동일 패턴으로 신설했다. spec 3건은 + form_mode / upload_count / page_change axis 와 1:1 매핑되며, 대상 화면에 data-testid 가 + 아직 없어 test.describe.skip placeholder 로 등록(각 spec docblock 에 활성화 절차 명시). + 실제 회귀 가드는 위 단위/Feature/레이아웃 렌더 테스트가 담당하고, spec 은 data-testid 보강 + + seed 로직 구현 후 활성화된다 (이커머스 product-seo-input-debounce 등과 동일 운용).