관리자 글쓰기 화면과 개발 환경 문서 정리
This commit is contained in:
@@ -1247,7 +1247,7 @@ onBeforeUnmount(() => {
|
||||
:class="getBlockClass(block)"
|
||||
contenteditable="true"
|
||||
spellcheck="true"
|
||||
:data-placeholder="index === 0 ? '본문을 입력하거나 / 를 눌러 블록을 선택하세요' : '/ 를 눌러 블록 선택'"
|
||||
:data-placeholder="index === 0 ? '본문을 입력하세요...' : '/ 를 눌러 블록 선택'"
|
||||
:data-show-placeholder="shouldShowPlaceholder(block, index)"
|
||||
@focus="activateBlock(block)"
|
||||
@input="updateBlockText($event, index)"
|
||||
|
||||
@@ -29,6 +29,7 @@ const autosaveTimer = ref(null)
|
||||
const autosaveNotice = ref(null)
|
||||
const autosaveStatus = ref('')
|
||||
const isRestoringAutosave = ref(false)
|
||||
const isSettingsOpen = ref(true)
|
||||
|
||||
/**
|
||||
* ISO 날짜를 datetime-local 입력값으로 변환
|
||||
@@ -136,6 +137,22 @@ const isScheduledPost = () => {
|
||||
return form.status === 'published' && Boolean(publishedAt) && new Date(publishedAt) > new Date()
|
||||
}
|
||||
|
||||
const editorStatusLabel = computed(() => {
|
||||
if (autosaveStatus.value) {
|
||||
return autosaveStatus.value
|
||||
}
|
||||
|
||||
if (form.status === 'published') {
|
||||
return isScheduledPost() ? '예약 발행' : '발행됨'
|
||||
}
|
||||
|
||||
if (form.status === 'private') {
|
||||
return '비공개'
|
||||
}
|
||||
|
||||
return '초안'
|
||||
})
|
||||
|
||||
/**
|
||||
* 게시물 입력값 생성
|
||||
* @returns {Object} 게시물 입력값
|
||||
@@ -422,6 +439,14 @@ const previewPost = () => {
|
||||
emit('preview', createPostPayload())
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 패널 표시 상태 전환
|
||||
* @returns {void}
|
||||
*/
|
||||
const toggleSettingsPanel = () => {
|
||||
isSettingsOpen.value = !isSettingsOpen.value
|
||||
}
|
||||
|
||||
watch(form, scheduleAutosave, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
@@ -455,12 +480,79 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="admin-post-form grid gap-6" @submit.prevent="submitPost">
|
||||
<div class="admin-post-form__main grid gap-5 lg:grid-cols-[minmax(0,1fr)_18rem]">
|
||||
<section class="admin-post-form__content grid gap-4">
|
||||
<form class="admin-post-form flex min-h-[calc(100vh-2.5rem)] flex-col bg-white" @submit.prevent="submitPost">
|
||||
<header class="admin-post-form__toolbar flex h-[82px] shrink-0 items-start border-b border-transparent bg-white p-6">
|
||||
<div class="admin-post-form__toolbar-inner flex h-[34px] min-w-0 flex-1 items-center justify-between">
|
||||
<div class="admin-post-form__toolbar-left flex h-full items-center gap-2">
|
||||
<NuxtLink class="admin-post-form__toolbar-link inline-flex items-center gap-2 rounded px-3 py-1.5 text-base text-black" to="/admin/posts">
|
||||
<span class="admin-post-form__toolbar-back text-lg leading-none" aria-hidden="true"><</span>
|
||||
<span>Posts</span>
|
||||
</NuxtLink>
|
||||
<span class="admin-post-form__toolbar-status rounded px-3 py-1.5 text-base text-[#8e9cac]">
|
||||
{{ editorStatusLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="admin-post-form__toolbar-actions flex h-full items-center gap-2">
|
||||
<button
|
||||
class="admin-post-form__toolbar-preview rounded px-3 py-1.5 text-base font-bold text-[#394047]"
|
||||
type="button"
|
||||
@click="previewPost"
|
||||
>
|
||||
미리보기
|
||||
</button>
|
||||
<button
|
||||
class="admin-post-form__toolbar-submit rounded px-3 py-1.5 text-base font-bold text-[#2bba3c] disabled:opacity-50"
|
||||
type="submit"
|
||||
:disabled="saving"
|
||||
>
|
||||
{{ saving ? '저장 중' : submitLabel }}
|
||||
</button>
|
||||
<button
|
||||
class="admin-post-form__settings-toggle grid size-[34px] place-items-center rounded text-[#394047] hover:bg-[#eff1f2]"
|
||||
type="button"
|
||||
:aria-pressed="isSettingsOpen"
|
||||
aria-label="게시물 설정 패널 전환"
|
||||
@click="toggleSettingsPanel"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2.16699C14.3242 2.16699 14.4998 2.39365 14.5 2.57129V13.4287C14.5 13.606 14.3242 13.834 14 13.834H11.5V2.16699H14ZM2 2.16699H10.5V13.834H2C1.6756 13.834 1.5 13.6064 1.5 13.4287V2.57129C1.50024 2.39409 1.67607 2.16699 2 2.16699Z" stroke="#394047"/>
|
||||
</svg>
|
||||
|
||||
<!-- <span class="admin-post-form__settings-toggle-icon block h-4 w-4 border-x-2 border-[#394047]" aria-hidden="true" /> -->
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="admin-post-form__main flex min-h-0 flex-1 flex-col bg-white lg:flex-row">
|
||||
<section class="admin-post-form__content mx-auto w-full max-w-[804px] px-8 pb-16 pt-14">
|
||||
<div class="admin-post-form__feature-block mb-9">
|
||||
<figure v-if="form.featuredImage" class="admin-post-form__featured-editor overflow-hidden rounded border border-line bg-white">
|
||||
<img class="admin-post-form__featured-editor-image aspect-[16/9] w-full bg-surface object-cover" :src="form.featuredImage" alt="">
|
||||
<figcaption class="admin-post-form__featured-editor-actions flex flex-wrap items-center gap-2 border-t border-line p-3">
|
||||
<button class="admin-post-form__featured-change rounded border border-line px-3 py-1.5 text-xs font-semibold" type="button" @click="openMediaPicker('featuredImage')">
|
||||
변경
|
||||
</button>
|
||||
<label class="admin-post-form__featured-reupload cursor-pointer rounded border border-line px-3 py-1.5 text-xs font-semibold">
|
||||
새 업로드
|
||||
<input class="sr-only" type="file" accept="image/*" @change="uploadFeaturedImage">
|
||||
</label>
|
||||
<button class="admin-post-form__featured-remove rounded border border-red-200 px-3 py-1.5 text-xs font-semibold text-red-700" type="button" @click="removeFeaturedImage">
|
||||
삭제
|
||||
</button>
|
||||
</figcaption>
|
||||
</figure>
|
||||
<div v-else class="admin-post-form__feature-empty flex h-6 items-start">
|
||||
<button class="admin-post-form__feature-add inline-flex items-center gap-1.5 text-sm text-[#8e9cac]" type="button" @click="openMediaPicker('featuredImage')">
|
||||
<span aria-hidden="true">+</span>
|
||||
<span>대표 이미지 추가</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
v-model="form.title"
|
||||
class="admin-post-form__title-input border-0 bg-transparent px-0 py-2 text-5xl font-semibold leading-tight text-ink outline-none placeholder:text-soft"
|
||||
class="admin-post-form__title-input mb-2 w-full border-0 bg-transparent px-0 py-0 text-5xl font-bold leading-tight text-ink outline-none placeholder:text-[#8e9cac]"
|
||||
type="text"
|
||||
placeholder="제목"
|
||||
required
|
||||
@@ -484,15 +576,25 @@ defineExpose({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-post-form__field grid gap-2 text-sm">
|
||||
<div class="admin-post-form__field admin-post-form__content-editor text-sm">
|
||||
<AdminBlockEditor ref="blockEditor" v-model="form.content" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="admin-post-form__settings grid content-start gap-4">
|
||||
<aside v-if="isSettingsOpen" class="admin-post-form__settings flex h-auto w-full shrink-0 flex-col border-t border-[#e3e6e8] bg-white lg:w-[420px] lg:border-l lg:border-t-0">
|
||||
<div class="admin-post-form__settings-header flex h-[82px] shrink-0 items-center justify-between px-6">
|
||||
<h2 class="admin-post-form__settings-title text-xl font-bold text-black">
|
||||
게시물 설정
|
||||
</h2>
|
||||
<button class="admin-post-form__settings-close grid size-6 place-items-center text-[#394047]" type="button" aria-label="게시물 설정 닫기" @click="toggleSettingsPanel">
|
||||
<span aria-hidden="true">x</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="admin-post-form__settings-body grid content-start gap-4 overflow-y-auto px-6 pb-8">
|
||||
<label class="admin-post-form__field grid gap-2 text-sm">
|
||||
<span class="admin-post-form__label font-medium">상태</span>
|
||||
<select v-model="form.status" class="admin-post-form__select rounded border border-line bg-white px-3 py-2">
|
||||
<select v-model="form.status" class="admin-post-form__select h-[38px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2">
|
||||
<option value="draft">초안</option>
|
||||
<option value="published">발행</option>
|
||||
<option value="private">비공개</option>
|
||||
@@ -505,7 +607,7 @@ defineExpose({
|
||||
</span>
|
||||
<input
|
||||
v-model="form.publishedAt"
|
||||
class="admin-post-form__input rounded border border-line bg-white px-3 py-2"
|
||||
class="admin-post-form__input h-[38px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2"
|
||||
type="datetime-local"
|
||||
>
|
||||
<span class="admin-post-form__hint text-xs text-muted">
|
||||
@@ -517,7 +619,7 @@ defineExpose({
|
||||
<span class="admin-post-form__label font-medium">슬러그</span>
|
||||
<input
|
||||
v-model="form.slug"
|
||||
class="admin-post-form__input rounded border border-line bg-white px-3 py-2"
|
||||
class="admin-post-form__input h-[38px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2"
|
||||
type="text"
|
||||
pattern="[a-z0-9가-힣]+(-[a-z0-9가-힣]+)*"
|
||||
required
|
||||
@@ -529,7 +631,7 @@ defineExpose({
|
||||
<span class="admin-post-form__label font-medium">요약</span>
|
||||
<textarea
|
||||
v-model="form.excerpt"
|
||||
class="admin-post-form__textarea min-h-24 rounded border border-line bg-white px-3 py-2"
|
||||
class="admin-post-form__textarea min-h-[108px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -537,12 +639,12 @@ defineExpose({
|
||||
<span class="admin-post-form__label font-medium">태그</span>
|
||||
<input
|
||||
v-model="form.tagsText"
|
||||
class="admin-post-form__input rounded border border-line bg-white px-3 py-2"
|
||||
class="admin-post-form__input h-[38px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2"
|
||||
type="text"
|
||||
>
|
||||
</label>
|
||||
|
||||
<div class="admin-post-form__seo grid gap-3 rounded border border-line bg-white p-4 text-sm">
|
||||
<div class="admin-post-form__seo grid gap-3 border-t border-[#e3e6e8] pt-5 text-sm">
|
||||
<div>
|
||||
<h2 class="admin-post-form__section-title text-sm font-semibold text-ink">
|
||||
SEO
|
||||
@@ -556,7 +658,7 @@ defineExpose({
|
||||
<span class="admin-post-form__label font-medium">SEO 제목</span>
|
||||
<input
|
||||
v-model="form.seoTitle"
|
||||
class="admin-post-form__input rounded border border-line bg-white px-3 py-2"
|
||||
class="admin-post-form__input h-[38px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2"
|
||||
type="text"
|
||||
maxlength="80"
|
||||
placeholder="비워두면 글 제목을 사용"
|
||||
@@ -570,7 +672,7 @@ defineExpose({
|
||||
<span class="admin-post-form__label font-medium">SEO 설명</span>
|
||||
<textarea
|
||||
v-model="form.seoDescription"
|
||||
class="admin-post-form__textarea min-h-24 rounded border border-line bg-white px-3 py-2"
|
||||
class="admin-post-form__textarea min-h-[108px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2"
|
||||
maxlength="180"
|
||||
placeholder="비워두면 요약을 사용"
|
||||
/>
|
||||
@@ -583,7 +685,7 @@ defineExpose({
|
||||
<span class="admin-post-form__label font-medium">Canonical URL</span>
|
||||
<input
|
||||
v-model="form.canonicalUrl"
|
||||
class="admin-post-form__input rounded border border-line bg-white px-3 py-2"
|
||||
class="admin-post-form__input h-[38px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2"
|
||||
type="url"
|
||||
placeholder="비워두면 기본 글 주소를 사용"
|
||||
>
|
||||
@@ -602,39 +704,6 @@ defineExpose({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="admin-post-form__field grid gap-2 text-sm">
|
||||
<span class="admin-post-form__label font-medium">대표 이미지</span>
|
||||
<figure v-if="form.featuredImage" class="admin-post-form__featured overflow-hidden rounded border border-line bg-white">
|
||||
<img class="admin-post-form__featured-image aspect-[4/3] w-full bg-surface object-cover" :src="form.featuredImage" alt="">
|
||||
<figcaption class="admin-post-form__featured-actions grid gap-2 p-3">
|
||||
<p class="admin-post-form__featured-url break-all text-xs text-muted">
|
||||
{{ form.featuredImage }}
|
||||
</p>
|
||||
<div class="admin-post-form__featured-buttons flex flex-wrap gap-2">
|
||||
<button class="admin-post-form__featured-change rounded border border-line px-3 py-1.5 text-xs font-semibold" type="button" @click="openMediaPicker('featuredImage')">
|
||||
변경
|
||||
</button>
|
||||
<label class="admin-post-form__featured-reupload cursor-pointer rounded border border-line px-3 py-1.5 text-xs font-semibold">
|
||||
새 업로드
|
||||
<input class="sr-only" type="file" accept="image/*" @change="uploadFeaturedImage">
|
||||
</label>
|
||||
<button class="admin-post-form__featured-remove rounded border border-red-200 px-3 py-1.5 text-xs font-semibold text-red-700" type="button" @click="removeFeaturedImage">
|
||||
삭제
|
||||
</button>
|
||||
</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
<div v-else class="admin-post-form__featured-empty grid gap-2 rounded border border-dashed border-line bg-white p-4">
|
||||
<button class="admin-post-form__featured-select rounded border border-line px-3 py-2 text-sm font-semibold" type="button" @click="openMediaPicker">
|
||||
미디어에서 선택
|
||||
</button>
|
||||
<label class="admin-post-form__featured-upload cursor-pointer rounded bg-[#15171a] px-3 py-2 text-center text-sm font-semibold text-white">
|
||||
{{ isUploadingFeaturedImage ? '업로드 중' : '새 이미지 업로드' }}
|
||||
<input class="sr-only" type="file" accept="image/*" @change="uploadFeaturedImage">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-post-form__field grid gap-2 text-sm">
|
||||
<span class="admin-post-form__label font-medium">OG 이미지</span>
|
||||
<figure v-if="form.ogImage" class="admin-post-form__og-image overflow-hidden rounded border border-line bg-white">
|
||||
@@ -667,32 +736,10 @@ defineExpose({
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="admin-post-form__actions flex justify-end gap-3 border-t border-line pt-5">
|
||||
<p v-if="autosaveStatus" class="admin-post-form__autosave-status mr-auto self-center text-xs text-muted">
|
||||
{{ autosaveStatus }}
|
||||
</p>
|
||||
<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"
|
||||
:disabled="saving"
|
||||
>
|
||||
{{ saving ? '저장 중' : submitLabel }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isMediaPickerOpen"
|
||||
class="admin-post-form__media-picker fixed inset-0 z-50 grid place-items-center bg-black/40 px-5 py-8"
|
||||
|
||||
Reference in New Issue
Block a user