From b03042b6d7b4f56a2ca02044906a3ee0093687db Mon Sep 17 00:00:00 2001 From: chym1217 Date: Mon, 29 Jun 2026 16:11:16 +0900 Subject: [PATCH] =?UTF-8?q?fix(page):=20=ED=8E=98=EC=9D=B4=EC=A7=80=C2=B7?= =?UTF-8?q?=EC=B2=A8=EB=B6=80=20=EC=82=AD=EC=A0=9C=EB=A5=BC=20=EB=AC=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=82=AD=EC=A0=9C=EB=A1=9C=20=EC=A0=84=ED=99=98?= =?UTF-8?q?=ED=95=B4=20=EC=8A=AC=EB=9F=AC=EA=B7=B8=20=EC=9E=AC=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 페이지 삭제 후 동일 슬러그로 재생성 시 "이미 사용 중" 오류가 나던 문제 수정. 슬러그 중복 체크(SoftDeletes 스코프로 삭제 행 제외)와 생성 검증(Rule::unique로 삭제 행 포함)이 soft delete를 다르게 취급해 발생. Page/PageAttachment의 SoftDeletes를 제거하고 deleted_at 컬럼을 마이그레이션으로 정리해 두 경로를 정합화. --- modules/_bundled/sirsoft-page/CHANGELOG.md | 4 + ...001_drop_soft_deletes_from_pages_table.php | 44 +++++++ ...ft_deletes_from_page_attachments_table.php | 47 +++++++ .../_bundled/sirsoft-page/src/Models/Page.php | 2 - .../src/Models/PageAttachment.php | 2 - .../Admin/PageAttachmentControllerTest.php | 2 +- .../Feature/Admin/PageControllerTest.php | 120 +++++++++++++++++- .../Repositories/PageRepositorySearchTest.php | 2 +- .../Services/PageAttachmentServiceTest.php | 4 +- .../tests/Unit/Services/PageServiceTest.php | 6 +- 10 files changed, 218 insertions(+), 15 deletions(-) create mode 100644 modules/_bundled/sirsoft-page/database/migrations/2026_06_29_000001_drop_soft_deletes_from_pages_table.php create mode 100644 modules/_bundled/sirsoft-page/database/migrations/2026_06_29_000002_drop_soft_deletes_from_page_attachments_table.php diff --git a/modules/_bundled/sirsoft-page/CHANGELOG.md b/modules/_bundled/sirsoft-page/CHANGELOG.md index a0a155e7..57359782 100644 --- a/modules/_bundled/sirsoft-page/CHANGELOG.md +++ b/modules/_bundled/sirsoft-page/CHANGELOG.md @@ -22,6 +22,10 @@ - 페이지 등록 / 수정 화면의 입력 행 컨테이너를 sirsoft-admin_basic 표준 시맨틱 (.row-stack) 과 정합 — 행 사이 옅은 회색 구분선(1px) 이 자동 적용되어 페이지 폼의 행 묶음 가독성 개선. +### Fixed + +- 페이지를 삭제한 뒤 같은 슬러그(주소)로 새 페이지를 다시 만들 수 있도록 수정했습니다. 이전에는 삭제한 페이지의 슬러그가 남아 "이미 사용 중"으로 표시되어 동일 주소로 페이지를 다시 만들 수 없었습니다. (#424 @gnuboard 님께서 제보해주셨습니다.) + ## [1.0.0-beta.3] - 2026-05-11 ### Changed diff --git a/modules/_bundled/sirsoft-page/database/migrations/2026_06_29_000001_drop_soft_deletes_from_pages_table.php b/modules/_bundled/sirsoft-page/database/migrations/2026_06_29_000001_drop_soft_deletes_from_pages_table.php new file mode 100644 index 00000000..5f4a2957 --- /dev/null +++ b/modules/_bundled/sirsoft-page/database/migrations/2026_06_29_000001_drop_soft_deletes_from_pages_table.php @@ -0,0 +1,44 @@ +whereNotNull('deleted_at')->delete(); + + Schema::table('pages', function (Blueprint $table) { + // 소프트 삭제 컬럼 제거 (페이지는 물리 삭제 방식으로 전환) + $table->dropSoftDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (! Schema::hasTable('pages') || Schema::hasColumn('pages', 'deleted_at')) { + return; + } + + Schema::table('pages', function (Blueprint $table) { + // 소프트 삭제 컬럼 복원 + $table->softDeletes(); + }); + } +}; diff --git a/modules/_bundled/sirsoft-page/database/migrations/2026_06_29_000002_drop_soft_deletes_from_page_attachments_table.php b/modules/_bundled/sirsoft-page/database/migrations/2026_06_29_000002_drop_soft_deletes_from_page_attachments_table.php new file mode 100644 index 00000000..2f5c0cec --- /dev/null +++ b/modules/_bundled/sirsoft-page/database/migrations/2026_06_29_000002_drop_soft_deletes_from_page_attachments_table.php @@ -0,0 +1,47 @@ +whereNotNull('deleted_at')->delete(); + + Schema::table('page_attachments', function (Blueprint $table) { + // deleted_at 인덱스 제거 (소프트 삭제 미사용으로 불필요) + $table->dropIndex(['deleted_at']); + // 소프트 삭제 컬럼 제거 (첨부는 물리 삭제 방식으로 전환) + $table->dropSoftDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (! Schema::hasTable('page_attachments') || Schema::hasColumn('page_attachments', 'deleted_at')) { + return; + } + + Schema::table('page_attachments', function (Blueprint $table) { + // 소프트 삭제 컬럼 복원 + $table->softDeletes()->comment('소프트 삭제 일시'); + // deleted_at 인덱스 복원 + $table->index('deleted_at'); + }); + } +}; diff --git a/modules/_bundled/sirsoft-page/src/Models/Page.php b/modules/_bundled/sirsoft-page/src/Models/Page.php index 863f39de..f00a4d5b 100644 --- a/modules/_bundled/sirsoft-page/src/Models/Page.php +++ b/modules/_bundled/sirsoft-page/src/Models/Page.php @@ -11,7 +11,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\SoftDeletes; use Laravel\Scout\Searchable; use Modules\Sirsoft\Page\Database\Factories\PageFactory; @@ -19,7 +18,6 @@ class Page extends Model implements FulltextSearchable { use HasFactory; use Searchable; - use SoftDeletes; /** @var array 활동 로그 추적 필드 */ public static array $activityLogFields = [ diff --git a/modules/_bundled/sirsoft-page/src/Models/PageAttachment.php b/modules/_bundled/sirsoft-page/src/Models/PageAttachment.php index 46ae7632..47876aaa 100644 --- a/modules/_bundled/sirsoft-page/src/Models/PageAttachment.php +++ b/modules/_bundled/sirsoft-page/src/Models/PageAttachment.php @@ -6,13 +6,11 @@ use App\Models\User; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\SoftDeletes; use Modules\Sirsoft\Page\Database\Factories\PageAttachmentFactory; class PageAttachment extends Model { use HasFactory; - use SoftDeletes; /** * 팩토리 클래스를 반환합니다. diff --git a/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageAttachmentControllerTest.php b/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageAttachmentControllerTest.php index 0c92732a..afa34550 100644 --- a/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageAttachmentControllerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageAttachmentControllerTest.php @@ -174,7 +174,7 @@ class PageAttachmentControllerTest extends FeatureTestCase $response->assertStatus(200) ->assertJsonPath('success', true); - $this->assertSoftDeleted('page_attachments', ['id' => $attachment->id]); + $this->assertDatabaseMissing('page_attachments', ['id' => $attachment->id]); } /** diff --git a/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php b/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php index ce42bb81..bbf0896c 100644 --- a/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php @@ -14,6 +14,7 @@ use App\Models\User; use App\Search\Engines\DatabaseFulltextEngine; use Laravel\Scout\EngineManager; use Modules\Sirsoft\Page\Models\Page; +use Modules\Sirsoft\Page\Models\PageAttachment; use Modules\Sirsoft\Page\Models\PageVersion; use Modules\Sirsoft\Page\Tests\FeatureTestCase; @@ -383,7 +384,7 @@ class PageControllerTest extends FeatureTestCase $response->assertStatus(200); $this->assertDatabaseHas('pages', ['id' => $page->id, 'slug' => 'test-slug-new']); - $this->assertDatabaseMissing('pages', ['slug' => 'test-slug-old', 'deleted_at' => null]); + $this->assertDatabaseMissing('pages', ['slug' => 'test-slug-old']); } /** @@ -593,7 +594,7 @@ class PageControllerTest extends FeatureTestCase // ─── 삭제 (destroy) ──────────────────────────────── /** - * 페이지를 삭제(소프트 삭제)할 수 있는지 확인 + * 페이지를 삭제(물리 삭제)할 수 있는지 확인 */ public function test_admin_can_delete_page(): void { @@ -609,8 +610,8 @@ class PageControllerTest extends FeatureTestCase $response->assertStatus(200) ->assertJsonPath('success', true); - // 소프트 삭제 확인 - $this->assertSoftDeleted('pages', ['id' => $page->id]); + // 물리 삭제 확인 (soft delete 미사용 — 행이 실제로 제거됨) + $this->assertDatabaseMissing('pages', ['id' => $page->id]); } /** @@ -624,6 +625,117 @@ class PageControllerTest extends FeatureTestCase $response->assertStatus(404); } + /** + * 페이지 삭제 후 동일 슬러그로 재생성할 수 있는지 확인 (#424-20 회귀) + * + * soft delete 시절에는 삭제된 페이지의 slug 가 잔존하여 동일 slug 재생성 시 + * Rule::unique 가 422 를 반환했다. hard delete 전환으로 재생성이 성공해야 한다. + */ + public function test_can_recreate_page_with_same_slug_after_delete(): void + { + $slug = 'test-reuse-slug'; + + // 1) 최초 생성 + $first = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-page/admin/pages', [ + 'slug' => $slug, + 'title' => ['ko' => '원본 페이지', 'en' => 'Original'], + 'content' => ['ko' => '본문', 'en' => 'Content'], + 'content_mode' => 'html', + ]); + $first->assertStatus(201); + $firstId = $first->json('data.id'); + + // 2) 삭제 + $this->actingAs($this->adminUser) + ->deleteJson("/api/modules/sirsoft-page/admin/pages/{$firstId}") + ->assertStatus(200); + + $this->assertDatabaseMissing('pages', ['id' => $firstId]); + + // 3) 동일 slug 로 재생성 — 성공해야 함 + $second = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-page/admin/pages', [ + 'slug' => $slug, + 'title' => ['ko' => '재생성 페이지', 'en' => 'Recreated'], + 'content' => ['ko' => '본문', 'en' => 'Content'], + 'content_mode' => 'html', + ]); + + $second->assertStatus(201) + ->assertJsonPath('data.slug', $slug); + } + + /** + * 페이지 삭제 후 슬러그 중복 체크와 생성 검증 결과가 일치하는지 확인 (#424-20 회귀) + * + * 삭제 후 check-slug 는 "사용 가능", 생성도 성공해야 한다 (두 경로 정합). + */ + public function test_check_slug_and_create_are_consistent_after_delete(): void + { + $slug = 'test-consistent-slug'; + + $first = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-page/admin/pages', [ + 'slug' => $slug, + 'title' => ['ko' => '원본', 'en' => 'Original'], + 'content_mode' => 'html', + ]); + $first->assertStatus(201); + $firstId = $first->json('data.id'); + + $this->actingAs($this->adminUser) + ->deleteJson("/api/modules/sirsoft-page/admin/pages/{$firstId}") + ->assertStatus(200); + + // check-slug 는 사용 가능(exists=false)으로 응답 + $check = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-page/admin/pages/check-slug', [ + 'slug' => $slug, + ]); + $check->assertStatus(200) + ->assertJsonPath('data.exists', false); + + // 생성도 성공 (check-slug 와 동일 결론) + $create = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-page/admin/pages', [ + 'slug' => $slug, + 'title' => ['ko' => '재생성', 'en' => 'Recreated'], + 'content_mode' => 'html', + ]); + $create->assertStatus(201); + } + + /** + * 첨부가 있는 페이지를 물리 삭제해도 삭제 흐름(첨부 정리 + 활동로그 기록)이 정상 완료되는지 확인 (#424-20) + * + * 활동로그 리스너는 삭제되는 페이지를 loggable 로 참조한다. hard delete 후 loggable 행이 + * 사라지더라도 삭제 응답이 200 이고 페이지/첨부 행이 모두 물리 삭제되어야 한다. + */ + public function test_deleting_page_with_attachment_completes_without_error(): void + { + $page = Page::factory()->create([ + 'slug' => 'test-delete-with-attach', + 'created_by' => $this->adminUser->id, + 'updated_by' => $this->adminUser->id, + ]); + + $attachment = PageAttachment::factory()->create([ + 'page_id' => $page->id, + 'created_by' => $this->adminUser->id, + ]); + + $response = $this->actingAs($this->adminUser) + ->deleteJson("/api/modules/sirsoft-page/admin/pages/{$page->id}"); + + $response->assertStatus(200) + ->assertJsonPath('success', true); + + // 페이지 + 첨부 모두 물리 삭제 + $this->assertDatabaseMissing('pages', ['id' => $page->id]); + $this->assertDatabaseMissing('page_attachments', ['id' => $attachment->id]); + } + // ─── 발행 토글 (publish) ─────────────────────────── /** diff --git a/modules/_bundled/sirsoft-page/tests/Feature/Repositories/PageRepositorySearchTest.php b/modules/_bundled/sirsoft-page/tests/Feature/Repositories/PageRepositorySearchTest.php index 57c3b709..6ca03668 100644 --- a/modules/_bundled/sirsoft-page/tests/Feature/Repositories/PageRepositorySearchTest.php +++ b/modules/_bundled/sirsoft-page/tests/Feature/Repositories/PageRepositorySearchTest.php @@ -68,7 +68,7 @@ class PageRepositorySearchTest extends TestCase */ protected function tearDown(): void { - Page::query()->withTrashed()->forceDelete(); + Page::query()->forceDelete(); if (isset($this->user)) { User::where('id', $this->user->id)->delete(); diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageAttachmentServiceTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageAttachmentServiceTest.php index 61a54acf..fc113226 100644 --- a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageAttachmentServiceTest.php +++ b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageAttachmentServiceTest.php @@ -162,7 +162,7 @@ class PageAttachmentServiceTest extends ModuleTestCase /** * 첨부파일을 삭제할 수 있는지 확인 */ - public function test_delete_attachment_soft_deletes(): void + public function test_delete_attachment_hard_deletes(): void { $page = Page::factory()->create([ 'slug' => 'test-svc-del-attach', @@ -176,7 +176,7 @@ class PageAttachmentServiceTest extends ModuleTestCase $result = $this->service->deleteAttachment($attachment); $this->assertTrue($result); - $this->assertSoftDeleted('page_attachments', ['id' => $attachment->id]); + $this->assertDatabaseMissing('page_attachments', ['id' => $attachment->id]); } // ─── reorder ─────────────────────────────────────── diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php index d9966f4d..2a7dabd9 100644 --- a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php +++ b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php @@ -144,16 +144,16 @@ class PageServiceTest extends ModuleTestCase // ─── deletePage ──────────────────────────────────── /** - * 페이지를 소프트 삭제할 수 있는지 확인 + * 페이지를 물리 삭제할 수 있는지 확인 */ - public function test_delete_page_soft_deletes(): void + public function test_delete_page_hard_deletes(): void { $page = $this->createTestPage('test-svc-delete'); $result = $this->service->deletePage($page); $this->assertTrue($result); - $this->assertSoftDeleted('pages', ['id' => $page->id]); + $this->assertDatabaseMissing('pages', ['id' => $page->id]); } // ─── changePublishStatus ───────────────────────────