diff --git a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md index 7a17b2de..e13f46fe 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-template-sirsoft-basic-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 + +- 미발행 페이지 관리자 미리보기 안내 배너(`user.page.preview_banner`) 일본어 번역 추가 — 관리자가 발행 전 페이지를 사용자 화면에서 미리 볼 때의 안내 문구가 일본어 로케일에서 자연스럽게 표시됩니다. + ## [1.0.0] - 2026-07-01 ### Added diff --git a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/user.json b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/user.json index 242d47d7..2213ce25 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/user.json +++ b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/user.json @@ -51,7 +51,8 @@ "published_at": "公開日", "create_page": "ページ作成", "attachments": "添付ファイル", - "download": "ダウンロード" + "download": "ダウンロード", + "preview_banner": "未公開ページをプレビュー中です" }, "mypage": { "page_title": "マイページ", diff --git a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json index 06e3c95a..c79774c2 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json +++ b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json @@ -12,7 +12,7 @@ "en": "G7 template (sirsoft-basic) Japanese language pack (bundled)", "ja": "G7 テンプレート (sirsoft-basic) 日本語 言語パック(バンドル)" }, - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "scope": "template", "target_identifier": "sirsoft-basic", diff --git a/modules/_bundled/sirsoft-page/CHANGELOG.md b/modules/_bundled/sirsoft-page/CHANGELOG.md index 8768c172..981a88a6 100644 --- a/modules/_bundled/sirsoft-page/CHANGELOG.md +++ b/modules/_bundled/sirsoft-page/CHANGELOG.md @@ -19,6 +19,7 @@ - 페이지 관리 목록의 페이지당 표시 개수 기본값이 화면 표시(20건)와 실제 조회(15건) 간에 어긋나던 문제를 수정했습니다 — 기본값을 20건으로 통일했습니다. - 페이지 관리 목록에서 아주 긴 검색어를 입력하면 오류 화면이 뜨던 문제를 수정했습니다 — 검색어 길이를 100자로 제한해 안내 메시지로 처리합니다. - 페이지 상세의 버전 이력에서 "변경내역" 항목이 `title`·`content` 같은 영문 필드명으로 표시되던 문제를 수정했습니다 — 이제 "제목", "내용", "SEO 정보" 처럼 알아보기 쉬운 이름으로 표시됩니다. +- 관리자가 발행 전 페이지를 사용자 화면에서 미리 확인할 수 있게 되었습니다. 미리보기 화면에는 미발행 상태임을 알리는 안내가 표시되며, 비로그인 사용자와 일반 회원에게는 이전과 동일하게 노출되지 않습니다. ## [1.0.0] - 2026-07-01 diff --git a/modules/_bundled/sirsoft-page/src/Console/Commands/PlaywrightSeedPage.php b/modules/_bundled/sirsoft-page/src/Console/Commands/PlaywrightSeedPage.php index 72ffe7e0..5922d8d1 100644 --- a/modules/_bundled/sirsoft-page/src/Console/Commands/PlaywrightSeedPage.php +++ b/modules/_bundled/sirsoft-page/src/Console/Commands/PlaywrightSeedPage.php @@ -2,34 +2,44 @@ namespace Modules\Sirsoft\Page\Console\Commands; +use App\Models\User; use Illuminate\Console\Command; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Page\Models\Page; +use Modules\Sirsoft\Page\Models\PageAttachment; /** - * Playwright E2E 용 페이지 도메인 시드 커맨드 (stub). + * Playwright E2E 용 페이지 도메인 시드 커맨드. * - * 페이지 + 첨부 등 모듈 도메인 데이터를 발급하여 stdout 에 JSON 으로 반환한다. 본 stub 은 - * 시그니처와 보안 가드만 구현되어 있으며, 실제 시드 로직은 페이지 모듈 후속 작업 세션에서 - * Factory 재사용으로 채워질 예정 (이커머스 PlaywrightSeedEcommerce 와 동일 패턴). + * 미발행 페이지 관리자 미리보기(#424-15) E2E 를 위해 고정 슬러그의 미발행/발행 페이지와 + * 첨부를 발급하여 stdout 에 JSON 으로 반환한다. 재실행 시 동일 슬러그를 물리 삭제 후 재생성해 + * 멱등성을 보장한다 (페이지·첨부는 hard delete 모델). * * 보안 가드 (코어 PlaywrightIssueToken 과 동일 3중 패턴): * ① CLI 한정 — `php_sapi_name() === 'cli'` * ② G7_PLAYWRIGHT_BYPASS=1 환경변수 옵트인 * ③ APP_DEBUG=true inline override — production + debug=false 환경에서도 동작 * - * 호출 예시 (활성화 후): - * $env:G7_PLAYWRIGHT_BYPASS='1'; php artisan playwright:seed-page --pages=3 --json + * 호출 예시: + * $env:G7_PLAYWRIGHT_BYPASS='1'; php artisan playwright:seed-page --attachments=2 --json */ class PlaywrightSeedPage extends Command { + /** + * 미리보기 E2E 용 고정 슬러그 (spec 에서 URL 로 사용). + */ + private const UNPUBLISHED_SLUG = 'test-e2e-unpublished-preview'; + + private const PUBLISHED_SLUG = 'test-e2e-published-preview'; + /** * 커맨드 이름 및 시그니처 * * @var string */ protected $signature = 'playwright:seed-page - {--pages=0 : 발급할 페이지 수} - {--attachments=0 : 페이지당 발급할 첨부 수} + {--attachments=2 : 페이지당 발급할 첨부 수} {--json : 결과를 JSON 으로 출력}'; /** @@ -42,9 +52,6 @@ class PlaywrightSeedPage extends Command /** * 커맨드를 실행합니다. * - * 본 stub 은 가드 통과 후 LogicException 을 throw 한다. - * 페이지 모듈 후속 작업 세션에서 stub 본문을 실제 시드 로직으로 교체할 때까지 활성화 차단. - * * @return int 종료 코드 */ public function handle(): int @@ -66,14 +73,67 @@ class PlaywrightSeedPage extends Command // ③ 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 로 유지하거나, 본 커맨드 본문을 실제 시드 로직으로 교체하세요.' - ); + $attachmentsPerPage = max(1, (int) $this->option('attachments')); + + $result = DB::transaction(function () use ($attachmentsPerPage) { + // 재실행 멱등: 고정 슬러그 페이지 물리 삭제 (첨부는 FK 로 함께 정리) + Page::whereIn('slug', [self::UNPUBLISHED_SLUG, self::PUBLISHED_SLUG])->get() + ->each(function (Page $page) { + PageAttachment::where('page_id', $page->id)->delete(); + $page->delete(); + }); + + $author = User::factory()->create(); + + $unpublished = Page::factory()->create([ + 'slug' => self::UNPUBLISHED_SLUG, + 'title' => ['ko' => '미발행 미리보기 대상', 'en' => 'Unpublished Preview Target'], + 'content' => ['ko' => '
미발행 본문
', 'en' => 'Unpublished body
'], + 'published' => false, + 'published_at' => null, + 'created_by' => $author->id, + 'updated_by' => $author->id, + ]); + + $published = Page::factory()->published()->create([ + 'slug' => self::PUBLISHED_SLUG, + 'title' => ['ko' => '발행 대조군', 'en' => 'Published Control'], + 'content' => ['ko' => '발행 본문
', 'en' => 'Published body
'], + 'created_by' => $author->id, + 'updated_by' => $author->id, + ]); + + $attachmentIds = []; + foreach ([$unpublished, $published] as $page) { + for ($i = 1; $i <= $attachmentsPerPage; $i++) { + // 첫 첨부는 이미지(미리보기 URL 생성 대상), 나머지는 일반 파일 + $factory = PageAttachment::factory(); + $factory = $i === 1 ? $factory->image() : $factory; + + $attachment = $factory->create([ + 'page_id' => $page->id, + 'order' => $i, + 'created_by' => $author->id, + ]); + $attachmentIds[] = $attachment->id; + } + } + + return [ + 'unpublished_slug' => self::UNPUBLISHED_SLUG, + 'published_slug' => self::PUBLISHED_SLUG, + 'unpublished_id' => $unpublished->id, + 'published_id' => $published->id, + 'attachment_ids' => $attachmentIds, + ]; + }); + + if ($this->option('json')) { + $this->line(json_encode($result, JSON_UNESCAPED_UNICODE)); + } else { + $this->info('페이지 시드 완료: 미발행='.self::UNPUBLISHED_SLUG.' / 발행='.self::PUBLISHED_SLUG); + } + + return self::SUCCESS; } } diff --git a/modules/_bundled/sirsoft-page/src/Http/Controllers/User/PublicPageController.php b/modules/_bundled/sirsoft-page/src/Http/Controllers/User/PublicPageController.php index 14cdd91d..0eea990e 100644 --- a/modules/_bundled/sirsoft-page/src/Http/Controllers/User/PublicPageController.php +++ b/modules/_bundled/sirsoft-page/src/Http/Controllers/User/PublicPageController.php @@ -2,8 +2,10 @@ namespace Modules\Sirsoft\Page\Http\Controllers\User; +use App\Enums\PermissionType; use App\Http\Controllers\Api\Base\PublicBaseController; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; use Modules\Sirsoft\Page\Http\Resources\PublicPageResource; use Modules\Sirsoft\Page\Services\PageService; @@ -29,13 +31,22 @@ class PublicPageController extends PublicBaseController /** * 슬러그로 발행된 페이지를 조회합니다. * + * 페이지 조회 권한(sirsoft-page.pages.read)을 가진 관리자는 미발행 페이지도 + * 사용자 화면에서 미리볼 수 있습니다(비로그인·일반 회원은 미발행 시 404 유지). + * + * @param Request $request HTTP 요청 * @param string $slug 페이지 슬러그 * @return JsonResponse 페이지 상세 응답 */ - public function show(string $slug): JsonResponse + // audit:allow controller-base-request-injection reason: GET 상세 조회. 인증 사용자만 read-only 참조($request->user())해 미리보기 권한 판정. 검증할 body 없음(slug 는 라우트 파라미터) + public function show(Request $request, string $slug): JsonResponse { try { - $page = $this->pageService->getPublishedPageBySlug($slug); + $user = $request->user(); + $canPreview = $user + && $user->hasPermission('sirsoft-page.pages.read', PermissionType::Admin); + + $page = $this->pageService->getPublishedPageBySlug($slug, $canPreview); if (! $page) { return $this->notFound('sirsoft-page::messages.page.not_found'); @@ -43,9 +54,11 @@ class PublicPageController extends PublicBaseController $page->load('attachments'); + $isPreview = $canPreview && ! $page->published; + return $this->successWithResource( 'sirsoft-page::messages.page.fetch_success', - new PublicPageResource($page) + (new PublicPageResource($page))->withPreview($isPreview) ); } catch (\Exception $e) { return $this->error('sirsoft-page::messages.page.fetch_failed', 500, $e->getMessage()); diff --git a/modules/_bundled/sirsoft-page/src/Http/Resources/PublicPageResource.php b/modules/_bundled/sirsoft-page/src/Http/Resources/PublicPageResource.php index 2bd7abb6..7f5f90a1 100644 --- a/modules/_bundled/sirsoft-page/src/Http/Resources/PublicPageResource.php +++ b/modules/_bundled/sirsoft-page/src/Http/Resources/PublicPageResource.php @@ -13,6 +13,27 @@ use Illuminate\Http\Request; */ class PublicPageResource extends BaseApiResource { + /** + * 미발행 페이지 관리자 미리보기 여부. + * + * true 이면 첨부 URL을 발행 가드가 없는 'admin' 컨텍스트로 생성하여 + * 관리자가 미발행 페이지의 첨부까지 미리볼 수 있도록 합니다. + */ + protected bool $preview = false; + + /** + * 미리보기(미발행 관리자 조회) 여부를 설정합니다. + * + * @param bool $preview 미리보기 여부 + * @return $this + */ + public function withPreview(bool $preview): self + { + $this->preview = $preview; + + return $this; + } + /** * 리소스를 배열로 변환합니다. * @@ -32,6 +53,7 @@ class PublicPageResource extends BaseApiResource ? ($this->content[$locale] ?? $this->content[$fallback] ?? (! empty($this->content) ? array_values($this->content)[0] : '')) : (string) ($this->content ?? ''), 'content_mode' => $this->content_mode ?? 'html', + 'is_preview' => $this->preview, 'published_at' => $this->published_at ? $this->formatDateTimeStringForUser($this->published_at) : null, @@ -39,7 +61,10 @@ class PublicPageResource extends BaseApiResource 'current_version' => $this->current_version, 'attachments' => $this->whenLoaded( 'attachments', - fn () => PageAttachmentResource::collectionFor($this->attachments, 'public') + fn () => PageAttachmentResource::collectionFor( + $this->attachments, + $this->preview ? 'admin' : 'public' + ) ), 'created_at' => $this->created_at ? $this->formatDateTimeStringForUser($this->created_at) diff --git a/modules/_bundled/sirsoft-page/src/Providers/PageServiceProvider.php b/modules/_bundled/sirsoft-page/src/Providers/PageServiceProvider.php index 29252e18..b50de7e5 100644 --- a/modules/_bundled/sirsoft-page/src/Providers/PageServiceProvider.php +++ b/modules/_bundled/sirsoft-page/src/Providers/PageServiceProvider.php @@ -3,12 +3,15 @@ namespace Modules\Sirsoft\Page\Providers; use App\Extension\BaseModuleServiceProvider; +use App\Seo\SitemapGenerator; +use Modules\Sirsoft\Page\Console\Commands\PlaywrightSeedPage; use Modules\Sirsoft\Page\Repositories\Contracts\PageAttachmentRepositoryInterface; use Modules\Sirsoft\Page\Repositories\Contracts\PageRepositoryInterface; use Modules\Sirsoft\Page\Repositories\Contracts\PageVersionRepositoryInterface; use Modules\Sirsoft\Page\Repositories\PageAttachmentRepository; use Modules\Sirsoft\Page\Repositories\PageRepository; use Modules\Sirsoft\Page\Repositories\PageVersionRepository; +use Modules\Sirsoft\Page\Seo\PageSitemapContributor; use Modules\Sirsoft\Page\Services\PageAttachmentService; /** @@ -20,8 +23,6 @@ class PageServiceProvider extends BaseModuleServiceProvider { /** * 모듈 식별자 - * - * @var string */ protected string $moduleIdentifier = 'sirsoft-page'; @@ -47,18 +48,23 @@ class PageServiceProvider extends BaseModuleServiceProvider /** * 서비스 부트스트랩 - * - * @return void */ public function boot(): void { parent::boot(); + // Artisan 커맨드 등록 (콘솔 환경에서만) + if ($this->app->runningInConsole()) { + $this->commands([ + PlaywrightSeedPage::class, + ]); + } + // Sitemap 기여자 등록 $this->app->booted(function () { - if ($this->app->bound(\App\Seo\SitemapGenerator::class)) { - $this->app->make(\App\Seo\SitemapGenerator::class)->registerContributor( - new \Modules\Sirsoft\Page\Seo\PageSitemapContributor() + if ($this->app->bound(SitemapGenerator::class)) { + $this->app->make(SitemapGenerator::class)->registerContributor( + new PageSitemapContributor ); } }); diff --git a/modules/_bundled/sirsoft-page/src/Services/PageService.php b/modules/_bundled/sirsoft-page/src/Services/PageService.php index a96ce93f..05de6c11 100644 --- a/modules/_bundled/sirsoft-page/src/Services/PageService.php +++ b/modules/_bundled/sirsoft-page/src/Services/PageService.php @@ -4,10 +4,13 @@ namespace Modules\Sirsoft\Page\Services; use App\Extension\HookManager; use App\Helpers\PermissionHelper; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Page\Models\Page; +use Modules\Sirsoft\Page\Models\PageVersion; use Modules\Sirsoft\Page\Repositories\Contracts\PageRepositoryInterface; use Modules\Sirsoft\Page\Repositories\Contracts\PageVersionRepositoryInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -300,7 +303,7 @@ class PageService * @param int $id 페이지 ID * @return Page 페이지 모델 * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws ModelNotFoundException */ public function getPage(int $id): Page { @@ -316,23 +319,32 @@ class PageService /** * 슬러그로 발행된 페이지를 조회합니다. * + * $allowUnpublished 가 true 이면 미발행 페이지도 반환합니다. 관리자 미리보기 + * (사용자 화면에서 발행 전 페이지 확인)를 위한 것으로, 권한 판정은 호출부(컨트롤러)가 + * 담당하고 이 메서드는 허용 여부만 플래그로 전달받습니다. + * * @param string $slug 페이지 슬러그 - * @return Page|null 발행된 페이지 모델 또는 null + * @param bool $allowUnpublished 미발행 페이지 반환 허용 여부 (관리자 미리보기) + * @return Page|null 페이지 모델 또는 null */ - public function getPublishedPageBySlug(string $slug): ?Page + public function getPublishedPageBySlug(string $slug, bool $allowUnpublished = false): ?Page { $page = $this->pageRepository->findBySlug($slug); - return ($page && $page->published) ? $page : null; + if (! $page) { + return null; + } + + return ($page->published || $allowUnpublished) ? $page : null; } /** * 페이지 버전 이력을 조회합니다. * * @param Page $page 페이지 모델 - * @return \Illuminate\Database\Eloquent\Collection 버전 목록 (최신순) + * @return Collection 버전 목록 (최신순) */ - public function getVersions(Page $page): \Illuminate\Database\Eloquent\Collection + public function getVersions(Page $page): Collection { return $this->pageVersionRepository->getVersionsByPage($page); } @@ -341,11 +353,11 @@ class PageService * 버전 ID로 페이지 버전을 조회합니다. * * @param int $versionId 버전 ID - * @return \Modules\Sirsoft\Page\Models\PageVersion 버전 모델 + * @return PageVersion 버전 모델 * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws ModelNotFoundException */ - public function getVersion(int $versionId): \Modules\Sirsoft\Page\Models\PageVersion + public function getVersion(int $versionId): PageVersion { return $this->pageVersionRepository->findOrFail($versionId); } @@ -369,7 +381,7 @@ class PageService * @param string $orderBy 정렬 컬럼 * @param string $direction 정렬 방향 (asc, desc) * @param int $limit 조회할 최대 항목 수 - * @return array{total: int, items: \Illuminate\Database\Eloquent\Collection} + * @return array{total: int, items: Collection} */ public function searchByKeyword(string $keyword, string $orderBy = 'created_at', string $direction = 'desc', int $limit = 10): array { diff --git a/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageControllerTest.php b/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageControllerTest.php index 58554e26..95edf8b7 100644 --- a/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageControllerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageControllerTest.php @@ -107,6 +107,146 @@ class PublicPageControllerTest extends FeatureTestCase $response->assertStatus(404); } + // ─── 미발행 페이지 관리자 미리보기 (#424-15) ──────── + + /** + * 페이지 조회 권한(pages.read) 관리자는 미발행 페이지를 미리볼 수 있는지 확인 + */ + public function test_admin_with_read_permission_can_preview_unpublished_page(): void + { + $admin = $this->createAdminUser(['sirsoft-page.pages.read']); + + Page::factory()->create([ + 'slug' => 'test-draft-preview', + 'published' => false, + 'created_by' => $admin->id, + 'updated_by' => $admin->id, + ]); + + $response = $this->actingAs($admin) + ->getJson('/api/modules/sirsoft-page/pages/test-draft-preview'); + + $response->assertStatus(200) + ->assertJsonPath('success', true) + ->assertJsonPath('data.slug', 'test-draft-preview') + ->assertJsonPath('data.is_preview', true); + } + + /** + * 미발행 미리보기 응답의 첨부 URL이 관리자용(발행 가드 없는) 라우트인지 확인 + */ + public function test_admin_preview_unpublished_page_attachments_use_admin_url(): void + { + $admin = $this->createAdminUser(['sirsoft-page.pages.read']); + + $page = Page::factory()->create([ + 'slug' => 'test-draft-preview-attach', + 'published' => false, + 'created_by' => $admin->id, + 'updated_by' => $admin->id, + ]); + + // 이미지 첨부 — preview_url 은 이미지에만 생성되므로 image mime 사용 + PageAttachment::create([ + 'page_id' => $page->id, + 'original_filename' => 'draft.png', + 'stored_filename' => 'stored-draft.png', + 'disk' => 'local', + 'path' => 'test/draft.png', + 'mime_type' => 'image/png', + 'size' => 1024, + 'collection' => 'attachments', + 'order' => 1, + 'created_by' => $admin->id, + ]); + + $response = $this->actingAs($admin) + ->getJson('/api/modules/sirsoft-page/pages/test-draft-preview-attach'); + + $response->assertStatus(200); + $downloadUrl = $response->json('data.attachments.0.download_url'); + $previewUrl = $response->json('data.attachments.0.preview_url'); + $this->assertStringContainsString('/admin/attachments/download/', $downloadUrl); + $this->assertStringContainsString('/admin/attachments/preview/', $previewUrl); + } + + /** + * 페이지 권한이 없는 일반 회원은 미발행 페이지를 미리볼 수 없음(404) + */ + public function test_non_admin_user_cannot_preview_unpublished_page(): void + { + $author = $this->createAdminUser([]); + $member = $this->createUser(); + + Page::factory()->create([ + 'slug' => 'test-draft-member', + 'published' => false, + 'created_by' => $author->id, + 'updated_by' => $author->id, + ]); + + $response = $this->actingAs($member) + ->getJson('/api/modules/sirsoft-page/pages/test-draft-member'); + + $response->assertStatus(404); + } + + /** + * pages.read 권한이 없는 관리자(다른 모듈 admin)는 미발행 페이지를 미리볼 수 없음(404) + */ + public function test_admin_without_pages_read_cannot_preview_unpublished_page(): void + { + // sirsoft-page.pages.read 가 아닌 다른 admin 권한만 보유 + $otherAdmin = $this->createAdminUser(['sirsoft-other.things.read']); + + Page::factory()->create([ + 'slug' => 'test-draft-other-admin', + 'published' => false, + 'created_by' => $otherAdmin->id, + 'updated_by' => $otherAdmin->id, + ]); + + $response = $this->actingAs($otherAdmin) + ->getJson('/api/modules/sirsoft-page/pages/test-draft-other-admin'); + + $response->assertStatus(404); + } + + /** + * 발행된 페이지의 첨부 URL은 관리자 요청이어도 공개용 라우트를 유지하는지 확인 + */ + public function test_published_page_attachments_use_public_url_even_for_admin(): void + { + $admin = $this->createAdminUser(['sirsoft-page.pages.read']); + + $page = Page::factory()->published()->create([ + 'slug' => 'test-published-admin-attach', + 'created_by' => $admin->id, + 'updated_by' => $admin->id, + ]); + + PageAttachment::create([ + 'page_id' => $page->id, + 'original_filename' => 'public.pdf', + 'stored_filename' => 'stored-public.pdf', + 'disk' => 'local', + 'path' => 'test/public.pdf', + 'mime_type' => 'application/pdf', + 'size' => 1024, + 'collection' => 'attachments', + 'order' => 1, + 'created_by' => $admin->id, + ]); + + $response = $this->actingAs($admin) + ->getJson('/api/modules/sirsoft-page/pages/test-published-admin-attach'); + + $response->assertStatus(200) + ->assertJsonPath('data.is_preview', false); + $downloadUrl = $response->json('data.attachments.0.download_url'); + $this->assertStringContainsString('/pages/attachment/', $downloadUrl); + } + /** * 발행된 페이지 조회 시 첨부파일 목록이 포함되는지 확인 */ @@ -133,7 +273,7 @@ class PublicPageControllerTest extends FeatureTestCase 'created_by' => $admin->id, ]); - $response = $this->getJson("/api/modules/sirsoft-page/pages/test-page-with-attach"); + $response = $this->getJson('/api/modules/sirsoft-page/pages/test-page-with-attach'); $response->assertStatus(200) ->assertJsonStructure([ diff --git a/modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-seed.ts b/modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-seed.ts index b8ae8837..a24010e8 100644 --- a/modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-seed.ts +++ b/modules/_bundled/sirsoft-page/tests/Playwright/fixtures/page-seed.ts @@ -20,15 +20,21 @@ import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); type SeedFixtures = { - /** 시드된 페이지 도메인 데이터 (페이지 ID + 첨부 ID 배열) */ - seededPage: { pageIds: number[]; attachmentIds: number[] }; + /** 시드된 페이지 도메인 데이터 (고정 슬러그 미발행/발행 페이지 + 첨부 ID 배열) */ + seededPage: { + unpublished_slug: string; + published_slug: string; + unpublished_id: number; + published_id: number; + attachment_ids: number[]; + }; }; export const test = base.extend