관리자 통계 대시보드 개선 및 v1.9.6 반영

통계 추이 차트 공통화, 유입 스택 막대, 집계 시작일 고정, traffic API 오류 수정, deploy 문서 정리를 포함한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 13:42:23 +09:00
parent 67ea58ccc2
commit ef9efd7af5
25 changed files with 4038 additions and 620 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -45,6 +45,7 @@ const mapPostRow = (row) => ({
showFeaturedImage: Boolean(row.show_featured_image),
isFeatured: Boolean(row.is_featured),
commentCount: Number(row.comment_count || 0),
totalViews: Number(row.total_views || 0),
seoTitle: row.seo_title || '',
seoDescription: row.seo_description || '',
canonicalUrl: row.canonical_url || '',
@@ -80,6 +81,7 @@ const mapPageRow = (row) => ({
renderMode: row.render_mode || 'markdown',
status: row.status || 'published',
featuredImage: row.featured_image,
totalViews: Number(row.total_views || 0),
createdAt: row.created_at.toISOString(),
updatedAt: row.updated_at.toISOString()
})
@@ -298,6 +300,11 @@ export const listPosts = async ({ includeMembership = false } = {}) => {
WHERE comments.post_id = posts.id
AND comments.status = 'published'
) AS comment_count,
(
SELECT COALESCE(SUM(post_analytics_daily.views), 0)::int
FROM post_analytics_daily
WHERE post_analytics_daily.post_id = posts.id
) AS total_views,
COALESCE(array_agg(tags.slug ORDER BY post_tags.sort_order ASC, post_tags.created_at ASC, tags.name ASC) FILTER (WHERE tags.slug IS NOT NULL), '{}') AS tags
FROM posts
LEFT JOIN post_tags ON post_tags.post_id = posts.id
@@ -338,6 +345,11 @@ export const listAdminPosts = async () => {
WHERE comments.post_id = posts.id
AND comments.status = 'published'
) AS comment_count,
(
SELECT COALESCE(SUM(post_analytics_daily.views), 0)::int
FROM post_analytics_daily
WHERE post_analytics_daily.post_id = posts.id
) AS total_views,
COALESCE(array_agg(tags.slug ORDER BY post_tags.sort_order ASC, post_tags.created_at ASC, tags.name ASC) FILTER (WHERE tags.slug IS NOT NULL), '{}') AS tags
FROM posts
LEFT JOIN post_tags ON post_tags.post_id = posts.id
@@ -595,7 +607,13 @@ export const listAdminPages = async () => {
}
const rows = await sql`
SELECT *
SELECT
pages.*,
(
SELECT COALESCE(SUM(page_analytics_daily.views), 0)::int
FROM page_analytics_daily
WHERE page_analytics_daily.page_id = pages.id
) AS total_views
FROM pages
ORDER BY created_at DESC
`

View File

@@ -0,0 +1,26 @@
import { createError, getQuery, getRouterParam } from 'h3'
import { requireAdminSession } from '../../../../../utils/admin-auth.js'
import { getPageAnalyticsDetail } from '../../../../../repositories/analytics-repository.js'
/**
* 관리자 페이지별 통계 API
* @param {import('h3').H3Event} event - 요청 이벤트
* @returns {Promise<Object>} 페이지별 통계
*/
export default defineEventHandler(async (event) => {
requireAdminSession(event)
const id = getRouterParam(event, 'id')
if (!id) {
throw createError({
statusCode: 400,
message: '페이지 ID가 필요합니다.'
})
}
const query = getQuery(event)
const days = Number(query.days) || 30
const period = String(query.period || 'daily')
return getPageAnalyticsDetail({ pageId: id, days, period })
})

View File

@@ -0,0 +1,26 @@
import { createError, getQuery, getRouterParam } from 'h3'
import { requireAdminSession } from '../../../../../utils/admin-auth.js'
import { getPostAnalyticsDetail } from '../../../../../repositories/analytics-repository.js'
/**
* 관리자 게시물별 통계 API
* @param {import('h3').H3Event} event - 요청 이벤트
* @returns {Promise<Object>} 게시물별 통계
*/
export default defineEventHandler(async (event) => {
requireAdminSession(event)
const id = getRouterParam(event, 'id')
if (!id) {
throw createError({
statusCode: 400,
message: '게시물 ID가 필요합니다.'
})
}
const query = getQuery(event)
const days = Number(query.days) || 30
const period = String(query.period || 'daily')
return getPostAnalyticsDetail({ postId: id, days, period })
})

View File

@@ -0,0 +1,19 @@
import { requireAdminSession } from '../../../../utils/admin-auth.js'
import { getAnalyticsRangeDetail } from '../../../../repositories/analytics-repository.js'
/**
* 관리자 날짜별 통계 상세 API
* @param {import('h3').H3Event} event - 요청 이벤트
* @returns {Promise<Object>} 날짜 상세
*/
export default defineEventHandler(async (event) => {
requireAdminSession(event)
const query = getQuery(event)
return getAnalyticsRangeDetail({
startDay: String(query.startDay || ''),
endDay: String(query.endDay || ''),
limit: Number(query.limit) || 5
})
})

View File

@@ -11,6 +11,7 @@ export default defineEventHandler(async (event) => {
const query = getQuery(event)
const days = Number(query.days) || 30
const period = String(query.period || 'weekly')
return getAnalyticsSummary({ days })
return getAnalyticsSummary({ days, period })
})