fix(page): 미발행 페이지를 권한 관리자가 사용자 화면에서 미리보도록 수정
미발행 페이지는 관리자여도 사용자 화면(/page/{slug})에 접근할 수 없어
발행 전 미리보기가 불가능했다. 페이지 조회 권한(sirsoft-page.pages.read)
보유 관리자는 미발행 페이지를 사용자 화면에서 미리볼 수 있게 하고, 미리보기
화면에 안내 배너를 노출한다. 비로그인·일반 회원은 기존대로 404를 유지한다.
- getPublishedPageBySlug 에 미리보기 허용 플래그 추가 (권한 판정은 컨트롤러)
- 미발행 미리보기 시 첨부 URL 을 admin 컨텍스트로 전환해 첨부까지 미리보기
- page/show.json 에 미발행 미리보기 안내 배너 + ko/en/ja 다국어
- E2E 시드 커맨드 구현 + 커맨드 등록 + 사용자 화면 미리보기 spec
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
"published_at": "公開日",
|
||||
"create_page": "ページ作成",
|
||||
"attachments": "添付ファイル",
|
||||
"download": "ダウンロード"
|
||||
"download": "ダウンロード",
|
||||
"preview_banner": "未公開ページをプレビュー中です"
|
||||
},
|
||||
"mypage": {
|
||||
"page_title": "マイページ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- 페이지 관리 목록의 페이지당 표시 개수 기본값이 화면 표시(20건)와 실제 조회(15건) 간에 어긋나던 문제를 수정했습니다 — 기본값을 20건으로 통일했습니다.
|
||||
- 페이지 관리 목록에서 아주 긴 검색어를 입력하면 오류 화면이 뜨던 문제를 수정했습니다 — 검색어 길이를 100자로 제한해 안내 메시지로 처리합니다.
|
||||
- 페이지 상세의 버전 이력에서 "변경내역" 항목이 `title`·`content` 같은 영문 필드명으로 표시되던 문제를 수정했습니다 — 이제 "제목", "내용", "SEO 정보" 처럼 알아보기 쉬운 이름으로 표시됩니다.
|
||||
- 관리자가 발행 전 페이지를 사용자 화면에서 미리 확인할 수 있게 되었습니다. 미리보기 화면에는 미발행 상태임을 알리는 안내가 표시되며, 비로그인 사용자와 일반 회원에게는 이전과 동일하게 노출되지 않습니다.
|
||||
|
||||
## [1.0.0] - 2026-07-01
|
||||
|
||||
|
||||
@@ -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' => '<p>미발행 본문</p>', 'en' => '<p>Unpublished body</p>'],
|
||||
'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' => '<p>발행 본문</p>', 'en' => '<p>Published body</p>'],
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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<SeedFixtures>({
|
||||
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', {
|
||||
const out = execSync('php artisan playwright:seed-page --attachments=2 --json', {
|
||||
cwd: coreRoot,
|
||||
encoding: 'utf-8',
|
||||
env: {
|
||||
@@ -36,7 +42,9 @@ export const test = base.extend<SeedFixtures>({
|
||||
G7_PLAYWRIGHT_BYPASS: '1',
|
||||
},
|
||||
});
|
||||
const seed = JSON.parse(out);
|
||||
// artisan 출력에 경고 등이 섞일 수 있으므로 마지막 JSON 라인만 파싱
|
||||
const jsonLine = out.trim().split(/\r?\n/).filter((l) => l.trim().startsWith('{')).pop() ?? '{}';
|
||||
const seed = JSON.parse(jsonLine);
|
||||
await use(seed);
|
||||
// TODO: teardown 시 cleanup 커맨드 호출
|
||||
},
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 미발행 페이지 관리자 미리보기 (#424-15).
|
||||
*
|
||||
* @scenario viewer=guest, viewer=member, viewer=other_admin, viewer=page_reader, published=published, published=unpublished
|
||||
* @effects page_reader_admin_previews_unpublished_page_with_200,
|
||||
* preview_banner_renders_when_is_preview_true,
|
||||
* preview_banner_hidden_for_published_or_guest,
|
||||
* guest_gets_404_for_unpublished_page,
|
||||
* member_gets_404_for_unpublished_page
|
||||
*
|
||||
* 정책: 페이지 조회 권한(sirsoft-page.pages.read) 보유 관리자만 사용자 화면(/page/{slug})에서
|
||||
* 미발행 페이지를 미리볼 수 있고, 이때 "미발행 페이지 미리보기 중" 배너가 노출된다.
|
||||
* 비로그인·일반 회원·페이지 권한 없는 관리자는 미발행 페이지에서 404(페이지를 찾을 수 없습니다) 를 본다.
|
||||
* 발행 페이지는 누구에게나 배너 없이 노출된다.
|
||||
*
|
||||
* DOM 식별: 레이아웃 노드 id 는 HTML id 로 렌더되지 않으므로(콘텐츠 노드는 id 속성 미출력)
|
||||
* 사용자에게 보이는 텍스트로 식별한다(getByText 는 콘텐츠 비동기 렌더를 auto-wait).
|
||||
* - 미리보기 배너: "미발행 페이지 미리보기 중입니다"
|
||||
* - not-found: "페이지를 찾을 수 없습니다"
|
||||
* - 시드 페이지 제목: 미발행="미발행 미리보기 대상" / 발행="발행 대조군"
|
||||
*
|
||||
* 시드: playwright:seed-page 가 고정 슬러그를 발급한다.
|
||||
* - 미발행: test-e2e-unpublished-preview (제목 "미발행 미리보기 대상")
|
||||
* - 발행: test-e2e-published-preview (제목 "발행 대조군")
|
||||
*/
|
||||
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 UNPUBLISHED_URL = '/page/test-e2e-unpublished-preview';
|
||||
const PUBLISHED_URL = '/page/test-e2e-published-preview';
|
||||
|
||||
// 앱 locale(ko/en)에 무관하게 매칭 — 실행 환경 기본 locale 이 EN 일 수 있다.
|
||||
// 제목은 seed 가 넣은 고정 문자열이라 locale 무관.
|
||||
const PREVIEW_BANNER_TEXT = /미발행 페이지 미리보기 중입니다|Previewing an unpublished page/;
|
||||
const NOT_FOUND_TEXT = /페이지를 찾을 수 없습니다|Page Not Found/;
|
||||
const UNPUBLISHED_TITLE = '미발행 미리보기 대상';
|
||||
const PUBLISHED_TITLE = '발행 대조군';
|
||||
|
||||
test.describe('미발행 페이지 관리자 미리보기 (#424-15)', () => {
|
||||
test('페이지 조회 권한 관리자는 미발행 페이지를 미리보고 배너가 노출된다', async ({
|
||||
page,
|
||||
pageReadToken,
|
||||
seededPage,
|
||||
}) => {
|
||||
expect(seededPage.unpublished_slug).toBe('test-e2e-unpublished-preview');
|
||||
|
||||
await authenticatePage(page, pageReadToken);
|
||||
await page.goto(UNPUBLISHED_URL);
|
||||
|
||||
// 미발행이지만 권한자라 본문 노출 + 미리보기 배너 노출
|
||||
await expect(page.getByRole('heading', { name: UNPUBLISHED_TITLE })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByText(PREVIEW_BANNER_TEXT)).toBeVisible();
|
||||
await expect(page.getByText(NOT_FOUND_TEXT)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('비로그인 사용자는 미발행 페이지에서 not-found 를 본다', async ({ page, seededPage }) => {
|
||||
expect(seededPage.unpublished_slug).toBe('test-e2e-unpublished-preview');
|
||||
|
||||
// 인증 없음(게스트)
|
||||
await page.goto(UNPUBLISHED_URL);
|
||||
|
||||
await expect(page.getByRole('heading', { name: NOT_FOUND_TEXT })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByText(PREVIEW_BANNER_TEXT)).toHaveCount(0);
|
||||
});
|
||||
|
||||
/*
|
||||
* "권한 없는 로그인 사용자 / pages.read 없는 다른 모듈 관리자 → 404" 케이스는 E2E 로
|
||||
* 재현 불가하여 PHPUnit 이 담당한다. 근거: PlaywrightIssueToken.makeAdminUser 는 발급 유저를
|
||||
* 무조건 코어 `admin` 롤에 부여하는데, 활성 DB 의 admin 롤은 시드로 sirsoft-page.pages.read 를
|
||||
* 포함한 전체 권한(131개)을 이미 보유한다. 따라서 issueToken() 으로는 "페이지 권한이 없는 유저"
|
||||
* 를 만들 수 없다(항상 pages.read 를 상속). 해당 게이트 분기는 아래 PHPUnit 이 격리 검증한다:
|
||||
* - PublicPageControllerTest::test_non_admin_user_cannot_preview_unpublished_page
|
||||
* - PublicPageControllerTest::test_admin_without_pages_read_cannot_preview_unpublished_page
|
||||
* (createAdminUser 는 admin 롤에 명시 권한만 syncWithoutDetaching 하여 격리 가능)
|
||||
*/
|
||||
|
||||
test('발행된 페이지는 배너 없이 노출된다 (관리자 포함)', async ({
|
||||
page,
|
||||
pageReadToken,
|
||||
seededPage,
|
||||
}) => {
|
||||
expect(seededPage.published_slug).toBe('test-e2e-published-preview');
|
||||
|
||||
await authenticatePage(page, pageReadToken);
|
||||
await page.goto(PUBLISHED_URL);
|
||||
|
||||
await expect(page.getByRole('heading', { name: PUBLISHED_TITLE })).toBeVisible({ timeout: 30_000 });
|
||||
// 발행 페이지는 is_preview=false → 배너 미노출
|
||||
await expect(page.getByText(PREVIEW_BANNER_TEXT)).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
@@ -401,6 +401,35 @@ class PageServiceTest extends ModuleTestCase
|
||||
$this->assertNull($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* allowUnpublished=true 이면 미발행 페이지도 반환하는지 확인 (#424-15 미리보기)
|
||||
*/
|
||||
public function test_get_published_page_by_slug_returns_draft_when_preview_allowed(): void
|
||||
{
|
||||
Page::factory()->create([
|
||||
'slug' => 'test-svc-draft-preview',
|
||||
'published' => false,
|
||||
'created_by' => $this->adminUser->id,
|
||||
'updated_by' => $this->adminUser->id,
|
||||
]);
|
||||
|
||||
$page = $this->service->getPublishedPageBySlug('test-svc-draft-preview', true);
|
||||
|
||||
$this->assertNotNull($page);
|
||||
$this->assertEquals('test-svc-draft-preview', $page->slug);
|
||||
$this->assertFalse((bool) $page->published);
|
||||
}
|
||||
|
||||
/**
|
||||
* allowUnpublished=true 여도 존재하지 않는 슬러그는 null 을 반환하는지 확인
|
||||
*/
|
||||
public function test_get_published_page_by_slug_returns_null_for_missing_even_when_preview_allowed(): void
|
||||
{
|
||||
$page = $this->service->getPublishedPageBySlug('test-svc-no-such-slug', true);
|
||||
|
||||
$this->assertNull($page);
|
||||
}
|
||||
|
||||
// ─── scope 검증 (AccessDeniedHttpException) ────
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# audit:allow test-scenario-coverage reason: |
|
||||
# 본 매니페스트는 미발행 페이지 관리자 미리보기(#424-15) 단일 결함의 권한 × 발행상태
|
||||
# cross product 를 기록한다. viewer(guest/member/other_admin/page_reader) × published
|
||||
# (published/unpublished) 8케이스 중 "미리보기 허용" 은 page_reader × unpublished 단 1조합이며,
|
||||
# 나머지는 기존 동작(발행=200 공개 / 미발행=404) 유지다. 실제 회귀 가드는 test_files 의
|
||||
# Feature/Unit/레이아웃 테스트가 결함별로 담당하므로 docblock @scenario 전수 마킹 대신
|
||||
# 통과 테스트로 커버한다.
|
||||
# E2E: 사용자 화면 미리보기 spec 은 페이지 모듈 Playwright 인프라(page-auth/page-seed fixture)
|
||||
# 를 재사용하되, playwright:seed-page 시드 로직 + 대상 화면 data-testid 보강 전까지
|
||||
# test.describe.skip placeholder 로 운용한다(기존 admin spec 3건과 동일 운용).
|
||||
feature: 미발행 페이지 관리자 미리보기 (권한자만 사용자 화면 노출 + 미리보기 배너 + 첨부 admin URL)
|
||||
|
||||
description: |
|
||||
#424-15: 미발행 페이지는 관리자여도 사용자 화면(/page/{slug})에 접근할 수 없던 결함을 수정한다.
|
||||
|
||||
게이트 정책:
|
||||
- 비로그인·일반 회원: 미발행 페이지 → 404 유지 (비공개).
|
||||
- 페이지 조회 권한(sirsoft-page.pages.read, admin) 보유자: 미발행 페이지 → 200 미리보기.
|
||||
- pages.read 없는 다른 모듈 admin: 404 (최소권한 — 관리자 상세 라우트와 동일 게이트).
|
||||
- 발행 페이지: 모두 200 (기존 동작 무변경).
|
||||
|
||||
구현:
|
||||
- PageService::getPublishedPageBySlug($slug, $allowUnpublished) — 허용 플래그로 미발행 반환.
|
||||
- PublicPageController::show — optional.sanctum 로 인증된 사용자의 hasPermission(pages.read, admin)
|
||||
판정 후 플래그 전달. 권한 판정은 컨트롤러, Service 는 플래그만 수신(게시판 PostController 패턴).
|
||||
- PublicPageResource::withPreview — is_preview 노출 + 첨부 컨텍스트를 admin(발행가드 없는 라우트)/
|
||||
public 로 분기. 미발행 미리보기 시 첨부 download/preview 가 admin URL 이라 첨부까지 미리보기 가능.
|
||||
- page/show.json(sirsoft-basic) — is_preview=true 일 때 "미발행 페이지 미리보기 중" 배너 노출.
|
||||
|
||||
안전:
|
||||
- SEO 캐시 오염 없음: 봇(게스트)은 미발행에 여전히 404 → 미리보기 콘텐츠가 봇 캐시에 안 들어감.
|
||||
- 정보 노출 최소: published 전체가 아닌 is_preview(bool)만 추가 노출.
|
||||
- 공개 첨부 컨트롤러 무변경: admin URL 컨텍스트 재사용으로 발행 가드·권한 자동 처리.
|
||||
|
||||
axes:
|
||||
viewer: [guest, member, other_admin, page_reader]
|
||||
published: [published, unpublished]
|
||||
|
||||
effects:
|
||||
- guest_sees_published_page
|
||||
- guest_gets_404_for_unpublished_page
|
||||
- member_gets_404_for_unpublished_page
|
||||
- other_admin_without_pages_read_gets_404_for_unpublished_page
|
||||
- page_reader_admin_previews_unpublished_page_with_200
|
||||
- preview_response_exposes_is_preview_true_only_for_unpublished_preview
|
||||
- preview_attachments_use_admin_context_url
|
||||
- published_page_attachments_use_public_context_url_even_for_admin
|
||||
- preview_banner_renders_when_is_preview_true
|
||||
- preview_banner_hidden_for_published_or_guest
|
||||
|
||||
test_files:
|
||||
- modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageControllerTest.php
|
||||
- modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php
|
||||
- templates/_bundled/sirsoft-basic/__tests__/layouts/page-show-preview-banner.test.tsx
|
||||
- modules/_bundled/sirsoft-page/tests/Playwright/specs/user/unpublished-admin-preview.spec.ts
|
||||
|
||||
notes: |
|
||||
회귀 가드 담당:
|
||||
- 게이트(권한 4변종 × 발행상태) + is_preview 노출 + 첨부 컨텍스트 URL → PublicPageControllerTest(Feature).
|
||||
- getPublishedPageBySlug 플래그 동작 격리 → PageServiceTest(Unit).
|
||||
- 배너 if=is_preview 의존 + 다국어 키 + 위치 → page-show-preview-banner.test.tsx(레이아웃 렌더).
|
||||
|
||||
E2E: 사용자 화면 미리보기 spec(unpublished-admin-preview.spec.ts)은 페이지 모듈 Playwright
|
||||
인프라(page-auth fixture 재사용, page-seed fixture)를 사용한다. playwright:seed-page 시드 로직과
|
||||
대상 화면 data-testid 보강 전까지 test.describe.skip placeholder 로 등록하며(기존 admin spec 3건과
|
||||
동일 운용), 실제 회귀 가드는 위 Feature/Unit/레이아웃 렌더 테스트가 담당한다.
|
||||
@@ -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
|
||||
|
||||
- 관리자가 발행 전 페이지를 사용자 화면에서 미리 볼 때, 미발행 상태임을 알리는 안내 표시를 추가했습니다.
|
||||
|
||||
## [1.0.0] - 2026-07-01
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @file page-show-preview-banner.test.tsx
|
||||
* @description 미발행 페이지 관리자 미리보기 배너 (이슈 #424-15)
|
||||
*
|
||||
* 정책: 페이지 조회 권한 관리자가 사용자 화면에서 미발행 페이지를 미리볼 때,
|
||||
* 상단에 "미발행 페이지 미리보기 중" 안내 배너를 노출한다. 백엔드(PublicPageResource)가
|
||||
* 미발행+권한자 응답에만 is_preview=true 를 내려주므로, 배너의 if 는 그 값에 의존한다.
|
||||
* 발행 페이지·게스트 응답은 is_preview=false → 배너 미노출.
|
||||
*
|
||||
* 검증 방식: 실제 page/show.json 레이아웃 트리 직접 분석 (DOM 비의존).
|
||||
* - 배너 블록이 존재하고 if 가 page.data.is_preview 에 의존하는지 고정
|
||||
* - 배너 문구가 다국어 키($t:user.page.preview_banner)로 렌더되는지 고정
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import pageShow from '../../layouts/page/show.json';
|
||||
|
||||
type Node = Record<string, unknown> & { children?: unknown };
|
||||
|
||||
/**
|
||||
* 트리를 재귀 순회하며 조건을 만족하는 모든 노드를 수집한다.
|
||||
*/
|
||||
function collectNodes(node: unknown, predicate: (n: Node) => boolean): Node[] {
|
||||
const result: Node[] = [];
|
||||
const walk = (cur: unknown): void => {
|
||||
if (Array.isArray(cur)) {
|
||||
cur.forEach(walk);
|
||||
return;
|
||||
}
|
||||
if (cur && typeof cur === 'object') {
|
||||
const obj = cur as Node;
|
||||
if (predicate(obj)) {
|
||||
result.push(obj);
|
||||
}
|
||||
Object.values(obj).forEach(walk);
|
||||
}
|
||||
};
|
||||
walk(node);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* id 로 노드를 찾는다.
|
||||
*/
|
||||
function findById(root: unknown, id: string): Node | undefined {
|
||||
return collectNodes(root, (n) => n.id === id)[0];
|
||||
}
|
||||
|
||||
describe('이슈 #424-15 — 미발행 페이지 미리보기 배너 (page/show.json)', () => {
|
||||
it('미리보기 배너 블록(page_preview_banner)이 존재한다', () => {
|
||||
const banner = findById(pageShow, 'page_preview_banner');
|
||||
expect(banner).toBeDefined();
|
||||
});
|
||||
|
||||
it('배너의 if 가 page.data.is_preview 에 의존한다 (발행/게스트는 미노출)', () => {
|
||||
const banner = findById(pageShow, 'page_preview_banner');
|
||||
expect(typeof banner?.if).toBe('string');
|
||||
expect(banner?.if as string).toContain('is_preview');
|
||||
// 단일 바인딩 형태 {{...}} 여야 엔진이 식으로 평가 (CLAUDE.md if 규칙)
|
||||
expect((banner?.if as string).trim()).toMatch(/^\{\{.*\}\}$/);
|
||||
});
|
||||
|
||||
it('배너 문구가 다국어 키로 렌더된다 (하드코딩 금지)', () => {
|
||||
const banner = findById(pageShow, 'page_preview_banner');
|
||||
const spans = collectNodes(banner, (n) => n.name === 'Span' && typeof n.text === 'string');
|
||||
const texts = spans.map((s) => s.text as string);
|
||||
expect(texts).toContain('$t:user.page.preview_banner');
|
||||
});
|
||||
|
||||
it('배너가 본문 카드(page_content_card) 내부, 제목 영역보다 앞에 위치한다', () => {
|
||||
const card = findById(pageShow, 'page_content_card');
|
||||
expect(card).toBeDefined();
|
||||
const children = card?.children as Node[];
|
||||
expect(Array.isArray(children)).toBe(true);
|
||||
// 첫 자식이 미리보기 배너여야 함 (제목/발행일보다 위)
|
||||
expect(children[0]?.id).toBe('page_preview_banner');
|
||||
});
|
||||
});
|
||||
@@ -51,7 +51,8 @@
|
||||
"published_at": "Published",
|
||||
"create_page": "Create Page",
|
||||
"attachments": "Attachments",
|
||||
"download": "Download"
|
||||
"download": "Download",
|
||||
"preview_banner": "Previewing an unpublished page"
|
||||
},
|
||||
"mypage": {
|
||||
"page_title": "My Page",
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
"published_at": "발행일",
|
||||
"create_page": "페이지 생성",
|
||||
"attachments": "첨부파일",
|
||||
"download": "다운로드"
|
||||
"download": "다운로드",
|
||||
"preview_banner": "미발행 페이지 미리보기 중입니다"
|
||||
},
|
||||
"mypage": {
|
||||
"page_title": "마이페이지",
|
||||
|
||||
@@ -80,6 +80,30 @@
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "page_preview_banner",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{page?.data?.is_preview}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mb-4 px-4 py-3 rounded-md bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-400 text-sm"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "eye",
|
||||
"className": "text-amber-600 dark:text-amber-400 text-sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:user.page.preview_banner"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sirsoft-basic",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sirsoft-basic",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sirsoft-basic",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Gnuboard7 Basic User Template Components - Nexibase Style",
|
||||
"type": "module",
|
||||
"main": "dist/components.js",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"ko": "Basic",
|
||||
"en": "Basic"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"description": {
|
||||
"ko": "그누보드7 기본 사용자 템플릿",
|
||||
|
||||
Reference in New Issue
Block a user