Merge branch 'feature/20251001-gil' into feature/202501107-all

This commit is contained in:
“hyeonggkim”
2025-11-06 15:22:53 +09:00
24 changed files with 637 additions and 672 deletions

View File

@@ -1,29 +1,27 @@
import fallback from './fallback/de'
export default defineI18nLocale(async (locale: string) => { export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 const runtimeConfig = useRuntimeConfig()
const config = useRuntimeConfig() const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const rootPath = config.public.staticUrl const multilingualFileName = 'test_common_template.json'
const runType = config.public.runType
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
try { try {
const { data } = await useFetch(translationApi, { const resultGetMultilingual = await useGetMultilingual({
method: 'GET', baseApiUrl: dataResourcesUrl,
headers: { fileName: multilingualFileName,
'Content-Type': 'application/json;charset=UTF-8'
}
}) })
// API 데이터에서 locale에 는 데이터 추출 // multilingual 객체에서 현재 locale에 해당하는 데이터 추출
const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환 const multilingualData = resultGetMultilingual?.value?.multilingual
if (multilingualData && typeof multilingualData === 'object') {
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위) // locale이 'ko'이므로 'ko' 키의 데이터를 반환
const finalResult = { ...apiData } const localeData = multilingualData[locale] || multilingualData['de'] || {}
return localeData
return finalResult }
} catch (error) {
console.error('Error fetching translation data:', error) return {}
// 에러 발생 시 common.json 데이터라도 반환 } catch (e) {
return commonData[locale] || {} console.error('[Exception] ko.defineI18nLocale: ', e)
return fallback
} }
}) })

View File

@@ -1,29 +1,27 @@
import fallback from './fallback/en'
export default defineI18nLocale(async (locale: string) => { export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 const runtimeConfig = useRuntimeConfig()
const config = useRuntimeConfig() const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const rootPath = config.public.staticUrl const multilingualFileName = 'test_common_template.json'
const runType = config.public.runType
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
try { try {
const { data } = await useFetch(translationApi, { const resultGetMultilingual = await useGetMultilingual({
method: 'GET', baseApiUrl: dataResourcesUrl,
headers: { fileName: multilingualFileName,
'Content-Type': 'application/json;charset=UTF-8'
}
}) })
// API 데이터에서 locale에 는 데이터 추출 // multilingual 객체에서 현재 locale에 해당하는 데이터 추출
const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환 const multilingualData = resultGetMultilingual?.value?.multilingual
if (multilingualData && typeof multilingualData === 'object') {
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위) // locale이 'ko'이므로 'ko' 키의 데이터를 반환
const finalResult = { ...apiData } const localeData = multilingualData[locale] || multilingualData['en'] || {}
return localeData
return finalResult }
} catch (error) {
console.error('Error fetching translation data:', error) return {}
// 에러 발생 시 common.json 데이터라도 반환 } catch (e) {
return commonData[locale] || {} console.error('[Exception] ko.defineI18nLocale: ', e)
return fallback
} }
}) })

View File

@@ -1,29 +1,27 @@
import fallback from './fallback/es'
export default defineI18nLocale(async (locale: string) => { export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 const runtimeConfig = useRuntimeConfig()
const config = useRuntimeConfig() const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const rootPath = config.public.staticUrl const multilingualFileName = 'test_common_template.json'
const runType = config.public.runType
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
try { try {
const { data } = await useFetch(translationApi, { const resultGetMultilingual = await useGetMultilingual({
method: 'GET', baseApiUrl: dataResourcesUrl,
headers: { fileName: multilingualFileName,
'Content-Type': 'application/json;charset=UTF-8'
}
}) })
// API 데이터에서 locale에 는 데이터 추출 // multilingual 객체에서 현재 locale에 해당하는 데이터 추출
const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환 const multilingualData = resultGetMultilingual?.value?.multilingual
if (multilingualData && typeof multilingualData === 'object') {
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위) // locale이 'ko'이므로 'ko' 키의 데이터를 반환
const finalResult = { ...apiData } const localeData = multilingualData[locale] || multilingualData['es'] || {}
return localeData
return finalResult }
} catch (error) {
console.error('Error fetching translation data:', error) return {}
// 에러 발생 시 common.json 데이터라도 반환 } catch (e) {
return commonData[locale] || {} console.error('[Exception] ko.defineI18nLocale: ', e)
return fallback
} }
}) })

View File

@@ -1,29 +1,27 @@
import fallback from './fallback/fr'
export default defineI18nLocale(async (locale: string) => { export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 const runtimeConfig = useRuntimeConfig()
const config = useRuntimeConfig() const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const rootPath = config.public.staticUrl const multilingualFileName = 'test_common_template.json'
const runType = config.public.runType
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
try { try {
const { data } = await useFetch(translationApi, { const resultGetMultilingual = await useGetMultilingual({
method: 'GET', baseApiUrl: dataResourcesUrl,
headers: { fileName: multilingualFileName,
'Content-Type': 'application/json;charset=UTF-8'
}
}) })
// API 데이터에서 locale에 는 데이터 추출 // multilingual 객체에서 현재 locale에 해당하는 데이터 추출
const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환 const multilingualData = resultGetMultilingual?.value?.multilingual
if (multilingualData && typeof multilingualData === 'object') {
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위) // locale이 'ko'이므로 'ko' 키의 데이터를 반환
const finalResult = { ...apiData } const localeData = multilingualData[locale] || multilingualData['fr'] || {}
return localeData
return finalResult }
} catch (error) {
console.error('Error fetching translation data:', error) return {}
// 에러 발생 시 common.json 데이터라도 반환 } catch (e) {
return commonData[locale] || {} console.error('[Exception] ko.defineI18nLocale: ', e)
return fallback
} }
}) })

View File

@@ -1,29 +1,27 @@
import fallback from './fallback/ja'
export default defineI18nLocale(async (locale: string) => { export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 const runtimeConfig = useRuntimeConfig()
const config = useRuntimeConfig() const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const rootPath = config.public.staticUrl const multilingualFileName = 'test_common_template.json'
const runType = config.public.runType
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
try { try {
const { data } = await useFetch(translationApi, { const resultGetMultilingual = await useGetMultilingual({
method: 'GET', baseApiUrl: dataResourcesUrl,
headers: { fileName: multilingualFileName,
'Content-Type': 'application/json;charset=UTF-8'
}
}) })
// API 데이터에서 locale에 는 데이터 추출 // multilingual 객체에서 현재 locale에 해당하는 데이터 추출
const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환 const multilingualData = resultGetMultilingual?.value?.multilingual
if (multilingualData && typeof multilingualData === 'object') {
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위) // locale이 'ko'이므로 'ko' 키의 데이터를 반환
const finalResult = { ...apiData } const localeData = multilingualData[locale] || multilingualData['ja'] || {}
return localeData
return finalResult }
} catch (error) {
console.error('Error fetching translation data:', error) return {}
// 에러 발생 시 common.json 데이터라도 반환 } catch (e) {
return commonData[locale] || {} console.error('[Exception] ko.defineI18nLocale: ', e)
return fallback
} }
}) })

