153 lines
4.5 KiB
JavaScript
153 lines
4.5 KiB
JavaScript
/** @type {number} 통계 키워드 최대 길이 */
|
|
const MAX_KEYWORD_LENGTH = 80
|
|
|
|
/** @type {Array<{ name: string, hostPattern: RegExp, keywordParams: string[] }>} 검색 유입 규칙 */
|
|
const SEARCH_SOURCE_RULES = [
|
|
{ name: '구글', hostPattern: /(^|\.)google\./i, keywordParams: ['q'] },
|
|
{ name: '네이버', hostPattern: /(^|\.)naver\.com$/i, keywordParams: ['query', 'q'] },
|
|
{ name: '다음', hostPattern: /(^|\.)daum\.net$|(^|\.)kakao\.com$/i, keywordParams: ['q', 'query'] },
|
|
{ name: '빙', hostPattern: /(^|\.)bing\.com$/i, keywordParams: ['q'] },
|
|
{ name: '줌', hostPattern: /(^|\.)zum\.com$/i, keywordParams: ['query', 'q'] }
|
|
]
|
|
|
|
/** @type {Array<{ name: string, hostPattern: RegExp }>} SNS 유입 규칙 */
|
|
const SOCIAL_SOURCE_RULES = [
|
|
{ name: '카카오톡', hostPattern: /(^|\.)kakao\.com$|(^|\.)kakaocdn\.net$/i },
|
|
{ name: '페이스북', hostPattern: /(^|\.)facebook\.com$|(^|\.)fb\.com$|(^|\.)fb\.me$/i },
|
|
{ name: '인스타그램', hostPattern: /(^|\.)instagram\.com$/i },
|
|
{ name: '트위터', hostPattern: /(^|\.)twitter\.com$|(^|\.)x\.com$|(^|\.)t\.co$/i },
|
|
{ name: '유튜브', hostPattern: /(^|\.)youtube\.com$|(^|\.)youtu\.be$/i }
|
|
]
|
|
|
|
/**
|
|
* 문자열 값을 안전한 통계 라벨로 정리한다.
|
|
* @param {string} value - 원본 문자열
|
|
* @param {number} maxLength - 최대 길이
|
|
* @returns {string} 정리된 문자열
|
|
*/
|
|
const normalizeAnalyticsText = (value, maxLength = 120) => {
|
|
return String(value || '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
.slice(0, maxLength)
|
|
}
|
|
|
|
/**
|
|
* URL 문자열을 URL 객체로 변환한다.
|
|
* @param {string} value - URL 문자열
|
|
* @returns {URL | null} URL 객체
|
|
*/
|
|
const parseAnalyticsUrl = (value) => {
|
|
const rawValue = String(value || '').trim()
|
|
if (!rawValue) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
return new URL(rawValue)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 검색 유입 키워드를 추출한다.
|
|
* @param {URL} url - referrer URL
|
|
* @param {string[]} params - 키워드 후보 파라미터
|
|
* @returns {string} 키워드
|
|
*/
|
|
const extractSearchKeyword = (url, params) => {
|
|
const keywordParams = [...params, 'keyword', 'search']
|
|
for (const param of keywordParams) {
|
|
const value = normalizeAnalyticsText(url.searchParams.get(param), MAX_KEYWORD_LENGTH)
|
|
if (value) {
|
|
return value
|
|
}
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
/**
|
|
* 유입 URL을 검색·SNS·직접·기타로 분류한다.
|
|
* @param {{ referrer?: string, currentUrl?: string }} input - 분류 입력
|
|
* @returns {{ sourceGroup: string, sourceName: string, keyword: string }} 유입 분류
|
|
*/
|
|
export const classifyAnalyticsTrafficSource = (input = {}) => {
|
|
const referrerUrl = parseAnalyticsUrl(input.referrer)
|
|
const currentUrl = parseAnalyticsUrl(input.currentUrl)
|
|
|
|
if (!referrerUrl) {
|
|
return {
|
|
sourceGroup: 'direct',
|
|
sourceName: '직접 유입',
|
|
keyword: ''
|
|
}
|
|
}
|
|
|
|
if (currentUrl && referrerUrl.hostname === currentUrl.hostname) {
|
|
return {
|
|
sourceGroup: 'direct',
|
|
sourceName: '직접 유입',
|
|
keyword: ''
|
|
}
|
|
}
|
|
|
|
const host = referrerUrl.hostname.replace(/^www\./i, '')
|
|
const searchRule = SEARCH_SOURCE_RULES.find((rule) => rule.hostPattern.test(host))
|
|
|
|
if (searchRule) {
|
|
return {
|
|
sourceGroup: 'search',
|
|
sourceName: searchRule.name,
|
|
keyword: extractSearchKeyword(referrerUrl, searchRule.keywordParams)
|
|
}
|
|
}
|
|
|
|
const socialRule = SOCIAL_SOURCE_RULES.find((rule) => rule.hostPattern.test(host))
|
|
|
|
if (socialRule) {
|
|
return {
|
|
sourceGroup: 'sns',
|
|
sourceName: socialRule.name,
|
|
keyword: ''
|
|
}
|
|
}
|
|
|
|
return {
|
|
sourceGroup: 'other',
|
|
sourceName: '기타 유입',
|
|
keyword: ''
|
|
}
|
|
}
|
|
|
|
/**
|
|
* User-Agent에서 디바이스와 OS를 분류한다.
|
|
* @param {string} userAgent - User-Agent
|
|
* @returns {{ deviceType: string, osName: string }} 디바이스 분류
|
|
*/
|
|
export const classifyAnalyticsDevice = (userAgent) => {
|
|
const value = String(userAgent || '')
|
|
const lowerValue = value.toLowerCase()
|
|
const isMobile = /mobile|iphone|ipod|android.*mobile|windows phone/i.test(value)
|
|
const isTablet = /ipad|tablet|android(?!.*mobile)/i.test(value)
|
|
let osName = '기타'
|
|
|
|
if (/iphone|ipad|ipod/i.test(value)) {
|
|
osName = 'iOS'
|
|
} else if (/android/i.test(value)) {
|
|
osName = 'Android'
|
|
} else if (/windows/i.test(value)) {
|
|
osName = 'Windows'
|
|
} else if (/mac os|macintosh|mac_powerpc/i.test(lowerValue)) {
|
|
osName = 'macOS'
|
|
} else if (/linux/i.test(value)) {
|
|
osName = 'Linux'
|
|
}
|
|
|
|
return {
|
|
deviceType: isMobile || isTablet ? '모바일' : 'PC',
|
|
osName
|
|
}
|
|
}
|