305 lines
8.8 KiB
JavaScript
305 lines
8.8 KiB
JavaScript
const ADSENSE_TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
|
const ADSENSE_API_BASE_URL = 'https://adsense.googleapis.com/v2'
|
|
const ADSENSE_REPORT_METRICS = [
|
|
'ESTIMATED_EARNINGS',
|
|
'PAGE_VIEWS',
|
|
'IMPRESSIONS',
|
|
'CLICKS',
|
|
'PAGE_VIEWS_RPM'
|
|
]
|
|
|
|
let cachedAccessToken = ''
|
|
let cachedAccessTokenExpiresAt = 0
|
|
|
|
/**
|
|
* AdSense API 연동 설정을 반환한다.
|
|
* @returns {{ clientId: string, clientSecret: string, refreshToken: string, accountName: string, currencyCode: string }} AdSense 설정
|
|
*/
|
|
const getAdsenseConfig = () => ({
|
|
clientId: process.env.GOOGLE_ADSENSE_CLIENT_ID || '',
|
|
clientSecret: process.env.GOOGLE_ADSENSE_CLIENT_SECRET || '',
|
|
refreshToken: process.env.GOOGLE_ADSENSE_REFRESH_TOKEN || '',
|
|
accountName: process.env.GOOGLE_ADSENSE_ACCOUNT_NAME || '',
|
|
currencyCode: process.env.GOOGLE_ADSENSE_CURRENCY_CODE || ''
|
|
})
|
|
|
|
/**
|
|
* AdSense API 호출에 필요한 설정이 모두 있는지 확인한다.
|
|
* @param {ReturnType<typeof getAdsenseConfig>} config - AdSense 설정
|
|
* @returns {boolean} 설정 완료 여부
|
|
*/
|
|
const isAdsenseConfigured = (config) => {
|
|
return Boolean(config.clientId && config.clientSecret && config.refreshToken && config.accountName)
|
|
}
|
|
|
|
/**
|
|
* Date 객체를 Google API Date 필드로 변환한다.
|
|
* @param {Date} date - 날짜
|
|
* @returns {{ year: string, month: string, day: string }} Google API Date 필드
|
|
*/
|
|
const toGoogleDateFields = (date) => ({
|
|
year: String(date.getFullYear()),
|
|
month: String(date.getMonth() + 1),
|
|
day: String(date.getDate())
|
|
})
|
|
|
|
/**
|
|
* 로컬 시간 기준 날짜 시작점을 만든다.
|
|
* @param {Date} date - 기준 날짜
|
|
* @returns {Date} 날짜 시작점
|
|
*/
|
|
const startOfLocalDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
|
|
|
/**
|
|
* 날짜에 일 단위를 더한다.
|
|
* @param {Date} date - 기준 날짜
|
|
* @param {number} days - 더할 일수
|
|
* @returns {Date} 계산된 날짜
|
|
*/
|
|
const addDays = (date, days) => {
|
|
const nextDate = new Date(date)
|
|
nextDate.setDate(nextDate.getDate() + days)
|
|
return nextDate
|
|
}
|
|
|
|
/**
|
|
* 이번 달 시작 날짜를 만든다.
|
|
* @param {Date} date - 기준 날짜
|
|
* @returns {Date} 이번 달 시작 날짜
|
|
*/
|
|
const startOfLocalMonth = (date) => new Date(date.getFullYear(), date.getMonth(), 1)
|
|
|
|
/**
|
|
* 빈 AdSense 지표를 만든다.
|
|
* @param {string} label - 기간 라벨
|
|
* @returns {Object} 빈 지표
|
|
*/
|
|
const createEmptyAdsenseMetric = (label) => ({
|
|
label,
|
|
estimatedEarnings: 0,
|
|
pageViews: 0,
|
|
impressions: 0,
|
|
clicks: 0,
|
|
pageViewsRpm: 0
|
|
})
|
|
|
|
/**
|
|
* 숫자 셀 값을 안전하게 변환한다.
|
|
* @param {unknown} value - 원본 값
|
|
* @returns {number} 숫자 값
|
|
*/
|
|
const toMetricNumber = (value) => {
|
|
const number = Number(value || 0)
|
|
return Number.isFinite(number) ? number : 0
|
|
}
|
|
|
|
/**
|
|
* Google OAuth refresh token으로 access token을 발급한다.
|
|
* @param {ReturnType<typeof getAdsenseConfig>} config - AdSense 설정
|
|
* @returns {Promise<string>} access token
|
|
*/
|
|
const getAdsenseAccessToken = async (config) => {
|
|
const now = Date.now()
|
|
|
|
if (cachedAccessToken && cachedAccessTokenExpiresAt > now + 60000) {
|
|
return cachedAccessToken
|
|
}
|
|
|
|
const response = await fetch(ADSENSE_TOKEN_URL, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: new URLSearchParams({
|
|
client_id: config.clientId,
|
|
client_secret: config.clientSecret,
|
|
refresh_token: config.refreshToken,
|
|
grant_type: 'refresh_token'
|
|
})
|
|
})
|
|
const data = await response.json().catch(() => ({}))
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error_description || data.error || 'AdSense access token 발급 실패')
|
|
}
|
|
|
|
cachedAccessToken = data.access_token || ''
|
|
cachedAccessTokenExpiresAt = now + (Number(data.expires_in || 0) * 1000)
|
|
|
|
if (!cachedAccessToken) {
|
|
throw new Error('AdSense access token 응답이 비어 있습니다.')
|
|
}
|
|
|
|
return cachedAccessToken
|
|
}
|
|
|
|
/**
|
|
* AdSense 보고서 URL을 만든다.
|
|
* @param {Object} options - 보고서 옵션
|
|
* @param {string} options.accountName - AdSense 계정 이름
|
|
* @param {Date} options.startDate - 시작일
|
|
* @param {Date} options.endDate - 종료일
|
|
* @param {string} options.currencyCode - 통화 코드
|
|
* @returns {URL} 보고서 URL
|
|
*/
|
|
const createAdsenseReportUrl = ({ accountName, startDate, endDate, currencyCode }) => {
|
|
const url = new URL(`${ADSENSE_API_BASE_URL}/${accountName}/reports:generate`)
|
|
const start = toGoogleDateFields(startDate)
|
|
const end = toGoogleDateFields(endDate)
|
|
|
|
for (const metric of ADSENSE_REPORT_METRICS) {
|
|
url.searchParams.append('metrics', metric)
|
|
}
|
|
|
|
url.searchParams.set('startDate.year', start.year)
|
|
url.searchParams.set('startDate.month', start.month)
|
|
url.searchParams.set('startDate.day', start.day)
|
|
url.searchParams.set('endDate.year', end.year)
|
|
url.searchParams.set('endDate.month', end.month)
|
|
url.searchParams.set('endDate.day', end.day)
|
|
url.searchParams.set('languageCode', 'ko-KR')
|
|
|
|
if (currencyCode) {
|
|
url.searchParams.set('currencyCode', currencyCode)
|
|
}
|
|
|
|
return url
|
|
}
|
|
|
|
/**
|
|
* AdSense 단일 기간 보고서를 조회한다.
|
|
* @param {Object} options - 조회 옵션
|
|
* @param {string} options.label - 기간 라벨
|
|
* @param {string} options.accessToken - access token
|
|
* @param {ReturnType<typeof getAdsenseConfig>} options.config - AdSense 설정
|
|
* @param {Date} options.startDate - 시작일
|
|
* @param {Date} options.endDate - 종료일
|
|
* @returns {Promise<Object>} 기간 지표
|
|
*/
|
|
const fetchAdsensePeriodReport = async ({ label, accessToken, config, startDate, endDate }) => {
|
|
const url = createAdsenseReportUrl({
|
|
accountName: config.accountName,
|
|
startDate,
|
|
endDate,
|
|
currencyCode: config.currencyCode
|
|
})
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`
|
|
}
|
|
})
|
|
const data = await response.json().catch(() => ({}))
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error?.message || `AdSense 보고서 조회 실패: ${response.status}`)
|
|
}
|
|
|
|
const row = Array.isArray(data.rows) ? data.rows[0] : null
|
|
const cells = row && Array.isArray(row.cells) ? row.cells : []
|
|
const headers = Array.isArray(data.headers) ? data.headers : []
|
|
const metric = createEmptyAdsenseMetric(label)
|
|
|
|
headers.forEach((header, index) => {
|
|
const value = toMetricNumber(cells[index]?.value)
|
|
|
|
if (header.name === 'ESTIMATED_EARNINGS') {
|
|
metric.estimatedEarnings = value
|
|
return
|
|
}
|
|
|
|
if (header.name === 'PAGE_VIEWS') {
|
|
metric.pageViews = value
|
|
return
|
|
}
|
|
|
|
if (header.name === 'IMPRESSIONS') {
|
|
metric.impressions = value
|
|
return
|
|
}
|
|
|
|
if (header.name === 'CLICKS') {
|
|
metric.clicks = value
|
|
return
|
|
}
|
|
|
|
if (header.name === 'PAGE_VIEWS_RPM') {
|
|
metric.pageViewsRpm = value
|
|
}
|
|
})
|
|
|
|
return metric
|
|
}
|
|
|
|
/**
|
|
* 관리자 대시보드용 AdSense 요약을 조회한다.
|
|
* @returns {Promise<Object>} AdSense 요약
|
|
*/
|
|
export const getAdsenseDashboardSummary = async () => {
|
|
const config = getAdsenseConfig()
|
|
|
|
if (!isAdsenseConfigured(config)) {
|
|
return {
|
|
configured: false,
|
|
ok: false,
|
|
message: 'AdSense API 환경 변수가 아직 설정되지 않았습니다.',
|
|
currencyCode: config.currencyCode || 'KRW',
|
|
generatedAt: new Date().toISOString(),
|
|
today: createEmptyAdsenseMetric('오늘'),
|
|
yesterday: createEmptyAdsenseMetric('어제'),
|
|
month: createEmptyAdsenseMetric('이번 달')
|
|
}
|
|
}
|
|
|
|
try {
|
|
const today = startOfLocalDay(new Date())
|
|
const yesterday = addDays(today, -1)
|
|
const monthStart = startOfLocalMonth(today)
|
|
const accessToken = await getAdsenseAccessToken(config)
|
|
const [todayMetric, yesterdayMetric, monthMetric] = await Promise.all([
|
|
fetchAdsensePeriodReport({
|
|
label: '오늘',
|
|
accessToken,
|
|
config,
|
|
startDate: today,
|
|
endDate: today
|
|
}),
|
|
fetchAdsensePeriodReport({
|
|
label: '어제',
|
|
accessToken,
|
|
config,
|
|
startDate: yesterday,
|
|
endDate: yesterday
|
|
}),
|
|
fetchAdsensePeriodReport({
|
|
label: '이번 달',
|
|
accessToken,
|
|
config,
|
|
startDate: monthStart,
|
|
endDate: today
|
|
})
|
|
])
|
|
|
|
return {
|
|
configured: true,
|
|
ok: true,
|
|
message: '',
|
|
currencyCode: config.currencyCode || 'KRW',
|
|
generatedAt: new Date().toISOString(),
|
|
today: todayMetric,
|
|
yesterday: yesterdayMetric,
|
|
month: monthMetric
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
configured: true,
|
|
ok: false,
|
|
message: error instanceof Error ? error.message : 'AdSense 통계를 불러오지 못했습니다.',
|
|
currencyCode: config.currencyCode || 'KRW',
|
|
generatedAt: new Date().toISOString(),
|
|
today: createEmptyAdsenseMetric('오늘'),
|
|
yesterday: createEmptyAdsenseMetric('어제'),
|
|
month: createEmptyAdsenseMetric('이번 달')
|
|
}
|
|
}
|
|
}
|