View File

@@ -1,17 +1,24 @@
import fallback from './fallback/ko'
export default defineI18nLocale(async (locale: string) => { export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 const runtimeConfig = useRuntimeConfig()
const config = useRuntimeConfig() const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const rootPath = config.public.staticUrl const multilingualFileName = 'test_common_template.json'
const runType = config.public.runType
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
try { try {
const result = (await commonFetch('GET', `${translationApi}`)) as any const resultGetMultilingual = await useGetMultilingual({
if(import.meta.client) { baseApiUrl: dataResourcesUrl,
console.log("🚀 ~ result:", result[locale]) fileName: multilingualFileName,
})
// multilingual 객체에서 현재 locale에 해당하는 데이터 추출
const multilingualData = resultGetMultilingual?.value?.multilingual
if (multilingualData && typeof multilingualData === 'object') {
// locale이 'ko'이므로 'ko' 키의 데이터를 반환
const localeData = multilingualData[locale] || multilingualData['ko'] || {}
return localeData
} }
return result[locale]
return {}
} catch (e) { } catch (e) {
console.error('[Exception] ko.defineI18nLocale: ', e) console.error('[Exception] ko.defineI18nLocale: ', e)
return fallback return fallback

View File

@@ -1,29 +1,27 @@
import fallback from './fallback/pt'
export default defineI18nLocale(async (locale: string) => { export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 const runtimeConfig = useRuntimeConfig()
const config = useRuntimeConfig() const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const rootPath = config.public.staticUrl const multilingualFileName = 'test_common_template.json'
const runType = config.public.runType
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
try { try {
const { data } = await useFetch(translationApi, { const resultGetMultilingual = await useGetMultilingual({
method: 'GET', baseApiUrl: dataResourcesUrl,
headers: { fileName: multilingualFileName,
'Content-Type': 'application/json;charset=UTF-8'
}
}) })
// API 데이터에서 locale에 는 데이터 추출 // multilingual 객체에서 현재 locale에 해당하는 데이터 추출
const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환 const multilingualData = resultGetMultilingual?.value?.multilingual
if (multilingualData && typeof multilingualData === 'object') {
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위) // locale이 'ko'이므로 'ko' 키의 데이터를 반환
const finalResult = { ...apiData } const localeData = multilingualData[locale] || multilingualData['pt'] || {}
return localeData
return finalResult }
} catch (error) {
console.error('Error fetching translation data:', error) return {}
// 에러 발생 시 common.json 데이터라도 반환 } catch (e) {
return commonData[locale] || {} console.error('[Exception] ko.defineI18nLocale: ', e)
return fallback
} }
}) })

View File

@@ -1,29 +1,27 @@
import fallback from './fallback/th'
export default defineI18nLocale(async (locale: string) => { export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 const runtimeConfig = useRuntimeConfig()
const config = useRuntimeConfig() const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const rootPath = config.public.staticUrl const multilingualFileName = 'test_common_template.json'
const runType = config.public.runType
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
try { try {
const { data } = await useFetch(translationApi, { const resultGetMultilingual = await useGetMultilingual({
method: 'GET', baseApiUrl: dataResourcesUrl,
headers: { fileName: multilingualFileName,
'Content-Type': 'application/json;charset=UTF-8'
}
}) })
// API 데이터에서 locale에 는 데이터 추출 // multilingual 객체에서 현재 locale에 해당하는 데이터 추출
const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환 const multilingualData = resultGetMultilingual?.value?.multilingual
if (multilingualData && typeof multilingualData === 'object') {
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위) // locale이 'ko'이므로 'ko' 키의 데이터를 반환
const finalResult = { ...apiData } const localeData = multilingualData[locale] || multilingualData['th'] || {}
return localeData
return finalResult }
} catch (error) {
console.error('Error fetching translation data:', error) return {}
// 에러 발생 시 common.json 데이터라도 반환 } catch (e) {
return commonData[locale] || {} console.error('[Exception] ko.defineI18nLocale: ', e)
return fallback
} }
}) })

View File

@@ -1,34 +1,27 @@
import fallback from './fallback/zh-cn'
export default defineI18nLocale(async (locale: string) => { export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 const runtimeConfig = useRuntimeConfig()
const config = useRuntimeConfig() const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const rootPath = config.public.staticUrl const multilingualFileName = 'test_common_template.json'
const runType = config.public.runType
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
try { try {
const { data } = await useFetch(translationApi, { const resultGetMultilingual = await useGetMultilingual({
method: 'GET', baseApiUrl: dataResourcesUrl,
headers: { fileName: multilingualFileName,
'Content-Type': 'application/json;charset=UTF-8'
}
}) })
// multilingual 객체에서 현재 locale에 해당하는 데이터 추출
if(locale === 'zh-cn') { const multilingualData = resultGetMultilingual?.value?.multilingual
locale = 'zh-CN' if (multilingualData && typeof multilingualData === 'object') {
// locale이 'ko'이므로 'ko' 키의 데이터를 반환
const localeData = multilingualData[locale] || multilingualData['zh-cn'] || {}
return localeData
} }
// API 데이터에서 locale에 맞는 데이터를 추출 return {}
const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환 } catch (e) {
console.error('[Exception] ko.defineI18nLocale: ', e)
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위) return fallback
const finalResult = { ...apiData }
return finalResult
} catch (error) {
console.error('Error fetching translation data:', error)
// 에러 발생 시 common.json 데이터라도 반환
return commonData[locale] || {}
} }
}) })

View File

