Files
sori.studio/pages/admin/index.vue
zenn 3623305119 v1.3.3: 자체 최소 통계 및 스플래시 localStorage 정리
- 일별 익명 방문자 해시·사이트/게시물 통계(030 마이그레이션)
- POST /api/analytics/pageview, 관리자 analytics API, 클라이언트 트래커
- 관리자 대시보드 통계 카드·인기 게시물 Top 5
- 스플래시: SITE_BRAND_LOGO_TEXT localStorage 제거
2026-05-20 12:15:13 +09:00

131 lines
4.8 KiB
Vue

<script setup>
definePageMeta({
layout: 'admin'
})
const { data: posts } = await useFetch('/admin/api/posts', {
default: () => []
})
const { data: analyticsSummary } = await useFetch('/admin/api/analytics/summary', {
default: () => ({
todayVisitors: 0,
visitorsLast7Days: 0,
pageViewsLast30Days: 0
})
})
const { data: topPosts } = await useFetch('/admin/api/analytics/posts', {
query: { days: 30, limit: 5 },
default: () => []
})
const publishedCount = computed(() => posts.value.filter((post) => post.status === 'published').length)
const draftCount = computed(() => posts.value.filter((post) => post.status === 'draft').length)
</script>
<template>
<section class="admin-dashboard">
<div class="admin-dashboard__header border-b border-line bg-paper p-6">
<p class="admin-dashboard__eyebrow text-xs font-semibold uppercase text-muted">
Admin
</p>
<h1 class="admin-dashboard__title mt-2 text-3xl font-semibold">
대시보드
</h1>
</div>
<div class="admin-dashboard__body space-y-6 bg-paper p-6">
<section class="admin-dashboard__analytics grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<article class="admin-dashboard__metric border border-line bg-white p-4">
<p class="admin-dashboard__metric-label text-xs font-semibold uppercase text-muted">
오늘 방문
</p>
<strong class="admin-dashboard__metric-value mt-2 block text-3xl text-ink">
{{ analyticsSummary.todayVisitors }}
</strong>
</article>
<article class="admin-dashboard__metric border border-line bg-white p-4">
<p class="admin-dashboard__metric-label text-xs font-semibold uppercase text-muted">
7 방문
</p>
<strong class="admin-dashboard__metric-value mt-2 block text-3xl text-ink">
{{ analyticsSummary.visitorsLast7Days }}
</strong>
</article>
<article class="admin-dashboard__metric border border-line bg-white p-4">
<p class="admin-dashboard__metric-label text-xs font-semibold uppercase text-muted">
30 조회
</p>
<strong class="admin-dashboard__metric-value mt-2 block text-3xl text-ink">
{{ analyticsSummary.pageViewsLast30Days }}
</strong>
</article>
<article class="admin-dashboard__metric border border-line bg-white p-4">
<p class="admin-dashboard__metric-label text-xs font-semibold uppercase text-muted">
게시물
</p>
<strong class="admin-dashboard__metric-value mt-2 block text-3xl text-ink">
{{ posts.length }}
</strong>
<p class="admin-dashboard__metric-note mt-2 text-xs text-muted">
발행 {{ publishedCount }} · 초안 {{ draftCount }}
</p>
</article>
</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">
인기 게시물 (30)
</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="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>
</div>
<div class="mt-1">
<dt class="inline">
읽음
</dt>
<dd class="inline font-semibold text-ink">
{{ item.reads }}
</dd>
</div>
</dl>
</li>
</ul>
<p
v-else
class="admin-dashboard__top-posts-empty mt-4 text-sm text-muted"
>
아직 집계된 게시물 조회 데이터가 없습니다.
</p>
</section>
</div>
</section>
</template>