v0.0.86: 미리보기 패딩, 태그 한글 유지, SEO 자동, 태그 관리 토스트

This commit is contained in:
2026-05-12 09:56:52 +09:00
parent bd71ca860c
commit 79fb354d91
10 changed files with 212 additions and 102 deletions

View File

@@ -34,21 +34,25 @@ onMounted(loadPreviewPost)
관리자 미리보기
</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">
<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 sm:mx-auto sm:max-w-[720px] sm:px-5">
{{ 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>
<section v-else-if="previewPost" class="admin-post-preview__body">
<div class="mx-auto max-w-[720px] px-4 sm:px-5">
<ContentRenderer>
<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>
<ContentMarkdownRenderer class="post-detail__content" :content="previewPost.content || ''" />
</ContentRenderer>
</div>
</section>
</div>
</template>

View File

@@ -9,8 +9,8 @@ const savingOrder = ref(false)
const promotingTagId = ref('')
const demotingTagId = ref('')
const deletingGeneralTagId = ref('')
const errorMessage = ref('')
const infoMessage = ref('')
const toast = ref(null)
let toastTimer = null
const generalTagQuery = ref('')
const generalTagSearchResults = ref([])
const generalTagSearchLoading = ref(false)
@@ -21,6 +21,20 @@ const { data: tags, refresh } = await useFetch('/admin/api/tags', {
const managedTags = computed(() => tags.value.filter((tag) => tag.tagType === 'managed'))
/**
* 피드백 토스트 표시
* @param {'success'|'error'|'info'} type - 토스트 타입
* @param {string} message - 표시 메시지
* @returns {void}
*/
const showToast = (type, message) => {
window.clearTimeout(toastTimer)
toast.value = { type, message }
toastTimer = window.setTimeout(() => {
toast.value = null
}, 3200)
}
/**
* 관리용 태그 드래그 시작
* @param {DragEvent} event - 드래그 이벤트
@@ -105,8 +119,6 @@ const saveManagedOrder = async () => {
}
savingOrder.value = true
errorMessage.value = ''
infoMessage.value = ''
try {
const reordered = await $fetch('/admin/api/tags/reorder', {
@@ -118,9 +130,9 @@ const saveManagedOrder = async () => {
tags.value = [...reordered]
await refresh()
infoMessage.value = '메인 태그 순서가 저장되었습니다.'
showToast('success', '메인 태그 순서가 저장되었습니다.')
} catch (error) {
errorMessage.value = error?.data?.message || '정렬 순서를 저장하지 못했습니다.'
showToast('error', error?.data?.message || '정렬 순서를 저장하지 못했습니다.')
} finally {
savingOrder.value = false
}
@@ -138,8 +150,6 @@ const searchGeneralTags = async () => {
}
generalTagSearchLoading.value = true
errorMessage.value = ''
infoMessage.value = ''
try {
generalTagSearchResults.value = await $fetch('/admin/api/tags', {
@@ -150,7 +160,7 @@ const searchGeneralTags = async () => {
}
})
} catch (error) {
errorMessage.value = error?.data?.message || '일반 태그 검색에 실패했습니다.'
showToast('error', error?.data?.message || '일반 태그 검색에 실패했습니다.')
} finally {
generalTagSearchLoading.value = false
}
@@ -167,8 +177,6 @@ const promoteToMainTag = async (tag) => {
}
promotingTagId.value = tag.id
errorMessage.value = ''
infoMessage.value = ''
try {
await $fetch(`/admin/api/tags/${tag.id}`, {
@@ -184,9 +192,9 @@ const promoteToMainTag = async (tag) => {
})
await refresh()
await searchGeneralTags()
infoMessage.value = `"${tag.name}" 태그를 메인 태그로 전환했습니다.`
showToast('success', `"${tag.name}" 태그를 메인 태그로 전환했습니다.`)
} catch (error) {
errorMessage.value = error?.data?.message || '메인 태그 전환에 실패했습니다.'
showToast('error', error?.data?.message || '메인 태그 전환에 실패했습니다.')
} finally {
promotingTagId.value = ''
}
@@ -203,8 +211,6 @@ const demoteToGeneralTag = async (tag) => {
}
demotingTagId.value = tag.id
errorMessage.value = ''
infoMessage.value = ''
try {
await $fetch(`/admin/api/tags/${tag.id}`, {
@@ -220,9 +226,9 @@ const demoteToGeneralTag = async (tag) => {
})
await refresh()
await searchGeneralTags()
infoMessage.value = `"${tag.name}" 태그를 일반 태그로 변경했습니다.`
showToast('success', `"${tag.name}" 태그를 일반 태그로 변경했습니다.`)
} catch (error) {
errorMessage.value = error?.data?.message || '일반 태그 변경에 실패했습니다.'
showToast('error', error?.data?.message || '일반 태그 변경에 실패했습니다.')
} finally {
demotingTagId.value = ''
}
@@ -239,8 +245,6 @@ const deleteGeneralTag = async (tag) => {
}
deletingGeneralTagId.value = tag.id
errorMessage.value = ''
infoMessage.value = ''
try {
await $fetch(`/admin/api/tags/${tag.id}`, {
@@ -248,14 +252,18 @@ const deleteGeneralTag = async (tag) => {
})
await refresh()
await searchGeneralTags()
infoMessage.value = `"${tag.name}" 일반 태그를 삭제했습니다.`
showToast('success', `"${tag.name}" 일반 태그를 삭제했습니다.`)
} catch (error) {
errorMessage.value = error?.data?.message || '일반 태그를 삭제하지 못했습니다.'
showToast('error', error?.data?.message || '일반 태그를 삭제하지 못했습니다.')
} finally {
deletingGeneralTagId.value = ''
}
}
onBeforeUnmount(() => {
window.clearTimeout(toastTimer)
})
</script>
<template>
@@ -277,13 +285,6 @@ const deleteGeneralTag = async (tag) => {
메인 태그는 홈페이지 카테고리 영역에서 사용되며 드래그 정렬이 가능합니다. 일반 태그는 검색으로 찾아 메인 태그로 전환할 있습니다.
</p>
<p v-if="errorMessage" class="admin-tags__error mt-6 rounded border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{{ errorMessage }}
</p>
<p v-if="infoMessage" class="mt-3 rounded border border-line bg-white px-4 py-3 text-sm text-muted">
{{ infoMessage }}
</p>
<div class="admin-tags__table mt-6 overflow-hidden border border-line">
<div class="flex items-center justify-between border-b border-line bg-[#f7f7f5] px-4 py-2.5">
<p class="text-xs font-semibold uppercase text-muted">메인 태그</p>
@@ -413,5 +414,18 @@ const deleteGeneralTag = async (tag) => {
<p v-if="tags.length === 0" class="admin-tags__empty mt-6 text-sm text-muted">
아직 등록된 태그가 없습니다.
</p>
<div
v-if="toast"
class="admin-tags__toast fixed right-5 top-5 z-50 rounded border px-4 py-3 text-sm font-semibold shadow-lg"
:class="{
'border-green-200 bg-green-50 text-green-800': toast.type === 'success',
'border-red-200 bg-red-50 text-red-800': toast.type === 'error',
'border-line bg-white text-ink': toast.type === 'info'
}"
role="status"
>
{{ toast.message }}
</div>
</section>
</template>