@@ -1,17 +1,24 @@
import fallback from './fallback/zh-tw'
export default defineI18nLocale(async (locale: string) => { export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116 const runtimeConfig = useRuntimeConfig()
const config = useRuntimeConfig() const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const rootPath = config.public.staticUrl const multilingualFileName = 'test_common_template.json'
const runType = config.public.runType
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
try { try {
const result = (await commonFetch('GET', `${translationApi}`)) as any const resultGetMultilingual = await useGetMultilingual({
if(import.meta.client) { baseApiUrl: dataResourcesUrl,
console.log("🚀 ~ result:", result[locale]) fileName: multilingualFileName,
})
// multilingual 객체에서 현재 locale에 해당하는 데이터 추출
const multilingualData = resultGetMultilingual?.value?.multilingual
if (multilingualData && typeof multilingualData === 'object') {
// locale이 'ko'이므로 'ko' 키의 데이터를 반환
const localeData = multilingualData[locale] || multilingualData['zh-tw'] || {}
return localeData
} }
return result[locale]
return {}
} catch (e) { } catch (e) {
console.error('[Exception] ko.defineI18nLocale: ', e) console.error('[Exception] ko.defineI18nLocale: ', e)
return fallback return fallback

View File

@@ -8,11 +8,12 @@ onMounted(() => {
const langCodes = gameData?.lang_codes const langCodes = gameData?.lang_codes
const defaultLangCode = gameData?.default_lang_code const defaultLangCode = gameData?.default_lang_code
const stoveGnbData = gameData?.stove_gnb_json const stoveGnbData = gameData?.stove_gnb_json
const designTheme = gameData?.design_theme
const currentDomain = const currentDomain =
window.location.protocol + '//' + window.location.hostname window.location.protocol + '//' + window.location.hostname
if (typeof window !== 'undefined' && (window as any).StoveGnb) { if (typeof window !== 'undefined' && (window as any).StoveGnb) {
mountedInstance = (window as any).StoveGnb.mount('#stove-wrap', { const stoveGnbOptions = {
logArea: currentDomain, logArea: currentDomain,
useLanguageCodeFromPath: true, useLanguageCodeFromPath: true,
serviceTitle: { serviceTitle: {
@@ -38,14 +39,18 @@ onMounted(() => {
}, },
mode: { mode: {
theme: { theme: {
default: support: ['light', 'dark'],
stoveGnbData?.skin_type === 'gnb-dark-mini' ? 'dark' : 'light', default: designTheme === 1 ? 'light' : 'dark',
support: ['dark', 'light'], // support: designTheme === 1 ? ['light'] : ['dark'],
}, },
mini: true, mini: true,
layout: 'wide',
fixed: false, fixed: false,
}, }
}) }
mountedInstance = (window as any).StoveGnb.mount('#stove-wrap', stoveGnbOptions)
console.log("🚀 ~ onMounted ~ stoveGnbOptions:", stoveGnbOptions)
} }
}) })

View File

@@ -1,20 +1,20 @@
<template> <template>
<footer id="footer" ref="footerRef" class="bg-black"> <footer id="footer" ref="footerRef" class="bg-black">
<div <div
class="inner relative max-w-7xl mx-auto px-10 py-9 text-[12px] text-gray-400 md:px-4 md:py-9 md:text-[12px]" class="inner relative max-w-7xl mx-auto px-5 md:px-10 py-4 text-[12px] text-gray-400 md:px-4 md:py-9 md:text-[12px]"
> >
<div class="menu-area"> <div class="menu-area py-4 pb-4">
<ul class="flex items-center flex-wrap md:gap-6"> <ul class="flex items-center flex-wrap gap-x-6 gap-y-2">
<li <li
v-for="(footerMenuItem, index) in footerLinks" v-for="(footerMenuItem, index) in footerLinks"
:key="index" :key="index"
class="text-sm md:text-[11px] md:tracking-[-0.5px] relative flex items-center" class="text-[15px] text-white/50 md:tracking-[-0.5px] relative flex items-center"
> >
<NuxtLink <NuxtLink
:to="footerMenuItem.url" :to="footerMenuItem.url"
:target="footerMenuItem.target" :target="footerMenuItem.target"
:class="[ :class="[
footerMenuItem.active === 'y' && 'text-[#e04600]', footerMenuItem.active === 'y' && 'text-white/50',
index === 2 && 'text-[#fff]', index === 2 && 'text-[#fff]',
'hover:text-gray-600 transition-colors', 'hover:text-gray-600 transition-colors',
]" ]"
@@ -23,13 +23,13 @@
</NuxtLink> </NuxtLink>
</li> </li>
<li class="relative"> <li class="relative">
<button class="hover:text-gray-600 transition-colors" @click="toggleAgeRating"> <button class="text-[15px] text-white/50 hover:text-gray-600 transition-colors" @click="toggleAgeRating">
{{ footerAgeRating }} {{ tm('Footer_AgeRating') }}
</button> </button>
<div v-if="showAgeRating" class="game-rating-card absolute bottom-6 left-1/2 -translate-x-1/2 bg-[#383838] rounded-lg w-[340px] mx-auto z-10"> <div v-if="showAgeRating" class="game-rating-card absolute bottom-6 left-1 md:left-1/2 md:-translate-x-1/2 bg-[#383838] rounded-lg border border-white/30 w-[340px] mx-auto z-10">
<!-- 헤더 --> <!-- 헤더 -->
<div class="px-6 py-4 rounded-t-lg flex justify-between items-center"> <div class="px-6 py-4 rounded-t-lg flex justify-between items-center">
<h3 class="text-white text-base">{{ footerAgeRating }}</h3> <h3 class="text-white text-base">{{ tm('Footer_AgeRating') }}</h3>
<button class="text-white hover:text-gray-300 transition-colors" @click="toggleAgeRating"> <button class="text-white hover:text-gray-300 transition-colors" @click="toggleAgeRating">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
@@ -81,23 +81,27 @@
</ul> </ul>
</div> </div>
<div class="address-area mt-6"> <div class="address-area mt-4 hidden sm:block">
<address class="not-italic text-gray-500"> <address class="not-italic text-white/50">
<div class="row my-1.5 leading-5"> <div class="row my-1.5 leading-5">
<span <span
v-dompurify-html="footerAddress" v-dompurify-html="tm('Footer_Address')"
class="[&_a]:cursor-pointer [&_a]:text-blue-500 [&_a]:underline" class="text-[13px] [&_a]:cursor-pointer [&_a]:text-white/50 [&_a]:underline"
></span> ></span>
</div> </div>
</address> </address>
</div> </div>
<div class="mt-6 md:mt-6"> <div class="language-area static md:absolute bottom-7 right-10 text-white mt-5 md:mt-0 md:bottom-5.5 md:right-4">
<div class="text-xs text-white/30">{{ footerCaution }}</div> <BlocksLanguageSwitcher />
</div> </div>
<div class="copyright-area mt-6 text-gray-500 md:mt-4"> <div class="mt-6 md:mt-6 hidden sm:block">
<span>&copy; Smilegate. All rights reserved</span> <div v-dompurify-html="tm('Footer_caution')" class="text-xs text-white/30"></div>
</div>
<div class="copyright-area mt-6 text-[13px] text-white/50 md:mt-4">
<span>{{ tm('Footer_Copyright') }}</span>
</div> </div>
<div class="logo-area flex mt-6 md:mt-6"> <div class="logo-area flex mt-6 md:mt-6">
@@ -107,33 +111,26 @@
class="smilegate" class="smilegate"
> >
<img <img
src="https://static-pubcomm.gate8.com/local/template/l9/common/logo_smilegate.png" :src="getImageHost(`/images/common/logo_smilegate.png`, { imageType: 'common' })"
alt="스마일게이트 로고" alt="스마일게이트 로고"
class="w-auto h-auto" class="w-auto h-auto"
/> />
</a> </a>
<a <a
v-if="setDevCi.dev_ci_yn" v-if="setDevCi.dev_ci_yn"
:href="footerData.use_dev_ci_url ? setDevCi.dev_ci_img_path : '#'" :href="footerData.use_dev_ci_url ? footerData.dev_ci_url : '#'"
target="_blank" target="_blank"
class="nx3 ml-2.5 md:ml-4" class="nx3 ml-2.5 md:ml-4"
> >
<img <img
:src="`${staticUrl}${setDevCi.dev_ci_img_path}`" :src="getImageHost(`${setDevCi.dev_ci_img_path}`, { imageType: 'game' })"
alt="CI" alt="CI"
class="w-auto h-[24px]" class="block w-auto h-[22px]"
/> />
</a> </a>
</div> </div>
<div
class="language-area absolute bottom-7 right-10 text-white md:bottom-5.5 md:right-4"
>
<BlocksLanguageSwitcher />
<!-- <SelectLanguage /> -->
<!-- <AtomsLanguageSwitcher /> -->
</div>
</div> </div>
</footer> </footer>
</template> </template>
@@ -141,20 +138,20 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FooterMenuItem, FooterData, DevCiConfig } from '#layers/types/Common' import type { FooterMenuItem, FooterData, DevCiConfig } from '#layers/types/Common'
const config = useRuntimeConfig() // Configuration
const staticUrl = config.public.staticUrl const runtimeConfig = useRuntimeConfig()
const runType = config.public.runType const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const multilingualFileName = 'test_common_template.json'
const translationApi = `${runType}/test/multilingual/test_common_template.json` // Multilingual
const resultGetMultilingual = await useGetMultilingual({
const result = await useApiData({ baseApiUrl: staticUrl, url: translationApi }) baseApiUrl: dataResourcesUrl,
fileName: multilingualFileName,
const { tm } = useI18n({ })
useScope: 'local', const { tm }: any = useI18n({
messages: result useScope: 'local',
messages: Object(resultGetMultilingual?.value?.multilingual),
}) })
// const { tm } = useI18n()
const gameDataStore = useGameDataStore() const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore) const { gameData } = storeToRefs(gameDataStore)
@@ -174,22 +171,16 @@ const setDevCi = ref<DevCiConfig>({
///local/template/common/grades_age ///local/template/common/grades_age
const getGameRatingImage = computed((): string[] => { const getGameRatingImage = computed((): string[] => {
const contentInfo = footerData.value.game_rating_info.rating_type.split(',') const contentInfo = footerData.value.game_rating_info.rating_type.split(',')
// rating_type 12, 15, 18, 19 에 따라 이미지명을 가져오고 이미지를 반환 const ageTypeMap: Record<string, string> = {
'12': 'Type12',
'15': 'Type15',
'19': 'Type19',
'all': 'TypeAll',
'e': 'TypeExempt'
}
return contentInfo.map(item => { return contentInfo.map(item => {
switch (item) { const type = ageTypeMap[item] || 'TypeTest'
case '12': return getImageHost(`/images/common/grades_age/${type}.svg`, { imageType: 'common' })
return getImageHost('/images/common/grades_age/Type12.svg', { imageType: 'common' })
case '15':
return getImageHost('/images/common/grades_age/Type15.svg', { imageType: 'common' })
case '19':
return getImageHost('/images/common/grades_age/Type19.svg', { imageType: 'common' })
case 'all':
return getImageHost('/images/common/grades_age/TypeAll.svg', { imageType: 'common' })
case 'e':
return getImageHost('/images/common/grades_age/TypeExempt.svg', { imageType: 'common' })
default:
return getImageHost('/images/common/grades_age/TypeTest.svg', { imageType: 'common' })
}
}) })
}) })
@@ -197,24 +188,20 @@ const getContentInfoImage = computed((): string[] => {
const contentInfo = footerData.value.game_rating_info.content_info.split(',') const contentInfo = footerData.value.game_rating_info.content_info.split(',')
contentInfo.pop() contentInfo.pop()
const contentTypeMap: Record<string, string> = {
'1': 'Type-sexual',
'2': 'Type-violence',
'3': 'Type-fear',
'4': 'Type-inapposite',
'5': 'Type-drug',
'6': 'Type-crime',
'7': 'Type-speculation',
}
return contentInfo.map(item => { return contentInfo.map(item => {
switch (item) { const type = contentTypeMap[item]
case '1': return type ? getImageHost(`/images/common/grades_use/${type}.svg`, { imageType: 'common' }) : ''
return getImageHost('/images/common/grades_use/Type-sexual.svg', { imageType: 'common' }) }).filter(Boolean)
case '2':
return getImageHost('/images/common/grades_use/Type-fear.svg', { imageType: 'common' })
case '3':
return getImageHost('/images/common/grades_use/Type-inapposite.svg', { imageType: 'common' })
case '4':
return getImageHost('/images/common/grades_use/Type-drug.svg', { imageType: 'common' })
case '5':
return getImageHost('/images/common/grades_use/Type-crime.svg', { imageType: 'common' })
case '6':
return getImageHost('/images/common/grades_use/Type-speculation.svg', { imageType: 'common' })
case '7':
return getImageHost('/images/common/grades_use/Type-violence.svg', { imageType: 'common' })
}
})
}) })
const showAgeRating = ref<boolean>(false) const showAgeRating = ref<boolean>(false)
@@ -222,37 +209,12 @@ const toggleAgeRating = (): void => {
showAgeRating.value = !showAgeRating.value showAgeRating.value = !showAgeRating.value
} }
// 템플릿에서 사용할 다국어 텍스트들
const footerAgeRating = computed((): string => {
const text = (tm as any)('Footer_AgeRating')
return typeof text === 'string' ? text : ''
})
const footerAgeRatingInfo = computed((): string[] => { const footerAgeRatingInfo = computed((): string[] => {
const info = (tm as any)('Footer_AgeRating_Info') const info = (tm as any)('Footer_AgeRating_Info')
return Array.isArray(info) ? info : [] return Array.isArray(info) ? info : []
}) })
const footerAddress = computed((): string => {
const address = (tm as any)('Footer_Address')
return typeof address === 'string' ? address : ''
})
const footerCaution = computed((): string => {
const caution = (tm as any)('Footer_caution')
return typeof caution === 'string' ? caution : ''
})
</script> </script>
<style scoped> <style scoped>
/* 태국어 폰트 크기 조정 */
@media (max-width: 411px) {
:global(.lang-th) .menu-area li {
font-size: 10px;
}
}
@media (max-width: 321px) {
:global(.lang-th) .menu-area li {
font-size: 9px;
}
}
</style> </style>

