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
|
||||
|
||||
- 관리자가 발행 전 페이지를 사용자 화면에서 미리 볼 때, 미발행 상태임을 알리는 안내 표시를 추가했습니다.
|
||||
|
||||
## [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