feat: 점검 페이지 추가, 언어 정책 추가

This commit is contained in:
“hyeonggkim”
2025-10-31 15:30:06 +09:00
parent 7f040d432b
commit 74851e01ed
13 changed files with 629 additions and 356 deletions

View File

@@ -1,24 +1,5 @@
<template>
<div class="select-language" :class="{ 'language-changing': isChanging }">
<!-- <select
v-model="selectedLocale"
:disabled="isChanging"
class="text-black px-2 py-1 rounded-md"
:class="{ 'opacity-50 cursor-not-allowed': isChanging }"
@change="switchLanguage"
>
<option
v-for="localeOption in availableLanguages"
:key="localeOption"
:value="localeOption"
>
{{ localeOption }}
</option>
</select>
<span v-if="isChanging" class="ml-2 text-sm text-gray-500">
변경 중...
</span>
-->
<button
:disabled="isChanging"
@@ -82,6 +63,9 @@
</template>
<script setup lang="ts">
const config = useRuntimeConfig()
const baseDomain = `${config.public.baseDomain}`
const gameDataStore = useGameDataStore()
// 사용 가능한 언어 목록
@@ -141,12 +125,21 @@ const switchLanguage = async () => {
isChanging.value = true
try {
// URL 경로를 통해 언어 변경
const path = switchLocalePath(selectedLocale.value as any)
if (path) {
// 페이지 데이터 초기화 (새로운 언어로 다시 로드되도록)
pageDataStore.clearPageData()
window.location.href = path
// URL 경로를 통해 언어 변경
const path = switchLocalePath(selectedLocale.value as any)
if (path) {
// 언어 쿠키 설정 (클라이언트 사이드) - 페이지 이동 전에 설정
const localeCookie = useCookie('LOCALE', {
domain: baseDomain,
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1년 (초 단위)
sameSite: 'lax'
})
localeCookie.value = selectedLocale.value.toUpperCase()
// 페이지 데이터 초기화 (새로운 언어로 다시 로드되도록)
pageDataStore.clearPageData()
window.location.href = path
// 언어 변경 및 라우팅
// await setLocale(selectedLocale.value as any)

View File

@@ -119,15 +119,10 @@
class="nx3 ml-2.5 md:ml-4"
>
<img
src="https://static-pubcomm.gate8.com/local/template/l9/common/logo_nx3.png"
:src="`${staticUrl}${setDevCi.dev_ci_img_path}`"
alt="CI"
class="w-auto h-auto"
/>
<!-- <img
:src="setDevCi.dev_ci_img_path"
alt="CI"
class="w-auto h-auto"
/> -->
</a>
</div>
@@ -144,6 +139,7 @@
</template>
<script setup lang="ts">
import type { FooterMenuItem, FooterData, DevCiConfig } from '#layers/types/Common'
const config = useRuntimeConfig()
const staticUrl = config.public.staticUrl
@@ -163,33 +159,7 @@ const { tm } = useI18n({
const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore)
// 타입 정의
interface FooterMenuItem {
title: string
url: string
target?: string
active?: string
}
interface GameRatingInfo {
company_name: string
rating_grade: string
reg_no: string
prod_date: string
rating_class_no: string
rating_type: string
content_info: string
}
interface FooterData {
game_rating_info: GameRatingInfo
use_dev_ci_url?: string
}
interface DevCiConfig {
dev_ci_yn: boolean
dev_ci_img_path: string
}
const path = ref<string>(`${staticUrl}/local/template/${gameData.value.s3_folder_name}`)
// 공통다국어 data
const footerLinks = computed((): FooterMenuItem[] => {
@@ -207,42 +177,43 @@ const getGameRatingImage = computed((): string[] => {
const contentInfo = footerData.value.game_rating_info.rating_type.split(',')
// rating_type 12, 15, 18, 19 에 따라 이미지명을 가져오고 이미지를 반환
return contentInfo.map(item => {
if (item === '12') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/Type12.svg`
} else if (item === '15') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/Type15.svg`
} else if (item === '19') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/Type19.svg`
} else if (item === 'all') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/TypeAll.svg`
} else if (item === 'e') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/TypeExempt.svg`
} else {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/TypeTest.svg`
switch (item) {
case '12':
return `${path.value}/common/grades_age/Type12.svg`
case '15':
return `${path.value}/common/grades_age/Type15.svg`
case '19':
return `${path.value}/common/grades_age/Type19.svg`
case 'all':
return `${path.value}/common/grades_age/TypeAll.svg`
case 'e':
return `${path.value}/common/grades_age/TypeExempt.svg`
default:
return `${path.value}/common/grades_age/TypeTest.svg`
}
})
})
const getContentInfoImage = computed((): string[] => {
const contentInfo = footerData.value.game_rating_info.content_info.split(',')
contentInfo.pop()
return contentInfo.map(item => {
if (item === '1') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-sexual.svg`
} else if (item === '2') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-fear.svg`
} else if (item === '3') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-inapposite.svg`
} else if (item === '4') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-drug.svg`
} else if (item === '5') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-crime.svg`
} else if (item === '6') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-speculation.svg`
} else if (item === '7') {
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-violence.svg`
switch (item) {
case '1':
return `${path.value}/common/grades_use/Type-sexual.svg`
case '2':
return `${path.value}/common/grades_use/Type-fear.svg`
case '3':
return `${path.value}/common/grades_use/Type-inapposite.svg`
case '4':
return `${path.value}/common/grades_use/Type-drug.svg`
case '5':
return `${path.value}/common/grades_use/Type-crime.svg`
case '6':
return `${path.value}/common/grades_use/Type-speculation.svg`
case '7':
return `${path.value}/common/grades_use/Type-violence.svg`
}
})
})

View File

@@ -1,23 +1,26 @@
export default defineNuxtRouteMiddleware(async (to) => {
try {
if (import.meta.client) {
const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore)
const config = useRuntimeConfig()
// const baseDomain = `${config.public.baseDomain}`
const stoveApiUrl = `${config.public.stoveApiUrl}`
console.log("🚀 ~ stoveApiUrl:", stoveApiUrl)
// const stoveGameId = `${config.public.stoveGameId}`
const stoveGameId = `${gameData.value.game_id}`
// const stoveMaintenanceApiUrl = `${config.public.stoveMaintenanceApiUrl}`
/* const localeCookie = useCookie('LOCALE', {
domain: baseDomain
}) */
// const localeCookie = useCookie('LOCALE', {
// domain: baseDomain
// })
// const finalLocale = csrGetFinalLocale(to.path)
const finalLocale = csrGetFinalLocale(to.path)
console.log("🚀 ~ finalLocale:", finalLocale)
// localeCookie.value = finalLocale.toUpperCase()
// 웹 점검 -----
const { isWebInspection, getInspectionDataExternal } = useGetInspectionDataExternal()
await getInspectionDataExternal({ baseApiUrl: stoveApiUrl, gameId: 'STOVE_LORD' })
await getInspectionDataExternal({ baseApiUrl: stoveApiUrl, gameId: stoveGameId })
// 게임 점검 -----
// const { checkGameMaintenance } = useGetGameMaintenance()
@@ -30,10 +33,10 @@ export default defineNuxtRouteMiddleware(async (to) => {
if (isWebInspection.value && !to.path.includes('inspection') && !to.path.includes('api')) {
// 점검 중인 경우
// return navigateTo(`/${finalLocale}/inspection`, { external: true })
return navigateTo(`/${finalLocale}/inspection`, { external: true })
} else if (!isWebInspection.value && to.path?.indexOf('inspection') !== -1) {
// 점검이 종료된 후 점검 페이지 접근시 메인으로 리다이렉트
// return navigateTo(`/${finalLocale}`, { external: true })
return navigateTo(`/${finalLocale}`, { external: true })
}
}
} catch (e) {

View File

@@ -16,7 +16,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
try {
if(to.matched) {
if(to.path.includes('inspection')) {
return
}
@@ -41,12 +41,12 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
})) as PageDataResponse | null
console.log("🚀 ~ response?.code:", response?.code)
if(response?.code === 91003) {
throw createError({
statusCode: 404,
statusMessage: 'Page not found',
})
}
// if(response?.code === 91003) {
// throw createError({
// statusCode: 404,
// statusMessage: 'Page not found',
// })
// }
if (response?.code === 0 && 'value' in response) {
store.setPageData(response.value)

View File

@@ -1,3 +1,4 @@
import { LRUCache } from 'lru-cache'
import {
getHeader,
getRequestHost,
@@ -7,28 +8,179 @@ import {
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.toUpperCase(), {
domain: baseDomain,
path: '/',
maxAge: 60 * 60 * 24 * 365 // 1년 (초 단위)
})
}
/**
* Locale Middleware 역할 함수
*
* @param event - 이벤트 객체
* @param finalLocale - 최종 언어
*/
function fnLocaleMiddleware(event: any, finalLocale: string) {
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 => {
const config = useRuntimeConfig()
// const runType = `${config.public.runType}`
const iBaseApiUrl = `${config.public.stoveApiUrlServer}`
const baseDomain = `${config.public.baseDomain}`
console.log("🚀 ~ baseDomain:", config.public.baseDomain)
// const url = getRequestURL(event)
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 fullPath = event.path
// 1-1. 정적 파일 패스
if (isStaticFile(event.path)) {
return
}
// 1-2. /inspection 패스
if (fullPath.includes('/inspection')) {
// 리턴 되기 전 언어 쿠키 세팅
const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
console.log("🚀 0000 ~ finalLocale:", finalLocale)
setFinalLocaleCookie(event, finalLocale, baseDomain)
return
}
// 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리
if (
url.pathname.startsWith('/api/') ||
url.pathname.startsWith('/_nuxt/') ||
url.pathname.startsWith('/favicon') ||
url.pathname.includes('.') ||
url.pathname.startsWith('/_')
fullPath.startsWith('/api/') ||
fullPath.startsWith('/_nuxt/') ||
fullPath.startsWith('/favicon') ||
fullPath.includes('/assets/') ||
fullPath.includes('.') ||
fullPath.startsWith('/_')
) {
return
}
// 캐시 키 생성
const cacheKey = 'inspection'
console.log("🚀 11111 ~ cacheKey:", cacheKey)
const host =
(getHeader(event, 'host') || getRequestHost(event)).toString() || ''
const baseDomain = process.env.BASE_DOMAIN || '.onstove.com'
const isGameDomainExtractable = host.includes(baseDomain)
if (isGameDomainExtractable) {
@@ -41,42 +193,66 @@ export default defineEventHandler(async event => {
const config = useRuntimeConfig()
const stoveApiUrlServer = config.public.stoveApiUrlServer
const apiUrl = `${stoveApiUrlServer}/pub-comm/v1.0/template/game`
let inspectionData
const langCode = ssrGetFinalLocale(
event?.node.req.url,
event.node.req.headers
)
// 2. 언어 코드 추출
const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
const queryParams: Record<string, string> = {
game_domain: event.context.gameDomain || '',
lang_code: langCode,
lang_code: finalLocale,
}
const response = (await $fetch(apiUrl, {
query: queryParams,
})) as GameDataResponse | null
// 언어패스 쿠키 굽기 - 장기방안에서는 굽지않음
// const langCoverages = response?.value?.lang_codes || []
// if(langCoverages.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)
// 점검 데이터 조회
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
// console.log("🚀 ~ inspectionData:", inspectionData)
let inspectionData
if (cache.has(cacheKey)) {
inspectionData = cache.get(cacheKey) as WebInspectionData
// console.log("🚀 22222 ~ 캐시키 있어 inspectionData:", inspectionData)
} else {
// 점검 데이터 조회
if (response.value.game_id) {
const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response.value.game_id}`
// console.log("🚀 ~ inspectionApiUrl:", inspectionApiUrl)
// 직접 $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)
}
if (inspectionData?.inspection_status === 0) {
// 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이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ
@@ -84,18 +260,78 @@ export default defineEventHandler(async event => {
* - 화이트 리스트 체크
*/
// 현재 경로가 점검 페이지가 아닐 경우 리다이렉트
const inspectionPath = `/${langCode}/inspection`
if (!url.pathname.includes('/inspection')) {
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인 경우 점검 페이지로 이동
event.node.res.statusCode = 302
event.node.res.setHeader('Location', inspectionPath)
event.node.res.end()
} else {
// 화이트 리스트인 경우
// -------------------------------------------------------------------------------
// [Locale Middleware]
// -------------------------------------------------------------------------------
fnLocaleMiddleware(event, finalLocale)
}
} else {
event.node.res.statusCode = 302
event.node.res.setHeader('Location', inspectionPath)
event.node.res.end()
return
}
} 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)

View File

@@ -0,0 +1,68 @@
import type { RenderResponse } from 'nitropack'
import type { H3Event } from 'h3'
import { defineNitroPlugin } from 'nitropack/runtime'
import { getTrueClientIp } from '#layers/utils/apiUtil'
function generateRequestId(): string {
return Date.now().toString(36) + Math.random().toString(36).substring(2)
}
function getIpAddress(event: H3Event): string {
return getTrueClientIp(event.node.req as any) || 'unknown'
}
export default defineNitroPlugin((nitroApp) => {
// 정적 파일 체크 함수 추가
const isStaticFile = (path: string): boolean => {
return /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i.test(path)
}
// 헬스체크 경로 체크 함수 추가
const isHealthCheck = (path: string): boolean => {
return path === '/health'
}
nitroApp.hooks.hook('request', (event) => {
// 정적 파일 요청은 로깅 제외
if (isStaticFile(event.path) || isHealthCheck(event.path)) {
return
}
// 상세 로깅을 위한 정보 수집
const startTime = Date.now()
const userAgent = event.node.req.headers['user-agent'] || ''
const method = event.method || ''
const headers = JSON.stringify(event.node.req.headers, null, 2)
const requestId = generateRequestId()
if (process.env.NODE_ENV !== 'development') {
console.log(
`Request Info {"requestId":"${requestId}", "type":"request","method":"${method}","url":"${event.path}","userIp":"${getIpAddress(event)}","userAgent":"${userAgent}", "headers" : "${headers}" }`
)
// 요청 완료 후 응답 상태 코드 로깅
event.node.res.on('finish', () => {
console.log(
`Response Info {"requestId":"${requestId}","type":"response","method":"${method}","url":"${event.path}","statusCode":${event.node.res.statusCode},"responseTime":"${Date.now() - startTime}ms","userIp":"${getIpAddress(event)}","userAgent":"${userAgent}","statusMessage":"${event.node.res.statusMessage}","responseHeader": ${JSON.stringify(event.node.res.getHeaders(), null, 2)}}`
)
console.log(
'==========================================================================================================================================================================================================================================================='
)
})
}
})
nitroApp.hooks.hook('error', (error) => {
console.error('[Nitro Error]', {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
})
})
// 응답 헤더에서 'x-powered-by' 제거
nitroApp.hooks.hook('render:response', (response: Partial<RenderResponse>) => {
if (response?.headers) {
delete response.headers['x-powered-by']
}
})
})

View File

@@ -27,4 +27,33 @@ interface ParsedCustomLinkOptions {
query?: Record<string, any>
}
export type { CommonRequestType, CommonResponseType, CommonPeriodType, ParsedCustomLinkOptions }
// 타입 정의
interface FooterMenuItem {
title: string
url: string
target?: string
active?: string
}
interface GameRatingInfo {
company_name: string
rating_grade: string
reg_no: string
prod_date: string
rating_class_no: string
rating_type: string
content_info: string
}
interface FooterData {
game_rating_info: GameRatingInfo
use_dev_ci_url?: string
}
interface DevCiConfig {
dev_ci_yn: boolean
dev_ci_img_path: string
}
export type { CommonRequestType, CommonResponseType, CommonPeriodType, ParsedCustomLinkOptions, FooterMenuItem, GameRatingInfo, FooterData, DevCiConfig }

View File

@@ -1,6 +1,6 @@
import { DEFAULT_LOCALE_CODE, DEFAULT_COVERAGES } from '@/i18n.config'
import { DEFAULT_LOCALE_CODE } from '../../i18n.config'
// 사용자 선호 언어 가져오기
// 사용자 선호 언어 조회
export const getPreferredLanguage = (acceptLanguageHeader = '') => {
const languages = acceptLanguageHeader
.split(',')
@@ -13,58 +13,18 @@ export const getPreferredLanguage = (acceptLanguageHeader = '') => {
return languages.length > 0 ? languages[0].code : null
}
export const getFinalLanguage = (path = '', defaultLocale: string, coverages: string[]) => {
// const nuxtApp = useNuxtApp()
let finalLocale = ''
let requestedLocale
let acceptLanguage: string
let defaultLang = 'en'
let defaultLangEn: string
if (defaultLocale) {
defaultLangEn = defaultLocale
} else {
defaultLangEn = 'en'
// 쿠키 파싱 유틸리티 함수
const parseCookies = (cookieHeader: string) => {
const cookies: Record<string, string> = {}
if (cookieHeader) {
cookieHeader.split(';').forEach((cookie) => {
const [name, value] = cookie.trim().split('=')
if (name && value) {
cookies[name] = decodeURIComponent(value)
}
})
}
requestedLocale = path?.split('/')[1]?.toLowerCase() ?? 'undefined'
if (import.meta.server) {
const headers = useRequestHeaders(['accept-language'])
acceptLanguage = headers['accept-language'] || defaultLangEn
defaultLang =
coverages.find((locale: string) => getPreferredLanguage(acceptLanguage)?.startsWith(locale)) || defaultLangEn
}
// const DEFAULT_COVERAGES = i18n.locales.map((locale) => locale.code)
const DEFAULT_COVERAGES = coverages
const requestedPage = path?.split('/')[2]?.toLowerCase() ?? undefined
const localeMap: Record<string, string> = {
'zh-tw': 'zh-TW',
'zh-cn': 'zh-CN'
}
if (localeMap[requestedLocale]) {
requestedLocale = localeMap[requestedLocale]
}
if (requestedLocale !== undefined && DEFAULT_COVERAGES.includes(requestedLocale)) {
finalLocale = requestedLocale
} else if (
requestedLocale === undefined ||
requestedLocale === '' ||
path !== '' ||
(requestedLocale !== undefined && !DEFAULT_COVERAGES.includes(requestedLocale) && requestedPage !== undefined)
) {
// 요청된 언어가 없을 때 or 잘못된 언어코드로 요청 시 브라우저 언어로 설정
finalLocale = defaultLang
} else {
// 그 외의 경우 기본 언어로 설정 (중국어 번체)
finalLocale = defaultLangEn
}
return finalLocale.toLowerCase()
return cookies
}
/**
@@ -73,34 +33,38 @@ export const getFinalLanguage = (path = '', defaultLocale: string, coverages: st
* @param {string} path - 현재 URL 경로
*/
export const csrGetFinalLocale = (path = '') => {
const config = useRuntimeConfig()
const baseDomain = `${config.public.baseDomain}`
let finalLocale = DEFAULT_LOCALE_CODE // 기본값 설정
const localeMap: Record<string, string> = {
'zh-tw': 'zh-TW',
'zh-cn': 'zh-CN'
}
// 1. URL 패스에 포함된 언어
if (path && path !== '' && path.split('/').length > 1) {
const pathLocal = path.split('/')[1]
// 쿼리스트링 제거한 순수 path 검사
if (path.includes('?')) {
path = path.split('?')[0]
}
const pathLocale = `${path.split('/')[1]}`.toLowerCase()
// URL 패스에 포함된 언어가 지원하는 언어인지 체크
if (pathLocal && pathLocal !== '' && DEFAULT_COVERAGES.includes(pathLocal)) {
finalLocale = pathLocal // .toLowerCase()
if (localeMap[pathLocal]) {
finalLocale = localeMap[pathLocal]
}
if (pathLocale && pathLocale !== '') {
finalLocale = pathLocale
return finalLocale
}
return finalLocale
}
// 2. 브라우저 언어
const browserLanguage = navigator.language || navigator.languages[0]
if (browserLanguage && browserLanguage !== '' && DEFAULT_COVERAGES.includes(browserLanguage)) {
finalLocale = browserLanguage // .toLowerCase()
if (localeMap[browserLanguage]) {
finalLocale = localeMap[browserLanguage]
}
// 2. LOCALE 쿠키 언어
const cookieLanguage = `${useCookie('LOCALE', { domain: baseDomain }).value}`.toLowerCase()
if (cookieLanguage && cookieLanguage !== '') {
finalLocale = cookieLanguage
return finalLocale
}
// 3. 브라우저 언어
const browserLanguage = `${navigator.language || navigator.languages[0]}`.toLowerCase()
if (browserLanguage && browserLanguage !== '' && coveragesLocales.includes(browserLanguage)) {
finalLocale = browserLanguage
return finalLocale
}
@@ -118,22 +82,31 @@ export const csrGetFinalLocale = (path = '') => {
*/
export const ssrGetFinalLocale = (path = '', headers: any) => {
let finalLocale = DEFAULT_LOCALE_CODE // 기본값 설정
try {
// 1. URL path에 포함된 언어 정보
if (path && path !== '' && path.split('/').length > 1) {
const pathLocale = path.split('/')[1]
// 쿼리스트링 제거한 순수 path 검사
if (path.includes('?')) {
path = path.split('?')[0]
}
const pathLocalee = `${path.split('/')[1]}`.toLowerCase()
// URL path에 포함된 언어 정보가 지원하는 언어인지 체크
if (pathLocale && pathLocale !== '' && DEFAULT_COVERAGES.includes(pathLocale)) {
finalLocale = pathLocale // .toLowerCase()
if (pathLocalee && pathLocalee !== '') {
finalLocale = pathLocalee
return finalLocale
}
}
// 2. 요청 헤더의 브라우저 언어 (accept-language)
// 2. LOCALE 쿠키 언어 (SSR에서는 headers에서 직접 파싱)
const cookieHeader = headers.cookie || ''
const cookies = parseCookies(cookieHeader)
const cookieLanguage = cookies.LOCALE ? `${cookies.LOCALE}`.toLowerCase() : ''
if (cookieLanguage && cookieLanguage !== '' && coveragesLocales.includes(cookieLanguage)) {
finalLocale = cookieLanguage
return finalLocale
}
// 3. 요청 헤더의 브라우저 언어 (accept-language)
if (headers && headers['accept-language']) {
const acceptLanguage = Array.isArray(headers['accept-language'])
? headers['accept-language'][0]
@@ -143,13 +116,11 @@ export const ssrGetFinalLocale = (path = '', headers: any) => {
const preferredLocale = getPreferredLanguage(acceptLanguage)
if (preferredLocale) {
// 선호 언어의 기본 코드와 일치하는 지원 로케일 찾기
const matchedLocale = DEFAULT_COVERAGES.find((locale: string) =>
const matchedLocale = coveragesLocales.find((locale: string) =>
preferredLocale.toLowerCase().startsWith(locale.toLowerCase())
)
if (matchedLocale) {
finalLocale = matchedLocale
// return matchedLocale.toLowerCase()
finalLocale = matchedLocale.toLowerCase()
return finalLocale
}
}
@@ -159,7 +130,6 @@ export const ssrGetFinalLocale = (path = '', headers: any) => {
// 3. 서비스 기본 언어
finalLocale = DEFAULT_LOCALE_CODE
} catch (e) {
console.error('[Exception] localeUtil.ssrGetFinalLocale: ', e)
finalLocale = DEFAULT_LOCALE_CODE
}
return finalLocale