v1.12.30 긴 접속 시간 표시와 중단 후 새 방문 처리 개선

This commit is contained in:
2026-10-21 18:45:44 +09:00
parent 51d5de148f
commit 7b6fc45bf3
19 changed files with 310 additions and 68 deletions

View File

@@ -8,6 +8,27 @@ export const formatAnalyticsCount = (value) => {
return new Intl.NumberFormat('ko-KR').format(Number.isFinite(number) ? number : 0)
}
/**
* 현재 접속 시간을 최대 두 단위로 짧게 표시한다.
* @param {number} seconds - 접속 시간(초)
* @returns {string} 일·시간·분·초 표시
*/
export const formatRealtimeDuration = (seconds) => {
const input = Number(seconds)
const value = Number.isFinite(input) ? Math.max(0, Math.floor(input)) : 0
if (value >= 86400) {
const hours = Math.floor(value % 86400 / 3600)
return `${formatAnalyticsCount(Math.floor(value / 86400))}일${hours ? ` ${hours}시간` : ''}`
}
if (value >= 3600) {
const minutes = Math.floor(value % 3600 / 60)
return `${formatAnalyticsCount(Math.floor(value / 3600))}시간${minutes ? ` ${minutes}분` : ''}`
}
const minutes = Math.floor(value / 60)
const remainder = value % 60
return minutes ? `${minutes}분${remainder ? ` ${remainder}초` : ''}` : `${remainder}초`
}
/**
* 접속 페이지의 표시 제목을 반환한다.
* @param {Object} page - 활성 페이지

30
lib/analytics-duration.js Normal file
View File

@@ -0,0 +1,30 @@
import { ANALYTICS_ACTIVE_SESSION_TTL_SECONDS, ANALYTICS_MAX_REALTIME_DURATION_SECONDS, ANALYTICS_NEW_VISIT_IDLE_SECONDS } from './analytics-shared.js'
/**
* heartbeat 실행이 끊긴 긴 구간을 제외하는 페이지별 시간 측정기를 만든다.
* @param {number} [startedAt] - 측정 시작 시각(ms)
* @returns {(now?: number) => { durationSeconds: number, restarted: boolean }} 접속 시간과 새 방문 여부를 반환하는 측정 함수
*/
export const createAnalyticsDurationTracker = (startedAt = Date.now()) => {
let lastSampleAt = startedAt
let durationMs = 0
/**
* 직전 측정 이후 실행이 이어진 구간만 누적한다.
* @param {number} [now] - 현재 시각(ms)
* @returns {{ durationSeconds: number, restarted: boolean }} 중단 구간을 제외한 시간과 새 방문 여부
*/
const sample = (now = Date.now()) => {
if (!Number.isFinite(now)) return { durationSeconds: Math.floor(durationMs / 1000), restarted: false }
const gapMs = now - lastSampleAt
lastSampleAt = now
const restarted = gapMs >= ANALYTICS_NEW_VISIT_IDLE_SECONDS * 1000
if (restarted) durationMs = 0
else if (gapMs > 0 && gapMs < ANALYTICS_ACTIVE_SESSION_TTL_SECONDS * 1000) {
durationMs = Math.min(durationMs + gapMs, ANALYTICS_MAX_REALTIME_DURATION_SECONDS * 1000)
}
return { durationSeconds: Math.floor(durationMs / 1000), restarted }
}
return sample
}

View File

@@ -72,15 +72,21 @@ export const normalizePostSlugForAnalytics = (slug) => (slug || '').trim()
*/
export const normalizePageSlugForAnalytics = (slug) => (slug || '').trim()
/** @type {number} heartbeat 체류시간 상한(초) */
/** @type {number} 평균 체류 통계에 반영할 콘텐츠별 시간 상한(초) */
export const ANALYTICS_MAX_DURATION_SECONDS = 1800
/** @type {number} 실시간 접속 시간의 PostgreSQL 정수 저장 상한(초) */
export const ANALYTICS_MAX_REALTIME_DURATION_SECONDS = 2147483647
/** @type {number} engaged_views 집계 최소 체류(초) */
export const ANALYTICS_ENGAGED_MIN_SECONDS = 10
/** @type {number} 현재 접속자 판정 TTL(초) */
export const ANALYTICS_ACTIVE_SESSION_TTL_SECONDS = 90
/** @type {number} 실행 중단 후 새 콘텐츠 방문으로 시작하는 기준(초) */
export const ANALYTICS_NEW_VISIT_IDLE_SECONDS = 1800
/** @type {number} 관리자 차트 최대 조회 기간(일) */
export const ANALYTICS_CHART_MAX_DAYS = 365
@@ -122,6 +128,17 @@ export const clampAnalyticsDurationSeconds = (seconds) => {
return Math.min(Math.floor(value), ANALYTICS_MAX_DURATION_SECONDS)
}
/**
* 실시간 접속 시간을 데이터베이스 정수 범위로 보정한다.
* @param {number} seconds - 접속 시간
* @returns {number} 보정된 초
*/
export const clampAnalyticsRealtimeDurationSeconds = (seconds) => {
const value = Number(seconds)
if (!Number.isFinite(value) || value < 0) return 0
return Math.min(Math.floor(value), ANALYTICS_MAX_REALTIME_DURATION_SECONDS)
}
/**
* 스크롤 비율을 0~1로 보정한다.
* @param {number} ratio - 스크롤 비율

View File

@@ -6,11 +6,13 @@ export {
ANALYTICS_COLLECTED_SINCE_DEFAULT,
ANALYTICS_ENGAGED_MIN_SECONDS,
ANALYTICS_MAX_DURATION_SECONDS,
ANALYTICS_MAX_REALTIME_DURATION_SECONDS,
ANALYTICS_RETENTION_PURGE_INTERVAL_MS,
ANALYTICS_SCROLL_THRESHOLDS,
ANALYTICS_VISITOR_HASH_RETENTION_DAYS,
normalizeAnalyticsCollectedSinceDay,
clampAnalyticsDurationSeconds,
clampAnalyticsRealtimeDurationSeconds,
clampAnalyticsScrollRatio,
getAnalyticsDayBefore,
getAnalyticsDayKey,