게시물 미리보기 기능 추가

This commit is contained in:
2026-05-03 10:18:22 +09:00
parent 8c5ccc94ec
commit e506a343bc
11 changed files with 123 additions and 12 deletions

View File

@@ -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"

View File

@@ -1,5 +1,13 @@
# 의사결정 이력
## 2026-05-03 v0.0.31
### 글 미리보기 저장 방식 결정
글 미리보기는 데이터베이스에 임시 초안 레코드를 만들지 않고 브라우저 저장소를 통해 현재 작성 폼 값을 전달한다. 저장 전 내용 확인이 목적이므로 DB에 미리보기용 글이 쌓이거나 슬러그 충돌을 만드는 일을 피하기 위해서다.
미리보기 화면은 `/admin/posts/preview` 관리자 경로에 두고, 공개 게시물 상세와 같은 `ContentRenderer`, `ProseHeaderCard`, `ContentMarkdownRenderer` 조합으로 본문을 렌더링한다. 이렇게 하면 저장 전에도 공개 화면에 가까운 결과를 빠르게 확인할 수 있다.
## 2026-05-03 v0.0.30
### OG 이미지 저장 방식 결정

View File

@@ -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 | 페이지 수정, 저장/삭제 토스트 |

View File

@@ -293,6 +293,8 @@ components/content/
- 자동 저장 키는 `SORI_ADMIN_POST_AUTOSAVE:new` 또는 `SORI_ADMIN_POST_AUTOSAVE:{postId}` 형식을 사용한다.
- 자동 저장본이 있으면 작성 화면에서 복원 또는 삭제를 선택할 수 있다.
- 글 저장 성공 시 해당 자동 저장본은 삭제한다.
- 미리보기 버튼은 현재 작성 폼 값을 `SORI_ADMIN_POST_PREVIEW` 브라우저 저장소에 기록한 뒤 `/admin/posts/preview` 새 탭에서 렌더링한다.
- 미리보기 화면은 공개 게시물 상세와 같은 본문 렌더링 컴포넌트를 사용하되, 데이터베이스에는 임시 글을 저장하지 않는다.
- 글 저장/수정/삭제 진행과 성공/실패 상태는 화면 우측 상단 토스트로 표시한다.
- 발행 상태에서 발행 시각을 미래로 지정하면 예약 발행으로 저장한다.
- 예약 발행 글은 관리자 목록에서 예약 상태로 표시하되 공개 게시물 목록과 상세 API에는 발행 시각 이후부터 노출한다.

View File

@@ -10,6 +10,7 @@
- [ ] 대표 이미지 브라우저 수동 QA: 기존 미디어 선택, 새 업로드, 썸네일 표시, 삭제/변경 확인
- [ ] 미디어 라이브러리 상세 모달 수동 QA: 검색, 사용 현황, 파일명 변경, 삭제 잠금 확인
- [ ] 콜아웃, 토글, 임베드 블록 브라우저 수동 QA: `/` 메뉴 선택, 저장/수정 왕복, 공개 렌더링 확인
- [ ] 게시물 미리보기 브라우저 수동 QA: 새 글/수정 글에서 저장 전 미리보기 탭 열림, 제목/본문 렌더링 확인
## 2차 관리자 개발
@@ -19,11 +20,6 @@
- [ ] 미디어 라이브러리 폴더 브라우저 수동 QA: 폴더 생성, 하위 폴더 표시, Ctrl/Command 복수 선택, Shift 범위 선택, 드래그 일괄 이동, 상세 모달 폴더 변경 확인
- [ ] 미디어 라이브러리 프로필/사이트 설정 이미지 사용처 추적
## 3차 관리자 개발
- [ ] 초안 자동 저장
- [ ] 글 미리보기
## 프론트엔드 개발
- [ ] 공개 화면 모바일 사이드바/네비게이션 방식 결정

View File

@@ -1,5 +1,13 @@
# 업데이트 이력
## v0.0.31
- 관리자 글 작성/수정 폼에 미리보기 버튼 추가.
- 저장 전 게시물 입력값을 브라우저 저장소에 담아 새 미리보기 탭으로 전달하도록 추가.
- 관리자 게시물 미리보기 화면 추가.
- 미리보기 화면이 공개 게시물 본문 렌더러와 같은 컴포넌트를 사용하도록 연결.
- 패키지 버전을 0.0.31로 갱신.
## v0.0.30
- 게시물 OG 이미지 필드 추가.

4
package-lock.json generated
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "sori.studio",
"version": "0.0.30",
"version": "0.0.31",
"private": true,
"type": "module",
"scripts": {

View File

@@ -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"

View File

@@ -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"

View 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>