게시물 미리보기 기능 추가
This commit is contained in:
@@ -14,7 +14,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['submit'])
|
||||
const emit = defineEmits(['submit', 'preview'])
|
||||
|
||||
const autosaveStoragePrefix = 'SORI_ADMIN_POST_AUTOSAVE'
|
||||
const slugTouched = ref(Boolean(props.initialPost.slug))
|
||||
@@ -414,6 +414,14 @@ const submitPost = () => {
|
||||
emit('submit', createPostPayload())
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 미리보기 요청
|
||||
* @returns {void}
|
||||
*/
|
||||
const previewPost = () => {
|
||||
emit('preview', createPostPayload())
|
||||
}
|
||||
|
||||
watch(form, scheduleAutosave, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
@@ -669,6 +677,13 @@ defineExpose({
|
||||
<NuxtLink class="admin-post-form__cancel rounded border border-line bg-white px-4 py-2 text-sm font-semibold" to="/admin/posts">
|
||||
취소
|
||||
</NuxtLink>
|
||||
<button
|
||||
class="admin-post-form__preview rounded border border-line bg-white px-4 py-2 text-sm font-semibold"
|
||||
type="button"
|
||||
@click="previewPost"
|
||||
>
|
||||
미리보기
|
||||
</button>
|
||||
<button
|
||||
class="admin-post-form__submit rounded bg-[#15171a] px-4 py-2 text-sm font-semibold text-white disabled:opacity-50"
|
||||
type="submit"
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-05-03 v0.0.31
|
||||
|
||||
### 글 미리보기 저장 방식 결정
|
||||
|
||||
글 미리보기는 데이터베이스에 임시 초안 레코드를 만들지 않고 브라우저 저장소를 통해 현재 작성 폼 값을 전달한다. 저장 전 내용 확인이 목적이므로 DB에 미리보기용 글이 쌓이거나 슬러그 충돌을 만드는 일을 피하기 위해서다.
|
||||
|
||||
미리보기 화면은 `/admin/posts/preview` 관리자 경로에 두고, 공개 게시물 상세와 같은 `ContentRenderer`, `ProseHeaderCard`, `ContentMarkdownRenderer` 조합으로 본문을 렌더링한다. 이렇게 하면 저장 전에도 공개 화면에 가까운 결과를 빠르게 확인할 수 있다.
|
||||
|
||||
## 2026-05-03 v0.0.30
|
||||
|
||||
### OG 이미지 저장 방식 결정
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
| 파일 | 화면 위치 |
|
||||
|------|-----------|
|
||||
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, 대표 이미지/OG 이미지 선택, 로컬 자동 저장, 예약 발행 시각 입력, SEO 설정 |
|
||||
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, 대표 이미지/OG 이미지 선택, 로컬 자동 저장, 예약 발행 시각 입력, SEO 설정, 미리보기 요청 |
|
||||
| components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 대표 이미지 선택 |
|
||||
| components/admin/AdminBlockEditor.vue | 관리자 글 블록형 에디터, 이미지/갤러리/콜아웃/토글/임베드 블록, 한글 조합 입력 처리, 하단 빈 입력 블록 유지 |
|
||||
| components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼 |
|
||||
@@ -60,6 +60,7 @@
|
||||
| pages/admin/posts/index.vue | 글 목록, 예약 발행 상태 표시 |
|
||||
| pages/admin/posts/new.vue | 글 작성, 저장 토스트 |
|
||||
| pages/admin/posts/[id].vue | 글 수정, 저장/삭제 토스트 |
|
||||
| pages/admin/posts/preview.vue | 글 미리보기 |
|
||||
| pages/admin/pages/index.vue | 페이지 목록 |
|
||||
| pages/admin/pages/new.vue | 페이지 작성, 저장 토스트 |
|
||||
| pages/admin/pages/[id].vue | 페이지 수정, 저장/삭제 토스트 |
|
||||
|
||||
@@ -293,6 +293,8 @@ components/content/
|
||||
- 자동 저장 키는 `SORI_ADMIN_POST_AUTOSAVE:new` 또는 `SORI_ADMIN_POST_AUTOSAVE:{postId}` 형식을 사용한다.
|
||||
- 자동 저장본이 있으면 작성 화면에서 복원 또는 삭제를 선택할 수 있다.
|
||||
- 글 저장 성공 시 해당 자동 저장본은 삭제한다.
|
||||
- 미리보기 버튼은 현재 작성 폼 값을 `SORI_ADMIN_POST_PREVIEW` 브라우저 저장소에 기록한 뒤 `/admin/posts/preview` 새 탭에서 렌더링한다.
|
||||
- 미리보기 화면은 공개 게시물 상세와 같은 본문 렌더링 컴포넌트를 사용하되, 데이터베이스에는 임시 글을 저장하지 않는다.
|
||||
- 글 저장/수정/삭제 진행과 성공/실패 상태는 화면 우측 상단 토스트로 표시한다.
|
||||
- 발행 상태에서 발행 시각을 미래로 지정하면 예약 발행으로 저장한다.
|
||||
- 예약 발행 글은 관리자 목록에서 예약 상태로 표시하되 공개 게시물 목록과 상세 API에는 발행 시각 이후부터 노출한다.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
- [ ] 대표 이미지 브라우저 수동 QA: 기존 미디어 선택, 새 업로드, 썸네일 표시, 삭제/변경 확인
|
||||
- [ ] 미디어 라이브러리 상세 모달 수동 QA: 검색, 사용 현황, 파일명 변경, 삭제 잠금 확인
|
||||
- [ ] 콜아웃, 토글, 임베드 블록 브라우저 수동 QA: `/` 메뉴 선택, 저장/수정 왕복, 공개 렌더링 확인
|
||||
- [ ] 게시물 미리보기 브라우저 수동 QA: 새 글/수정 글에서 저장 전 미리보기 탭 열림, 제목/본문 렌더링 확인
|
||||
|
||||
## 2차 관리자 개발
|
||||
|
||||
@@ -19,11 +20,6 @@
|
||||
- [ ] 미디어 라이브러리 폴더 브라우저 수동 QA: 폴더 생성, 하위 폴더 표시, Ctrl/Command 복수 선택, Shift 범위 선택, 드래그 일괄 이동, 상세 모달 폴더 변경 확인
|
||||
- [ ] 미디어 라이브러리 프로필/사이트 설정 이미지 사용처 추적
|
||||
|
||||
## 3차 관리자 개발
|
||||
|
||||
- [ ] 초안 자동 저장
|
||||
- [ ] 글 미리보기
|
||||
|
||||
## 프론트엔드 개발
|
||||
|
||||
- [ ] 공개 화면 모바일 사이드바/네비게이션 방식 결정
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v0.0.31
|
||||
|
||||
- 관리자 글 작성/수정 폼에 미리보기 버튼 추가.
|
||||
- 저장 전 게시물 입력값을 브라우저 저장소에 담아 새 미리보기 탭으로 전달하도록 추가.
|
||||
- 관리자 게시물 미리보기 화면 추가.
|
||||
- 미리보기 화면이 공개 게시물 본문 렌더러와 같은 컴포넌트를 사용하도록 연결.
|
||||
- 패키지 버전을 0.0.31로 갱신.
|
||||
|
||||
## v0.0.30
|
||||
|
||||
- 게시물 OG 이미지 필드 추가.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.31",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.31",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.31",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -62,6 +62,20 @@ const showStoredToast = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 미리보기
|
||||
* @param {Object} payload - 게시물 입력값
|
||||
* @returns {void}
|
||||
*/
|
||||
const previewPost = (payload) => {
|
||||
localStorage.setItem('SORI_ADMIN_POST_PREVIEW', JSON.stringify({
|
||||
...payload,
|
||||
id: id.value,
|
||||
previewedAt: new Date().toISOString()
|
||||
}))
|
||||
window.open('/admin/posts/preview', '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 수정 저장
|
||||
* @param {Object} payload - 게시물 입력값
|
||||
@@ -155,7 +169,7 @@ onBeforeUnmount(() => {
|
||||
<p v-if="errorMessage" class="admin-post-edit__error mb-5 rounded border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<AdminPostForm ref="postForm" :initial-post="post" submit-label="변경 저장" :saving="saving" @submit="savePost" />
|
||||
<AdminPostForm ref="postForm" :initial-post="post" submit-label="변경 저장" :saving="saving" @submit="savePost" @preview="previewPost" />
|
||||
<div
|
||||
v-if="toast"
|
||||
class="admin-post-edit__toast fixed right-5 top-5 z-50 rounded border px-4 py-3 text-sm font-semibold shadow-lg"
|
||||
|
||||
@@ -23,6 +23,19 @@ const showToast = (type, message) => {
|
||||
}, 3200)
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 게시물 미리보기
|
||||
* @param {Object} payload - 게시물 입력값
|
||||
* @returns {void}
|
||||
*/
|
||||
const previewPost = (payload) => {
|
||||
localStorage.setItem('SORI_ADMIN_POST_PREVIEW', JSON.stringify({
|
||||
...payload,
|
||||
previewedAt: new Date().toISOString()
|
||||
}))
|
||||
window.open('/admin/posts/preview', '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 게시물 저장
|
||||
* @param {Object} payload - 게시물 입력값
|
||||
@@ -71,7 +84,7 @@ onBeforeUnmount(() => {
|
||||
<p v-if="errorMessage" class="admin-post-editor__error mb-5 rounded border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<AdminPostForm ref="postForm" submit-label="글 저장" :saving="saving" @submit="savePost" />
|
||||
<AdminPostForm ref="postForm" submit-label="글 저장" :saving="saving" @submit="savePost" @preview="previewPost" />
|
||||
<div
|
||||
v-if="toast"
|
||||
class="admin-post-editor__toast fixed right-5 top-5 z-50 rounded border px-4 py-3 text-sm font-semibold shadow-lg"
|
||||
|
||||
54
pages/admin/posts/preview.vue
Normal file
54
pages/admin/posts/preview.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: 'post'
|
||||
})
|
||||
|
||||
const previewPost = ref(null)
|
||||
const previewError = ref('')
|
||||
|
||||
/**
|
||||
* 미리보기 저장 값을 읽어 온다
|
||||
* @returns {void}
|
||||
*/
|
||||
const loadPreviewPost = () => {
|
||||
const rawPreview = localStorage.getItem('SORI_ADMIN_POST_PREVIEW')
|
||||
|
||||
if (!rawPreview) {
|
||||
previewError.value = '미리보기 데이터가 없습니다.'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
previewPost.value = JSON.parse(rawPreview)
|
||||
} catch {
|
||||
previewError.value = '미리보기 데이터를 읽지 못했습니다.'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadPreviewPost)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-post-preview">
|
||||
<div class="admin-post-preview__banner mb-5 rounded border border-line bg-white px-4 py-3 text-sm font-semibold text-ink">
|
||||
관리자 미리보기
|
||||
</div>
|
||||
|
||||
<p v-if="previewError" class="admin-post-preview__error rounded border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
{{ previewError }}
|
||||
</p>
|
||||
|
||||
<ContentRenderer v-else-if="previewPost">
|
||||
<ProseHeaderCard>
|
||||
<p class="post-detail__eyebrow text-sm uppercase text-white/70">
|
||||
PREVIEW
|
||||
</p>
|
||||
<h1 class="post-detail__title mt-3 text-4xl font-semibold leading-tight">
|
||||
{{ previewPost.title || '제목 없음' }}
|
||||
</h1>
|
||||
</ProseHeaderCard>
|
||||
|
||||
<ContentMarkdownRenderer class="post-detail__content" :content="previewPost.content || ''" />
|
||||
</ContentRenderer>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user