v1.12.25 대시보드 숫자 표기 및 접속자 목록 간소화
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { formatAnalyticsCount as formatNumber } from '~/lib/analytics-display.js'
|
||||
import { ANALYTICS_COLLECTED_SINCE_DEFAULT, getAnalyticsDayKey } from '~/lib/analytics-shared.js'
|
||||
import {
|
||||
getAnalyticsTrendDescription,
|
||||
@@ -204,15 +205,6 @@ const getAdsenseCurrencyCode = (currencyCode) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자를 천 단위 구분 문자열로 변환한다.
|
||||
* @param {number} value - 숫자 값
|
||||
* @returns {string} 표시 문자열
|
||||
*/
|
||||
const formatNumber = (value) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(Number(value || 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* AdSense 금액을 통화 문자열로 변환한다.
|
||||
* @param {number} value - 금액
|
||||
@@ -317,10 +309,10 @@ const formatTrendValue = (key, value) => {
|
||||
}
|
||||
|
||||
if (key === 'visitors') {
|
||||
return `${Number(value || 0)}명`
|
||||
return `${formatNumber(value)}명`
|
||||
}
|
||||
|
||||
return `${Number(value || 0)}회`
|
||||
return `${formatNumber(value)}회`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,7 +368,7 @@ const formatDeviceSummary = (row) => {
|
||||
const visitors = Number(row?.visitors || 0)
|
||||
const pageViews = Number(row?.pageViews || 0)
|
||||
|
||||
return `${visitors}명 / ${pageViews}회 / ${formatDeviceVisitorPercent(visitors)}`
|
||||
return `${formatNumber(visitors)}명 / ${formatNumber(pageViews)}회 / ${formatDeviceVisitorPercent(visitors)}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,7 +390,7 @@ const chartMetrics = computed(() => [
|
||||
{
|
||||
key: 'visitors',
|
||||
title: '방문자수',
|
||||
label: `${getTrendSummaryValue('visitors')}명`,
|
||||
label: `${formatNumber(getTrendSummaryValue('visitors'))}명`,
|
||||
metricLabel: '방문자수'
|
||||
},
|
||||
{
|
||||
@@ -410,7 +402,7 @@ const chartMetrics = computed(() => [
|
||||
{
|
||||
key: 'scroll50Reach',
|
||||
title: '50% 스크롤 도달',
|
||||
label: `${getTrendSummaryValue('scroll50Reach')}회`,
|
||||
label: `${formatNumber(getTrendSummaryValue('scroll50Reach'))}회`,
|
||||
metricLabel: '50% 스크롤 도달'
|
||||
}
|
||||
])
|
||||
@@ -477,70 +469,6 @@ const selectDefaultTrendDetail = () => {
|
||||
shouldSelectDefaultTrendDetail.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 접속자가 보고 있는 화면명을 반환한다.
|
||||
* @param {Object} session - 접속 세션
|
||||
* @returns {string} 화면명
|
||||
*/
|
||||
const getSessionViewingTitle = (session) => {
|
||||
if (session.postTitle) {
|
||||
return session.postTitle
|
||||
}
|
||||
|
||||
if (session.pageTitle) {
|
||||
return session.pageTitle
|
||||
}
|
||||
|
||||
if (session.path === '/') {
|
||||
return '홈'
|
||||
}
|
||||
|
||||
return session.path || '알 수 없음'
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 접속자가 열어둔 페이지명을 반환한다.
|
||||
* @param {Object} page - 접속 페이지
|
||||
* @returns {string} 페이지명
|
||||
*/
|
||||
const getActivePageTitle = (page) => {
|
||||
if (page.postTitle) {
|
||||
return page.postTitle
|
||||
}
|
||||
|
||||
if (page.pageTitle) {
|
||||
return page.pageTitle
|
||||
}
|
||||
|
||||
if (page.path === '/') {
|
||||
return '홈'
|
||||
}
|
||||
|
||||
return page.path || '알 수 없음'
|
||||
}
|
||||
|
||||
/**
|
||||
* 접속 페이지의 공개 바로가기 경로를 반환한다.
|
||||
* @param {Object} page - 접속 페이지
|
||||
* @returns {string} 공개 경로
|
||||
*/
|
||||
const getActivePageHref = (page) => {
|
||||
if (page.postSlug) {
|
||||
return `/post/${encodeURIComponent(page.postSlug)}`
|
||||
}
|
||||
|
||||
if (page.pageSlug) {
|
||||
return `/pages/${encodeURIComponent(page.pageSlug)}`
|
||||
}
|
||||
|
||||
const path = String(page.path || '').trim()
|
||||
if (path.startsWith('/') && !path.startsWith('/admin')) {
|
||||
return path
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 목록의 날짜를 짧은 문자열로 변환한다.
|
||||
* @param {string | null | undefined} value - ISO 날짜
|
||||
@@ -568,7 +496,7 @@ const formatCompactDate = (value) => {
|
||||
* @returns {string} 스크롤 50/75/100 도달 수
|
||||
*/
|
||||
const formatScrollCompact = (row) => {
|
||||
return `${Number(row.scroll50 || 0)}.${Number(row.scroll75 || 0)}.${Number(row.scroll100 || 0)}`
|
||||
return `${formatNumber(row.scroll50)}.${formatNumber(row.scroll75)}.${formatNumber(row.scroll100)}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -591,9 +519,9 @@ const formatScrollParts = (row) => {
|
||||
*/
|
||||
const formatTopPostMeta = (row) => {
|
||||
return [
|
||||
{ key: 'views', label: 'V', value: Number(row.views || 0), colorClass: 'text-[#2563eb]' },
|
||||
{ key: 'reads', label: 'R', value: Number(row.reads || 0), colorClass: 'text-[#16a34a]' },
|
||||
{ key: 'monthly', label: 'M', value: Number(row.monthlyViews || 0), colorClass: 'text-[#d97706]' },
|
||||
{ key: 'views', label: 'V', value: formatNumber(row.views), colorClass: 'text-[#2563eb]' },
|
||||
{ key: 'reads', label: 'R', value: formatNumber(row.reads), colorClass: 'text-[#16a34a]' },
|
||||
{ key: 'monthly', label: 'M', value: formatNumber(row.monthlyViews), colorClass: 'text-[#d97706]' },
|
||||
{ key: 'created', label: 'C', value: formatCompactDate(row.createdAt), colorClass: 'text-muted' },
|
||||
{ key: 'scroll', label: 'S', value: formatScrollCompact(row), colorClass: 'text-muted', parts: formatScrollParts(row) }
|
||||
]
|
||||
@@ -606,9 +534,9 @@ const formatTopPostMeta = (row) => {
|
||||
*/
|
||||
const formatTopPageMeta = (row) => {
|
||||
return [
|
||||
{ key: 'views', label: 'V', value: Number(row.views || 0), colorClass: 'text-[#2563eb]' },
|
||||
{ key: 'visitors', label: 'R', value: Number(row.visitors || 0), colorClass: 'text-[#16a34a]' },
|
||||
{ key: 'monthly', label: 'M', value: Number(row.monthlyViews || 0), colorClass: 'text-[#d97706]' },
|
||||
{ key: 'views', label: 'V', value: formatNumber(row.views), colorClass: 'text-[#2563eb]' },
|
||||
{ key: 'visitors', label: 'R', value: formatNumber(row.visitors), colorClass: 'text-[#16a34a]' },
|
||||
{ key: 'monthly', label: 'M', value: formatNumber(row.monthlyViews), colorClass: 'text-[#d97706]' },
|
||||
{ key: 'created', label: 'C', value: formatCompactDate(row.createdAt), colorClass: 'text-muted' },
|
||||
{ key: 'scroll', label: 'S', value: formatScrollCompact(row), colorClass: 'text-muted', parts: formatScrollParts(row) }
|
||||
]
|
||||
@@ -660,25 +588,23 @@ watch(trendRows, () => {
|
||||
<section class="admin-dashboard__summary flex flex-wrap items-center gap-x-8 gap-y-2 border border-line bg-white px-4 py-3 text-sm">
|
||||
<p class="admin-dashboard__summary-item text-muted">
|
||||
현재 접속자
|
||||
<strong class="ml-2 text-lg text-ink">{{ realtime.summary.onlineNow }}</strong>
|
||||
<strong class="ml-2 text-lg text-ink">{{ formatNumber(realtime.summary.onlineNow) }}</strong>
|
||||
</p>
|
||||
<p class="admin-dashboard__summary-item text-muted">
|
||||
오늘 방문자
|
||||
<strong class="ml-2 text-lg text-ink">{{ analyticsSummary.todayVisitors }}</strong>
|
||||
<strong class="ml-2 text-lg text-ink">{{ formatNumber(analyticsSummary.todayVisitors) }}</strong>
|
||||
</p>
|
||||
<p class="admin-dashboard__summary-item text-muted">
|
||||
누적 방문자
|
||||
<strong class="ml-2 text-lg text-ink">{{ analyticsSummary.totalVisitors }}</strong>
|
||||
<strong class="ml-2 text-lg text-ink">{{ formatNumber(analyticsSummary.totalVisitors) }}</strong>
|
||||
</p>
|
||||
<p class="admin-dashboard__summary-item text-muted">
|
||||
게시물 수
|
||||
<strong class="ml-2 text-lg text-ink">{{ posts.length }}</strong>
|
||||
<span class="ml-1 text-xs">발행 {{ publishedCount }} · 초안 {{ draftCount }}</span>
|
||||
<strong class="ml-2 text-lg text-ink">{{ formatNumber(posts.length) }}</strong>
|
||||
<span class="ml-1 text-xs">발행 {{ formatNumber(publishedCount) }} · 초안 {{ formatNumber(draftCount) }}</span>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<AdminCommentList class="admin-dashboard__recent-comments" />
|
||||
|
||||
<section class="admin-dashboard__adsense">
|
||||
<p
|
||||
v-if="adsenseSummary.configured && !adsenseSummary.ok"
|
||||
@@ -825,7 +751,7 @@ watch(trendRows, () => {
|
||||
</p>
|
||||
</div>
|
||||
<p class="admin-dashboard__traffic-total text-xs text-muted">
|
||||
페이지뷰 {{ selectedSummaryPageViews }}회
|
||||
페이지뷰 {{ formatNumber(selectedSummaryPageViews) }}회
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -849,7 +775,7 @@ watch(trendRows, () => {
|
||||
페이지뷰
|
||||
</p>
|
||||
<strong class="mt-1 block text-lg text-ink">
|
||||
{{ selectedTrendDetail.summary.pageViews }}
|
||||
{{ formatNumber(selectedTrendDetail.summary.pageViews) }}
|
||||
</strong>
|
||||
</article>
|
||||
<article class="border border-line bg-paper p-3">
|
||||
@@ -857,7 +783,7 @@ watch(trendRows, () => {
|
||||
방문자
|
||||
</p>
|
||||
<strong class="mt-1 block text-lg text-ink">
|
||||
{{ selectedTrendDetail.summary.visitors }}
|
||||
{{ formatNumber(selectedTrendDetail.summary.visitors) }}
|
||||
</strong>
|
||||
</article>
|
||||
<article class="border border-line bg-paper p-3">
|
||||
@@ -873,7 +799,7 @@ watch(trendRows, () => {
|
||||
50% 스크롤
|
||||
</p>
|
||||
<strong class="mt-1 block text-lg text-ink">
|
||||
{{ selectedTrendDetail.summary.scroll50Reach }}
|
||||
{{ formatNumber(selectedTrendDetail.summary.scroll50Reach) }}
|
||||
</strong>
|
||||
</article>
|
||||
</div>
|
||||
@@ -929,7 +855,7 @@ watch(trendRows, () => {
|
||||
>
|
||||
.
|
||||
</span>
|
||||
<span :class="part.colorClass">{{ part.value }}</span>
|
||||
<span :class="part.colorClass">{{ formatNumber(part.value) }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -997,7 +923,7 @@ watch(trendRows, () => {
|
||||
>
|
||||
.
|
||||
</span>
|
||||
<span :class="part.colorClass">{{ part.value }}</span>
|
||||
<span :class="part.colorClass">{{ formatNumber(part.value) }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -1051,7 +977,7 @@ watch(trendRows, () => {
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 text-[11px] text-muted">
|
||||
조회 {{ row.pageViews }} · 방문자 {{ row.visitors }}
|
||||
조회 {{ formatNumber(row.pageViews) }} · 방문자 {{ formatNumber(row.visitors) }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -1093,7 +1019,7 @@ watch(trendRows, () => {
|
||||
class="flex items-center justify-between gap-2"
|
||||
>
|
||||
<span>{{ row.osName || '-' }}</span>
|
||||
<span>{{ row.visitors }}명 · {{ formatDeviceVisitorPercent(row.visitors) }}</span>
|
||||
<span>{{ formatNumber(row.visitors) }}명 · {{ formatDeviceVisitorPercent(row.visitors) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -1110,98 +1036,27 @@ watch(trendRows, () => {
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<AdminCommentList class="admin-dashboard__recent-comments" />
|
||||
|
||||
<section class="admin-dashboard__live border border-line bg-white p-4">
|
||||
<div class="admin-dashboard__live-header flex items-center justify-between gap-4">
|
||||
<h2 class="admin-dashboard__section-title text-sm font-semibold text-ink">
|
||||
현재 접속자
|
||||
</h2>
|
||||
<p class="admin-dashboard__live-count text-xs text-muted">
|
||||
{{ realtime.summary.onlineNow }}명 표시
|
||||
{{ formatNumber(realtime.sessions.length) }}명 표시 · 전체 {{ formatNumber(realtime.summary.onlineNow) }}명
|
||||
</p>
|
||||
</div>
|
||||
<ul
|
||||
v-if="realtime.sessions.length"
|
||||
class="admin-dashboard__live-list mt-4 divide-y divide-line"
|
||||
>
|
||||
<li
|
||||
v-for="session in realtime.sessions"
|
||||
:key="session.activeKey"
|
||||
class="admin-dashboard__live-item flex items-center gap-3 py-3"
|
||||
>
|
||||
<img
|
||||
v-if="session.user?.avatarUrl"
|
||||
:src="session.user.avatarUrl"
|
||||
:alt="session.user.username"
|
||||
class="admin-dashboard__live-avatar h-9 w-9 shrink-0 rounded-full object-cover"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="admin-dashboard__live-avatar admin-dashboard__live-avatar--placeholder flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-paper text-xs font-semibold text-muted"
|
||||
>
|
||||
{{ session.isLoggedIn ? (session.user?.username || '?').slice(0, 1) : '?' }}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="admin-dashboard__live-name text-sm font-medium text-ink">
|
||||
{{ session.isLoggedIn ? session.user?.username : '익명 방문자' }}
|
||||
</p>
|
||||
<p
|
||||
v-if="Number(session.pageCount || 0) <= 1"
|
||||
class="admin-dashboard__live-path group mt-0.5 flex min-w-0 items-center gap-1 text-xs text-muted"
|
||||
>
|
||||
<span class="min-w-0 truncate">{{ getSessionViewingTitle(session) }}</span>
|
||||
<a
|
||||
v-if="getActivePageHref(session)"
|
||||
:href="getActivePageHref(session)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="admin-dashboard__live-link grid h-5 w-5 shrink-0 place-items-center rounded border border-line bg-white text-muted opacity-0 transition hover:border-ink hover:text-ink group-hover:opacity-100 focus:opacity-100"
|
||||
aria-label="현재 페이지 새 창으로 열기"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M14 5h5v5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M10 14 19 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M19 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</a>
|
||||
</p>
|
||||
<ul
|
||||
v-if="Number(session.pageCount || 0) > 1"
|
||||
class="admin-dashboard__live-pages mt-1 space-y-0.5 text-[11px] leading-snug text-muted"
|
||||
>
|
||||
<li
|
||||
v-for="page in session.pages"
|
||||
:key="page.key"
|
||||
class="admin-dashboard__live-page group flex min-w-0 items-center gap-1"
|
||||
>
|
||||
<span class="min-w-0 truncate">{{ getActivePageTitle(page) }}</span>
|
||||
<a
|
||||
v-if="getActivePageHref(page)"
|
||||
:href="getActivePageHref(page)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="admin-dashboard__live-link grid h-5 w-5 shrink-0 place-items-center rounded border border-line bg-white text-muted opacity-0 transition hover:border-ink hover:text-ink group-hover:opacity-100 focus:opacity-100"
|
||||
aria-label="열린 페이지 새 창으로 열기"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M14 5h5v5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M10 14 19 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M19 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="admin-dashboard__live-meta shrink-0 text-right text-xs text-muted">
|
||||
<p class="font-medium text-ink">
|
||||
접속 유지 {{ formatEngagedDuration(session.durationSeconds) }}
|
||||
</p>
|
||||
<p class="mt-1 text-[11px] leading-snug text-muted">
|
||||
IP {{ session.ipAddress || '-' }}
|
||||
</p>
|
||||
<p v-if="Number(session.pageCount || 0) > 1" class="mt-0.5 text-[11px] leading-snug text-muted">
|
||||
열린 페이지 {{ session.pageCount }}개
|
||||
</p>
|
||||
<ul v-if="realtime.sessions.length" class="admin-dashboard__live-list mt-3 divide-y divide-line">
|
||||
<li v-for="session in realtime.sessions" :key="session.activeKey" class="admin-dashboard__live-item grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)_auto] items-center gap-3 py-2 text-xs xl:grid-cols-[minmax(0,1fr)_minmax(0,3fr)_6rem_8rem]">
|
||||
<div class="admin-dashboard__live-identity flex min-w-0 items-center gap-2">
|
||||
<img v-if="session.user?.avatarUrl" :src="session.user.avatarUrl" :alt="session.user.username" class="admin-dashboard__live-avatar size-6 shrink-0 rounded-full object-cover">
|
||||
<span v-else class="admin-dashboard__live-avatar grid size-6 shrink-0 place-items-center rounded-full bg-paper text-muted">{{ session.isLoggedIn ? (session.user?.username || '?').slice(0, 1) : '?' }}</span>
|
||||
<span class="admin-dashboard__live-name truncate font-medium text-ink" :title="session.isLoggedIn ? session.user?.username : '익명 방문자'">{{ session.isLoggedIn ? session.user?.username : '익명 방문자' }}</span>
|
||||
</div>
|
||||
<AdminRealtimePages :session="session" />
|
||||
<span class="admin-dashboard__live-duration whitespace-nowrap text-right text-muted" title="접속 유지시간">{{ formatEngagedDuration(session.durationSeconds) }}</span>
|
||||
<span class="admin-dashboard__live-ip hidden truncate text-right text-muted xl:block" :title="session.ipAddress">{{ session.ipAddress || '-' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p
|
||||
|
||||
Reference in New Issue
Block a user