Files
web-temp/layers/server/middleware/gameData.ts

376 lines
13 KiB
TypeScript

import { LRUCache } from 'lru-cache'
import {
getHeader,
getRequestHost,
defineEventHandler,
} from 'h3'
import { ssrGetFinalLocale } from '../../utils/localeUtil'
import type { GameDataResponse } from '../../types/api/gameData'
import type { ResGetInspectionData } from '../../types/InspectionType'
import { isStaticFile } from '#layers/utils/commonUtil'
/**
* 캐시 제어 헤더를 설정하는 공통 함수
*
* @param event - 이벤트 객체
* @param cacheMode - 캐시 모드 설정 ('no-cache', 'short', 'medium', 'default')
* @param customMaxAge - 커스텀 max-age 값 (초 단위)
*/
function setCacheHeaders(
event: { node: { res: { setHeader: (name: string, value: string) => void } } },
cacheMode: 'no-cache' | 'short' | 'medium' | 'default',
customMaxAge?: number
): void {
// 원래 setHeader 함수 참조 저장
const originalSetHeader = event.node.res.setHeader
// Cache-Control 헤더 설정값 결정
let cacheControl: string
switch (cacheMode) {
case 'no-cache':
cacheControl = 'no-cache, no-store, must-revalidate'
// no-cache 모드일 때는 추가 헤더도 설정
event.node.res.setHeader('Pragma', 'no-cache')
event.node.res.setHeader('Expires', '0')
break
case 'short':
cacheControl = `public, max-age=${customMaxAge || 10}`
break
case 'medium':
cacheControl = `public, max-age=${customMaxAge || 15}`
break
case 'default':
default:
cacheControl = `public, max-age=${customMaxAge || 60}`
break
}
// Cache-Control 헤더를 강제로 설정하기 위해 setHeader 메소드 오버라이드
event.node.res.setHeader = function (name: string, value: string) {
if (name.toLowerCase() === 'cache-control') {
return originalSetHeader.call(this, name, cacheControl)
}
return originalSetHeader.call(this, name, value)
}
// 바로 캐시 제어 헤더 적용
event.node.res.setHeader('Cache-Control', cacheControl)
}
const cache = new LRUCache({
max: 100, // 캐시에 저장할 최대 항목 수
ttl: 1000 * 30 // 30초 동안 캐시 유지
})
/**
* 최종 언어 쿠키 세팅
*
* @param event - 이벤트 객체
* @param finalLocale - 최종 언어
* @param baseDomain - 기본 도메인
*/
function setFinalLocaleCookie(event: any, finalLocale: string, baseDomain: string) {
console.log("🚀 ~ 구어 구어 setFinalLocaleCookie:============", finalLocale)
setCookie(event, 'LOCALE', finalLocale.toUpperCase(), {
domain: baseDomain,
path: '/',
maxAge: 60 * 60 * 24 * 365 // 1년 (초 단위)
})
}
/**
* Locale Middleware 역할 함수
*
* @param event - 이벤트 객체
* @param finalLocale - 최종 언어
*/
function fnLocaleMiddleware(event: any, finalLocale: string) {
// 이미 응답이 종료되었는지 확인
if (event.node.res.headersSent || event.node.res.writableEnded) {
return
}
const path = event?.node.req.url || ''
let arrPath = []
let queryString = ''
if (path.includes('?')) {
// 쿼리스트링 포함 시 순수 경로만 추출
arrPath = path.split('?')[0].split('/')
queryString = path.split('?')[1]
} else {
arrPath = path.split('/')
queryString = ''
}
// 최종 언어 세팅된 경로 생성
const pathLocale = arrPath.length > 1 ? arrPath[1] : ''
// URL에서 현재 언어와 최종 언어가 다르면 리다이렉트
if (pathLocale !== finalLocale) {
let newLocalePath = ''
if (pathLocale === '') {
newLocalePath = `/${finalLocale}`
} else {
arrPath[1] = finalLocale
newLocalePath = arrPath.join('/')
}
if (queryString !== '') {
newLocalePath += `?${queryString}`
}
event.node.res.statusCode = 302
event.node.res.setHeader('Location', newLocalePath)
event.node.res.end()
}
}
export default defineEventHandler(async event => {
// HMR 요청 필터링 (개발 모드에서 중복 실행 방지)
if (event.path.startsWith('/_nuxt/') || event.path.startsWith('/__nuxt')) {
return
}
// 이미 응답이 종료되었는지 확인 (리다이렉트 등으로 인한 중복 실행 방지)
if (event.node.res.headersSent || event.node.res.writableEnded) {
return
}
// const runType = `${config.public.runType}`
// console.log("🚀 ~ baseDomain:", config.public.baseDomain)
// const url = getRequestURL(event)
// if (['local', 'local-gate8', 'dev'].includes(runType)) {
// Sandbox 이상 환경에서만 동작 및 확인 가능 (local, dev는 통과 처리)
// try {
// 언어 코드 추출
// const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
// console.log("🚀 ~ finalLocale:", finalLocale)
// setFinalLocaleCookie(event, finalLocale, baseDomain)
// -------------------------------------------------------------------------------
// [Locale Middleware]
// -------------------------------------------------------------------------------
// fnLocaleMiddleware(event, finalLocale)
// } catch (e) {
// console.error('[Exception] /server/middleware/middleware-02-global: ', e)
// }
// }
const config = useRuntimeConfig()
const iBaseApiUrl = `${config.public.stoveApiUrlServer}`
const baseDomain = `${config.public.baseDomain}`
const stoveApiUrlBaseServer = config.public.stoveApiUrlServer
const apiUrl = `${stoveApiUrlBaseServer}/pub-comm/v1.0/template/game`
let initGameData: GameDataResponse | null = null
let initLangCodes: string[] | null = null
let finalLocale
let cleanHost
let initDefaultLocale
const host =
(getHeader(event, 'host') || getRequestHost(event)).toString() || ''
const isGameDomainExtractable = host.includes(baseDomain)
if (isGameDomainExtractable) {
cleanHost = host.split(':')[0]
event.context.gameDomain = cleanHost
}
try {
const queryParams: Record<string, string> = {
game_domain: cleanHost || '',
lang_code: '',
}
const initResponse = (await $fetch(apiUrl, {
query: queryParams,
})) as GameDataResponse | null
initGameData = initResponse || null
// console.log("🚀 ~ 00000 initGameData:", initGameData)
initLangCodes = initResponse?.value?.lang_codes || null
initDefaultLocale = initResponse?.value?.default_lang_code || null
console.log("🚀 ~ 000111 initLangCodes:", initLangCodes)
} catch (error) {
console.error('init gameData load error:', error)
}
const fullPath = event.path
// 1-1. 정적 파일 패스
if (isStaticFile(event.path)) {
return
}
// 1-2. /inspection 패스
if (fullPath.includes('/inspection')) {
// 리턴 되기 전 언어 쿠키 세팅
finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers, initLangCodes, initDefaultLocale)
setFinalLocaleCookie(event, finalLocale, baseDomain)
return
}
// 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리
if (
fullPath.startsWith('/api/') ||
fullPath.startsWith('/_nuxt/') ||
fullPath.startsWith('/favicon') ||
fullPath.includes('/assets/') ||
fullPath.includes('.') ||
fullPath.startsWith('/_')
) {
return
}
// 캐시 키 생성
const cacheKey = 'inspection'
try {
// 이미 응답이 종료되었는지 확인
if (event.node.res.headersSent || event.node.res.writableEnded) {
return
}
// 2. 언어 코드 추출
finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers, initLangCodes, initDefaultLocale)
const queryParams: Record<string, string> = {
game_domain: cleanHost || '',
lang_code: finalLocale,
}
const response = (await $fetch(apiUrl, {
query: queryParams,
})) as GameDataResponse | null
// 언어패스 쿠키 굽기 - 장기방안에서는 굽지않음
if(initLangCodes?.includes(finalLocale)) {
setFinalLocaleCookie(event, finalLocale, baseDomain)
}
if (response?.code === 0 && 'value' in response) {
event.context.gameData = response.value
event.context.googleAnalyticsId = response.value?.ga_code
// console.log('🚀 ~ gameData:', response.value)
// 점검 데이터 조회
let inspectionData
if (cache.has(cacheKey)) {
inspectionData = cache.get(cacheKey) as WebInspectionData
} else {
// 점검 데이터 조회
if (response?.value?.game_id) {
const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response?.value?.game_id}`
// 직접 $fetch 사용 (composable 사용하지 않음)
const inspectionResponse = await $fetch<ResGetInspectionData>(inspectionApiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
inspectionData = inspectionResponse?.value?.inspection
cache.set(cacheKey, inspectionData) // 캐시에 저장
// console.log("🚀 ~ inspectionData:", inspectionData)
}
}
// 4. 현재 시간과 점검 기간 비교
const currentTime = Date.now()
const tsStartDate = inspectionData?.ts_start_date || 0
const tsEndDate = inspectionData?.ts_end_date || 0
const timeUntilInspectionSeconds = Math.floor((tsStartDate - currentTime) / 1000)
// 5. 점검 상태별 캐시 설정
if (inspectionData?.inspection_status === 1 && currentTime >= tsStartDate && currentTime <= tsEndDate) {
/**
* 점검 중인 경우
* - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ
* - 점검 URL 경로가 아닐 경우 no-cache 설정
* - 화이트 리스트 체크
*/
// 현재 경로가 점검 페이지가 아닐 경우 리다이렉트
const inspectionPath = `/${finalLocale}/inspection`
if (fullPath !== inspectionPath) {
setCacheHeaders(event, 'no-cache')
}
// 점검 중일 때 IP 필터링 활성화 여부 확인
if (inspectionData?.ip_filter_use_yn === 'Y') {
const clientIP = getTrueClientIp(event.node.req as any)
// 허용된 IP 목록 확인
if (!inspectionData?.ip_filter_list?.includes(clientIP)) {
// 허용되지 않은 IP인 경우 점검 페이지로 이동
if (!event.node.res.headersSent && !event.node.res.writableEnded) {
event.node.res.statusCode = 302
event.node.res.setHeader('Location', inspectionPath)
event.node.res.end()
}
} else {
// 화이트 리스트인 경우
// -------------------------------------------------------------------------------
// [Locale Middleware]
// -------------------------------------------------------------------------------
fnLocaleMiddleware(event, finalLocale)
}
} else {
if (!event.node.res.headersSent && !event.node.res.writableEnded) {
event.node.res.statusCode = 302
event.node.res.setHeader('Location', inspectionPath)
event.node.res.end()
}
}
} else {
/**
* 점검이 아닌 경우
* - 홈 경로는 no-cache
* - 점검 예정 시간에 따른 캐시 설정
* - 점검 5분 전: 짧은 캐시 (10초)
* - 점검 30분 전: 중간 캐시 (15초)
* - 점검 30분 이후: 기본 캐시 (60초)
*/
// 홈 경로: 캐시 없음
const isHomePath = [
'',
'/'
//, ...Object.values(DEFAULT_LOCALE_COVERAGES).flatMap((locale) => [`/${locale}`, `/${locale}/`])
].includes(fullPath)
if (isHomePath) {
setCacheHeaders(event, 'no-cache')
} else {
// 점검 예정 시간에 따른 캐시 설정
if (tsStartDate > 0 && timeUntilInspectionSeconds > 0) {
if (timeUntilInspectionSeconds < 300) {
// 점검 5분 전: 짧은 캐시 (10초)
setCacheHeaders(event, 'short', 10)
} else if (timeUntilInspectionSeconds < 1800) {
// 점검 30분 전: 중간 캐시 (15초)
setCacheHeaders(event, 'medium', 15)
} else {
// 점검 30분 이후: 기본 캐시 (60초)
setCacheHeaders(event, 'default')
}
}
}
// -------------------------------------------------------------------------------
// [Locale Middleware]
// -------------------------------------------------------------------------------
fnLocaleMiddleware(event, finalLocale)
}
}
} catch (error) {
console.error('gameData load error:', error)
}
})