관리자 통계 제외와 콘텐츠 링크 정리

This commit is contained in:
2026-06-30 12:32:18 +09:00
parent 901463e5f3
commit 4e2ec3d7a2
23 changed files with 209 additions and 64 deletions

View File

@@ -159,6 +159,13 @@ const getPageStatusClass = (page) => {
return 'text-[#99A3AD]'
}
/**
* 페이지 공개 여부 확인
* @param {Object} page - 페이지
* @returns {boolean} 공개 여부
*/
const isPublicPage = (page) => page.status === 'published'
/**
* 날짜 표시 형식 변환
* @param {string | null} value - ISO 날짜 문자열
@@ -302,14 +309,29 @@ const deletePage = async (page) => {
</tr>
</thead>
<tbody class="admin-pages__table-body divide-y divide-line bg-white">
<tr v-for="page in pages" :key="page.id" class="admin-pages__row">
<tr v-for="page in pages" :key="page.id" class="admin-pages__row group">
<td class="admin-pages__cell px-4 py-4">
<NuxtLink class="admin-pages__title-link font-semibold hover:opacity-70" :to="`/admin/pages/${page.id}`">
{{ page.title }}
</NuxtLink>
<p class="admin-pages__slug mt-1 text-xs text-muted">
/pages/{{ page.slug }}
</p>
<div class="admin-pages__slug-row mt-1 flex items-center gap-1 text-xs text-muted">
<span class="admin-pages__slug truncate">/pages/{{ page.slug }}</span>
<NuxtLink
v-if="isPublicPage(page)"
class="admin-pages__preview-link inline-flex size-5 items-center justify-center rounded text-[#8e9cac] opacity-0 transition-colors hover:text-[#394047] focus:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#8e9cac] group-hover:opacity-100 group-focus-within:opacity-100"
:to="`/pages/${page.slug}`"
target="_blank"
rel="noopener noreferrer"
aria-label="공개 페이지 보기"
title="공개 페이지 보기"
@click.stop
>
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M17 7 7 17" />
<path d="M8 7h9v9" />
</svg>
</NuxtLink>
</div>
</td>
<td class="admin-pages__cell px-4 py-4">
<span class="admin-pages__status-text text-xs" :class="getPageStatusClass(page)">

View File

@@ -224,7 +224,7 @@ const getPostStatusLabel = (post) => {
}
if (statusKey === 'published') {
return '발행'
return '공개'
}
if (statusKey === 'members') {
@@ -728,7 +728,7 @@ watch(openPostMenuId, async (postId) => {
<span class="admin-posts__filter-select-wrap relative block">
<select v-model="statusFilter" class="admin-posts__filter-select h-10 appearance-none rounded border border-line bg-white pl-3 pr-9 text-sm text-[#394047] outline-none transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac]">
<option value="all">전체 상태</option>
<option value="published">발행</option>
<option value="published">공개</option>
<option value="draft">초안</option>
<option value="scheduled">예약</option>
<option value="members">멤버십</option>
@@ -802,7 +802,7 @@ watch(openPostMenuId, async (postId) => {
</tr>
</thead>
<tbody class="admin-posts__table-body divide-y divide-line bg-white">
<tr v-for="post in filteredPosts" :key="post.id" class="admin-posts__row">
<tr v-for="post in filteredPosts" :key="post.id" class="admin-posts__row group">
<td class="admin-posts__cell admin-posts__cell-featured w-10 px-2 py-4 text-center">
<span
v-if="post.isFeatured"
@@ -835,9 +835,24 @@ watch(openPostMenuId, async (postId) => {
댓글 {{ post.commentCount || 0 }}
</span>
</div>
<p class="admin-posts__slug mt-1 text-xs text-muted">
/post/{{ post.slug }}
</p>
<div class="admin-posts__slug-row mt-1 flex items-center gap-1 text-xs text-muted">
<span class="admin-posts__slug truncate">/post/{{ post.slug }}</span>
<NuxtLink
v-if="isPublicPost(post)"
class="admin-posts__preview-link inline-flex size-5 items-center justify-center rounded text-[#8e9cac] opacity-0 transition-colors hover:text-[#394047] focus:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#8e9cac] group-hover:opacity-100 group-focus-within:opacity-100"
:to="`/post/${post.slug}`"
target="_blank"
rel="noopener noreferrer"
aria-label="공개 글 보기"
title="공개 글 보기"
@click.stop
>
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M17 7 7 17" />
<path d="M8 7h9v9" />
</svg>
</NuxtLink>
</div>
</td>
<td class="admin-posts__cell px-4 py-4">
<span

View File

@@ -156,6 +156,7 @@ const mapLatestPost = (post) => {
featuredImage: post.featuredImage,
featuredImageThumbnail: post.featuredImageThumbnail || '',
tagName: tagMeta.name,
tagSlug: primaryTagSlug || '',
tagColor: tagMeta.color,
publishedAt: formatPostDate(post.publishedAt),
publishedAtIso: post.publishedAt || '',
@@ -539,7 +540,16 @@ watch([latestPosts, hasHomeInfeedAd], () => {
<time v-if="post.publishedAt" :datetime="post.publishedAtIso">{{ post.publishedAt }}</time>
<template v-if="post.tagName">
<span v-if="post.publishedAt" class="text-[var(--site-line)]">/</span>
<NuxtLink
v-if="post.tagSlug"
class="rounded-md px-1.5 py-px font-medium text-[var(--site-text)]"
:style="{ backgroundColor: `${post.tagColor}1a` }"
:to="`/tag/${post.tagSlug}`"
>
{{ post.tagName }}
</NuxtLink>
<span
v-else
class="rounded-md px-1.5 py-px font-medium text-[var(--site-text)]"
:style="{ backgroundColor: `${post.tagColor}1a` }"
>
@@ -556,11 +566,11 @@ watch([latestPosts, hasHomeInfeedAd], () => {
</div>
</div>
<button
<NuxtLink
class="absolute top-0 right-0 flex cursor-pointer items-center gap-1 hover:opacity-75"
:class="isPostFeedCards ? '' : 'md:top-auto md:right-0 md:bottom-0 md:invisible md:opacity-0 md:transition-[opacity,visibility] md:duration-200 md:group-hover:visible md:group-hover:opacity-100'"
type="button"
aria-label="Share this post"
:to="post.to"
aria-label="게시물 보기"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -575,7 +585,7 @@ watch([latestPosts, hasHomeInfeedAd], () => {
<path d="M17 7 7 17" />
<path d="M8 7h9v9" />
</svg>
</button>
</NuxtLink>
</div>
</article>

View File

@@ -8,30 +8,43 @@ const { data: tags } = await useFetch('/api/tags?tagType=all', {
})
/**
* 태그 슬러그로 공개 태그 이름을 찾는다.
* 태그 슬러그로 공개 태그 정보를 찾는다.
* @param {string | undefined} slug - 태그 슬러그
* @returns {string} 태그 이름
* @returns {{ name: string, slug: string }} 태그 정보
*/
const getTagName = (slug) => {
const getTagMeta = (slug) => {
if (!slug) {
return ''
return {
name: '',
slug: ''
}
}
return tags.value.find((tag) => tag.slug === slug)?.name || String(slug).toUpperCase()
const matchedTag = tags.value.find((tag) => tag.slug === slug)
return {
name: matchedTag?.name || String(slug).toUpperCase(),
slug
}
}
const postCards = computed(() => posts.value.map((post) => ({
title: post.title,
excerpt: createPostSummary(post.excerpt, post.content, {
maxLength: 320,
appendEllipsis: false
}),
featuredImage: post.featuredImage,
featuredImageThumbnail: post.featuredImageThumbnail || '',
tag: getTagName(post.tags?.[0]),
publishedAt: formatPostDate(post.publishedAt),
to: `/post/${post.slug}`
})))
const postCards = computed(() => posts.value.map((post) => {
const primaryTag = getTagMeta(post.tags?.[0])
return {
title: post.title,
excerpt: createPostSummary(post.excerpt, post.content, {
maxLength: 320,
appendEllipsis: false
}),
featuredImage: post.featuredImage,
featuredImageThumbnail: post.featuredImageThumbnail || '',
tag: primaryTag.name,
tagSlug: primaryTag.slug,
publishedAt: formatPostDate(post.publishedAt),
to: `/post/${post.slug}`
}
}))
</script>
<template>

View File

@@ -35,6 +35,7 @@ const tagPosts = computed(() => posts.value
featuredImage: post.featuredImage,
featuredImageThumbnail: post.featuredImageThumbnail || '',
tag: primaryTag?.name || primaryTagSlug.toUpperCase(),
tagSlug: primaryTagSlug,
tagColor: primaryTag?.color || '#4d4d4d',
isFeatured: Boolean(post.isFeatured),
commentCount: Number(post.commentCount || 0),
@@ -105,12 +106,13 @@ const tagPosts = computed(() => posts.value
<div class="flex flex-wrap items-center gap-2 text-xs site-muted sm:gap-1.5">
<time v-if="post.publishedAt" :datetime="post.publishedAtIso">{{ post.publishedAt }}</time>
<span class="text-[var(--site-line)]">/</span>
<span
<NuxtLink
class="rounded-md px-1.5 py-px font-medium text-[var(--site-text)]"
:style="{ backgroundColor: `${post.tagColor}1a` }"
:to="`/tag/${post.tagSlug}`"
>
{{ post.tag }}
</span>
</NuxtLink>
<span class="text-[var(--site-line)]">/</span>
<span class="flex items-center gap-0.75">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="-mt-px">
@@ -120,10 +122,10 @@ const tagPosts = computed(() => posts.value
</span>
</div>
</div>
<button
<NuxtLink
class="absolute top-0 right-0 flex cursor-pointer items-center gap-1 hover:opacity-75 md:top-auto md:right-0 md:bottom-0 md:invisible md:opacity-0 md:transition-[opacity,visibility] md:duration-200 md:group-hover:visible md:group-hover:opacity-100"
type="button"
aria-label="Share this post"
:to="post.to"
aria-label="게시물 보기"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -138,7 +140,7 @@ const tagPosts = computed(() => posts.value
<path d="M17 7 7 17" />
<path d="M8 7h9v9" />
</svg>
</button>
</NuxtLink>
</div>
</div>
</article>