diff --git a/modules/_bundled/sirsoft-page/CHANGELOG.md b/modules/_bundled/sirsoft-page/CHANGELOG.md index 6ec667c0..7b856326 100644 --- a/modules/_bundled/sirsoft-page/CHANGELOG.md +++ b/modules/_bundled/sirsoft-page/CHANGELOG.md @@ -30,6 +30,7 @@ - 새 페이지 등록 화면에서 슬러그를 입력하기 전 주소 미리보기가 "/page/{{slug}}" 처럼 표시되던 문제를 수정했습니다. 이제 입력 전에는 "/page/example" 로 보이고, 입력하면 입력한 주소로 즉시 바뀝니다. (#424 @gnuboard 님께서 제보해주셨습니다.) - 관리자 화면에서 페이지 첨부파일의 다운로드 / 미리보기가 관리자 전용 주소로 열리도록 수정했습니다. 이전에는 관리자 화면에서도 공개용 주소가 사용되어 비공개 페이지의 첨부파일을 관리자가 열지 못하는 경우가 있었습니다. (#424 @gnuboard 님께서 제보해주셨습니다.) - 페이지를 수정하거나 삭제한 직후 해당 페이지 상세 화면의 검색엔진용 캐시가 즉시 갱신되도록 수정했습니다. 이전에는 일부 캐시가 갱신되지 않아 변경 전 내용이 검색엔진에 남는 경우가 있었습니다. (#424 @gnuboard 님께서 제보해주셨습니다.) +- 페이지 목록에서 여러 페이지를 한 번에 발행 / 미발행할 때 활동 이력에 발행 여부가 기록되지 않던 문제를 수정했습니다. 이제 일괄 처리에서도 페이지별로 발행 / 미발행 기록이 남습니다. (#424 @gnuboard 님께서 제보해주셨습니다.) ## [1.0.0-beta.3] - 2026-05-11 diff --git a/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageController.php b/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageController.php index ead898eb..fae71deb 100644 --- a/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageController.php +++ b/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageController.php @@ -212,9 +212,12 @@ class PageController extends AdminBaseController $validated = $request->validated(); $count = $this->pageService->bulkChangePublishStatus($validated['ids'], $validated['published']); - return $this->success('sirsoft-page::messages.page.bulk_publish_success', [ - 'count' => $count, - ]); + return $this->success( + 'sirsoft-page::messages.page.bulk_publish_success', + ['count' => $count], + 200, + ['count' => $count], + ); } catch (\Exception $e) { return $this->error('sirsoft-page::messages.page.bulk_publish_failed', 500, $e->getMessage()); } diff --git a/modules/_bundled/sirsoft-page/src/Repositories/Contracts/PageRepositoryInterface.php b/modules/_bundled/sirsoft-page/src/Repositories/Contracts/PageRepositoryInterface.php index 0450f7a0..7560151e 100644 --- a/modules/_bundled/sirsoft-page/src/Repositories/Contracts/PageRepositoryInterface.php +++ b/modules/_bundled/sirsoft-page/src/Repositories/Contracts/PageRepositoryInterface.php @@ -97,13 +97,4 @@ interface PageRepositoryInterface * @return int 일치하는 페이지 수 */ public function countByKeyword(string $keyword): int; - - /** - * 여러 페이지의 발행 상태를 일괄 변경합니다. - * - * @param array $ids 페이지 ID 목록 - * @param array $data 업데이트할 데이터 (published, updated_by 등) - * @return int 변경된 페이지 수 - */ - public function bulkUpdatePublished(array $ids, array $data): int; } diff --git a/modules/_bundled/sirsoft-page/src/Repositories/PageRepository.php b/modules/_bundled/sirsoft-page/src/Repositories/PageRepository.php index 58094387..a7313e74 100644 --- a/modules/_bundled/sirsoft-page/src/Repositories/PageRepository.php +++ b/modules/_bundled/sirsoft-page/src/Repositories/PageRepository.php @@ -289,18 +289,6 @@ class PageRepository implements PageRepositoryInterface } } - /** - * 여러 페이지의 발행 상태를 일괄 변경합니다. - * - * @param array $ids 페이지 ID 목록 - * @param array $data 업데이트할 데이터 (published, updated_by 등) - * @return int 변경된 페이지 수 - */ - public function bulkUpdatePublished(array $ids, array $data): int - { - return Page::whereIn('id', $ids)->update($data); - } - /** * filters 배열을 search/search_field로 정규화합니다. * diff --git a/modules/_bundled/sirsoft-page/src/Services/PageService.php b/modules/_bundled/sirsoft-page/src/Services/PageService.php index 4e8a09bc..5591994b 100644 --- a/modules/_bundled/sirsoft-page/src/Services/PageService.php +++ b/modules/_bundled/sirsoft-page/src/Services/PageService.php @@ -211,16 +211,40 @@ class PageService */ public function bulkChangePublishStatus(array $ids, bool $published): int { - $updateData = [ - 'published' => $published, - 'updated_by' => Auth::id(), - ]; - - if ($published) { - $updateData['published_at'] = now(); + if (empty($ids)) { + return 0; } - return $this->pageRepository->bulkUpdatePublished($ids, $updateData); + return DB::transaction(function () use ($ids, $published) { + $userId = Auth::id(); + $count = 0; + + foreach ($ids as $id) { + // 존재하지 않는 페이지가 섞이면 예외 → 트랜잭션 전체 롤백 (all-or-nothing) + $page = $this->pageRepository->findOrFail((int) $id); + + HookManager::doAction('sirsoft-page.page.before_publish', $page, $published); + + $updateData = [ + 'published' => $published, + 'updated_by' => $userId, + ]; + + // 발행 시 published_at 갱신 + if ($published) { + $updateData['published_at'] = now(); + } + + $page = $this->pageRepository->update($page, $updateData); + + // 페이지별 after_publish 발화 → 활동로그 per-item 기록 (이슈 #424-19) + HookManager::doAction('sirsoft-page.page.after_publish', $page, $published); + + $count++; + } + + return $count; + }); } /** diff --git a/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php b/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php index 7588df3e..00171291 100644 --- a/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php @@ -882,6 +882,11 @@ class PageControllerTest extends FeatureTestCase ->assertJsonPath('success', true) ->assertJsonPath('data.count', 3); + // 성공 메시지의 :count 플레이스홀더가 실제 건수로 치환되는지 확인 (이슈 #424-19 회귀) + $message = $response->json('message'); + $this->assertStringNotContainsString(':count', $message, '메시지에 :count 플레이스홀더가 치환되지 않고 남았습니다.'); + $this->assertStringContainsString('3', $message, '메시지에 변경 건수(3)가 포함되어야 합니다.'); + // DB 확인 foreach ($ids as $id) { $this->assertDatabaseHas('pages', ['id' => $id, 'published' => true]); diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php index 2a7dabd9..abae047a 100644 --- a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php +++ b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php @@ -4,6 +4,7 @@ namespace Modules\Sirsoft\Page\Tests\Unit\Services; use App\Enums\PermissionType; use App\Enums\ScopeType; +use App\Extension\HookManager; use App\Helpers\PermissionHelper; use App\Models\Permission; use App\Models\Role; @@ -206,6 +207,105 @@ class PageServiceTest extends ModuleTestCase ); $this->assertEquals(3, $count); + + // 일괄 발행 후 published/published_at 가 실제 갱신되는지 확인 + foreach ($pages as $page) { + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, + 'published' => true, + ]); + } + } + + /** + * 일괄 발행 시 after_publish 훅이 페이지 수만큼 발화되는지 확인 (이슈 #424-19 회귀) + * + * 일괄변경 경로가 after_publish 훅을 발화하지 않으면 활동로그가 기록되지 않는다. + * 단일 발행(changePublishStatus)과 동일하게 페이지별(per-item) 발화를 보장한다. + */ + public function test_bulk_change_publish_status_fires_after_publish_hook_per_page(): void + { + $pages = Page::factory()->count(3)->create([ + 'published' => false, + 'created_by' => $this->adminUser->id, + 'updated_by' => $this->adminUser->id, + ]); + + $firedPageIds = []; + $firedPublishedFlags = []; + HookManager::addAction( + 'sirsoft-page.page.after_publish', + function (Page $page, bool $published) use (&$firedPageIds, &$firedPublishedFlags) { + $firedPageIds[] = $page->id; + $firedPublishedFlags[] = $published; + } + ); + + $this->service->bulkChangePublishStatus($pages->pluck('id')->toArray(), true); + + // 페이지 수만큼 발화 + 각 페이지 id 가 1회씩 + $this->assertCount(3, $firedPageIds); + $this->assertEqualsCanonicalizing( + $pages->pluck('id')->toArray(), + $firedPageIds + ); + // 발행 인자가 모두 true + $this->assertSame([true, true, true], $firedPublishedFlags); + } + + /** + * 일괄 미발행 시 after_publish 훅이 published=false 로 발화되는지 확인 (이슈 #424-19 회귀) + */ + public function test_bulk_change_publish_status_fires_unpublish_hook_per_page(): void + { + $pages = Page::factory()->published()->count(3)->create([ + 'created_by' => $this->adminUser->id, + 'updated_by' => $this->adminUser->id, + ]); + + $firedPublishedFlags = []; + HookManager::addAction( + 'sirsoft-page.page.after_publish', + function (Page $page, bool $published) use (&$firedPublishedFlags) { + $firedPublishedFlags[] = $published; + } + ); + + $this->service->bulkChangePublishStatus($pages->pluck('id')->toArray(), false); + + $this->assertSame([false, false, false], $firedPublishedFlags); + } + + /** + * 일괄 처리 도중 한 건이라도 실패하면 전체 롤백되는지 확인 (이슈 #424-19 회귀) + * + * all-or-nothing: 존재하지 않는 id 가 섞이면 정상 페이지도 발행되지 않아야 한다. + */ + public function test_bulk_change_publish_status_rolls_back_on_failure(): void + { + $pages = Page::factory()->count(2)->create([ + 'published' => false, + 'created_by' => $this->adminUser->id, + 'updated_by' => $this->adminUser->id, + ]); + + $ids = $pages->pluck('id')->toArray(); + $ids[] = 999999; // 존재하지 않는 페이지 id + + try { + $this->service->bulkChangePublishStatus($ids, true); + $this->fail('존재하지 않는 페이지 id 포함 시 예외가 발생해야 한다.'); + } catch (\Throwable $e) { + // 예외 발생 기대 + } + + // 정상 페이지 2건도 발행되지 않아야 함 (전체 롤백) + foreach ($pages as $page) { + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, + 'published' => false, + ]); + } } // ─── restoreVersion ────────────────────────────────