v1.4.5: 게시물 작성자·편집 링크·목록 요약 보정

- posts.author_id 마이그레이션 및 owner/admin 단일 계정 환경에서만 기존 글 backfill
- 공개 상세: 글쓴이 본인일 때만 공유 옆 수정 링크 표시, 수정 시각 제거
- 목록 요약: excerpt 없을 때 본문 fallback, post-summary-clamp로 말줄임 처리
- 회원 세션 API에 isAdmin·role 추가
This commit is contained in:
2026-05-22 14:43:22 +09:00
parent 8f53210756
commit 38ca3a4709
16 changed files with 215 additions and 47 deletions

View File

@@ -13,9 +13,6 @@ const { data: tags } = await useFetch('/api/tags', {
const { data: posts } = await useFetch('/api/posts', {
default: () => []
})
const { data: siteSettings } = await useFetch('/api/site-settings', {
default: () => ({ showPostUpdatedAt: false })
})
if (!post.value) {
throw createError({
@@ -46,16 +43,19 @@ const primaryTagMeta = computed(() => {
})
const publishedAtLabel = computed(() => formatPostDate(post.value.publishedAt || null))
const updatedAtLabel = computed(() => {
if (!siteSettings.value?.showPostUpdatedAt || !wasPostUpdatedAfterPublish(post.value)) {
return ''
}
return `수정: ${formatPostDateTime(post.value.updatedAt)}`
})
const authorLabel = computed(() => 'sori.studio')
const shareModalOpen = ref(false)
const copyButtonLabel = ref('Copy link')
const currentMember = ref(null)
const currentAdmin = ref(null)
const canEditPost = computed(() => Boolean(
post.value?.authorId
&& (
currentMember.value?.id === post.value.authorId
|| currentAdmin.value?.userId === post.value.authorId
)
))
const postEditPath = computed(() => `/admin/posts/${post.value.id}`)
const currentIndex = computed(() => posts.value.findIndex((item) => item.slug === post.value.slug))
const previousPost = computed(() => {
@@ -79,11 +79,12 @@ const config = useRuntimeConfig()
const siteUrl = computed(() => String(config.public.siteUrl || '').replace(/\/$/g, ''))
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 ogImage = computed(() => post.value.featuredImage || '')
const postSummary = computed(() => createPostSummary(post.value.excerpt, post.value.content))
const seoDescription = computed(() => post.value.seoDescription || postSummary.value || 'sori.studio 개인 블로그')
const shareMetadata = computed(() => ({
title: post.value.title || 'sori.studio',
description: post.value.excerpt || 'sori.studio 개인 블로그',
description: seoDescription.value,
image: post.value.featuredImage || '',
url: pageUrl.value
}))
@@ -120,6 +121,28 @@ const shareLinks = computed(() => [
}
])
/**
* 현재 로그인 회원·관리자 정보를 불러온다.
* @returns {Promise<void>}
*/
const fetchCurrentViewer = async () => {
try {
currentMember.value = await $fetch('/api/auth/me')
} catch {
currentMember.value = null
}
try {
currentAdmin.value = await $fetch('/admin/api/auth/me')
} catch {
currentAdmin.value = null
}
}
onMounted(() => {
fetchCurrentViewer()
})
/**
* 절대 URL 생성
* @param {string} value - 원본 URL
@@ -244,10 +267,6 @@ useHead(() => ({
{{ publishedAtLabel }}
</time>
<time v-if="updatedAtLabel" :datetime="post.updatedAt" class="admin-post-detail__updated-at">
{{ updatedAtLabel }}
</time>
<a href="#" class="hover:opacity-75">
{{ authorLabel }}
</a>
@@ -272,11 +291,26 @@ useHead(() => ({
</a>
</div>
<button class="absolute right-0 bottom-4 flex cursor-pointer items-center gap-1 hover:opacity-75" type="button" aria-label="Share this post" data-post-share-toggle @click="openShareModal">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 4v4c-6.575 1.028-9.02 6.788-10 12c-.037.206 5.384-5.962 10-6v4l8-7-8-7z" />
</svg>
</button>
<div class="absolute right-0 bottom-4 flex items-center gap-3">
<NuxtLink
v-if="canEditPost"
class="flex cursor-pointer items-center gap-1 hover:opacity-75"
:to="postEditPath"
target="_blank"
rel="noreferrer"
aria-label="Edit this post"
data-post-edit-link
>
<svg xmlns="http://www.w3.org/2000/svg" height="18" viewBox="0 -960 960 960" width="18" fill="currentColor" aria-hidden="true">
<path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z" />
</svg>
</NuxtLink>
<button class="flex cursor-pointer items-center gap-1 hover:opacity-75" type="button" aria-label="Share this post" data-post-share-toggle @click="openShareModal">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 4v4c-6.575 1.028-9.02 6.788-10 12c-.037.206 5.384-5.962 10-6v4l8-7-8-7z" />
</svg>
</button>
</div>
</div>
<figure v-if="post.featuredImage" class="relative mt-2.5 w-full">