운영 HTTP에서 관리자 세션이 유지되지 않던 문제를 쿠키 공통화로 수정하고, 통계 클라이언트 분리·조회 오류·기간별 차트를 보강했다. 방문자 해시는 32일 초과분만 정리하고 일별 집계는 누적 보관한다. Co-authored-by: Cursor <cursoragent@cursor.com>
143 lines
4.0 KiB
JavaScript
143 lines
4.0 KiB
JavaScript
/** @type {RegExp} 추적 제외 경로 */
|
|
const EXCLUDED_PATH_PATTERN = /^\/(admin|signin|signup|forgot-password|settings)(\/|$)/
|
|
|
|
/** @type {RegExp} 봇 User-Agent 패턴 */
|
|
const BOT_USER_AGENT_PATTERN = /bot|crawl|spider|slurp|preview|headless|lighthouse|bytespider|facebookexternalhit/i
|
|
|
|
/**
|
|
* 오늘 날짜(UTC)를 YYYY-MM-DD로 반환한다.
|
|
* @returns {string} 날짜 문자열
|
|
*/
|
|
export const getAnalyticsDayKey = () => {
|
|
const now = new Date()
|
|
return now.toISOString().slice(0, 10)
|
|
}
|
|
|
|
/**
|
|
* 기준일에서 지정 일수만큼 이전 날짜를 YYYY-MM-DD로 반환한다.
|
|
* @param {string} dayKey - 기준일(YYYY-MM-DD)
|
|
* @param {number} daysBefore - 며칠 전(0이면 같은 날)
|
|
* @returns {string} 시작일
|
|
*/
|
|
export const getAnalyticsDayBefore = (dayKey, daysBefore) => {
|
|
const offset = Math.max(Number(daysBefore) || 0, 0)
|
|
const base = new Date(`${dayKey}T00:00:00.000Z`)
|
|
base.setUTCDate(base.getUTCDate() - offset)
|
|
return base.toISOString().slice(0, 10)
|
|
}
|
|
|
|
/**
|
|
* 통계 추적 대상 경로인지 확인한다.
|
|
* @param {string} path - 요청 경로
|
|
* @returns {boolean} 추적 가능 여부
|
|
*/
|
|
export const isTrackableAnalyticsPath = (path) => {
|
|
const normalized = (path || '').trim()
|
|
if (!normalized.startsWith('/')) {
|
|
return false
|
|
}
|
|
|
|
if (EXCLUDED_PATH_PATTERN.test(normalized)) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* 봇 User-Agent 여부
|
|
* @param {string} userAgent - User-Agent
|
|
* @returns {boolean} 봇 여부
|
|
*/
|
|
export const isBotUserAgent = (userAgent) => {
|
|
const value = (userAgent || '').trim()
|
|
if (!value) {
|
|
return false
|
|
}
|
|
|
|
return BOT_USER_AGENT_PATTERN.test(value)
|
|
}
|
|
|
|
/**
|
|
* 게시물 slug 정규화
|
|
* @param {string} slug - slug
|
|
* @returns {string} 정규화된 slug
|
|
*/
|
|
export const normalizePostSlugForAnalytics = (slug) => (slug || '').trim()
|
|
|
|
/** @type {number} heartbeat 체류시간 상한(초) */
|
|
export const ANALYTICS_MAX_DURATION_SECONDS = 1800
|
|
|
|
/** @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_CHART_MAX_DAYS = 365
|
|
|
|
/** @type {number} 일별 방문자 해시 보관 기간(일) */
|
|
export const ANALYTICS_VISITOR_HASH_RETENTION_DAYS = 32
|
|
|
|
/** @type {number} 통계 정리 최소 실행 간격(ms) */
|
|
export const ANALYTICS_RETENTION_PURGE_INTERVAL_MS = 6 * 60 * 60 * 1000
|
|
|
|
/** @type {number[]} 스크롤 구간 임계값 */
|
|
export const ANALYTICS_SCROLL_THRESHOLDS = [0.25, 0.5, 0.75, 1]
|
|
|
|
/**
|
|
* 체류시간(초)을 상한 내로 보정한다.
|
|
* @param {number} seconds - 체류시간
|
|
* @returns {number} 보정된 초
|
|
*/
|
|
export const clampAnalyticsDurationSeconds = (seconds) => {
|
|
const value = Number(seconds)
|
|
if (!Number.isFinite(value) || value < 0) {
|
|
return 0
|
|
}
|
|
|
|
return Math.min(Math.floor(value), ANALYTICS_MAX_DURATION_SECONDS)
|
|
}
|
|
|
|
/**
|
|
* 스크롤 비율을 0~1로 보정한다.
|
|
* @param {number} ratio - 스크롤 비율
|
|
* @returns {number} 보정된 비율
|
|
*/
|
|
export const clampAnalyticsScrollRatio = (ratio) => {
|
|
const value = Number(ratio)
|
|
if (!Number.isFinite(value) || value < 0) {
|
|
return 0
|
|
}
|
|
|
|
return Math.min(value, 1)
|
|
}
|
|
|
|
/**
|
|
* 새로 통과한 스크롤 구간 컬럼명 목록을 반환한다.
|
|
* @param {number} previousRatio - 이전 최대 스크롤
|
|
* @param {number} nextRatio - 갱신된 최대 스크롤
|
|
* @returns {Array<'scroll_25' | 'scroll_50' | 'scroll_75' | 'scroll_100'>} 신규 구간
|
|
*/
|
|
export const getNewScrollBucketColumns = (previousRatio, nextRatio) => {
|
|
const previous = clampAnalyticsScrollRatio(previousRatio)
|
|
const next = clampAnalyticsScrollRatio(nextRatio)
|
|
const columns = []
|
|
|
|
if (previous < 0.25 && next >= 0.25) {
|
|
columns.push('scroll_25')
|
|
}
|
|
if (previous < 0.5 && next >= 0.5) {
|
|
columns.push('scroll_50')
|
|
}
|
|
if (previous < 0.75 && next >= 0.75) {
|
|
columns.push('scroll_75')
|
|
}
|
|
if (previous < 1 && next >= 1) {
|
|
columns.push('scroll_100')
|
|
}
|
|
|
|
return columns
|
|
}
|