글 설정 태그와 대표 이미지 흐름 정리

This commit is contained in:
2026-05-07 15:55:20 +09:00
parent 0f60039126
commit f757c3db78
11 changed files with 240 additions and 132 deletions

View File

@@ -46,6 +46,13 @@
margin: 0;
background: var(--site-bg);
}
html.admin-post-editor-document,
body.admin-post-editor-document {
height: 100%;
overflow: hidden;
background: #ffffff;
}
}
@layer components {

View File

@@ -40,12 +40,13 @@ const isMediaPickerOpen = ref(false)
const mediaPickerTarget = ref('featuredImage')
const isLoadingMedia = ref(false)
const isUploadingFeaturedImage = ref(false)
const isUploadingOgImage = ref(false)
const autosaveTimer = ref(null)
const autosaveNotice = ref(null)
const autosaveStatus = ref('')
const isRestoringAutosave = ref(false)
const isSettingsOpen = ref(true)
const tagInput = ref('')
const activeMediaPickerTab = ref('upload')
/**
* ISO 날짜를 datetime-local 입력값으로 변환
@@ -95,9 +96,7 @@ const form = reactive({
featuredImage: props.initialPost.featuredImage || '',
seoTitle: props.initialPost.seoTitle || '',
seoDescription: props.initialPost.seoDescription || '',
canonicalUrl: props.initialPost.canonicalUrl || '',
noindex: Boolean(props.initialPost.noindex),
ogImage: props.initialPost.ogImage || '',
status: props.initialPost.status || 'draft',
publishedAt: toDateTimeLocalValue(props.initialPost.publishedAt),
tagsText: props.initialPost.tags?.join(', ') || ''
@@ -145,6 +144,8 @@ const parseTags = (value) => [...new Set(value
.map((tag) => toSlug(tag))
.filter(Boolean))]
const selectedTags = computed(() => parseTags(form.tagsText))
/**
* 예약 발행 여부 확인
* @returns {boolean} 예약 발행 여부
@@ -188,12 +189,12 @@ const createPostPayload = () => {
featuredImage: form.featuredImage.trim() || null,
seoTitle: form.seoTitle.trim(),
seoDescription: form.seoDescription.trim(),
canonicalUrl: form.canonicalUrl.trim(),
canonicalUrl: '',
noindex: form.noindex,
ogImage: form.ogImage.trim() || null,
ogImage: null,
status: form.status,
publishedAt,
tags: parseTags(form.tagsText)
tags: selectedTags.value
}
}
@@ -209,9 +210,7 @@ const createAutosavePayload = () => ({
featuredImage: form.featuredImage,
seoTitle: form.seoTitle,
seoDescription: form.seoDescription,
canonicalUrl: form.canonicalUrl,
noindex: form.noindex,
ogImage: form.ogImage,
status: form.status,
publishedAt: form.publishedAt,
tagsText: form.tagsText
@@ -230,8 +229,6 @@ const isEmptyAutosavePayload = (payload) => ![
payload.featuredImage,
payload.seoTitle,
payload.seoDescription,
payload.canonicalUrl,
payload.ogImage,
payload.tagsText
].some((value) => String(value || '').trim())
@@ -339,6 +336,7 @@ const fetchMediaItems = async () => {
*/
const openMediaPicker = async (target = 'featuredImage') => {
mediaPickerTarget.value = target
activeMediaPickerTab.value = 'upload'
isMediaPickerOpen.value = true
await fetchMediaItems()
}
@@ -370,15 +368,73 @@ const removeFeaturedImage = () => {
}
/**
* OG 이미지 삭제
* 태그 입력값을 배지 목록에 추가
* @returns {void}
*/
const removeOgImage = () => {
form.ogImage = ''
const addTagFromInput = () => {
const nextTag = toSlug(tagInput.value)
if (!nextTag) {
tagInput.value = ''
return
}
form.tagsText = [...new Set([...selectedTags.value, nextTag])].join(', ')
tagInput.value = ''
}
/**
* 태그 배지 삭제
* @param {string} tag - 삭제할 태그
* @returns {void}
*/
const removeTag = (tag) => {
form.tagsText = selectedTags.value.filter((item) => item !== tag).join(', ')
}
/**
* 태그 입력 키 처리
* @param {KeyboardEvent} event - 키보드 이벤트
* @returns {void}
*/
const handleTagKeydown = (event) => {
if (event.key === 'Enter' || event.key === ',') {
event.preventDefault()
addTagFromInput()
return
}
if (event.key === 'Backspace' && !tagInput.value && selectedTags.value.length) {
event.preventDefault()
removeTag(selectedTags.value.at(-1))
}
}
/**
* 대표 이미지 파일 업로드
* @param {File} file - 업로드 파일
* @returns {Promise<void>}
*/
const uploadFeaturedImageFile = async (file) => {
const formData = new FormData()
formData.append('files', file)
isUploadingFeaturedImage.value = true
try {
const result = await $fetch('/admin/api/uploads', {
method: 'POST',
body: formData
})
form.featuredImage = result.files?.[0]?.url || ''
await fetchMediaItems()
closeMediaPicker()
} finally {
isUploadingFeaturedImage.value = false
}
}
/**
* 대표 이미지 파일 입력 처리
* @param {Event} event - 파일 입력 이벤트
* @returns {Promise<void>}
*/
@@ -389,48 +445,26 @@ const uploadFeaturedImage = async (event) => {
return
}
const formData = new FormData()
formData.append('files', files[0])
isUploadingFeaturedImage.value = true
try {
const result = await $fetch('/admin/api/uploads', {
method: 'POST',
body: formData
})
form.featuredImage = result.files?.[0]?.url || ''
await uploadFeaturedImageFile(files[0])
} finally {
event.target.value = ''
isUploadingFeaturedImage.value = false
}
}
/**
* OG 이미지 파일 업로드
* @param {Event} event - 파일 입력 이벤트
* 대표 이미지 드롭 업로드
* @param {DragEvent} event - 드롭 이벤트
* @returns {Promise<void>}
*/
const uploadOgImage = async (event) => {
const files = event.target.files
const dropFeaturedImage = async (event) => {
const files = event.dataTransfer?.files
if (!files?.length) {
return
}
const formData = new FormData()
formData.append('files', files[0])
isUploadingOgImage.value = true
try {
const result = await $fetch('/admin/api/uploads', {
method: 'POST',
body: formData
})
form.ogImage = result.files?.[0]?.url || ''
} finally {
event.target.value = ''
isUploadingOgImage.value = false
}
await uploadFeaturedImageFile(files[0])
}
/**
@@ -684,14 +718,35 @@ defineExpose({
/>
</label>
<label class="admin-post-form__field grid gap-2 text-sm">
<span class="admin-post-form__label font-medium">태그</span>
<input
v-model="form.tagsText"
class="admin-post-form__input h-[38px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2 transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac] focus:outline-none"
type="text"
>
</label>
<div class="admin-post-form__field grid gap-1 text-sm">
<span class="admin-post-form__label h-[22px] font-bold text-[#15171a]">Tags</span>
<label class="admin-post-form__tag-editor flex min-h-[38px] w-full items-center gap-2 rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-1 transition-colors hover:border-[#c8ced3] focus-within:border-[#8e9cac]">
<span
v-for="tag in selectedTags"
:key="tag"
class="admin-post-form__tag-badge inline-flex h-6 shrink-0 items-center gap-1.5 rounded-[3px] bg-[#ecd2de] px-2 text-sm text-[#e04e87]"
>
<span>{{ tag }}</span>
<button
class="admin-post-form__tag-remove grid size-3 place-items-center rounded text-[#e04e87] transition-colors hover:bg-[#e7c3d2]"
type="button"
:aria-label="`${tag} 태그 삭제`"
@click="removeTag(tag)"
>
<span aria-hidden="true">x</span>
</button>
</span>
<input
v-model="tagInput"
class="admin-post-form__tag-input min-w-0 flex-1 border-0 bg-transparent p-0 text-sm text-black outline-none placeholder:text-[#8e9cac]"
type="text"
placeholder="태그 입력"
@blur="addTagFromInput"
@keydown="handleTagKeydown"
>
<span class="admin-post-form__tag-chevron text-xs text-[#394047]" aria-hidden="true"></span>
</label>
</div>
<div class="admin-post-form__seo grid gap-3 border-t border-[#e3e6e8] pt-5 text-sm">
<div>
@@ -730,16 +785,6 @@ defineExpose({
</span>
</label>
<label class="admin-post-form__field grid gap-2">
<span class="admin-post-form__label font-medium">Canonical URL</span>
<input
v-model="form.canonicalUrl"
class="admin-post-form__input h-[38px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2 transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac] focus:outline-none"
type="url"
placeholder="비워두면 기본 글 주소를 사용"
>
</label>
<label class="admin-post-form__checkbox flex items-start gap-2 text-sm">
<input
v-model="form.noindex"
@@ -752,39 +797,6 @@ defineExpose({
</span>
</label>
</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">
<img class="admin-post-form__og-preview aspect-[1.91/1] w-full bg-surface object-cover" :src="form.ogImage" alt="">
<figcaption class="admin-post-form__og-actions grid gap-2 p-3">
<p class="admin-post-form__og-url break-all text-xs text-muted">
{{ form.ogImage }}
</p>
<div class="admin-post-form__og-buttons flex flex-wrap gap-2">
<button class="admin-post-form__og-change rounded border border-line px-3 py-1.5 text-xs font-semibold transition-colors hover:border-[#c8ced3] hover:bg-[#eff1f2]" type="button" @click="openMediaPicker('ogImage')">
변경
</button>
<label class="admin-post-form__og-reupload cursor-pointer rounded border border-line px-3 py-1.5 text-xs font-semibold transition-colors hover:border-[#c8ced3] hover:bg-[#eff1f2]">
업로드
<input class="sr-only" type="file" accept="image/*" @change="uploadOgImage">
</label>
<button class="admin-post-form__og-remove rounded border border-red-200 px-3 py-1.5 text-xs font-semibold text-red-700 transition-colors hover:bg-red-50" type="button" @click="removeOgImage">
삭제
</button>
</div>
</figcaption>
</figure>
<div v-else class="admin-post-form__og-empty grid gap-2 rounded border border-dashed border-line bg-white p-4">
<button class="admin-post-form__og-select rounded border border-line px-3 py-2 text-sm font-semibold transition-colors hover:border-[#c8ced3] hover:bg-[#eff1f2]" type="button" @click="openMediaPicker('ogImage')">
미디어에서 선택
</button>
<label class="admin-post-form__og-upload cursor-pointer rounded bg-[#15171a] px-3 py-2 text-center text-sm font-semibold text-white transition-colors hover:bg-black">
{{ isUploadingOgImage ? '업로드 중' : '새 이미지 업로드' }}
<input class="sr-only" type="file" accept="image/*" @change="uploadOgImage">
</label>
</div>
</div>
</div>
<div v-if="showDelete" class="admin-post-form__settings-bottom shrink-0 border-t border-[#e3e6e8] px-8 py-6">
<button
@@ -807,34 +819,82 @@ defineExpose({
aria-modal="true"
@click.self="closeMediaPicker"
>
<section class="admin-post-form__media-picker-panel max-h-[80vh] w-full max-w-4xl overflow-hidden bg-white text-ink shadow-xl">
<section class="admin-post-form__media-picker-panel flex max-h-[86vh] min-h-[620px] w-full max-w-5xl flex-col overflow-hidden bg-white text-ink shadow-xl">
<div class="admin-post-form__media-picker-header flex items-center justify-between border-b border-line px-5 py-4">
<h2 class="admin-post-form__media-picker-title text-lg font-semibold">
{{ mediaPickerTarget === 'ogImage' ? 'OG 이미지 선택' : '대표 이미지 선택' }}
대표 이미지
</h2>
<button class="admin-post-form__media-picker-close rounded border border-line px-3 py-1.5 text-sm font-semibold transition-colors hover:bg-[#eff1f2]" type="button" @click="closeMediaPicker">
닫기
</button>
</div>
<div class="admin-post-form__media-picker-body max-h-[62vh] overflow-y-auto p-5">
<p v-if="isLoadingMedia" class="admin-post-form__media-picker-loading text-sm text-muted">
미디어를 불러오는 중입니다.
</p>
<div v-else-if="mediaItems.length" class="admin-post-form__media-picker-grid grid grid-cols-3 gap-2 sm:grid-cols-4 md:grid-cols-6">
<button
v-for="item in mediaItems"
:key="item.url"
class="admin-post-form__media-picker-item overflow-hidden border border-line bg-white text-left transition hover:border-[#8e9cac] hover:shadow-sm"
type="button"
@click="selectPickedImage(item)"
>
<img class="admin-post-form__media-picker-image aspect-square w-full bg-surface object-cover" :src="item.url" :alt="item.title">
<span class="admin-post-form__media-picker-name block truncate px-2 py-1.5 text-xs font-semibold text-ink">{{ item.name }}</span>
</button>
<div class="admin-post-form__media-picker-tabs flex border-b border-line px-5">
<button
class="admin-post-form__media-picker-tab border-b-2 px-3 py-3 text-sm font-semibold transition-colors"
:class="activeMediaPickerTab === 'upload' ? 'border-[#15171a] text-[#15171a]' : 'border-transparent text-muted hover:text-[#15171a]'"
type="button"
@click="activeMediaPickerTab = 'upload'"
>
이미지 업로드
</button>
<button
class="admin-post-form__media-picker-tab border-b-2 px-3 py-3 text-sm font-semibold transition-colors"
:class="activeMediaPickerTab === 'library' ? 'border-[#15171a] text-[#15171a]' : 'border-transparent text-muted hover:text-[#15171a]'"
type="button"
@click="activeMediaPickerTab = 'library'"
>
미디어 라이브러리
</button>
</div>
<div class="admin-post-form__media-picker-body flex-1 overflow-y-auto p-5">
<div
v-if="activeMediaPickerTab === 'upload'"
class="admin-post-form__media-upload-zone grid min-h-[420px] place-items-center border border-dashed border-[#cfd5da] bg-white text-center"
@dragover.prevent
@drop.prevent="dropFeaturedImage"
>
<div class="admin-post-form__media-upload-inner grid gap-3">
<p class="admin-post-form__media-upload-title text-lg font-semibold text-[#15171a]">
파일을 끌어 업로드
</p>
<p class="admin-post-form__media-upload-or text-sm text-muted">
또는
</p>
<label class="admin-post-form__media-upload-button mx-auto inline-flex h-10 cursor-pointer items-center justify-center rounded border border-[#2b78d0] px-8 text-sm font-semibold text-[#1f6fbf] transition-colors hover:bg-blue-50">
{{ isUploadingFeaturedImage ? '업로드 중' : '파일 선택' }}
<input class="sr-only" type="file" accept="image/*" @change="uploadFeaturedImage">
</label>
</div>
</div>
<p v-else class="admin-post-form__media-picker-empty border border-dashed border-line p-8 text-center text-sm text-muted">
선택할 미디어가 없습니다.
</p>
<template v-else>
<p v-if="isLoadingMedia" class="admin-post-form__media-picker-loading text-sm text-muted">
미디어를 불러오는 중입니다.
</p>
<div v-else-if="mediaItems.length" class="admin-post-form__media-picker-grid grid grid-cols-3 gap-2 sm:grid-cols-4 md:grid-cols-6">
<button
v-for="item in mediaItems"
:key="item.url"
class="admin-post-form__media-picker-item overflow-hidden border border-line bg-white text-left transition hover:border-[#8e9cac] hover:shadow-sm"
type="button"
@click="selectPickedImage(item)"
>
<img class="admin-post-form__media-picker-image aspect-square w-full bg-surface object-cover" :src="item.url" :alt="item.title">
<span class="admin-post-form__media-picker-name block truncate px-2 py-1.5 text-xs font-semibold text-ink">{{ item.name }}</span>
</button>
</div>
<p v-else class="admin-post-form__media-picker-empty border border-dashed border-line p-8 text-center text-sm text-muted">
선택할 미디어가 없습니다.
</p>
</template>
</div>
<div class="admin-post-form__media-picker-footer flex h-14 shrink-0 items-center justify-end border-t border-line px-5">
<button
class="admin-post-form__media-picker-confirm h-9 rounded border border-line px-4 text-sm font-semibold text-muted disabled:cursor-not-allowed disabled:opacity-50"
type="button"
disabled
>
대표 이미지 설정
</button>
</div>
</section>
</div>

View File

@@ -1,5 +1,15 @@
# 의사결정 이력
## 2026-05-07 v0.0.42
### 태그 입력과 대표 이미지 선택 흐름 결정
관리자 글 설정의 태그 입력은 단순 텍스트 필드가 아니라 배지형 입력으로 처리한다. 태그 입력 중 Enter가 폼 제출로 전파되면 의도치 않게 게시물이 저장 또는 발행될 수 있으므로, Enter와 쉼표는 태그 추가 동작으로만 사용한다.
Canonical URL과 OG 이미지는 별도 입력 항목에서 제외한다. 현재 운영 흐름에서는 기본 글 주소와 대표 이미지가 자연스러운 기본값이며, 별도 OG 이미지를 관리하면 글 설정 패널이 불필요하게 길어지고 대표 이미지와 공유 이미지가 어긋날 수 있기 때문이다.
대표 이미지는 업로드 탭과 미디어 라이브러리 탭을 함께 제공한다. 작성자는 새 이미지를 바로 올릴 수도 있고, 이미 업로드한 이미지를 재사용할 수도 있어야 하기 때문이다.
## 2026-05-07 v0.0.41
### 명령 메뉴 계층과 개발 도구 표시 결정

View File

@@ -26,7 +26,7 @@
| 파일 | 화면 위치 |
|------|-----------|
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost 스타일 전체 화면 에디터, 좌우 분할 설정 패널, 설정 패널 전환 애니메이션, Post URL 보기 액션, 하단 삭제 액션, 대표 이미지/OG 이미지 선택, 로컬 자동 저장, 예약 발행 시각 입력, SEO 설정, 미리보기 요청 |
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost 스타일 전체 화면 에디터, 좌우 분할 설정 패널, 설정 패널 전환 애니메이션, Post URL 보기 액션, 하단 삭제 액션, 대표 이미지 업로드/미디어 선택, 배지형 태그 입력, 로컬 자동 저장, 예약 발행 시각 입력, SEO 설정, 미리보기 요청 |
| components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 대표 이미지 선택 |
| components/admin/AdminBlockEditor.vue | 관리자 글 블록형 에디터, 이미지/갤러리/콜아웃/토글/임베드 블록, 한글 조합 입력 처리, Shift+Enter 줄바꿈, 코드 블록 단축 변환, AFFiNE 참고 세로 막대형 블록 핸들 선택/삭제/드래그 이동과 삽입선 표시, 하단 빈 입력 블록 유지, 본문 placeholder 표시 |
| components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼 |

View File

@@ -306,6 +306,7 @@ components/content/
- 공개 가능한 글의 보기 액션은 게시물 설정 패널의 Post URL 라벨 오른쪽에 표시한다.
- 글 삭제 액션은 게시물 설정 패널 하단의 빨간 outline 버튼으로 제공한다.
- 대표 이미지는 본문 제목 위의 에디터 흐름 안에서 추가, 변경, 삭제한다.
- 대표 이미지 선택 모달은 이미지 업로드 탭과 미디어 라이브러리 탭을 제공한다.
- 제목 입력에서 Enter를 누르면 본문 첫 블록으로 포커스를 이동한다.
- 새 글 작성처럼 본문이 비어 있는 경우에도 빈 문단 블록을 먼저 생성한다.
- 글 작성/수정 중인 입력값은 브라우저 `localStorage`에 자동 저장한다.
@@ -319,13 +320,14 @@ components/content/
- 예약 발행 글은 관리자 목록에서 예약 상태로 표시하되 공개 게시물 목록과 상세 API에는 발행 시각 이후부터 노출한다.
- 관리자 글 에디터의 실제 입력 텍스트 색상은 placeholder보다 진하게 표시한다.
- 관리자 작성 화면과 공개 본문은 같은 마크다운 렌더링 기준을 사용한다.
- 대표 이미지는 URL 직접 입력이 아니라 미디어 선택 또는 새 이미지 업로드로 설정한다.
- 태그 입력은 배지형 입력으로 제공하며 Enter 또는 쉼표 입력 시 태그를 추가하고, 배지의 x 버튼으로 삭제한다.
- 대표 이미지는 URL 직접 입력이 아니라 미디어 선택 또는 이미지 업로드 탭으로 설정한다.
- 대표 이미지가 설정되면 관리자 글 폼에 썸네일과 삭제/변경 액션을 표시한다.
- 글 SEO 설정은 SEO 제목, SEO 설명, canonical URL, 검색엔진 노출 제외 여부를 저장한다.
- 글 SEO 설정은 SEO 제목, SEO 설명, 검색엔진 노출 제외 여부를 저장한다.
- Canonical URL은 별도 입력을 받지 않고 기본 글 주소를 사용한다.
- 공개 게시물 상세 화면은 SEO 제목이 없으면 글 제목, SEO 설명이 없으면 요약을 메타 태그 기본값으로 사용한다.
- 검색엔진 노출 제외가 켜진 글은 robots 메타를 `noindex, nofollow`로 출력한다.
- 글 OG 이미지는 미디어 선택 또는 새 이미지 업로드로 설정하며, 공개 상세 화면의 `og:image`와 Twitter large image 카드 사용한다.
- OG 이미지가 없으면 대표 이미지를 `og:image` fallback으로 사용한다.
- 공개 상세 화면의 `og:image`와 Twitter large image 카드는 대표 이미지를 기본값으로 사용한다.
- 이미지 블록은 관리자 업로드 API로 이미지를 업로드하고 `![alt](url){width=wide}` 형식으로 저장한다.
- 이미지/갤러리 삽입 시 파일명은 alt 값으로 자동 입력하지 않는다.
- 이미지 블록 alt 입력은 이미지 hover 또는 focus 상태에서만 표시한다.
@@ -390,7 +392,7 @@ components/content/
- 미디어 파일 경로, 사용 현황, 용량 등 세부 정보는 썸네일 더블 클릭 시 열리는 상세 모달에서 표시한다.
- 미디어 폴더는 실제 파일 경로를 옮기지 않고 `media_metadata` 테이블에 URL별 경로 메타데이터로 저장한다.
- 글쓰기 미디어 선택 창은 업로드 미디어 목록에서 이미지를 선택해 단일 이미지 또는 갤러리에 삽입한다.
- 미디어 사용 현황은 게시물/페이지의 대표 이미지, 게시물 OG 이미지, 본문 내 URL을 기준으로 표시한다.
- 미디어 사용 현황은 게시물/페이지의 대표 이미지 본문 내 URL을 기준으로 표시한다.
- 사용 중인 미디어는 저장된 콘텐츠 URL이 깨지지 않도록 파일명 변경과 삭제를 차단한다.
- 향후 미디어 라이브러리는 프로필/사이트 설정 이미지 사용처 추적을 제공한다.
@@ -452,6 +454,6 @@ APP_PORT=43118
## 버전 관리
- 현재 버전: v0.0.41
- 현재 버전: v0.0.42
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정

View File

@@ -1,5 +1,16 @@
# 업데이트 이력
## v0.0.42
- 관리자 글쓰기 화면의 문서 스크롤 잠금 클래스를 html/body에 직접 적용하도록 보강.
- 관리자 글 설정의 태그 입력을 Figma 기준 배지형 입력으로 수정.
- 태그 입력 중 Enter가 게시물 저장으로 이어지던 문제 수정.
- 관리자 글 설정에서 Canonical URL과 OG 이미지 입력 UI 제거.
- 게시물 저장 시 Canonical URL은 기본 글 주소, OG 이미지는 대표 이미지를 따르도록 정리.
- 대표 이미지 선택 모달에 이미지 업로드와 미디어 라이브러리 탭 추가.
- 기술 명세 현재 버전을 v0.0.42로 갱신.
- 패키지 버전을 0.0.42로 갱신.
## v0.0.41
- 관리자 블록 에디터 `/` 명령 메뉴가 아래 블록 텍스트와 겹쳐 보이던 문제 수정.

View File

@@ -4,6 +4,32 @@ const route = useRoute()
const isPostEditorRoute = computed(() => route.path === '/admin/posts/new'
|| (route.path.startsWith('/admin/posts/') && route.path !== '/admin/posts/preview'))
const editorDocumentClass = 'admin-post-editor-document'
/**
* 글쓰기 전체 화면 문서 스크롤 잠금 적용
* @returns {void}
*/
const syncPostEditorDocumentClass = () => {
if (!import.meta.client) {
return
}
document.documentElement.classList.toggle(editorDocumentClass, isPostEditorRoute.value)
document.body.classList.toggle(editorDocumentClass, isPostEditorRoute.value)
}
watchEffect(syncPostEditorDocumentClass)
onBeforeUnmount(() => {
if (!import.meta.client) {
return
}
document.documentElement.classList.remove(editorDocumentClass)
document.body.classList.remove(editorDocumentClass)
})
/**
* 관리자 로그아웃
* @returns {Promise<void>} 로그아웃 처리 결과

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "sori.studio",
"version": "0.0.41",
"version": "0.0.42",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sori.studio",
"version": "0.0.41",
"version": "0.0.42",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",

View File

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

View File

@@ -21,8 +21,7 @@ const siteUrl = computed(() => String(config.public.siteUrl || '').replace(/\/$/
const pageUrl = computed(() => `${siteUrl.value}/post/${post.value.slug}`)
const seoTitle = computed(() => post.value.seoTitle || post.value.title)
const seoDescription = computed(() => post.value.seoDescription || post.value.excerpt || 'sori.studio 개인 블로그')
const canonicalUrl = computed(() => post.value.canonicalUrl || pageUrl.value)
const ogImage = computed(() => post.value.ogImage || post.value.featuredImage || '')
const ogImage = computed(() => post.value.featuredImage || '')
/**
* 절대 URL 생성
@@ -46,7 +45,7 @@ useHead(() => ({
link: [
{
rel: 'canonical',
href: canonicalUrl.value
href: pageUrl.value
}
],
meta: [

View File

@@ -203,13 +203,6 @@ const getContentMediaUsage = (contentItem, url) => {
})
}
if (contentItem.ogImage === url) {
usages.push({
location: 'ogImage',
label: 'OG 이미지'
})
}
if (contentItem.content?.includes(url)) {
usages.push({
location: 'content',