v0.0.86: 미리보기 패딩, 태그 한글 유지, SEO 자동, 태그 관리 토스트
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user