인기 페이지 통계와 추천 사이트 메타데이터 추가 v1.5.9

This commit is contained in:
2026-05-27 10:34:07 +09:00
parent d7a3149ea1
commit fd9416c0e4
22 changed files with 596 additions and 94 deletions

View File

@@ -43,6 +43,11 @@ const { data: topPosts, refresh: refreshTopPosts } = await useFetch('/admin/api/
default: () => []
})
const { data: topPages, refresh: refreshTopPages } = await useFetch('/admin/api/analytics/pages', {
query: topPostsQuery,
default: () => []
})
const { data: realtime, refresh: refreshRealtime } = await useFetch('/admin/api/analytics/realtime', {
query: { limit: 20 },
default: () => ({
@@ -250,6 +255,10 @@ const getSessionViewingTitle = (session) => {
return session.postTitle
}
if (session.pageTitle) {
return session.pageTitle
}
if (session.path === '/') {
return '홈'
}
@@ -263,6 +272,7 @@ onMounted(() => {
refreshTimer = window.setInterval(() => {
refreshSummary()
refreshTopPosts()
refreshTopPages()
refreshRealtime()
}, 30000)
})
@@ -276,6 +286,7 @@ onUnmounted(() => {
watch(selectedAnalyticsDays, () => {
refreshSummary()
refreshTopPosts()
refreshTopPages()
})
</script>
@@ -435,74 +446,145 @@ watch(selectedAnalyticsDays, () => {
</p>
</section>
<section class="admin-dashboard__top-posts border border-line bg-white p-4">
<h2 class="admin-dashboard__section-title text-sm font-semibold text-ink">
인기 게시물 ({{ analyticsRangeLabel }})
</h2>
<ul
v-if="topPosts.length"
class="admin-dashboard__top-posts-list mt-4 space-y-3 text-sm"
>
<li
v-for="(item, index) in topPosts"
:key="item.id"
class="admin-dashboard__top-posts-item flex items-start justify-between gap-4 border-b border-line pb-3 last:border-b-0 last:pb-0"
<div class="admin-dashboard__popular-grid grid gap-6 xl:grid-cols-2">
<section class="admin-dashboard__top-posts border border-line bg-white p-4">
<h2 class="admin-dashboard__section-title text-sm font-semibold text-ink">
인기 게시물 ({{ analyticsRangeLabel }})
</h2>
<ul
v-if="topPosts.length"
class="admin-dashboard__top-posts-list mt-4 space-y-3 text-sm"
>
<div class="min-w-0 flex-1">
<p class="admin-dashboard__top-posts-rank text-xs font-semibold uppercase text-muted">
#{{ index + 1 }}
</p>
<NuxtLink
:to="`/post/${item.slug}`"
class="admin-dashboard__top-posts-title mt-1 block truncate font-medium text-ink hover:underline"
target="_blank"
>
{{ item.title }}
</NuxtLink>
</div>
<dl class="admin-dashboard__top-posts-stats shrink-0 text-right text-xs text-muted">
<div>
<dt class="inline">
조회
</dt>
<dd class="inline font-semibold text-ink">
{{ item.views }}
</dd>
<li
v-for="(item, index) in topPosts"
:key="item.id"
class="admin-dashboard__top-posts-item flex items-start justify-between gap-4 border-b border-line pb-3 last:border-b-0 last:pb-0"
>
<div class="min-w-0 flex-1">
<p class="admin-dashboard__top-posts-rank text-xs font-semibold uppercase text-muted">
#{{ index + 1 }}
</p>
<NuxtLink
:to="`/post/${item.slug}`"
class="admin-dashboard__top-posts-title mt-1 block truncate font-medium text-ink hover:underline"
target="_blank"
>
{{ item.title }}
</NuxtLink>
</div>
<div class="mt-1">
<dt class="inline">
읽음
</dt>
<dd class="inline font-semibold text-ink">
{{ item.reads }}
</dd>
<dl class="admin-dashboard__top-posts-stats shrink-0 text-right text-xs text-muted">
<div>
<dt class="inline">
조회
</dt>
<dd class="inline font-semibold text-ink">
{{ item.views }}
</dd>
</div>
<div class="mt-1">
<dt class="inline">
읽음
</dt>
<dd class="inline font-semibold text-ink">
{{ item.reads }}
</dd>
</div>
<div class="mt-1">
<dt class="inline">
체류
</dt>
<dd class="inline font-semibold text-ink">
{{ formatEngagedDuration(item.avgEngagedSeconds) }}
</dd>
</div>
<div class="mt-1">
<dt class="inline">
50/75/100%
</dt>
<dd class="inline font-semibold text-ink">
{{ item.scroll50 }}/{{ item.scroll75 }}/{{ item.scroll100 }}
</dd>
</div>
</dl>
</li>
</ul>
<p
v-else
class="admin-dashboard__top-posts-empty mt-4 text-sm text-muted"
>
아직 집계된 게시물 조회 데이터가 없습니다.
</p>
</section>
<section class="admin-dashboard__top-pages border border-line bg-white p-4">
<h2 class="admin-dashboard__section-title text-sm font-semibold text-ink">
인기 페이지 ({{ analyticsRangeLabel }})
</h2>
<ul
v-if="topPages.length"
class="admin-dashboard__top-pages-list mt-4 space-y-3 text-sm"
>
<li
v-for="(item, index) in topPages"
:key="item.id"
class="admin-dashboard__top-pages-item flex items-start justify-between gap-4 border-b border-line pb-3 last:border-b-0 last:pb-0"
>
<div class="min-w-0 flex-1">
<p class="admin-dashboard__top-pages-rank text-xs font-semibold uppercase text-muted">
#{{ index + 1 }}
</p>
<NuxtLink
:to="`/pages/${item.slug}`"
class="admin-dashboard__top-pages-title mt-1 block truncate font-medium text-ink hover:underline"
target="_blank"
>
{{ item.title }}
</NuxtLink>
</div>
<div class="mt-1">
<dt class="inline">
체류
</dt>
<dd class="inline font-semibold text-ink">
{{ formatEngagedDuration(item.avgEngagedSeconds) }}
</dd>
</div>
<div class="mt-1">
<dt class="inline">
50/75/100%
</dt>
<dd class="inline font-semibold text-ink">
{{ item.scroll50 }}/{{ item.scroll75 }}/{{ item.scroll100 }}
</dd>
</div>
</dl>
</li>
</ul>
<p
v-else
class="admin-dashboard__top-posts-empty mt-4 text-sm text-muted"
>
아직 집계된 게시물 조회 데이터가 없습니다.
</p>
</section>
<dl class="admin-dashboard__top-pages-stats shrink-0 text-right text-xs text-muted">
<div>
<dt class="inline">
조회
</dt>
<dd class="inline font-semibold text-ink">
{{ item.views }}
</dd>
</div>
<div class="mt-1">
<dt class="inline">
방문자
</dt>
<dd class="inline font-semibold text-ink">
{{ item.visitors }}
</dd>
</div>
<div class="mt-1">
<dt class="inline">
체류
</dt>
<dd class="inline font-semibold text-ink">
{{ formatEngagedDuration(item.avgEngagedSeconds) }}
</dd>
</div>
<div class="mt-1">
<dt class="inline">
50/75/100%
</dt>
<dd class="inline font-semibold text-ink">
{{ item.scroll50 }}/{{ item.scroll75 }}/{{ item.scroll100 }}
</dd>
</div>
</dl>
</li>
</ul>
<p
v-else
class="admin-dashboard__top-pages-empty mt-4 text-sm text-muted"
>
아직 집계된 페이지 조회 데이터가 없습니다.
</p>
</section>
</div>
</div>
</section>
</template>