인기 콘텐츠 참여 지표 원장 보정

This commit is contained in:
2026-07-29 15:25:45 +09:00
parent 378afbe79c
commit 8a6963e8e1
8 changed files with 133 additions and 18 deletions

View File

@@ -773,6 +773,42 @@ const getPostContentSessionAggregate = async (sql, postId, startDay, endDay) =>
return rows[0] || {}
}
/**
* 여러 게시물 콘텐츠 세션 집계를 조회한다.
* @param {import('postgres').Sql} sql - DB 클라이언트
* @param {string[]} postIds - 게시물 ID 목록
* @param {string} startDay - 시작일
* @param {string} endDay - 종료일
* @returns {Promise<Map<string, Object>>} 게시물 ID별 원장 집계
*/
const getPostContentSessionAggregateMap = async (sql, postIds, startDay, endDay) => {
const normalizedPostIds = [...new Set(postIds.map((postId) => String(postId || '').trim()).filter(Boolean))]
if (!normalizedPostIds.length) {
return new Map()
}
const rows = await sql`
SELECT
post_id,
COUNT(*) FILTER (WHERE view_recorded)::int AS views,
COUNT(DISTINCT visitor_hash) FILTER (WHERE view_recorded)::int AS visitors,
COUNT(*) FILTER (WHERE read_recorded)::int AS reads,
COUNT(*) FILTER (WHERE engaged_recorded)::int AS engaged_views,
COALESCE(SUM(duration_seconds), 0)::int AS total_engaged_seconds,
COUNT(*) FILTER (WHERE scroll_50_recorded)::int AS scroll_50,
COUNT(*) FILTER (WHERE scroll_75_recorded)::int AS scroll_75,
COUNT(*) FILTER (WHERE scroll_100_recorded)::int AS scroll_100,
COUNT(*)::int AS content_sessions
FROM analytics_content_sessions
WHERE post_id IN ${sql(normalizedPostIds)}
AND day BETWEEN ${startDay}::date AND ${endDay}::date
GROUP BY post_id
`
return new Map(rows.map((row) => [String(row.post_id), row]))
}
/**
* 페이지 콘텐츠 세션 집계를 조회한다.
* @param {import('postgres').Sql} sql - DB 클라이언트
@@ -800,6 +836,41 @@ const getPageContentSessionAggregate = async (sql, pageId, startDay, endDay) =>
return rows[0] || {}
}
/**
* 여러 페이지 콘텐츠 세션 집계를 조회한다.
* @param {import('postgres').Sql} sql - DB 클라이언트
* @param {string[]} pageIds - 페이지 ID 목록
* @param {string} startDay - 시작일
* @param {string} endDay - 종료일
* @returns {Promise<Map<string, Object>>} 페이지 ID별 원장 집계
*/
const getPageContentSessionAggregateMap = async (sql, pageIds, startDay, endDay) => {
const normalizedPageIds = [...new Set(pageIds.map((pageId) => String(pageId || '').trim()).filter(Boolean))]
if (!normalizedPageIds.length) {
return new Map()
}
const rows = await sql`
SELECT
page_id,
COUNT(*) FILTER (WHERE view_recorded)::int AS views,
COUNT(DISTINCT visitor_hash) FILTER (WHERE view_recorded)::int AS visitors,
COUNT(*) FILTER (WHERE engaged_recorded)::int AS engaged_views,
COALESCE(SUM(duration_seconds), 0)::int AS total_engaged_seconds,
COUNT(*) FILTER (WHERE scroll_50_recorded)::int AS scroll_50,
COUNT(*) FILTER (WHERE scroll_75_recorded)::int AS scroll_75,
COUNT(*) FILTER (WHERE scroll_100_recorded)::int AS scroll_100,
COUNT(*)::int AS content_sessions
FROM analytics_content_sessions
WHERE page_id IN ${sql(normalizedPageIds)}
AND day BETWEEN ${startDay}::date AND ${endDay}::date
GROUP BY page_id
`
return new Map(rows.map((row) => [String(row.page_id), row]))
}
/**
* 게시물 전체 콘텐츠 세션 스크롤 집계를 조회한다.
* @param {import('postgres').Sql} sql - DB 클라이언트
@@ -2714,15 +2785,32 @@ export const getAnalyticsTopPosts = async (options = {}) => {
LIMIT ${limit}
`
const shouldUseContentRows = canUseContentSessionAggregateForRange(rangeStartDay, today)
const contentRowMap = shouldUseContentRows
? await getPostContentSessionAggregateMap(sql, rows.map((row) => row.id), rangeStartDay, today)
: new Map()
return rows.map((row) => {
const engagedViews = Number(row.engaged_views || 0)
const totalEngagedSeconds = Number(row.total_engaged_seconds || 0)
const contentRow = contentRowMap.get(String(row.id))
const metricRow = hasContentSessionAggregateRows(contentRow)
? {
...row,
reads: contentRow.reads,
engaged_views: contentRow.engaged_views,
total_engaged_seconds: contentRow.total_engaged_seconds,
scroll_50: contentRow.scroll_50,
scroll_75: contentRow.scroll_75,
scroll_100: contentRow.scroll_100
}
: row
const engagedViews = Number(metricRow.engaged_views || 0)
const totalEngagedSeconds = Number(metricRow.total_engaged_seconds || 0)
const engagementStats = normalizeContentEngagementStats({
views: row.views,
reads: row.reads,
scroll50: row.scroll_50,
scroll75: row.scroll_75,
scroll100: row.scroll_100
reads: metricRow.reads,
scroll50: metricRow.scroll_50,
scroll75: metricRow.scroll_75,
scroll100: metricRow.scroll_100
})
return {
@@ -2782,14 +2870,30 @@ export const getAnalyticsTopPages = async (options = {}) => {
LIMIT ${limit}
`
const shouldUseContentRows = canUseContentSessionAggregateForRange(rangeStartDay, today)
const contentRowMap = shouldUseContentRows
? await getPageContentSessionAggregateMap(sql, rows.map((row) => row.id), rangeStartDay, today)
: new Map()
return rows.map((row) => {
const engagedViews = Number(row.engaged_views || 0)
const totalEngagedSeconds = Number(row.total_engaged_seconds || 0)
const contentRow = contentRowMap.get(String(row.id))
const metricRow = hasContentSessionAggregateRows(contentRow)
? {
...row,
engaged_views: contentRow.engaged_views,
total_engaged_seconds: contentRow.total_engaged_seconds,
scroll_50: contentRow.scroll_50,
scroll_75: contentRow.scroll_75,
scroll_100: contentRow.scroll_100
}
: row
const engagedViews = Number(metricRow.engaged_views || 0)
const totalEngagedSeconds = Number(metricRow.total_engaged_seconds || 0)
const engagementStats = normalizeContentEngagementStats({
views: row.views,
scroll50: row.scroll_50,
scroll75: row.scroll_75,
scroll100: row.scroll_100
scroll50: metricRow.scroll_50,
scroll75: metricRow.scroll_75,
scroll100: metricRow.scroll_100
})
return {