View File

@@ -1,22 +0,0 @@
interface ReqApiData {
baseApiUrl: string
url: string
}
export const useApiData = async (req: ReqApiData): Promise<any> => {
const dataUrl = `${req.baseApiUrl}/${req.url}` // 정상 URL 경로
try {
const fetch = await $fetch<any>(dataUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
return fetch
} catch (error) {
console.log('error', error)
return []
}
}

View File

@@ -15,8 +15,8 @@ export const useGetGameDataExternal = () => {
console.log('🚀 ~ getGameDataExternal ~ req:', req) console.log('🚀 ~ getGameDataExternal ~ req:', req)
// const config = useRuntimeConfig() // const config = useRuntimeConfig()
const config = useRuntimeConfig() const config = useRuntimeConfig()
const stoveApiUrl = `${config.public.stoveApiUrl}` const stoveApiBaseUrl = config.public.stoveApiUrl
const apiUrl = `${stoveApiUrl}/pub-comm/v1.0/template/game?game_domain=${req.gameDomain}&lang_code=${req.langCode}` const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/game?game_domain=${req.gameDomain}&lang_code=${req.langCode}`
try { try {
const response = (await commonFetch('GET', apiUrl)) as GameDataResponse const response = (await commonFetch('GET', apiUrl)) as GameDataResponse

View File

@@ -11,6 +11,7 @@ import GrBoard01 from '#layers/templates/GrBoard01/index.vue'
import GrContents01 from '#layers/templates/GrContents01/index.vue' import GrContents01 from '#layers/templates/GrContents01/index.vue'
import FxVideo01 from '#layers/templates/FxVideo01/index.vue' import FxVideo01 from '#layers/templates/FxVideo01/index.vue'
import FxDownload01 from '#layers/templates/FxDownload01/index.vue' import FxDownload01 from '#layers/templates/FxDownload01/index.vue'
import FxSecure01 from '#layers/templates/FxSecure01/index.vue'
import FxPreregist01 from '#layers/templates/FxPreregist01/index.vue' import FxPreregist01 from '#layers/templates/FxPreregist01/index.vue'
const templateRegistry = { const templateRegistry = {
@@ -27,6 +28,7 @@ const templateRegistry = {
GR_CONTENTS_01: { component: GrContents01 }, GR_CONTENTS_01: { component: GrContents01 },
FX_VIDEO_01: { component: FxVideo01 }, FX_VIDEO_01: { component: FxVideo01 },
FX_DOWNLOAD_01: { component: FxDownload01 }, FX_DOWNLOAD_01: { component: FxDownload01 },
FX_SECURE_01: { component: FxSecure01 },
FX_PREREGIST_01: { component: FxPreregist01 }, FX_PREREGIST_01: { component: FxPreregist01 },
} as const } as const

View File

@@ -26,6 +26,7 @@ export const useTokenValidation = () => {
const showLoginModal = (alertKey: string) => { const showLoginModal = (alertKey: string) => {
modalStore.handleOpenConfirm({ modalStore.handleOpenConfirm({
contentText: tm(alertKey), contentText: tm(alertKey),
isShowDimmed: true,
confirmButtonText: tm('Text_StoveLogin'), confirmButtonText: tm('Text_StoveLogin'),
modalName: 'modal-login', modalName: 'modal-login',
confirmButtonEvent: () => { confirmButtonEvent: () => {

View File

@@ -7,8 +7,8 @@ export default defineNuxtRouteMiddleware(async (to) => {
const config = useRuntimeConfig() const config = useRuntimeConfig()
// const baseDomain = `${config.public.baseDomain}` // const baseDomain = `${config.public.baseDomain}`
const stoveApiUrl = `${config.public.stoveApiUrl}` const stoveApiBaseUrl = config.public.stoveApiUrl
const stoveGameId = `${gameData.value.game_id}` const stoveGameId = gameData.value.game_id
// const stoveMaintenanceApiUrl = `${config.public.stoveMaintenanceApiUrl}` // const stoveMaintenanceApiUrl = `${config.public.stoveMaintenanceApiUrl}`
// const localeCookie = useCookie('LOCALE', { // const localeCookie = useCookie('LOCALE', {
@@ -20,7 +20,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
// 웹 점검 ----- // 웹 점검 -----
const { isWebInspection, getInspectionDataExternal } = useGetInspectionDataExternal() const { isWebInspection, getInspectionDataExternal } = useGetInspectionDataExternal()
await getInspectionDataExternal({ baseApiUrl: stoveApiUrl, gameId: stoveGameId }) await getInspectionDataExternal({ baseApiUrl: stoveApiBaseUrl, gameId: stoveGameId })
// 게임 점검 ----- // 게임 점검 -----

View File

@@ -3,17 +3,25 @@ import { usePageDataStore } from '#layers/stores/usePageDataStore'
import { useGetGameDomain } from '#layers/composables/useGetGameDomain' import { useGetGameDomain } from '#layers/composables/useGetGameDomain'
import { usePathResolver } from '#layers/composables/usePathResolver' import { usePathResolver } from '#layers/composables/usePathResolver'
import type { PageDataResponse } from '#layers/types/api/pageData' import type { PageDataResponse } from '#layers/types/api/pageData'
import type {
GameDataValue,
} from '#layers/types/api/gameData'
export default defineNuxtRouteMiddleware(async (to, _from) => { export default defineNuxtRouteMiddleware(async (to, _from) => {
if (!import.meta.client) return
const config = useRuntimeConfig() const config = useRuntimeConfig()
const stoveApiBaseUrl = config.public.stoveApiUrl
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
const store = usePageDataStore() const store = usePageDataStore()
const gameDomain = useGetGameDomain() const gameDomain = useGetGameDomain()
const { getPathAfterLanguage } = usePathResolver() const { getPathAfterLanguage } = usePathResolver()
const headers = useRequestHeaders() const headers = useRequestHeaders()
const langCode = ssrGetFinalLocale(to.path, headers) const gameDataStore = useGameDataStore()
const gameData = gameDataStore.gameData as GameDataValue
const stoveApiBaseUrl = config.public.stoveApiUrl const langCode = ssrGetFinalLocale(to.path, headers, gameData?.lang_codes, gameData?.default_lang_code)
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
try { try {
if (to.path.includes('inspection')) { if (to.path.includes('inspection')) {

View File

@@ -3,7 +3,6 @@ import {
getHeader, getHeader,
getRequestHost, getRequestHost,
defineEventHandler, defineEventHandler,
getRequestURL,
} from 'h3' } from 'h3'
import { ssrGetFinalLocale } from '../../utils/localeUtil' import { ssrGetFinalLocale } from '../../utils/localeUtil'
import type { GameDataResponse } from '../../types/api/gameData' import type { GameDataResponse } from '../../types/api/gameData'
@@ -123,10 +122,7 @@ function fnLocaleMiddleware(event: any, finalLocale: string) {
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const config = useRuntimeConfig()
// const runType = `${config.public.runType}` // const runType = `${config.public.runType}`
const iBaseApiUrl = `${config.public.stoveApiUrlServer}`
const baseDomain = `${config.public.baseDomain}`
// console.log("🚀 ~ baseDomain:", config.public.baseDomain) // console.log("🚀 ~ baseDomain:", config.public.baseDomain)
// const url = getRequestURL(event) // const url = getRequestURL(event)
@@ -147,6 +143,45 @@ export default defineEventHandler(async event => {
// } // }
// } // }
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`
const 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 const fullPath = event.path
// 1-1. 정적 파일 패스 // 1-1. 정적 파일 패스
@@ -157,8 +192,8 @@ export default defineEventHandler(async event => {
// 1-2. /inspection 패스 // 1-2. /inspection 패스
if (fullPath.includes('/inspection')) { if (fullPath.includes('/inspection')) {
// 리턴 되기 전 언어 쿠키 세팅 // 리턴 되기 전 언어 쿠키 세팅
// const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers) finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers, initLangCodes, initDefaultLocale)
// setFinalLocaleCookie(event, finalLocale, baseDomain) setFinalLocaleCookie(event, finalLocale, baseDomain)
return return
} }
@@ -178,30 +213,16 @@ export default defineEventHandler(async event => {
const cacheKey = 'inspection' const cacheKey = 'inspection'
// console.log("🚀 11111 ~ cacheKey:", cacheKey) // console.log("🚀 11111 ~ cacheKey:", cacheKey)
const host =
(getHeader(event, 'host') || getRequestHost(event)).toString() || ''
const isGameDomainExtractable = host.includes(baseDomain)
if (isGameDomainExtractable) {
const cleanHost = host.split(':')[0]
event.context.gameDomain = cleanHost
}
// gameData를 직접 가져와서 context에 저장 (API 호출 없이) // gameData를 직접 가져와서 context에 저장 (API 호출 없이)
try { try {
const config = useRuntimeConfig()
const stoveApiUrlServer = config.public.stoveApiUrlServer
const apiUrl = `${stoveApiUrlServer}/pub-comm/v1.0/template/game`
// console.log("🚀 ~ apiUrl:", apiUrl)
// 2. 언어 코드 추출 // 2. 언어 코드 추출
// const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers) finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers, initLangCodes, initDefaultLocale)
console.log("🚀 222 finalLocale:", finalLocale)
const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
const queryParams: Record<string, string> = { const queryParams: Record<string, string> = {
game_domain: event.context.gameDomain || '', game_domain: cleanHost || '',
lang_code: finalLocale, lang_code: finalLocale,
} }
const response = (await $fetch(apiUrl, { const response = (await $fetch(apiUrl, {
@@ -209,10 +230,9 @@ export default defineEventHandler(async event => {
})) as GameDataResponse | null })) as GameDataResponse | null
// 언어패스 쿠키 굽기 - 장기방안에서는 굽지않음 // 언어패스 쿠키 굽기 - 장기방안에서는 굽지않음
// const langCoverages = response?.value?.lang_codes || [] if(initLangCodes?.includes(finalLocale)) {
// if(langCoverages.includes(finalLocale)) { setFinalLocaleCookie(event, finalLocale, baseDomain)
// setFinalLocaleCookie(event, finalLocale, baseDomain) }
// }

View File

@@ -0,0 +1,272 @@
<script setup lang="ts">
import { getComponentGroup } from '#layers/utils/dataUtil'
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
import { getImageHost } from '#layers/utils/styleUtil'
// Props
interface Props {
components: PageDataTemplateComponents
pageVerTmplSeq: number
}
const props = defineProps<Props>()
const { handleTokenValidation } = useTokenValidation()
// Configuration
const config = useRuntimeConfig()
const dataResourcesUrl = config.public.dataResourcesUrl as string
const multilingualFileName = 'test_homepage_brand_secure.json'
const stoveApiBaseUrl = config.public.stoveApiUrl
// Multilingual
const resultGetMultilingual = await useGetMultilingual({
baseApiUrl: dataResourcesUrl,
fileName: multilingualFileName,
})
const { tm }: any = useI18n({
useScope: 'local',
messages: Object(resultGetMultilingual?.value?.multilingual),
})
const isLogin = ref(false)
const secureSetting = ref({
otpLoginYn: 'N',
abroadLoginBlockYn: 'N',
pcRegisterYn: 'N'
})
// 회원 보안 설정 설정
const handleSecureSetting = (url: string) => {
window.open(url, '_blank')
}
// 로그인 유효성 체크
const checkLoginValidation = async () => {
const accessToken = useCookie('SUAT')
const validateTokenResult = await handleTokenValidation(
accessToken.value || ''
)
isLogin.value = validateTokenResult
}
// 회원 보안 설정 조회
const fnGetSecuritySetting = async () => {
const accessToken = useCookie('SUAT')
checkLoginValidation()
const apiBase = `${stoveApiBaseUrl}/auth-secure/v1.0`
const headers = {
Authorization: `Bearer ${accessToken.value}`,
'Content-Type': 'application/json;charset=UTF-8'
}
try {
const result = await commonFetch('GET', `${apiBase}/security/setting`, { headers })
if (result?.code === 0 && Array.isArray(result.value)) {
const arrSecure = result.value
const getValue = (key: string) => arrSecure.find((f: any) => f.key === key)?.value ?? 'N'
secureSetting.value = {
otpLoginYn: getValue('OTP_LOGIN_YN'),
abroadLoginBlockYn: getValue('ABROAD_LOGIN_BLOCK_YN'),
pcRegisterYn: getValue('PC_REGISTER_YN')
}
}
} catch (e) {
console.error(e)
}
}
// Data
const backgroundData = computed(() =>
getComponentGroup(props.components, 'background')
)
// Computed
const secureCards = computed(() => {
return [
{
id: 'SECURE_CARD_0',
title: tm('Secure_Stove_otp') || '스토브 인증기 (OTP)',
description: tm('Secure_Stove_otp_desc') || '스토브 앱으로 인증 후 안전하게 로그인하세요.',
status: secureSetting.value.otpLoginYn,
benefitTitle: tm('Secure_Stove_otp_benefits') || '스토브 OTP 혜택',
benefitDesc: tm('Secure_Defense_bonus_10') || '방어력 +10',
benefitIcon: '/images/common/img_OTP.png',
buttonDisabled: false,
url: tm('Secure_OtpLogin_Url'),
},
{
id: 'SECURE_CARD_1',
title: tm('Secure_Block_foreign_login') || '해외 로그인 차단',
description: tm('Secure_Block_foreign_login_desc') || '접속 국가를 제한하여 의심 로그인을 차단해요.',
status: secureSetting.value.abroadLoginBlockYn,
benefitTitle: '',
benefitDesc: '',
benefitIcon: '',
buttonDisabled: true,
url: tm('Secure_AbroadLogin_Url'),
},
{
id: 'SECURE_CARD_2',
title: tm('Secure_Trusted_pc_management') || '지정 PC 관리',
description: tm('Secure_Trusted_pc_desc') || '지정 PC에서만 로그인할 수 있게 설정해 보세요.',
status: secureSetting.value.pcRegisterYn,
benefitTitle: '',
benefitDesc: '',
benefitIcon: '',
buttonDisabled: false,
url: tm('Secure_PcRegister_Url'),
},
]
})
// 유의사항 내용 다국어 조회
const cautionText = computed(() => {
return tm('Secure_Notice_Content') || []
})
onMounted(() => {
fnGetSecuritySetting()
})
</script>
<template>
<WidgetsFixMainTitle
:title="tm('Secure_Page_Title') || '보안 강화 캠페인'"
:resources-data="backgroundData"
class="mx-auto"
/>
<div class="section-container static">
<section class="section-secure bg-[#F0F0F0] pb-50">
<div class="section-static content-standa md:max-w-[1300px] mx-auto">
<!-- Title Section -->
<div class="flex flex-col md:flex-row w-full md:items-end justify-between gap-5 mb-6">
<h3 class="text-[#1F1F1F] text-2xl font-bold leading-8 tracking-[-0.72px]">
{{ tm('Secure_Section_Title') || '보안 서비스' }}
</h3>
<p class="text-gray-500 text-[14px] font-[400] leading-[24px] tracking-[-0.42px] text-left md:text-right">
{{ tm('Secure_Section_Description') || '*OTP / 해외 로그인 차단 / 지정 PC 관리 설정하고, 로드나인 계정을 보다 안전하게 보호하세요.' }}
</p>
</div>
<!-- Secure Cards -->
<div class="grid grid-cols-1 sm:grid-cols-3 w-full gap-3 md:gap-5 mb-6">
<div
v-for="card in secureCards"
:key="card.id"
class="flex-1 min-h-[308px] md:min-h-[384px] p-[10px] md:p-4 bg-[#FFFFFF] rounded-2xl flex flex-col gap-3 transition-all duration-300 ease-in-out"
>
<!-- Card Content -->
<div class="flex-1 p-[10px] md:p-8 flex flex-col gap-[8px] md:gap-3 text-left">
<!-- Badge -->
<div class="inline-flex">
<span
:class="[
'px-1.5 md:px-2 py-0.5 md:py-1 rounded-full text-[12px] md:text-[14px] font-medium leading-5',
card.status === 'Y'
? 'bg-[#E2EAFF] text-[#3C75FF]'
: 'bg-[#EBEBEB] text-[#999999]',
]"
>
{{ card.status === 'Y' ? tm('Secure_Enabled') : tm('Secure_Disabled') }}
</span>
</div>
<!-- Title -->
<h4 class="text-[#1F1F1F] text-[18px] md:text-[24px] font-bold leading-[26px] md:leading-[34px] tracking-[-0.54px] md:tracking-[-0.72px]">
{{ card.title }}
</h4>
<!-- Description -->
<p class="flex-1 text-[#999999] text-[14px] md:text-base font-[400] leading-[22px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.48px]">
{{ card.description }}
</p>
</div>
<!-- Benefit Section -->
<div
:class="[
'self-stretch p-[10px] md:p-4 rounded-2xl flex flex-col gap-4',
card.benefitTitle ? 'bg-[#F0F4FF]' : '',
]"
>
<!-- Benefit Info -->
<div
v-if="card.benefitTitle"
class="flex items-center gap-[12px]"
>
<div
v-if="card.benefitIcon"
class="w-[48px] h-[48px] bg-[#3C75FF] rounded-[8px] flex items-center justify-center"
>
<img
:src="getImageHost(card.benefitIcon, { imageType: 'common' })"
:alt="card.benefitTitle"
class="w-[48px] h-[48px] object-contain rounded-2xl"
loading="lazy"
draggable="false"
/>
</div>
<div class="flex-1 flex flex-col text-left">
<div class="text-[#3C75FF] text-[14px] md:text-[18px] font-bold leading-[22px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.54px]">
{{ card.benefitTitle }}
</div>
<div
v-if="card.benefitDesc"
class="text-[#3C75FF] text-[12px] md:text-[13px] font-[400] leading-[18px] md:leading-[22px] tracking-[-0.325px] opacity-90"
>
{{ card.benefitDesc }}
</div>
</div>
</div>
<!-- Button -->
<AtomsButton
v-if="card.status === 'N'"
type="external"
button-size="size-small md:size-large"
background-color="#000000"
text-color="#FFFFFF"
@click="isLogin ? handleSecureSetting(card.url) : checkLoginValidation()"
>
<span>{{ tm('Secure_Action_setup') }}</span>
</AtomsButton>
<AtomsButton
v-else
type="action"
button-size="size-small md:size-large"
background-color="#EBEBEB"
text-color="#999999"
disabled
>
<span>{{ tm('Secure_Action_complete') }}</span>
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.4298 2.80644L6.84645 0.240655C7.52385 -0.0802185 8.30948 -0.0802184 8.98688 0.240655L14.4035 2.80645C15.2767 3.22003 15.8333 4.09952 15.8333 5.06564V7.65038C15.8333 13.399 10.6191 16.1288 8.65401 16.9535C8.18024 17.1523 7.6531 17.1523 7.17932 16.9535C5.21423 16.1288 -0.000131724 13.399 2.49573e-09 7.65038L1.11287e-05 5.06566C6.95637e-06 4.09953 0.556675 3.22002 1.4298 2.80644ZM11.4226 7.4063C11.748 7.08086 11.748 6.55323 11.4226 6.22779C11.0972 5.90235 10.5695 5.90235 10.2441 6.22779L7.5 8.97187L6.00592 7.47779C5.68049 7.15235 5.15285 7.15235 4.82741 7.47779C4.50197 7.80323 4.50197 8.33086 4.82741 8.6563L6.91074 10.7396C7.23618 11.0651 7.76382 11.0651 8.08926 10.7396L11.4226 7.4063Z" fill="#999999"/>
</svg>
</AtomsButton>
</div>
</div>
</div>
<!-- Caution Section -->
<div class="self-stretch p-8 bg-[#FAFAFA] rounded-2xl flex flex-col gap-3 text-left">
<h5 class="text-[#333333] text-[20px] font-bold leading-[30px] tracking-[-0.6px]">
{{ tm('Secure_Notice') }}
</h5>
<ul class="relative flex flex-col items-start justify-start w-full">
<li
v-for="caution in cautionText"
:key="caution"
v-dompurify-html="caution"
class="relative pl-[22px] before:content-[''] before:absolute before:top-[10px] before:left-[9px] before:w-[3px] before:h-[3px] before:rounded-full before:bg-[#999999] text-[#999999] text-[14px] font-[400] leading-[24px] tracking-[-0.42px]"
>
</li>
</ul>
</div>
</div>
</section>
</div>
</template>

View File

@@ -38,6 +38,11 @@ export const csrGetFinalLocale = (path = '', coveragesLocales: string[]) => {
let finalLocale = DEFAULT_LOCALE_CODE // 기본값 설정 let finalLocale = DEFAULT_LOCALE_CODE // 기본값 설정
// coveragesLocales가 빈 배열이거나 유효하지 않은 경우 기본 언어 반환
if (!coveragesLocales || !Array.isArray(coveragesLocales) || coveragesLocales.length === 0) {
return finalLocale
}
// 1. URL 패스에 포함된 언어 // 1. URL 패스에 포함된 언어
if (path && path !== '' && path.split('/').length > 1) { if (path && path !== '' && path.split('/').length > 1) {
// 쿼리스트링 제거한 순수 path 검사 // 쿼리스트링 제거한 순수 path 검사
@@ -80,9 +85,14 @@ export const csrGetFinalLocale = (path = '', coveragesLocales: string[]) => {
* @param {string} path - 현재 URL 경로 * @param {string} path - 현재 URL 경로
* @param {any} headers - 요청 헤더 * @param {any} headers - 요청 헤더
*/ */
export const ssrGetFinalLocale = (path = '', headers: any) => { export const ssrGetFinalLocale = (path = '', headers: any, coveragesLocales: string[], defaultLocale: string) => {
let finalLocale = DEFAULT_LOCALE_CODE // 기본값 설정 let finalLocale = defaultLocale || DEFAULT_LOCALE_CODE // 기본값 설정
try { try {
// coveragesLocales가 빈 배열이거나 유효하지 않은 경우 기본 언어 반환
if (!coveragesLocales || !Array.isArray(coveragesLocales) || coveragesLocales.length === 0) {
return finalLocale
}
// 1. URL path에 포함된 언어 정보 // 1. URL path에 포함된 언어 정보
if (path && path !== '' && path.split('/').length > 1) { if (path && path !== '' && path.split('/').length > 1) {
// 쿼리스트링 제거한 순수 path 검사 // 쿼리스트링 제거한 순수 path 검사
@@ -91,7 +101,7 @@ export const ssrGetFinalLocale = (path = '', headers: any) => {
} }
const pathLocalee = `${path.split('/')[1]}`.toLowerCase() const pathLocalee = `${path.split('/')[1]}`.toLowerCase()
// URL path에 포함된 언어 정보가 지원하는 언어인지 체크 // URL path에 포함된 언어 정보가 지원하는 언어인지 체크
if (pathLocalee && pathLocalee !== '') { if (pathLocalee && pathLocalee !== '' && coveragesLocales.includes(pathLocalee)) {
finalLocale = pathLocalee finalLocale = pathLocalee
return finalLocale return finalLocale
} }
@@ -101,7 +111,7 @@ export const ssrGetFinalLocale = (path = '', headers: any) => {
const cookieHeader = headers.cookie || '' const cookieHeader = headers.cookie || ''
const cookies = parseCookies(cookieHeader) const cookies = parseCookies(cookieHeader)
const cookieLanguage = cookies.LOCALE ? `${cookies.LOCALE}`.toLowerCase() : '' const cookieLanguage = cookies.LOCALE ? `${cookies.LOCALE}`.toLowerCase() : ''
if (cookieLanguage && cookieLanguage !== '') { if (cookieLanguage && cookieLanguage !== '' && coveragesLocales.includes(cookieLanguage)) {
finalLocale = cookieLanguage finalLocale = cookieLanguage
return finalLocale return finalLocale
} }
@@ -116,21 +126,21 @@ export const ssrGetFinalLocale = (path = '', headers: any) => {
const preferredLocale = getPreferredLanguage(acceptLanguage) const preferredLocale = getPreferredLanguage(acceptLanguage)
if (preferredLocale) { if (preferredLocale) {
// 선호 언어의 기본 코드와 일치하는 지원 로케일 찾기 // 선호 언어의 기본 코드와 일치하는 지원 로케일 찾기
// const matchedLocale = coveragesLocales.find((locale: string) => const matchedLocale = coveragesLocales.find((locale: string) =>
// preferredLocale.toLowerCase().startsWith(locale.toLowerCase()) preferredLocale.toLowerCase().startsWith(locale.toLowerCase())
// ) )
// if (matchedLocale) { if (matchedLocale) {
// finalLocale = matchedLocale.toLowerCase() finalLocale = matchedLocale.toLowerCase()
// return finalLocale return finalLocale
// } }
} }
} }
} }
// 3. 서비스 기본 언어 // 3. 서비스 기본 언어
finalLocale = DEFAULT_LOCALE_CODE finalLocale = defaultLocale
} catch (e) { } catch {
finalLocale = DEFAULT_LOCALE_CODE finalLocale = defaultLocale
} }
return finalLocale return finalLocale
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,286 +0,0 @@
import { LRUCache } from 'lru-cache'
// import { DEFAULT_LOCALE_COVERAGES } from '@/i18n.config'
import { getTrueClientIp } from '#layers/utils/apiUtil'
import { ssrGetFinalLocale } from '#layers/utils/localeUtil'
import type { ResGetInspectionData, WebInspectionData } from '#layers/types/InspectionType'
import { isStaticFile } from '#layers/utils/commonUtil'
console.log("🚀 ~ setCacheHeaders ~ event.node.res.setHeader:")
/**
* 캐시 제어 헤더를 설정하는 공통 함수
*
* @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)
}
// 바로 캐시 제어 헤더 적용
}
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 gameId = `${event.context.gameData?.game_id}`
const baseDomain = `${config.public.baseDomain}`
if (['local', 'local-gate8', 'dev'].includes(runType)) {
// Sandbox 이상 환경에서만 동작 및 확인 가능 (local, dev는 통과 처리)
try {
// 언어 코드 추출
const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
setFinalLocaleCookie(event, finalLocale, baseDomain)
// -------------------------------------------------------------------------------
// [Locale Middleware]
// -------------------------------------------------------------------------------
fnLocaleMiddleware(event, finalLocale)
} catch (e) {
console.error('[Exception] /server/middleware/middleware-global: ', e)
}
} else {
// -------------------------------------------------------------------------------
// [Inspection Middleware]
// -------------------------------------------------------------------------------
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)
setFinalLocaleCookie(event, finalLocale, baseDomain)
return
}
// 1-3. 특정 경로 패스 (API, 리소스)
if (
fullPath.startsWith('/api/') ||
fullPath.startsWith('/_nuxt/') ||
fullPath.includes('/assets/') ||
fullPath.includes('favicon')
) {
return
}
// 캐시 키 생성
const cacheKey = 'inspection'
try {
// 2. 언어 코드 추출
const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
setFinalLocaleCookie(event, finalLocale, baseDomain)
// 초기화
let inspectionData
// 3. 캐시된 데이터가 없거나 만료되었을 때만 API 호출
if (cache.has(cacheKey)) {
inspectionData = cache.get(cacheKey) as WebInspectionData
} else {
const apiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${gameId}`
// 직접 $fetch 사용 (composable 사용하지 않음)
const response = await $fetch<ResGetInspectionData>(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
inspectionData = response?.value?.inspection as WebInspectionData
console.log("🚀 00000 inspectionData:", inspectionData)
cache.set(cacheKey, 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 설정
* - 화이트 리스트 체크
*/
// 점검 url path 가 아닐 경우, 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인 경우 점검 페이지로 이동
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()
}
} 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 (e) {
console.error('[Exception] /server/middleware/middleware-02-global: ', e)
}
}
})