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) { setCookie(event, 'LOCALE', finalLocale.toLowerCase(), { 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 runtimeConfig = useRuntimeConfig() const iBaseApiUrl = `${runtimeConfig.public.stoveApiUrlServer}` const baseDomain = `${runtimeConfig.public.baseDomain}` const stoveApiUrlBaseServer = runtimeConfig.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 = { 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('🚀 ~ 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 path = event?.node.req.url || '' let queryStringF = '' let fValue = '' let test500 = false if (path.includes('?')) { queryStringF = path.split('?')[1] // 쿼리스트링에서 f 파라미터 값 추출 try { const urlParams = new URLSearchParams(queryStringF) fValue = urlParams.get('f') || '' // 테스트용 500 에러 발생 (예: ?test500=true) test500 = urlParams.get('test500') === 'true' } catch (e) { console.error('쿼리스트링 파싱 에러:', e) } } // 테스트용 500 에러 발생 if (test500) { throw new Error('테스트용 500 에러 발생') } // 미리보기 API 호출 처리 if (fValue === 'preview') { cleanHost = 'samplegame.onstove.com' } const queryParams: Record = { 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( 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_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 { 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) // 500 에러 발생 시 /error 페이지로 리다이렉트 if (!event.node.res.headersSent && !event.node.res.writableEnded) { // 언어 코드 추출 시도 let finalLocale = 'ko' // 기본값 try { finalLocale = ssrGetFinalLocale( event?.node.req.url, event.node.req.headers, initLangCodes, initDefaultLocale ) } catch (e) { console.error('Locale extraction error:', e) } // finalLocale이 undefined인 경우 기본값으로 'ko' 설정 console.log("🚀 ~ 여기도 타? error:", error) throw createError({ statusCode: error.statusCode, statusMessage: error.statusMessage, }) // if (!finalLocale) { // finalLocale = 'ko' // } // const errorPath = `/${finalLocale}/error?message=${error.message}` // event.node.res.statusCode = 302 // event.node.res.setHeader('Location', errorPath) // event.node.res.end() } } })