From 2473ca72425b85148a0cdfa625051592ba7880a1 Mon Sep 17 00:00:00 2001 From: chym1217 Date: Wed, 1 Jul 2026 17:09:31 +0900 Subject: [PATCH] =?UTF-8?q?fix(page):=20=EB=B2=84=EC=A0=84=20=EB=B3=B5?= =?UTF-8?q?=EC=9B=90=20=ED=9B=84=20=EA=B2=80=EC=83=89=EC=97=94=EC=A7=84=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=EA=B0=80=20=EC=A6=89=EC=8B=9C=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=EB=90=98=EB=8F=84=EB=A1=9D=20after=5Frestore=20?= =?UTF-8?q?=ED=9B=85=20=EA=B5=AC=EB=8F=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 버전 복원 시 발화되는 after_restore 훅을 SeoPageCacheListener가 구독하지 않아, 복원 후에도 검색봇에 복원 전 버전이 계속 노출되던 문제를 수정. 복원은 콘텐츠 변경의 일종이므로 수정/삭제와 동일한 onPageChange 무효화 정책을 재사용. --- modules/_bundled/sirsoft-page/CHANGELOG.md | 2 +- .../src/Listeners/SeoPageCacheListener.php | 5 + .../Feature/Admin/PageControllerTest.php | 97 +++++++++++++++++++ .../Listeners/SeoPageCacheListenerTest.php | 4 + .../page-attachment-order-and-seo-cache.yaml | 12 ++- 5 files changed, 118 insertions(+), 2 deletions(-) diff --git a/modules/_bundled/sirsoft-page/CHANGELOG.md b/modules/_bundled/sirsoft-page/CHANGELOG.md index 981a88a6..f291d03a 100644 --- a/modules/_bundled/sirsoft-page/CHANGELOG.md +++ b/modules/_bundled/sirsoft-page/CHANGELOG.md @@ -13,7 +13,7 @@ - 페이지를 수정하다가 새 페이지 등록 화면으로 이동했을 때, 직전 페이지의 첨부파일·제목·슬러그와 "사용 가능한 슬러그입니다" 안내가 등록 화면에 그대로 남아 보이던 문제를 수정했습니다. - 새 페이지 등록 화면에서 슬러그를 입력하기 전 주소 미리보기가 "/page/{{slug}}" 처럼 표시되던 문제를 수정했습니다. 이제 입력 전에는 "/page/example" 로 보이고, 입력하면 입력한 주소로 즉시 바뀝니다. - 관리자 화면에서 페이지 첨부파일의 다운로드 / 미리보기가 관리자 전용 주소로 열리도록 수정했습니다. 이전에는 관리자 화면에서도 공개용 주소가 사용되어 비공개 페이지의 첨부파일을 관리자가 열지 못하는 경우가 있었습니다. -- 페이지를 수정하거나 삭제한 직후 해당 페이지 상세 화면의 검색엔진용 캐시가 즉시 갱신되도록 수정했습니다. 이전에는 일부 캐시가 갱신되지 않아 변경 전 내용이 검색엔진에 남는 경우가 있었습니다. +- 페이지를 수정·삭제하거나 이전 버전으로 복원한 직후 해당 페이지 상세 화면의 검색엔진용 캐시가 즉시 갱신되도록 수정했습니다. 이전에는 일부 캐시가 갱신되지 않아 변경 전 내용이 검색엔진에 남는 경우가 있었습니다. - 페이지 목록에서 여러 페이지를 한 번에 발행 / 미발행할 때 활동 이력에 발행 여부가 기록되지 않던 문제를 수정했습니다. 이제 일괄 처리에서도 페이지별로 발행 / 미발행 기록이 남습니다. - 페이지 관리 목록에서 검색 조건 '전체'로 슬러그를 검색해도 결과가 나오지 않던 문제를 수정했습니다 — '전체'는 제목과 슬러그를 함께 검색하고, '제목'을 고르면 제목만 검색됩니다. - 페이지 관리 목록의 페이지당 표시 개수 기본값이 화면 표시(20건)와 실제 조회(15건) 간에 어긋나던 문제를 수정했습니다 — 기본값을 20건으로 통일했습니다. diff --git a/modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php b/modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php index 4b5f407a..1855946d 100644 --- a/modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php +++ b/modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php @@ -35,6 +35,11 @@ class SeoPageCacheListener implements HookListenerInterface 'method' => 'onPageChange', 'priority' => 20, ], + // 버전 복원도 콘텐츠(title/content/seo_meta) 변경이므로 수정과 동일하게 무효화 (이슈 #424-14) + 'sirsoft-page.page.after_restore' => [ + 'method' => 'onPageChange', + 'priority' => 20, + ], ]; } diff --git a/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php b/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php index b4e9e263..3cd7f155 100644 --- a/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php @@ -11,6 +11,7 @@ use App\Helpers\PermissionHelper; use App\Models\Permission; use App\Models\Role; use App\Models\User; +use App\Seo\Contracts\SeoCacheManagerInterface; use Modules\Sirsoft\Page\Models\Page; use Modules\Sirsoft\Page\Models\PageAttachment; use Modules\Sirsoft\Page\Models\PageVersion; @@ -1297,6 +1298,102 @@ class PageControllerTest extends FeatureTestCase $this->assertEquals(3, $page->current_version); // 복원 후 버전 증가 } + /** + * 버전 복원 시 페이지 상세 SEO 캐시가 무효화되는지 확인 (이슈 #424-14) + * + * 복원은 title/content/seo_meta 를 이전 버전으로 되돌리므로 수정과 동일하게 + * 검색봇용 SEO 캐시를 갱신해야 한다. 복원 전 상세 URL 캐시를 적재한 뒤 복원하면 + * after_restore 훅 → SeoPageCacheListener 가 해당 캐시를 무효화해야 한다. + * (회귀 배경: after_restore 미구독으로 복원 후에도 봇에 복원 전 버전이 잔존) + */ + public function test_restore_version_invalidates_page_detail_seo_cache(): void + { + $page = Page::factory()->create([ + 'slug' => 'restore-seo-cache', + 'title' => ['ko' => '현재 제목', 'en' => 'Current Title'], + 'current_version' => 2, + 'created_by' => $this->adminUser->id, + 'updated_by' => $this->adminUser->id, + ]); + + $version1 = PageVersion::create([ + 'page_id' => $page->id, + 'version' => 1, + 'title' => ['ko' => '원래 제목', 'en' => 'Original Title'], + 'content' => ['ko' => '원래 내용', 'en' => 'Original Content'], + 'content_mode' => 'html', + 'created_by' => $this->adminUser->id, + ]); + + PageVersion::create([ + 'page_id' => $page->id, + 'version' => 2, + 'title' => ['ko' => '현재 제목', 'en' => 'Current Title'], + 'content' => ['ko' => '현재 내용', 'en' => 'Current Content'], + 'content_mode' => 'html', + 'created_by' => $this->adminUser->id, + ]); + + // 복원 전 페이지 상세 URL 캐시 적재 (봇 응답 캐시 시뮬레이션) + $cache = app(SeoCacheManagerInterface::class); + $cache->put("/page/{$page->slug}", 'ko', '현재 내용'); + $this->assertContains("/page/{$page->slug}", $cache->getCachedUrls()); + + $response = $this->actingAs($this->adminUser) + ->postJson("/api/modules/sirsoft-page/admin/pages/{$page->id}/versions/{$version1->id}/restore"); + + $response->assertStatus(200); + + // 복원 후 상세 캐시가 무효화되어 봇이 새로 렌더한다 + $this->assertNotContains("/page/{$page->slug}", $cache->getCachedUrls()); + } + + /** + * 버전 복원 후 새 버전이 생성되고 기존 버전 이력이 모두 보존되는지 확인 (이슈 #424-14, 검수항목 3·4) + * + * 복원은 다음 버전 번호로 새 스냅샷을 만들며, 복원 대상을 포함한 기존 버전 이력을 + * 삭제하지 않고 그대로 보존해야 한다. + */ + public function test_restore_version_creates_new_version_and_preserves_history(): void + { + $page = Page::factory()->create([ + 'slug' => 'restore-history-preserved', + 'title' => ['ko' => '현재 제목', 'en' => 'Current Title'], + 'current_version' => 2, + 'created_by' => $this->adminUser->id, + 'updated_by' => $this->adminUser->id, + ]); + + $version1 = PageVersion::create([ + 'page_id' => $page->id, + 'version' => 1, + 'title' => ['ko' => '원래 제목', 'en' => 'Original Title'], + 'content' => ['ko' => '원래 내용', 'en' => 'Original Content'], + 'content_mode' => 'html', + 'created_by' => $this->adminUser->id, + ]); + + PageVersion::create([ + 'page_id' => $page->id, + 'version' => 2, + 'title' => ['ko' => '현재 제목', 'en' => 'Current Title'], + 'content' => ['ko' => '현재 내용', 'en' => 'Current Content'], + 'content_mode' => 'html', + 'created_by' => $this->adminUser->id, + ]); + + $this->actingAs($this->adminUser) + ->postJson("/api/modules/sirsoft-page/admin/pages/{$page->id}/versions/{$version1->id}/restore") + ->assertStatus(200); + + // 기존 버전 1·2 는 보존되고, 복원으로 버전 3 이 새로 생성되어 총 3건 + $versions = PageVersion::where('page_id', $page->id)->orderBy('version')->get(); + $this->assertCount(3, $versions); + $this->assertEquals([1, 2, 3], $versions->pluck('version')->all()); + // 복원 대상(버전 1) 레코드가 삭제되지 않고 남아 있다 + $this->assertNotNull(PageVersion::find($version1->id)); + } + // ─── 인증/권한 차단 ──────────────────────────────── /** diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php index 5e595919..c0bec93f 100644 --- a/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php @@ -40,14 +40,18 @@ class SeoPageCacheListenerTest extends ModuleTestCase $this->assertArrayHasKey('sirsoft-page.page.after_create', $hooks); $this->assertArrayHasKey('sirsoft-page.page.after_update', $hooks); $this->assertArrayHasKey('sirsoft-page.page.after_delete', $hooks); + // 버전 복원도 SEO 캐시 무효화가 필요하다 (이슈 #424-14: 복원 후 봇에 복원 전 버전 잔존 회귀 방지) + $this->assertArrayHasKey('sirsoft-page.page.after_restore', $hooks); $this->assertEquals('onPageChange', $hooks['sirsoft-page.page.after_create']['method']); $this->assertEquals('onPageChange', $hooks['sirsoft-page.page.after_update']['method']); $this->assertEquals('onPageChange', $hooks['sirsoft-page.page.after_delete']['method']); + $this->assertEquals('onPageChange', $hooks['sirsoft-page.page.after_restore']['method']); $this->assertEquals(20, $hooks['sirsoft-page.page.after_create']['priority']); $this->assertEquals(20, $hooks['sirsoft-page.page.after_update']['priority']); $this->assertEquals(20, $hooks['sirsoft-page.page.after_delete']['priority']); + $this->assertEquals(20, $hooks['sirsoft-page.page.after_restore']['priority']); } // ─── onPageChange: 캐시 무효화 상태 검증 ────────────── 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 bf40d02b..6748fd7a 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 @@ -48,11 +48,17 @@ description: | - changes_summary.changed_fields 를 $t('sirsoft-page.admin.page.detail.versions.field_labels.') 로 매핑. + 14 (버전 복원 후 SEO 캐시 무효화): + - 특정 버전으로 복원하면 title/content/seo_meta 가 이전 버전으로 되돌아가므로, 수정과 + 동일하게 페이지 상세 SEO 캐시가 즉시 갱신돼야 한다. + - 회귀: SeoPageCacheListener 가 after_restore 를 미구독해 복원 후에도 봇에 복원 전 버전이 + 잔존 → after_restore 를 onPageChange 로 구독 추가(수정/삭제와 동일 무효화 정책). + axes: url_context: [public, admin] form_mode: [create, edit] upload_count: [single, multiple] - page_change: [create, update, delete] + page_change: [create, update, delete, restore] cached_entry: [layout_tagged, url_only] effects: @@ -67,6 +73,8 @@ 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_restore_invalidates_page_detail_seo_cache + - page_restore_creates_new_version_and_preserves_history - page_version_changed_fields_render_localized_labels test_files: @@ -83,6 +91,8 @@ notes: | 패턴으로 정렬했다. 생성 모드 업로드 순서 연결은 PageAttachmentServiceTest, 수정 모드 reorder 와 잔존 버그(라우트 전환 시 key 리마운트)는 admin_page_form 레이아웃 렌더 테스트로 확인. 6-1 은 백엔드 Feature 테스트, 12-2 는 리스너 unit 테스트, 13 은 레이아웃 렌더 테스트로 커버. + 14 는 리스너 unit 테스트(after_restore 구독 단언) + Admin Feature 테스트(복원 엔드포인트 → + 상세 SEO 캐시 무효화 + 새 버전 생성·이력 보존)로 커버. E2E: 페이지 모듈에 Playwright 인프라(playwright.config.ts + page-auth/page-seed fixture + playwright:seed-page stub 커맨드)를 이커머스와 동일 패턴으로 신설했다. spec 3건은