관리자 대시보드 통계 상세 UX 개선

This commit is contained in:
2026-07-08 14:20:24 +09:00
parent eb840b4224
commit 14c7245f06
10 changed files with 547 additions and 607 deletions

View File

@@ -22,6 +22,7 @@ import {
import { getPostgresClient } from './postgres-client.js'
import { getRuntimeEnvValue } from '../utils/runtime-env.js'
import { getMemberSession } from '../utils/member-auth.js'
import { getClientIp } from '../utils/request-ip.js'
/** @type {number} 마지막 통계 보관 정리 실행 시각 */
let lastAnalyticsRetentionPurgeAt = 0
@@ -788,6 +789,7 @@ export const recordAnalyticsHeartbeat = async (input) => {
const maxScrollRatio = clampAnalyticsScrollRatio(input.maxScrollRatio)
const memberSession = getMemberSession(input.event)
const userId = memberSession?.userId || null
const ipAddress = getClientIp(input.event)
const previousRows = await sql`
SELECT duration_seconds, max_scroll_ratio
@@ -810,6 +812,7 @@ export const recordAnalyticsHeartbeat = async (input) => {
post_slug,
page_id,
page_slug,
ip_address,
duration_seconds,
max_scroll_ratio
)
@@ -821,6 +824,7 @@ export const recordAnalyticsHeartbeat = async (input) => {
${postSlug},
${pageId},
${pageSlug},
${ipAddress},
${durationSeconds},
${maxScrollRatio}
)
@@ -832,6 +836,7 @@ export const recordAnalyticsHeartbeat = async (input) => {
post_slug = EXCLUDED.post_slug,
page_id = EXCLUDED.page_id,
page_slug = EXCLUDED.page_slug,
ip_address = EXCLUDED.ip_address,
duration_seconds = GREATEST(analytics_active_sessions.duration_seconds, EXCLUDED.duration_seconds),
max_scroll_ratio = GREATEST(analytics_active_sessions.max_scroll_ratio, EXCLUDED.max_scroll_ratio),
last_seen_at = now()
@@ -1355,6 +1360,7 @@ export const getAnalyticsActiveSessions = async (options = {}) => {
analytics_active_sessions.path,
analytics_active_sessions.post_slug,
analytics_active_sessions.page_slug,
analytics_active_sessions.ip_address,
analytics_active_sessions.duration_seconds,
analytics_active_sessions.max_scroll_ratio,
analytics_active_sessions.last_seen_at,
@@ -1377,6 +1383,7 @@ export const getAnalyticsActiveSessions = async (options = {}) => {
path: row.path,
postSlug: row.post_slug || '',
pageSlug: row.page_slug || '',
ipAddress: row.ip_address || '',
postTitle: row.post_title || '',
pageTitle: row.page_title || '',
durationSeconds: Number(row.duration_seconds || 0),
@@ -1511,6 +1518,7 @@ export const getAnalyticsRangeDetail = async (options = {}) => {
}
await purgeAnalyticsRetention(sql)
const monthlyStartDay = getAnalyticsDayBefore(endDay, 29)
const summaryRows = await sql`
SELECT
@@ -1573,13 +1581,20 @@ export const getAnalyticsRangeDetail = async (options = {}) => {
posts.id,
posts.title,
posts.slug,
posts.created_at,
COALESCE(SUM(post_analytics_daily.views), 0)::int AS views,
COALESCE(SUM(post_analytics_daily.views) FILTER (
WHERE post_analytics_daily.day >= ${monthlyStartDay}::date
), 0)::int AS monthly_views,
COALESCE(SUM(post_analytics_daily.reads), 0)::int AS reads,
COALESCE(SUM(post_analytics_daily.visitors), 0)::int AS visitors
COALESCE(SUM(post_analytics_daily.visitors), 0)::int AS visitors,
COALESCE(SUM(post_analytics_daily.scroll_50), 0)::int AS scroll_50,
COALESCE(SUM(post_analytics_daily.scroll_75), 0)::int AS scroll_75,
COALESCE(SUM(post_analytics_daily.scroll_100), 0)::int AS scroll_100
FROM post_analytics_daily
INNER JOIN posts ON posts.id = post_analytics_daily.post_id
WHERE post_analytics_daily.day BETWEEN ${startDay}::date AND ${endDay}::date
GROUP BY posts.id, posts.title, posts.slug
GROUP BY posts.id, posts.title, posts.slug, posts.created_at
ORDER BY views DESC, reads DESC, posts.published_at DESC NULLS LAST
LIMIT ${limit}
`
@@ -1589,12 +1604,19 @@ export const getAnalyticsRangeDetail = async (options = {}) => {
pages.id,
pages.title,
pages.slug,
pages.created_at,
COALESCE(SUM(page_analytics_daily.views), 0)::int AS views,
COALESCE(SUM(page_analytics_daily.visitors), 0)::int AS visitors
COALESCE(SUM(page_analytics_daily.views) FILTER (
WHERE page_analytics_daily.day >= ${monthlyStartDay}::date
), 0)::int AS monthly_views,
COALESCE(SUM(page_analytics_daily.visitors), 0)::int AS visitors,
COALESCE(SUM(page_analytics_daily.scroll_50), 0)::int AS scroll_50,
COALESCE(SUM(page_analytics_daily.scroll_75), 0)::int AS scroll_75,
COALESCE(SUM(page_analytics_daily.scroll_100), 0)::int AS scroll_100
FROM page_analytics_daily
INNER JOIN pages ON pages.id = page_analytics_daily.page_id
WHERE page_analytics_daily.day BETWEEN ${startDay}::date AND ${endDay}::date
GROUP BY pages.id, pages.title, pages.slug
GROUP BY pages.id, pages.title, pages.slug, pages.created_at
ORDER BY views DESC, visitors DESC, pages.updated_at DESC NULLS LAST
LIMIT ${limit}
`
@@ -1636,16 +1658,26 @@ export const getAnalyticsRangeDetail = async (options = {}) => {
id: row.id,
title: row.title,
slug: row.slug,
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null,
views: Number(row.views || 0),
monthlyViews: Number(row.monthly_views || 0),
reads: Number(row.reads || 0),
visitors: Number(row.visitors || 0)
visitors: Number(row.visitors || 0),
scroll50: Number(row.scroll_50 || 0),
scroll75: Number(row.scroll_75 || 0),
scroll100: Number(row.scroll_100 || 0)
})),
topPages: pageRows.map((row) => ({
id: row.id,
title: row.title,
slug: row.slug,
createdAt: row.created_at ? new Date(row.created_at).toISOString() : null,
views: Number(row.views || 0),
visitors: Number(row.visitors || 0)
monthlyViews: Number(row.monthly_views || 0),
visitors: Number(row.visitors || 0),
scroll50: Number(row.scroll_50 || 0),
scroll75: Number(row.scroll_75 || 0),
scroll100: Number(row.scroll_100 || 0)
}))
}
}