Merge commit '0e1f57942b476631c81ff3ee27505b89a24565dc' into feature/20251103_cl_FX_VIDEO_01
This commit is contained in:
2
.env.dev
2
.env.dev
@@ -13,7 +13,7 @@ STOVE_API_URL_SERVERL=https://i-api-dev.onstove.com
|
||||
STOVE_M_API_URL=https://maintenance.gate8.com
|
||||
|
||||
# STOVE - GNB
|
||||
STOVE_GNB=https://js-cdn-dev.onstove.com/libs/common-gnb/latest/cp-header.js
|
||||
STOVE_GNB=https://js-cdn-dev.onstove.com/libs/common-gnb/latest/stove-gnb.js
|
||||
|
||||
# STOVE - Client Download
|
||||
STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/launcher-pack.js
|
||||
|
||||
@@ -13,7 +13,7 @@ STOVE_API_URL_SERVERL=https://i-api.onstove.com
|
||||
STOVE_M_API_URL=https://maintenance.onstove.com
|
||||
|
||||
# STOVE - GNB
|
||||
STOVE_GNB=https://js-cdn.onstove.com/libs/common-gnb/latest/cp-header.js
|
||||
STOVE_GNB=https://js-cdn.onstove.com/libs/common-gnb/latest/stove-gnb.js
|
||||
|
||||
# STOVE - Client Download
|
||||
STOVE_LAUNCHER_SCRIPT=https://js-cdn.onstove.com/libs/stove-js-service/latest/launcher-pack.js
|
||||
|
||||
2
.env.qa
2
.env.qa
@@ -13,7 +13,7 @@ STOVE_API_URL_SERVERL=https://i-api.gate8.com
|
||||
STOVE_M_API_URL=https://maintenance.gate8.com
|
||||
|
||||
# STOVE - GNB
|
||||
STOVE_GNB=https://js-cdn-qa.onstove.com/libs/common-gnb/latest/cp-header.js
|
||||
STOVE_GNB=https://js-cdn-qa.onstove.com/libs/common-gnb/latest/stove-gnb.js
|
||||
|
||||
# STOVE - Client Download
|
||||
STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/launcher-pack.js
|
||||
|
||||
@@ -13,7 +13,7 @@ STOVE_API_URL_SERVERL=https://i-api.gate8.com
|
||||
STOVE_M_API_URL=https://maintenance.gate8.com
|
||||
|
||||
# STOVE - GNB
|
||||
STOVE_GNB=https://js-cdn.gate8.com/libs/common-gnb/latest/cp-header.js
|
||||
STOVE_GNB=https://js-cdn.gate8.com/libs/common-gnb/latest/stove-gnb.js
|
||||
|
||||
# STOVE - Client Download
|
||||
STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/launcher-pack.js
|
||||
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
@seed-next:registry=https://git.sginfra.net/api/v4/groups/4424/-/packages/npm/
|
||||
# @stove-ui:registry=https://git.sginfra.net/api/v4/projects/557/packages/npm/
|
||||
@@ -9,6 +9,7 @@ const currentLayout = computed(() => getLayoutType(pageData.value))
|
||||
|
||||
definePageMeta({
|
||||
layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화
|
||||
middleware: ['inspection']
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const currentLayout = computed(() => getLayoutType(pageData.value))
|
||||
|
||||
definePageMeta({
|
||||
layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화
|
||||
middleware: ['inspection']
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
95
app/pages/error.vue
Normal file
95
app/pages/error.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-[#191919]">
|
||||
<LayoutsStoveHeader />
|
||||
<div class="flex-1 flex items-center justify-center bg-[#F0F0F0] pt-[223px] pb-[223px] px-5 max-[374px]:px-5 md:px-[530px]">
|
||||
<div class="flex flex-col items-center gap-6 w-full max-w-[513px] max-[374px]:max-w-[335px] md:max-w-[860px] py-10 max-[374px]:gap-6 md:gap-6 md:py-20">
|
||||
<!-- Stove Logo -->
|
||||
<div class="flex items-center justify-center w-[123.2px] h-7">
|
||||
<img
|
||||
src="/images/common/logo-stove.svg"
|
||||
alt="Stove"
|
||||
class="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error Icon and Text -->
|
||||
<div class="flex flex-col items-center gap-4 w-full">
|
||||
<!-- Error Icon -->
|
||||
<div class="flex items-center justify-center">
|
||||
<img
|
||||
src="/images/common/img_error.png"
|
||||
alt="Error"
|
||||
class="w-40 h-40 md:w-60 md:h-60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error Text -->
|
||||
<div class="flex flex-col items-center gap-1 w-full">
|
||||
<h1 class="font-['Spoqa_Han_Sans_Neo',sans-serif] font-medium text-xl md:text-2xl leading-[1.5] md:leading-[1.4166666666666667] tracking-[-0.03em] text-center text-[#1F1F1F] m-0">
|
||||
{{ errorTitle }}
|
||||
</h1>
|
||||
<p class="font-['Spoqa_Han_Sans_Neo',sans-serif] font-normal text-sm md:text-base leading-[1.7142857142857142] md:leading-[1.625] tracking-[-0.03em] text-center text-[#666666] m-0">
|
||||
{{ errorDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Home Button -->
|
||||
<button
|
||||
class="flex items-center justify-center font-['Spoqa_Han_Sans_Neo',sans-serif] font-medium text-sm leading-[1.4285714285714286em] tracking-[-0.03em] text-white bg-[#FC4420] border border-black/10 rounded-lg px-10 h-12 cursor-pointer transition-all duration-300 backdrop-blur-[30px] shadow-[0px_0.9131946563720703px_1.4666459560394287px_0px_rgba(0,0,0,0.06),0px_2.194533586502075px_3.5245540142059326px_0px_rgba(0,0,0,0.09)] hover:bg-[#E03D1C] hover:shadow-[0px_0.9131946563720703px_1.4666459560394287px_0px_rgba(0,0,0,0.06),0px_2.194533586502075px_3.5245540142059326px_0px_rgba(0,0,0,0.09),0px_4.1321120262146px_6.636422634124756px_0px_rgba(0,0,0,0.12)] active:translate-y-px"
|
||||
@click="handleGoHome"
|
||||
>
|
||||
{{ homeButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NuxtError } from '#app'
|
||||
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
||||
import { useLocalePath } from '#i18n'
|
||||
|
||||
const props = defineProps({
|
||||
error: Object as () => NuxtError,
|
||||
})
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
const localePath = useLocalePath()
|
||||
|
||||
const gameName = computed(() => gameData.value?.game_name || '게임')
|
||||
const homeButtonText = computed(() => `${gameName.value} 홈페이지 가기`)
|
||||
|
||||
const handleGoHome = () => {
|
||||
const homePath = localePath('/')
|
||||
navigateTo(homePath)
|
||||
}
|
||||
|
||||
// 에러 상태 코드에 따른 메시지 설정
|
||||
const errorTitle = computed(() => {
|
||||
if (props.error?.statusCode === 404) {
|
||||
return '페이지를 찾을 수 없어요.'
|
||||
}
|
||||
return '페이지를 찾을 수 없어요.'
|
||||
})
|
||||
|
||||
const errorDescription = computed(() => {
|
||||
if (props.error?.statusCode === 404) {
|
||||
return '주소가 바뀌었거나 잘못 입력된 것 같아요. 주소를 다시 확인해 주세요.'
|
||||
}
|
||||
return '주소가 바뀌었거나 잘못 입력된 것 같아요. 주소를 다시 확인해 주세요.'
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
layout: 'only-stove'
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
/* :deep(.game-wrapper) {
|
||||
display: none !important;
|
||||
} */
|
||||
</style>
|
||||
@@ -7,6 +7,7 @@ const { pageData } = storeToRefs(pageDataStore)
|
||||
|
||||
const currentLayout = computed(() => getLayoutType(pageData.value))
|
||||
|
||||
|
||||
definePageMeta({
|
||||
layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화
|
||||
})
|
||||
|
||||
380
app/pages/inspection/index.vue
Normal file
380
app/pages/inspection/index.vue
Normal file
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<BlocksStoveGnbNew class="min-h-[48px]" />
|
||||
|
||||
</header>
|
||||
<section class="inspection-section">
|
||||
<clientOnly>
|
||||
<!-- 로고 -->
|
||||
<div class="inspection-logo">
|
||||
<img :src="logoImgUrl" alt="logo" class="w-full h-full object-contain" />
|
||||
</div>
|
||||
|
||||
<div class="inspection-content">
|
||||
<!-- 점검 메시지 -->
|
||||
|
||||
<h1 class="inspection-title text-lg md:text-2xl">
|
||||
{{ tm('Inspection_Now_Maintenance') }}
|
||||
</h1>
|
||||
|
||||
<div class="inspection-cards">
|
||||
<!-- 점검 시간 카드 -->
|
||||
<div v-if="webInspectionData" class="inspection-card inspection-time-card">
|
||||
<h2 class="card-title text-base text-md md:text-lg">{{ tm('Inspection_Maintenance_Time') }}</h2>
|
||||
<div class="inspection-time text-sm md:text-base font-medium">
|
||||
<div v-dompurify-html="getLocaleTimezone('', '')" class="time-row"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 온스토브 & 다운로드 카드 -->
|
||||
<div class="inspection-bottom-cards">
|
||||
<!-- 온스토브 카드 -->
|
||||
<div class="inspection-card inspection-stove-card">
|
||||
<h3 :class="{ 'text-center': !launchingStatus }" class="card-title text-base md:text-lg">
|
||||
<span v-if="!launchingStatus" v-dompurify-html="tm('Inspection_Game_During_Maintenance')"></span>
|
||||
<span v-else v-dompurify-html="tm('org_Inspection_During_Maintenance')"></span>
|
||||
</h3>
|
||||
<div class="button-group justify-center">
|
||||
<!-- <a
|
||||
:href="communityUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inspection-btn inspection-btn-outline inspection-btn-community"
|
||||
:class="{ 'bg-[var(--primary)]' : !launchingStatus }"
|
||||
>
|
||||
<span>{{ tm('Inspection_Community_Btn') || '공식 커뮤니티' }}</span>
|
||||
<AtomsIconsLongArrowRightLine :size="16" color="#1F1F1F" />
|
||||
</a> -->
|
||||
|
||||
<AtomsButtonVariant
|
||||
type="custom"
|
||||
class="inspection-btn inspection-btn-community color-black text-sm md:text-base"
|
||||
:class="{ 'inspection-btn-outline flex-1' : launchingStatus }"
|
||||
@click="handleCommunityClick"
|
||||
>
|
||||
<span>{{ tm('Inspection_Community_Btn') }}</span>
|
||||
<AtomsIconsLongArrowRightLine :size="16" color="#1F1F1F" />
|
||||
|
||||
</AtomsButtonVariant>
|
||||
|
||||
<AtomsButtonVariant
|
||||
v-if="launchingStatus"
|
||||
type="custom"
|
||||
platform="pc"
|
||||
class="inspection-btn inspection-btn-primary w-auto color-black text-sm md:text-base flex-1"
|
||||
@click="handleGameStart"
|
||||
>
|
||||
<span>게임 시작</span>
|
||||
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.3098 1.49172C8.86574 1.28049 7.15098 1.28055 5.70176 1.49167C3.50821 1.81315 1.81986 3.50786 1.49213 5.69168L1.4918 5.69391C1.38564 6.41904 1.33331 7.19181 1.33331 7.99998C1.33331 8.80815 1.3857 9.58136 1.49187 10.3065C1.81362 12.4934 3.50335 14.1805 5.69381 14.5079L5.69577 14.5082C6.42109 14.6143 7.19417 14.6666 7.99717 14.6666C8.80032 14.6666 9.57311 14.6143 10.3035 14.5083L10.3046 14.5082C12.4928 14.1865 14.1802 12.4917 14.5078 10.3083L14.5082 10.3061C14.6143 9.58092 14.6666 8.80815 14.6666 7.99998C14.6666 7.19527 14.6203 6.42148 14.5137 5.69347C14.1921 3.50726 12.4966 1.81316 10.3098 1.49172ZM6.38756 8.95267C6.39301 9.15365 6.40004 9.35195 6.40866 9.54742L6.40968 9.57054C6.41945 9.78867 6.43118 10.0033 6.44489 10.2141C6.45959 10.4433 6.68743 10.5836 6.88293 10.4823C7.22837 10.3029 7.58116 10.1096 7.9413 9.90379C8.20002 9.7541 8.46167 9.6013 8.72479 9.43914C8.98791 9.27854 9.24516 9.11482 9.49505 8.95111C9.84196 8.72502 10.1771 8.49581 10.4961 8.26817C10.6769 8.14031 10.6769 7.85965 10.4961 7.7318C10.1771 7.50415 9.84343 7.2765 9.49505 7.04886C9.24516 6.88514 8.98791 6.72298 8.72479 6.56082C8.46167 6.39866 8.20002 6.2443 7.9413 6.09618C7.58263 5.89036 7.22837 5.69702 6.88293 5.5177C6.68743 5.41636 6.45959 5.55669 6.44489 5.78589C6.43118 5.99671 6.41945 6.21129 6.40968 6.42943L6.40866 6.45254C6.40004 6.64801 6.39301 6.84631 6.38756 7.0473C6.37874 7.3607 6.37433 7.67722 6.37433 7.99998C6.37433 8.32274 6.37874 8.64082 6.38756 8.95267Z" fill="#332C2A"/>
|
||||
</svg>
|
||||
</AtomsButtonVariant>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 다운로드 카드 -->
|
||||
<div v-if="launchingStatus" class="inspection-card inspection-download-card">
|
||||
<h3 class="card-title text-base md:text-lg">
|
||||
{{ tm('Inspection_Txt_Download') || '게임 다운로드' }}
|
||||
</h3>
|
||||
<div class="flex flex-row gap-3">
|
||||
<AtomsButtonLauncher
|
||||
v-for="(btn, index) in enabledMarkets" :key="index"
|
||||
:class="getButtonClass(btn.platform)"
|
||||
class="h-[48px]"
|
||||
:platform="btn.platform as any"
|
||||
:url="btn.url"
|
||||
:type="btn.platform === 'pc' ? 'default' : 'single'"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ getButtonText(btn.platform) }}
|
||||
</AtomsButtonLauncher>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</clientOnly>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { globalDateFormat } from '@seed-next/date';
|
||||
import { useCheckGameStart } from '#layers/composables/useGameStart'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const rootPath = config.public.staticUrl
|
||||
const runType = config.public.runType
|
||||
const translationApi = `${rootPath}/${runType}/test`
|
||||
|
||||
// const isClient = import.meta.client
|
||||
|
||||
const inspectionStore = useInspectionStore()
|
||||
const { webInspectionData } = storeToRefs(inspectionStore)
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
|
||||
const resultGetMultilingual = await useGetMultilingual({
|
||||
baseApiUrl: translationApi,
|
||||
fileName: 'test_common_inspection.json'
|
||||
})
|
||||
const { tm, locale } = useI18n({
|
||||
useScope: 'local',
|
||||
messages: Object(resultGetMultilingual.value.multilingual)
|
||||
})
|
||||
|
||||
// locale에 따라 뒤에 KST 또는 UTC 추가 ko, en, zh-tw, ja
|
||||
// ko: (KST)
|
||||
// en: (UTC)
|
||||
// zh-tw: 台灣時間 (KST)
|
||||
// ja: (JST)
|
||||
// 나머지: (KST)
|
||||
const getLocaleTimezone = (localeType: string, region) => {
|
||||
const tsStartDate = webInspectionData.value?.ts_start_date || 0
|
||||
const tsEndDate = webInspectionData.value?.ts_end_date || 0
|
||||
const currentLocale = localeType ? localeType : locale.value
|
||||
switch (currentLocale) {
|
||||
case 'ko':
|
||||
return `
|
||||
${globalDateFormat(new Date(tsStartDate), currentLocale, region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), currentLocale, region || 'KR', {useFullDate: true})} (KST)<br>
|
||||
${globalDateFormat(new Date(tsStartDate), 'en', region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), 'en', region || '', {useFullDate: true})} (UTC)
|
||||
`
|
||||
case 'en':
|
||||
return `${globalDateFormat(new Date(tsStartDate), currentLocale, region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), currentLocale, region || '', {useFullDate: true})} (UTC)`
|
||||
case 'zh-tw':
|
||||
return `
|
||||
${globalDateFormat(new Date(tsStartDate), currentLocale, region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), currentLocale, region || '', {useFullDate: true})} (台灣時間)<br>
|
||||
${globalDateFormat(new Date(tsStartDate), 'en', region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), 'en', region || '', {useFullDate: true})} (UTC)
|
||||
`
|
||||
case 'ja':
|
||||
return `
|
||||
${globalDateFormat(new Date(tsStartDate), currentLocale, region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), currentLocale, region || '', {useFullDate: true})} (JST)<br>
|
||||
${globalDateFormat(new Date(tsStartDate), 'en', region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), 'en', region || '', {useFullDate: true})} (UTC)
|
||||
`
|
||||
default:
|
||||
return `
|
||||
${globalDateFormat(new Date(tsStartDate), currentLocale, region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), currentLocale, region || '', {useFullDate: true})} (KST)<br>
|
||||
${globalDateFormat(new Date(tsStartDate), 'en', region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), 'en', region || '', {useFullDate: true})} (UTC)
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
const launchingStatus = computed(() => {
|
||||
return webInspectionData.value?.launching_status
|
||||
})
|
||||
|
||||
|
||||
// const market_json = {
|
||||
// pc: { url: 'https://apps.apple.com/app/id1234567890', use_yn: 0 },
|
||||
// app_store: { url: 'https://apps.apple.com/app/id1234567890', use_yn: 0 },
|
||||
// google_play: { url: 'https://play.google.com/store/apps/details?id=example', use_yn: 1 }
|
||||
// }
|
||||
|
||||
//gameData.value.market_json 값 중 use_yn === 1 인 항목만 배열로 변환
|
||||
const enabledMarkets = computed(() => {
|
||||
return Object.entries(gameData.value.market_json)
|
||||
// return Object.entries(market_json)
|
||||
.filter(([, info]: [string, any]) => info && info.use_yn === 1)
|
||||
.map(([platform, info]: [string, any]) => ({ platform, url: info.url as string }))
|
||||
})
|
||||
|
||||
|
||||
const logoImgUrl = computed(() => {
|
||||
const currentLocale = locale.value || 'ko'
|
||||
const localeData = (webInspectionData.value as any)?.[currentLocale]
|
||||
return getImageHost(localeData.img_json.bi_large)
|
||||
})
|
||||
|
||||
const communityUrl = computed(() => {
|
||||
const currentLocale = locale.value || 'ko'
|
||||
const localeData = (webInspectionData.value as any)?.[currentLocale]
|
||||
return localeData?.url_json?.community
|
||||
})
|
||||
|
||||
const handleCommunityClick = () => {
|
||||
window.open(communityUrl.value, '_blank')
|
||||
}
|
||||
// 버튼 클래스 결정 함수
|
||||
const getButtonClass = (platform: string) => {
|
||||
// pc가 있으면 pc만 flex-1, 나머지는 기본
|
||||
const hasPc = enabledMarkets.value.some(btn => btn.platform === 'pc')
|
||||
if(hasPc) {
|
||||
return platform === 'pc' ? `flex-1 btn-platform-pc` : ``
|
||||
}
|
||||
//pc가 없으면서 하나만 있으면 ''
|
||||
if (enabledMarkets.value.length === 1) {
|
||||
return ``
|
||||
}
|
||||
return `flex-1`
|
||||
}
|
||||
|
||||
const getButtonText = (platform: string) => {
|
||||
const hasPc = enabledMarkets.value.some(btn => btn.platform === 'pc')
|
||||
// pc가 있으면 pc만 텍스트 노출
|
||||
if (hasPc) {
|
||||
return platform === 'pc' ? tm('platform_pc') : ''
|
||||
}
|
||||
// pc가 없으면 google_play와 app_store만 텍스트 노출
|
||||
if (platform === 'google_play') {
|
||||
return tm('platform_google_play')
|
||||
}
|
||||
if (platform === 'app_store') {
|
||||
return tm('platform_app_store')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const { validateLauncher } = useCheckGameStart()
|
||||
const handleGameStart = () => {
|
||||
validateLauncher()
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['inspection'],
|
||||
layout: 'only-stove',
|
||||
showLoading: false
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.inspection-section {
|
||||
@apply flex flex-col items-center gap-10 px-10 py-[120px] pb-[200px] min-h-[calc(100vh-48px)];
|
||||
background-color: #F0F0F0;
|
||||
}
|
||||
|
||||
.inspection-logo {
|
||||
@apply w-[944px] h-[50px] flex-shrink-0;
|
||||
}
|
||||
|
||||
.inspection-logo img {
|
||||
@apply w-full h-full object-contain;
|
||||
}
|
||||
|
||||
.inspection-content {
|
||||
@apply flex flex-col items-center gap-10 w-full max-w-[944px];
|
||||
}
|
||||
|
||||
.inspection-title {
|
||||
@apply text-center font-bold tracking-[-0.72px] text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-cards {
|
||||
@apply flex flex-col gap-5 w-full;
|
||||
}
|
||||
|
||||
.inspection-card {
|
||||
@apply bg-white rounded-2xl p-6 md:p-8;
|
||||
}
|
||||
|
||||
.inspection-time-card {
|
||||
@apply flex flex-col items-center gap-4;
|
||||
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@apply text-center leading-[30px] font-bold tracking-[-0.6px] text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-time {
|
||||
@apply flex flex-col items-center gap-0 md:gap-2;
|
||||
}
|
||||
|
||||
.time-row {
|
||||
@apply text-center leading-[26px] font-medium tracking-[-0.48px] text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-bottom-cards {
|
||||
@apply flex flex-row gap-5 w-full;
|
||||
}
|
||||
|
||||
.inspection-stove-card,
|
||||
.inspection-download-card {
|
||||
@apply flex flex-col justify-between gap-4 flex-1;
|
||||
}
|
||||
|
||||
.inspection-stove-card .card-title {
|
||||
@apply leading-[26px] font-bold tracking-[-0.54px] text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-download-card .card-title {
|
||||
@apply text-left leading-[26px] font-bold tracking-[-0.54px] text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-content-text {
|
||||
@apply text-left leading-[26px] font-medium tracking-[-0.48px] text-[#1F1F1F];
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
@apply flex flex-row gap-3 w-full;
|
||||
}
|
||||
|
||||
.inspection-btn {
|
||||
@apply flex items-center justify-center gap-1 px-6 md:px-8 w-auto h-10 md:h-12 rounded-lg;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
}
|
||||
|
||||
.inspection-btn span {
|
||||
@apply text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-btn-outline {
|
||||
@apply bg-white;
|
||||
}
|
||||
|
||||
.inspection-btn-outline:hover {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
|
||||
.inspection-btn-primary {
|
||||
@apply bg-[var(--primary)] border-[var(--primary)] text-[#000];
|
||||
}
|
||||
|
||||
.inspection-btn-primary span {
|
||||
@apply text-black;
|
||||
}
|
||||
|
||||
.inspection-btn-primary:hover {
|
||||
@apply bg-[#B89D7A];
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.inspection-section {
|
||||
@apply px-5 py-20 pb-32;
|
||||
}
|
||||
|
||||
.inspection-logo {
|
||||
@apply w-full max-w-[944px];
|
||||
}
|
||||
|
||||
.inspection-time-card {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.inspection-bottom-cards {
|
||||
@apply flex-col;
|
||||
}
|
||||
|
||||
.inspection-stove-card,
|
||||
.inspection-download-card {
|
||||
@apply w-full;
|
||||
}
|
||||
}
|
||||
.inspection-btn-community {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
.inspection-btn-community.inspection-btn-outline {
|
||||
@apply bg-white border-none;
|
||||
}
|
||||
|
||||
.inspection-btn-community.inspection-btn-outline:hover {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
</style>
|
||||
95
error.vue
Normal file
95
error.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-[#191919]">
|
||||
<LayoutsStoveHeader />
|
||||
<div class="flex-1 flex items-center justify-center bg-[#F0F0F0] pt-[223px] pb-[223px] px-5 max-[374px]:px-5 md:px-[530px]">
|
||||
<div class="flex flex-col items-center gap-6 w-full max-w-[513px] max-[374px]:max-w-[335px] md:max-w-[860px] py-10 max-[374px]:gap-6 md:gap-6 md:py-20">
|
||||
<!-- Stove Logo -->
|
||||
<div class="flex items-center justify-center w-[123.2px] h-7">
|
||||
<img
|
||||
src="/images/common/logo-stove.svg"
|
||||
alt="Stove"
|
||||
class="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error Icon and Text -->
|
||||
<div class="flex flex-col items-center gap-4 w-full">
|
||||
<!-- Error Icon -->
|
||||
<div class="flex items-center justify-center">
|
||||
<img
|
||||
src="/images/common/img_error.png"
|
||||
alt="Error"
|
||||
class="w-40 h-40 md:w-60 md:h-60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error Text -->
|
||||
<div class="flex flex-col items-center gap-1 w-full">
|
||||
<h1 class="font-['Spoqa_Han_Sans_Neo',sans-serif] font-medium text-xl md:text-2xl leading-[1.5] md:leading-[1.4166666666666667] tracking-[-0.03em] text-center text-[#1F1F1F] m-0">
|
||||
{{ errorTitle }}
|
||||
</h1>
|
||||
<p class="font-['Spoqa_Han_Sans_Neo',sans-serif] font-normal text-sm md:text-base leading-[1.7142857142857142] md:leading-[1.625] tracking-[-0.03em] text-center text-[#666666] m-0">
|
||||
{{ errorDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Home Button -->
|
||||
<button
|
||||
class="flex items-center justify-center font-['Spoqa_Han_Sans_Neo',sans-serif] font-medium text-sm leading-[1.4285714285714286em] tracking-[-0.03em] text-white bg-[#FC4420] border border-black/10 rounded-lg px-10 h-12 cursor-pointer transition-all duration-300 backdrop-blur-[30px] shadow-[0px_0.9131946563720703px_1.4666459560394287px_0px_rgba(0,0,0,0.06),0px_2.194533586502075px_3.5245540142059326px_0px_rgba(0,0,0,0.09)] hover:bg-[#E03D1C] hover:shadow-[0px_0.9131946563720703px_1.4666459560394287px_0px_rgba(0,0,0,0.06),0px_2.194533586502075px_3.5245540142059326px_0px_rgba(0,0,0,0.09),0px_4.1321120262146px_6.636422634124756px_0px_rgba(0,0,0,0.12)] active:translate-y-px"
|
||||
@click="handleGoHome"
|
||||
>
|
||||
{{ homeButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NuxtError } from '#app'
|
||||
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
||||
import { useLocalePath } from '#i18n'
|
||||
|
||||
const props = defineProps({
|
||||
error: Object as () => NuxtError,
|
||||
})
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
const localePath = useLocalePath()
|
||||
|
||||
const gameName = computed(() => gameData.value?.game_name || '게임')
|
||||
const homeButtonText = computed(() => `${gameName.value} 홈페이지 가기`)
|
||||
|
||||
const handleGoHome = () => {
|
||||
const homePath = localePath('/')
|
||||
navigateTo(homePath)
|
||||
}
|
||||
|
||||
// 에러 상태 코드에 따른 메시지 설정
|
||||
const errorTitle = computed(() => {
|
||||
if (props.error?.statusCode === 404) {
|
||||
return '페이지를 찾을 수 없어요.'
|
||||
}
|
||||
return '페이지를 찾을 수 없어요.'
|
||||
})
|
||||
|
||||
const errorDescription = computed(() => {
|
||||
if (props.error?.statusCode === 404) {
|
||||
return '주소가 바뀌었거나 잘못 입력된 것 같아요. 주소를 다시 확인해 주세요.'
|
||||
}
|
||||
return '주소가 바뀌었거나 잘못 입력된 것 같아요. 주소를 다시 확인해 주세요.'
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
layout: 'only-stove'
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
/* :deep(.game-wrapper) {
|
||||
display: none !important;
|
||||
} */
|
||||
</style>
|
||||
@@ -1,10 +1,11 @@
|
||||
export default defineI18nLocale(async (locale: string) => {
|
||||
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116
|
||||
const config = useRuntimeConfig()
|
||||
const rootPath = config.public.staticUrl
|
||||
const runType = config.public.runType
|
||||
// const config = useRuntimeConfig()
|
||||
// const rootPath = config.public.staticUrl
|
||||
// const runType = config.public.runType
|
||||
|
||||
const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
|
||||
// const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
|
||||
const translationApi = `https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json`
|
||||
|
||||
try {
|
||||
const { data } = await useFetch(translationApi, {
|
||||
@@ -15,7 +16,7 @@ export default defineI18nLocale(async (locale: string) => {
|
||||
})
|
||||
|
||||
// API 데이터에서 locale에 맞는 데이터를 추출
|
||||
const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
const apiData = data.value?.['ko'] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
|
||||
|
||||
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
|
||||
const finalResult = { ...apiData }
|
||||
|
||||
@@ -1,43 +1,124 @@
|
||||
<template>
|
||||
<div class="bg-white">
|
||||
<select
|
||||
v-model="selectedLocale"
|
||||
<div class="select-language" :class="{ 'language-changing': isChanging }">
|
||||
|
||||
<button
|
||||
:disabled="isChanging"
|
||||
class="text-black px-2 py-1 rounded-md"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg text-[#CCCCCC] transition-all duration-300 w-[180px] bg-[#292929] border border-[#595959]"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': isChanging }"
|
||||
@change="switchLanguage"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<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">
|
||||
변경 중...
|
||||
<!-- 지구본 아이콘 -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5964_1685)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6666 8.00065C14.6666 11.6825 11.6818 14.6673 7.99992 14.6673C4.31802 14.6673 1.33325 11.6825 1.33325 8.00065C1.33325 4.31875 4.31802 1.33398 7.99992 1.33398C11.6818 1.33398 14.6666 4.31875 14.6666 8.00065ZM6.89756 13.2199C6.03596 11.8504 5.50924 10.2901 5.36895 8.66732H2.70785C2.99033 10.9326 4.69347 12.7567 6.89756 13.2199ZM2.70785 7.33398H5.36895C5.50924 5.71116 6.03596 4.15086 6.89756 2.78138C4.69347 3.24458 2.99033 5.06868 2.70785 7.33398ZM13.292 8.66732C13.0095 10.9326 11.3064 12.7567 9.10228 13.2199C9.96388 11.8504 10.4906 10.2901 10.6309 8.66732H13.292ZM13.292 7.33398C13.0095 5.06868 11.3064 3.24458 9.10228 2.78138C9.96388 4.15086 10.4906 5.71116 10.6309 7.33398H13.292ZM7.99992 12.468C7.28662 11.3201 6.84273 10.0202 6.70801 8.66732H9.29183C9.15711 10.0202 8.71322 11.3201 7.99992 12.468ZM6.70801 7.33398H9.29183C9.15711 5.98112 8.71322 4.68121 7.99992 3.5333C7.28662 4.68121 6.84273 5.98112 6.70801 7.33398Z" fill="#CCCCCC"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5964_1685">
|
||||
<rect width="16" height="16" fill="#CCCCCC"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="flex-1 text-sm text-left transition-all duration-300">
|
||||
{{ isChanging ? '언어 변경 중...' : getLanguageName(selectedLocale) }}
|
||||
</span>
|
||||
<!-- 로딩 스피너 -->
|
||||
<svg v-if="isChanging" class="w-3 h-3 animate-spin" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="#CCCCCC" stroke-width="2" stroke-linecap="round" stroke-dasharray="31.416" stroke-dashoffset="31.416">
|
||||
<animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
|
||||
<animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
<!-- 드롭다운 화살표 -->
|
||||
<svg
|
||||
v-else
|
||||
class="w-3 h-3 text-gray-300 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': isDropdownOpen }"
|
||||
viewBox="0 0 12 12" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.69999 4.285L9.59499 7.125C9.91999 7.445 9.69499 8 9.23499 8H2.75999C2.29999 8 2.07499 7.445 2.39999 7.125L5.29499 4.285C5.68499 3.905 6.30499 3.905 6.69499 4.285H6.69999Z" fill="#EBEBEB"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="isDropdownOpen" class="dropdown-menu">
|
||||
<div v-for="localeItem in availableLanguages" :key="localeItem.code" class="dropdown-menu-item">
|
||||
<button
|
||||
class="dropdown-menu-item-button"
|
||||
:class="{ 'current': localeItem.code === selectedLocale }"
|
||||
@click="selectLanguage(localeItem.code)"
|
||||
>
|
||||
<svg
|
||||
width="15"
|
||||
height="11"
|
||||
viewBox="0 0 15 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="transition-opacity duration-200"
|
||||
>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6339 0.366117C15.1221 0.854272 15.1221 1.64573 14.6339 2.13388L6.30057 10.4672C6.05437 10.7134 5.71664 10.8458 5.36872 10.8324C5.0208 10.8191 4.69421 10.6612 4.46762 10.3968L0.300952 5.53571C-0.148326 5.01155 -0.0876239 4.22243 0.436533 3.77315C0.960691 3.32387 1.74982 3.38458 2.19909 3.90873L5.48729 7.74496L12.8661 0.366117C13.3543 -0.122039 14.1458 -0.122039 14.6339 0.366117Z" fill="#FC4420"/>
|
||||
</svg>
|
||||
<span class="text-sm">{{ localeItem.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const gameDataStore = useGameDataStore()
|
||||
const config = useRuntimeConfig()
|
||||
const baseDomain = `${config.public.baseDomain}`
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
// 사용 가능한 언어 목록
|
||||
const availableLanguages = computed(() => {
|
||||
return gameDataStore.gameData?.lang_codes || ['ko']
|
||||
return gameData.value?.lang_codes?.map(localeCode => ({
|
||||
code: localeCode,
|
||||
name: getLanguageName(localeCode)
|
||||
})) || [{ code: 'ko', name: '한국어' }]
|
||||
})
|
||||
|
||||
// 언어 코드를 한국어 이름으로 변환하는 함수
|
||||
const getLanguageName = (localeCode: string) => {
|
||||
const languageNames: Record<string, string> = {
|
||||
'ko': '한국어',
|
||||
'en': 'English',
|
||||
'ja': '日本語',
|
||||
'zh-cn': '简体中文',
|
||||
'zh-tw': '繁體中文',
|
||||
'es': 'Español',
|
||||
'fr': 'Français',
|
||||
'de': 'Deutsch',
|
||||
'pt': 'Português',
|
||||
'th': 'ไทย',
|
||||
'it': 'Italiano'
|
||||
}
|
||||
return languageNames[localeCode] || localeCode
|
||||
}
|
||||
|
||||
const { locale, setLocale } = useI18n()
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
const router = useRouter()
|
||||
const pageDataStore = usePageDataStore()
|
||||
|
||||
const selectedLocale = ref(locale.value)
|
||||
const isChanging = ref(false)
|
||||
const isDropdownOpen = ref(false)
|
||||
// 드롭다운 토글 함수
|
||||
const toggleDropdown = () => {
|
||||
if (!isChanging.value) {
|
||||
isDropdownOpen.value = !isDropdownOpen.value
|
||||
}
|
||||
}
|
||||
// 언어 선택 함수
|
||||
const selectLanguage = async (localeCode: string) => {
|
||||
if (localeCode === selectedLocale.value || isChanging.value) {
|
||||
isDropdownOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 언어 변경 함수 (CSR 방식)
|
||||
selectedLocale.value = localeCode as any
|
||||
isDropdownOpen.value = false
|
||||
await switchLanguage()
|
||||
}
|
||||
// 언어 변경 함수 (서버 미드웨어를 통한 gameData 갱신)
|
||||
const switchLanguage = async () => {
|
||||
if (!selectedLocale.value || isChanging.value) return
|
||||
|
||||
@@ -45,22 +126,39 @@ const switchLanguage = async () => {
|
||||
|
||||
try {
|
||||
// URL 경로를 통해 언어 변경
|
||||
const path = switchLocalePath(selectedLocale.value)
|
||||
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)
|
||||
// await router.push(path)
|
||||
// await setLocale(selectedLocale.value as any)
|
||||
|
||||
// 전체 페이지에 페이드 아웃 효과 적용
|
||||
// document.body.style.transition = 'opacity 0.1s ease-out'
|
||||
// document.body.style.opacity = '0'
|
||||
|
||||
// // 페이드 아웃 완료 후 페이지 이동
|
||||
// await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 서버 미드웨어를 통해 gameData 갱신을 위해 페이지 새로고침
|
||||
// 이렇게 하면 서버 미드웨어가 새로운 언어로 gameData를 다시 가져옴
|
||||
|
||||
// 페이지 새로고침을 통해 데이터 재로드 보장
|
||||
await nextTick()
|
||||
window.location.reload()
|
||||
}
|
||||
} catch {
|
||||
// 오류 발생 시 이전 언어로 복원
|
||||
selectedLocale.value = locale.value
|
||||
// 페이드 효과 복원
|
||||
document.body.style.opacity = '1'
|
||||
} finally {
|
||||
isChanging.value = false
|
||||
}
|
||||
@@ -71,3 +169,84 @@ watch(locale, newLocale => {
|
||||
selectedLocale.value = newLocale
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.select-language {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 180px;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* 페이지 전환 애니메이션 */
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.page-enter-from,
|
||||
.page-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 언어 변경 시 전체 페이지 페이드 효과 */
|
||||
.language-changing {
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 전체 페이지 전환 효과 */
|
||||
body {
|
||||
transition: opacity 0.5s ease-out;
|
||||
}
|
||||
.select-language select {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
padding: 0 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
margin-bottom: 4px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: #292929;
|
||||
border: 1px solid #595959;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.dropdown-menu-item-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
text-align: left;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.dropdown-menu-item-button svg {
|
||||
opacity: 0;
|
||||
}
|
||||
.dropdown-menu-item-button.current {
|
||||
color: #fc4420;
|
||||
font-weight: 500;
|
||||
}
|
||||
.dropdown-menu-item-button.current svg {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
</style>
|
||||
61
layers/components/blocks/StoveGnbNew.vue
Normal file
61
layers/components/blocks/StoveGnbNew.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div id="stove-wrapper" class="relative z-[5]" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
GameDataValue,
|
||||
} from '#layers/types/api/gameData'
|
||||
let mountedInstance: any = null
|
||||
|
||||
onMounted(() => {
|
||||
const gameDataStore = useGameDataStore()
|
||||
const gameData = gameDataStore.gameData as GameDataValue
|
||||
const langCodes = gameData?.lang_codes
|
||||
const defaultLangCode = gameData?.default_lang_code
|
||||
const gnbData = gameData?.stove_gnb_json
|
||||
|
||||
const currentDomain = window.location.protocol + '//' + window.location.hostname;
|
||||
if (typeof window !== 'undefined' && (window as any).StoveGnb) {
|
||||
mountedInstance = (window as any).StoveGnb.mount('#stove-wrapper', {
|
||||
logArea: currentDomain,
|
||||
useLanguageCodeFromPath: true,
|
||||
serviceTitle: {
|
||||
pc: '',
|
||||
mobile: ''
|
||||
},
|
||||
widget: {
|
||||
notification: true,
|
||||
stoveDownload: true,
|
||||
languageSelect: false,
|
||||
themeSelect: false,
|
||||
stoveMenu: {
|
||||
active: false,
|
||||
mobile: true
|
||||
},
|
||||
},
|
||||
global: {
|
||||
languageCoverages: langCodes,
|
||||
defaultSelectedLanguage: defaultLangCode || 'en',
|
||||
},
|
||||
loginMethod: {
|
||||
redirectCurrentPage: true,
|
||||
},
|
||||
mode: {
|
||||
theme: {
|
||||
default: gnbData?.skin_type === 'gnb-dark-mini' ? 'dark' : 'light',
|
||||
support: ['dark', 'light'],
|
||||
},
|
||||
mini: true,
|
||||
fixed:false,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (mountedInstance && typeof mountedInstance.destroy === 'function') {
|
||||
mountedInstance.destroy()
|
||||
}
|
||||
mountedInstance = null
|
||||
})
|
||||
</script>
|
||||
@@ -24,12 +24,12 @@
|
||||
</li>
|
||||
<li class="relative">
|
||||
<button class="hover:text-gray-600 transition-colors" @click="toggleAgeRating">
|
||||
{{ tm('Footer_AgeRating') }}
|
||||
{{ footerAgeRating }}
|
||||
</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 class="px-6 py-4 rounded-t-lg flex justify-between items-center">
|
||||
<h3 class="text-white text-base">{{ tm('Footer_AgeRating') }}</h3>
|
||||
<h3 class="text-white text-base">{{ footerAgeRating }}</h3>
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
@@ -55,23 +55,23 @@
|
||||
<div class="px-6 py-6 rounded-b-lg bg-[#A31639]">
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-start border-b border-white/10 pb-2">
|
||||
<span class="text-white text-sm flex-1">{{ tm('Footer_AgeRating_Info')[0] }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.company_name }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerAgeRatingInfo[0] }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.title }}</span>
|
||||
</div>
|
||||
<div class="flex flex-start border-b border-white/10 pb-2">
|
||||
<span class="text-white text-sm flex-1">{{ tm('Footer_AgeRating_Info')[1] }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerAgeRatingInfo[1] }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.rating_grade }}</span>
|
||||
</div>
|
||||
<div class="flex flex-start border-b border-white/10 pb-2">
|
||||
<span class="text-white text-sm flex-1">{{ tm('Footer_AgeRating_Info')[2] }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerAgeRatingInfo[2] }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.reg_no }}</span>
|
||||
</div>
|
||||
<div class="flex flex-start border-b border-white/10 pb-2">
|
||||
<span class="text-white text-sm flex-1">{{ tm('Footer_AgeRating_Info')[3] }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerAgeRatingInfo[3] }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.prod_date }}</span>
|
||||
</div>
|
||||
<div class="flex flex-start">
|
||||
<span class="text-white text-sm flex-1">{{ tm('Footer_AgeRating_Info')[4] }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerAgeRatingInfo[4] }}</span>
|
||||
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.rating_class_no }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,7 +85,7 @@
|
||||
<address class="not-italic text-gray-500">
|
||||
<div class="row my-1.5 leading-5">
|
||||
<span
|
||||
v-dompurify-html="tm('Footer_Address')"
|
||||
v-dompurify-html="footerAddress"
|
||||
class="[&_a]:cursor-pointer [&_a]:text-blue-500 [&_a]:underline"
|
||||
></span>
|
||||
</div>
|
||||
@@ -93,7 +93,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-6 md:mt-6">
|
||||
<div class="text-xs text-white/30">{{ tm('Footer_caution') }}</div>
|
||||
<div class="text-xs text-white/30">{{ footerCaution }}</div>
|
||||
</div>
|
||||
|
||||
<div class="copyright-area mt-6 text-gray-500 md:mt-4">
|
||||
@@ -114,20 +114,15 @@
|
||||
</a>
|
||||
<a
|
||||
v-if="setDevCi.dev_ci_yn"
|
||||
:href="footerData.use_dev_ci_url"
|
||||
:href="footerData.use_dev_ci_url ? setDevCi.dev_ci_img_path : '#'"
|
||||
target="_blank"
|
||||
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"
|
||||
class="w-auto h-[24px]"
|
||||
/>
|
||||
<!-- <img
|
||||
:src="setDevCi.dev_ci_img_path"
|
||||
alt="CI"
|
||||
class="w-auto h-auto"
|
||||
/> -->
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -144,74 +139,106 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const { tm } = useI18n()
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
import type { FooterMenuItem, FooterData, DevCiConfig } from '#layers/types/Common'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const staticUrl = config.public.staticUrl
|
||||
const runType = config.public.runType
|
||||
|
||||
const translationApi = `${runType}/test/multilingual/test_common_template.json`
|
||||
|
||||
const result = await useApiData({ baseApiUrl: staticUrl, url: translationApi })
|
||||
|
||||
const { tm } = useI18n({
|
||||
useScope: 'local',
|
||||
messages: result
|
||||
})
|
||||
|
||||
// const { tm } = useI18n()
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
// const path = ref<string>(`${staticUrl}/local/template/${gameData.value.s3_folder_name}`)
|
||||
|
||||
// 공통다국어 data
|
||||
const footerLinks = computed(() => {
|
||||
return tm('Footer_Menu') as unknown as any[]
|
||||
const footerLinks = computed((): FooterMenuItem[] => {
|
||||
const menu = (tm as any)('Footer_Menu')
|
||||
return Array.isArray(menu) ? menu as FooterMenuItem[] : []
|
||||
})
|
||||
const footerData = ref(gameData.value?.footer_json as any)
|
||||
const setDevCi = ref({
|
||||
const footerData = ref(gameData.value?.footer_json as unknown as FooterData)
|
||||
const setDevCi = ref<DevCiConfig>({
|
||||
dev_ci_yn: gameData.value?.footer_dev_ci_img_yn as boolean,
|
||||
dev_ci_img_path: gameData.value?.footer_dev_ci_img_path as string,
|
||||
})
|
||||
|
||||
///local/template/common/grades_age
|
||||
const getGameRatingImage = computed(() => {
|
||||
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 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' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const getContentInfoImage = computed(() => {
|
||||
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 getImageHost('/images/common/grades_use/Type-sexual.svg', { imageType: 'common' })
|
||||
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(false)
|
||||
const toggleAgeRating = () => {
|
||||
const showAgeRating = ref<boolean>(false)
|
||||
const toggleAgeRating = (): void => {
|
||||
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 info = (tm as any)('Footer_AgeRating_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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -171,7 +171,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<header class="header">
|
||||
<BlocksStoveGnb class="min-h-[48px]" />
|
||||
<BlocksStoveGnbNew class="h-[48px]" />
|
||||
|
||||
<div class="game-wrap" :class="{ 'is-fixed': isPassedStoveGnb }">
|
||||
<AtomsLocaleLink to="/brand" class="mx-auto md:hidden">
|
||||
|
||||
18
layers/components/layouts/StoveHeader.vue
Normal file
18
layers/components/layouts/StoveHeader.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="header">
|
||||
<BlocksStoveGnbNew class="min-h-[48px]" />
|
||||
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
@apply bg-theme-foreground text-theme-foreground-reversal relative z-[100];
|
||||
}
|
||||
|
||||
</style>
|
||||
c
|
||||
22
layers/composables/useApiData.ts
Normal file
22
layers/composables/useApiData.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
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 []
|
||||
}
|
||||
}
|
||||
|
||||
54
layers/composables/useGetGameDataExternal.ts
Normal file
54
layers/composables/useGetGameDataExternal.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { GameDataResponse, GameDataRequest } from '#layers/types/api/gameData'
|
||||
|
||||
export const useGetGameDataExternal = () => {
|
||||
const { setGameData } = useGameDataStore()
|
||||
const logPrefix = {
|
||||
exception: '[Exception] /composables/useGetGameDataExternal',
|
||||
failure: '[Failure] /composables/useGetGameDataExternal'
|
||||
}
|
||||
const webGameData = ref<GameDataResponse | null>(null)
|
||||
|
||||
const getGameDataExternal = async (req: GameDataRequest) => {
|
||||
console.log("🚀 ~ getGameDataExternal ~ req:", req)
|
||||
// const config = useRuntimeConfig()
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiUrl = `${config.public.stoveApiUrl}`
|
||||
const apiUrl = `${stoveApiUrl}/pub-comm/v1.0/template/game?game_domain=${req.gameDomain}&lang_code=${req.langCode}`
|
||||
|
||||
try {
|
||||
const response = (await commonFetch('GET', apiUrl)) as GameDataResponse
|
||||
console.log("🚀 ~ getGameDataExternal ~ response:", response)
|
||||
|
||||
// FIXME: 테스트용 데이터 ---------------------------------------------------
|
||||
/* if (['local', 'local-gate8', 'dev'].includes(`${config.public.runType}`)) {
|
||||
response.value = {
|
||||
inspection_status: 1,
|
||||
inspection: {
|
||||
inspection_status: 1,
|
||||
start_date: '2025-09-19 10:00:00',
|
||||
end_date: '2025-09-19 12:00:00',
|
||||
ts_start_date: new Date().getTime(),
|
||||
ts_end_date: new Date().getTime(),
|
||||
back_ground_image_type: 'image',
|
||||
back_ground_image_url: 'https://www.onstove.com',
|
||||
inspection_title1: '',
|
||||
inspection_title2: ''
|
||||
}
|
||||
}
|
||||
} */
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
|
||||
if (response?.value) {
|
||||
webGameData.value = response
|
||||
|
||||
setGameData(response.value)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`${logPrefix.exception}.getGameDataExternal: `, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return { webGameData, getGameDataExternal }
|
||||
}
|
||||
97
layers/composables/useGetGameMaintenance.ts
Normal file
97
layers/composables/useGetGameMaintenance.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { ReqGameMaintenance, ResGameMaintenance } from '#layers/types/GameMaintenanceType'
|
||||
|
||||
/**
|
||||
* 게임 점검
|
||||
*/
|
||||
const useGetGameMaintenance = () => {
|
||||
const inspectionStore = useInspectionStore()
|
||||
const logPrefix = {
|
||||
exception: '[Exception] /composables/useGetGameMaintenance',
|
||||
failure: '[Failure] /composables/useGetGameMaintenance'
|
||||
}
|
||||
const isGameMaintenance = ref(false) // 게임 서버 점검 여부
|
||||
|
||||
// [Setter] 게임 서버 점검 여부 세팅
|
||||
const setIsGameMaintenance = (status: boolean) => {
|
||||
isGameMaintenance.value = status
|
||||
}
|
||||
|
||||
// 게임 점검이 아닌 경우 일괄 세팅
|
||||
const setGameMaintenanceFalse = () => {
|
||||
setIsGameMaintenance(false)
|
||||
inspectionStore.setGameMaintenanceStatus(false)
|
||||
inspectionStore.setGameMaintenanceData({ ts_start_date: 0, ts_end_date: 0, detail_link: '' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 게임 서버 점검 여부
|
||||
*
|
||||
* @param {ReqGameMaintenance} req
|
||||
* @description https://wiki.smilegate.net/pages/viewpage.action?pageId=362619887
|
||||
*/
|
||||
const checkGameMaintenance = async (req: ReqGameMaintenance) => {
|
||||
let res: ResGameMaintenance = {} as ResGameMaintenance
|
||||
try {
|
||||
const baseApiUrl = req.baseApiUrl || ''
|
||||
|
||||
// Path Variables
|
||||
const category = req.category || 'GAME'
|
||||
const serviceId1 = req.service_id1 || ''
|
||||
const lang = req.lang || 'ko'
|
||||
|
||||
const url = `${baseApiUrl}/v2.0/maintenances/${category}/${serviceId1}/${lang}`
|
||||
|
||||
res = (await commonFetch('GET', url, {})) as ResGameMaintenance
|
||||
|
||||
if (res != null && res.code === 0) {
|
||||
// FIXME: 테스트용 데이터 ---------------------------------------------------
|
||||
/* const config = useRuntimeConfig()
|
||||
if (['local', 'local-gate8', 'dev'].includes(`${config.public.runType}`)) {
|
||||
res.value = {
|
||||
total_count: 1,
|
||||
list: [
|
||||
{
|
||||
start_at: new Date().getTime(),
|
||||
end_at: new Date().getTime(),
|
||||
languages: [{ link: 'https://www.onstove.com', lang: 'ko', title: '', content: '' }],
|
||||
maintenance_no: 0,
|
||||
category: '',
|
||||
service_id1: '',
|
||||
service_id2: [],
|
||||
type: '',
|
||||
description: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
} */
|
||||
// ------------------------------------------------------------------------
|
||||
if (Number(res.value?.total_count) > 0 && res.value?.list != null && res.value?.list.length > 0) {
|
||||
setIsGameMaintenance(true) // 서버 1개 이상 점검일 경우 점검 중으로 간주
|
||||
inspectionStore.setGameMaintenanceData({
|
||||
ts_start_date: res.value?.list[0].start_at || 0,
|
||||
ts_end_date: res.value?.list[0].end_at || 0,
|
||||
detail_link: res.value?.list[0].languages[0].link || ''
|
||||
})
|
||||
inspectionStore.setGameMaintenanceStatus(true)
|
||||
} else {
|
||||
setGameMaintenanceFalse()
|
||||
}
|
||||
} else {
|
||||
// [500] 내부 서버 에러
|
||||
// [70001] 부적절한 엑세스 토큰
|
||||
// [70051] 부적절한 파라미터 요청 - {param_key}
|
||||
// [70052] 데이터를 찾을 수 않음
|
||||
setGameMaintenanceFalse()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`${logPrefix.exception}.checkGameMaintenance: `, e)
|
||||
res = { code: -99999, message: `${e}` }
|
||||
setGameMaintenanceFalse()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return { isGameMaintenance, checkGameMaintenance }
|
||||
}
|
||||
|
||||
export { useGetGameMaintenance }
|
||||
70
layers/composables/useGetInspectionDataExternal.ts
Normal file
70
layers/composables/useGetInspectionDataExternal.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { WebInspectionData, ReqGetInspectionData, ResGetInspectionData } from '#layers/types/InspectionType'
|
||||
|
||||
/**
|
||||
* 웹 점검
|
||||
*/
|
||||
export const useGetInspectionDataExternal = () => {
|
||||
const inspectionStore = useInspectionStore()
|
||||
const logPrefix = {
|
||||
exception: '[Exception] /composables/useGetInspectionDataExternal',
|
||||
failure: '[Failure] /composables/useGetInspectionDataExternal'
|
||||
}
|
||||
const webInspectionData = ref<WebInspectionData | null>(null)
|
||||
const isWebInspection = ref(false) // 웹 점검 여부
|
||||
|
||||
// [Setter] 웹 점검 여부 세팅
|
||||
const setIsWebInspection = (status: boolean) => {
|
||||
isWebInspection.value = status
|
||||
}
|
||||
|
||||
/**
|
||||
* 웹 점검 여부
|
||||
*
|
||||
* @param {ReqGetInspectionData} req
|
||||
* @description https://wiki.smilegate.net/pages/viewpage.action?pageId=563198067
|
||||
*/
|
||||
const getInspectionDataExternal = async (req: ReqGetInspectionData) => {
|
||||
// const config = useRuntimeConfig()
|
||||
const apiUrl = `${req.baseApiUrl}/pub-comm/v3.0/inspection/${req.gameId}`
|
||||
|
||||
try {
|
||||
const response = (await commonFetch('GET', apiUrl)) as ResGetInspectionData
|
||||
console.log("🚀 ~ getInspectionDataExternal ~ response:", response)
|
||||
|
||||
// FIXME: 테스트용 데이터 ---------------------------------------------------
|
||||
/* if (['local', 'local-gate8', 'dev'].includes(`${config.public.runType}`)) {
|
||||
response.value = {
|
||||
inspection_status: 1,
|
||||
inspection: {
|
||||
inspection_status: 1,
|
||||
start_date: '2025-09-19 10:00:00',
|
||||
end_date: '2025-09-19 12:00:00',
|
||||
ts_start_date: new Date().getTime(),
|
||||
ts_end_date: new Date().getTime(),
|
||||
back_ground_image_type: 'image',
|
||||
back_ground_image_url: 'https://www.onstove.com',
|
||||
inspection_title1: '',
|
||||
inspection_title2: ''
|
||||
}
|
||||
}
|
||||
} */
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (response?.value && response.value.inspection) {
|
||||
webInspectionData.value = response.value.inspection
|
||||
isWebInspection.value = response.value.inspection_status === 1
|
||||
|
||||
inspectionStore.setWebInspectionData(webInspectionData.value)
|
||||
inspectionStore.setWebInspectionStatus(isWebInspection.value)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`${logPrefix.exception}.getInspectionDataExternal: `, e)
|
||||
}
|
||||
|
||||
if (webInspectionData.value !== null) {
|
||||
setIsWebInspection(isWebInspection.value)
|
||||
}
|
||||
}
|
||||
|
||||
return { webInspectionData, isWebInspection, getInspectionDataExternal }
|
||||
}
|
||||
6
layers/layouts/onlyStove.vue
Normal file
6
layers/layouts/onlyStove.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LayoutsStoveHeader />
|
||||
<slot />
|
||||
</template>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
||||
import type { GameDataRequest, GameDataValue } from '#layers/types/api/gameData'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
// 서버 사이드에서는 스킵
|
||||
@@ -6,13 +6,6 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
return
|
||||
}
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
|
||||
// gameData가 로드되지 않았으면 스킵 (다른 미들웨어에서 로드됨)
|
||||
if (!gameDataStore.gameData) {
|
||||
return
|
||||
}
|
||||
|
||||
// 현재 경로에서 언어 코드 추출
|
||||
// 예: /ko/about/story -> ko
|
||||
// 예: /en/test/page -> en
|
||||
@@ -20,10 +13,23 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
const match = to.path.match(languagePattern)
|
||||
const currentLangCode = match ? match[1] : null
|
||||
|
||||
// 허용된 언어 코드 목록
|
||||
const allowedLangCodes = gameDataStore.gameData.lang_codes || []
|
||||
//현재 url에서 게임 도메인만 추출
|
||||
const currentDomain = window.location.hostname;
|
||||
const req: GameDataRequest = {
|
||||
gameDomain: `${currentDomain}`,
|
||||
langCode: `${currentLangCode}`,
|
||||
}
|
||||
const { getGameDataExternal } = useGetGameDataExternal()
|
||||
await getGameDataExternal(req)
|
||||
|
||||
// 현재 언어가 허용된 언어 목록에 없으면 404로 리다이렉트
|
||||
const gameDataStore = useGameDataStore()
|
||||
const gameData = gameDataStore.gameData as GameDataValue
|
||||
const langCodes = gameData?.lang_codes
|
||||
|
||||
// 허용된 언어 코드 목록
|
||||
const allowedLangCodes = langCodes || []
|
||||
|
||||
// 현재 언어가 허용된 언어 목록에 없으면 에러 페이지로 이동
|
||||
if (currentLangCode && !allowedLangCodes.includes(currentLangCode)) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
|
||||
46
layers/middleware/inspection.ts
Normal file
46
layers/middleware/inspection.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
try {
|
||||
if (import.meta.client) {
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
console.log("🚀 ~ 00000 gameData:", gameData.value)
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
// const baseDomain = `${config.public.baseDomain}`
|
||||
const stoveApiUrl = `${config.public.stoveApiUrl}`
|
||||
const stoveGameId = `${gameData.value.game_id}`
|
||||
// const stoveMaintenanceApiUrl = `${config.public.stoveMaintenanceApiUrl}`
|
||||
|
||||
// const localeCookie = useCookie('LOCALE', {
|
||||
// domain: baseDomain
|
||||
// })
|
||||
|
||||
const finalLocale = csrGetFinalLocale(to.path)
|
||||
// localeCookie.value = finalLocale.toUpperCase()
|
||||
|
||||
// 웹 점검 -----
|
||||
const { isWebInspection, getInspectionDataExternal } = useGetInspectionDataExternal()
|
||||
await getInspectionDataExternal({ baseApiUrl: stoveApiUrl, gameId: stoveGameId })
|
||||
|
||||
|
||||
// 게임 점검 -----
|
||||
// const { checkGameMaintenance } = useGetGameMaintenance()
|
||||
// await checkGameMaintenance({
|
||||
// baseApiUrl: stoveMaintenanceApiUrl,
|
||||
// category: 'GAME',
|
||||
// service_id1: stoveGameId,
|
||||
// lang: `${finalLocale}`.toLowerCase()
|
||||
// })
|
||||
|
||||
if (isWebInspection.value && !to.path.includes('inspection') && !to.path.includes('api')) {
|
||||
// 점검 중인 경우
|
||||
return navigateTo(`/${finalLocale}/inspection`, { external: true })
|
||||
} else if (!isWebInspection.value && to.path?.indexOf('inspection') !== -1) {
|
||||
// 점검이 종료된 후 점검 페이지 접근시 메인으로 리다이렉트
|
||||
return navigateTo(`/${finalLocale}`, { external: true })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Exception] /middleware/inspection: ', e)
|
||||
}
|
||||
})
|
||||
@@ -16,6 +16,10 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
|
||||
|
||||
try {
|
||||
if(to.path.includes('inspection')) {
|
||||
return
|
||||
}
|
||||
|
||||
const pageUrl = getPathAfterLanguage(to.path)
|
||||
|
||||
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
|
||||
@@ -36,9 +40,17 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
loading: true,
|
||||
})) as PageDataResponse | null
|
||||
|
||||
console.log("🚀 ~ response?.code:", response?.code)
|
||||
// if(response?.code === 91003) {
|
||||
// throw createError({
|
||||
// statusCode: 404,
|
||||
// statusMessage: 'Page not found',
|
||||
// })
|
||||
// }
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
store.setPageData(response.value)
|
||||
console.log('🚀 ~ pageData:', response.value)
|
||||
// console.log('🚀 ~ pageData:', response.value)
|
||||
} else {
|
||||
store.clearPageData()
|
||||
}
|
||||
|
||||
12
layers/plugins/error-handler.ts
Normal file
12
layers/plugins/error-handler.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
|
||||
console.log("🚀 000000 ~ error:", error)
|
||||
// handle error, e.g. report to a service
|
||||
}
|
||||
|
||||
// Also possible
|
||||
nuxtApp.hook('vue:error', (error, instance, info) => {
|
||||
console.log("🚀1111 ~ error:", error)
|
||||
// handle error, e.g. report to a service
|
||||
})
|
||||
})
|
||||
11
layers/server/api/clientIp.ts
Normal file
11
layers/server/api/clientIp.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { getTrueClientIp } from '#layers/utils/apiUtil'
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
let clientIP = ''
|
||||
try {
|
||||
clientIP = getTrueClientIp(event.node.req)
|
||||
} catch (e) {
|
||||
console.error('[Exception] /server/api/clientIp - Cannot Get Client IP: ', e)
|
||||
}
|
||||
return clientIP || ''
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import {
|
||||
getHeader,
|
||||
getRequestHost,
|
||||
@@ -6,24 +7,179 @@ import {
|
||||
} 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.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 url = getRequestURL(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)
|
||||
|
||||
// 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)
|
||||
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) {
|
||||
@@ -37,38 +193,139 @@ export default defineEventHandler(async event => {
|
||||
const stoveApiUrlServer = config.public.stoveApiUrlServer
|
||||
const apiUrl = `${stoveApiUrlServer}/pub-comm/v1.0/template/game`
|
||||
|
||||
const langCode = ssrGetFinalLocale(
|
||||
event?.node.req.url,
|
||||
event.node.req.headers
|
||||
)
|
||||
// console.log("🚀 ~ apiUrl:", apiUrl)
|
||||
|
||||
// 2. 언어 코드 추출
|
||||
const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
|
||||
|
||||
// URL의 첫 번째 path를 lang_code로 사용 (파비콘, API 경로 제외)
|
||||
// const pathSegments = url.pathname
|
||||
// .split('/')
|
||||
// .filter(
|
||||
// segment =>
|
||||
// segment &&
|
||||
// !segment.includes('favicon') &&
|
||||
// !segment.includes('api') &&
|
||||
// !segment.startsWith('_')
|
||||
// )
|
||||
// const langCode = pathSegments[0] || 'ko'
|
||||
|
||||
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)
|
||||
// console.log('🚀 ~ gameData:', response.value)
|
||||
|
||||
// 점검 데이터 조회
|
||||
let inspectionData
|
||||
if (cache.has(cacheKey)) {
|
||||
inspectionData = cache.get(cacheKey) as WebInspectionData
|
||||
} else {
|
||||
// 점검 데이터 조회
|
||||
if (response?.value?.game_id) {
|
||||
const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response?.value?.game_id}`
|
||||
// 직접 $fetch 사용 (composable 사용하지 않음)
|
||||
const inspectionResponse = await $fetch<ResGetInspectionData>(inspectionApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
inspectionData = inspectionResponse?.value?.inspection
|
||||
cache.set(cacheKey, inspectionData) // 캐시에 저장
|
||||
// console.log("🚀 ~ inspectionData:", inspectionData)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4. 현재 시간과 점검 기간 비교
|
||||
const currentTime = Date.now()
|
||||
const tsStartDate = inspectionData?.ts_start_date || 0
|
||||
const tsEndDate = inspectionData?.ts_end_date || 0
|
||||
const timeUntilInspectionSeconds = Math.floor((tsStartDate - currentTime) / 1000)
|
||||
|
||||
// 5. 점검 상태별 캐시 설정
|
||||
if (inspectionData?.inspection_status === 1 && currentTime >= tsStartDate && currentTime <= tsEndDate) {
|
||||
/**
|
||||
* 점검 중인 경우
|
||||
* - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ
|
||||
* - 점검 URL 경로가 아닐 경우 no-cache 설정
|
||||
* - 화이트 리스트 체크
|
||||
*/
|
||||
// 현재 경로가 점검 페이지가 아닐 경우 리다이렉트
|
||||
const inspectionPath = `/${finalLocale}/inspection`
|
||||
if (fullPath !== inspectionPath) {
|
||||
setCacheHeaders(event, 'no-cache')
|
||||
}
|
||||
|
||||
// 점검 중일 때 IP 필터링 활성화 여부 확인
|
||||
if (inspectionData?.ip_filter_use_yn === 'Y') {
|
||||
const clientIP = getTrueClientIp(event.node.req as any)
|
||||
|
||||
// 허용된 IP 목록 확인
|
||||
if (!inspectionData?.ip_filter_list?.includes(clientIP)) {
|
||||
// 허용되지 않은 IP인 경우 점검 페이지로 이동
|
||||
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 (error) {
|
||||
console.error('gameData load error:', error)
|
||||
}
|
||||
|
||||
68
layers/server/plugins/nitroPlugin.ts
Normal file
68
layers/server/plugins/nitroPlugin.ts
Normal 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']
|
||||
}
|
||||
})
|
||||
})
|
||||
36
layers/stores/inspectionStore.ts
Normal file
36
layers/stores/inspectionStore.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { WebInspectionData } from '#layers/types/InspectionType'
|
||||
import type { GameMaintenanceData } from '#layers/types/GameMaintenanceType'
|
||||
|
||||
export const useInspectionStore = defineStore('inspection', () => {
|
||||
const webInspectionData = ref<WebInspectionData | null>(null) // 웹 점검 정보
|
||||
const webInspectionStatus = ref<boolean | null>(null) // 웹 점검 상태
|
||||
const gameMaintenanceData = ref<GameMaintenanceData | null>(null) // 게임 점검 정보
|
||||
const gameMaintenanceStatus = ref<boolean | null>(null) // 게임 점검 상태
|
||||
|
||||
const setWebInspectionData = (data: WebInspectionData) => {
|
||||
webInspectionData.value = data
|
||||
}
|
||||
|
||||
const setWebInspectionStatus = (status: boolean) => {
|
||||
webInspectionStatus.value = status
|
||||
}
|
||||
|
||||
const setGameMaintenanceData = (data: GameMaintenanceData) => {
|
||||
gameMaintenanceData.value = data
|
||||
}
|
||||
|
||||
const setGameMaintenanceStatus = (status: boolean) => {
|
||||
gameMaintenanceStatus.value = status
|
||||
}
|
||||
|
||||
return {
|
||||
webInspectionData,
|
||||
webInspectionStatus,
|
||||
gameMaintenanceData,
|
||||
gameMaintenanceStatus,
|
||||
setWebInspectionData,
|
||||
setWebInspectionStatus,
|
||||
setGameMaintenanceData,
|
||||
setGameMaintenanceStatus
|
||||
}
|
||||
})
|
||||
13
layers/stores/useCallerInfoStore.ts
Normal file
13
layers/stores/useCallerInfoStore.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const useCallerInfoStore = defineStore('callerInfoStore', () => {
|
||||
const callerId = ref<string | null>('')
|
||||
const callerDetail = ref<string | null>('')
|
||||
|
||||
const setCallerId = (paramCallerId: string | null) => {
|
||||
callerId.value = paramCallerId
|
||||
}
|
||||
const setCallerDetail = (paramCalleDetail: string | null) => {
|
||||
callerDetail.value = paramCalleDetail
|
||||
}
|
||||
|
||||
return { callerId, callerDetail, setCallerId, setCallerDetail }
|
||||
})
|
||||
115
layers/stores/useCommonStore.ts
Normal file
115
layers/stores/useCommonStore.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useWindowSize, useWindowScroll } from '@vueuse/core'
|
||||
|
||||
interface DeviceMode {
|
||||
mode: 'desktop' | 'mobile'
|
||||
browser: 'chrome' | 'crawler' | 'edge' | 'firefox' | 'safari' | null
|
||||
isDesktop: boolean
|
||||
isMobile: boolean
|
||||
isTablet: boolean
|
||||
isIos: boolean
|
||||
isAndroid: boolean
|
||||
isDeviceReady: boolean
|
||||
}
|
||||
|
||||
export const useCommonStore = defineStore('commonStore', () => {
|
||||
const stoveGnbHeight = 48
|
||||
|
||||
const useDeviceData = useDevice()
|
||||
const { width: windowWidth, height: windowHeight } = useWindowSize()
|
||||
const { x: windowX, y: windowY } = useWindowScroll({ behavior: 'smooth' })
|
||||
|
||||
const device = ref<DeviceMode>({
|
||||
mode: useDeviceData.isMobile || useDeviceData.isTablet ? 'mobile' : 'desktop',
|
||||
browser: useDeviceData.isChrome
|
||||
? 'chrome'
|
||||
: useDeviceData.isCrawler
|
||||
? 'crawler'
|
||||
: useDeviceData.isEdge
|
||||
? 'edge'
|
||||
: useDeviceData.isFirefox
|
||||
? 'firefox'
|
||||
: useDeviceData.isSafari
|
||||
? 'safari'
|
||||
: null,
|
||||
isDesktop: useDeviceData.isDesktop,
|
||||
isMobile: useDeviceData.isMobile,
|
||||
isTablet: useDeviceData.isTablet,
|
||||
isIos: useDeviceData.isIos,
|
||||
isAndroid: useDeviceData.isAndroid,
|
||||
isDeviceReady: false
|
||||
})
|
||||
|
||||
const isPassedStoveGnb = ref(false)
|
||||
const scrollFixedXValue = ref('0px')
|
||||
const footerRef = ref<HTMLElement | null>(null)
|
||||
const isLoading = ref<boolean>(true)
|
||||
const isScrollLock = ref<boolean>(false)
|
||||
|
||||
const updateDeviceMode = () => {
|
||||
device.value.mode = useDeviceData.isMobile || useDeviceData.isTablet ? 'mobile' : 'desktop'
|
||||
device.value.browser = useDeviceData.isChrome
|
||||
? 'chrome'
|
||||
: useDeviceData.isCrawler
|
||||
? 'crawler'
|
||||
: useDeviceData.isEdge
|
||||
? 'edge'
|
||||
: useDeviceData.isFirefox
|
||||
? 'firefox'
|
||||
: useDeviceData.isSafari
|
||||
? 'safari'
|
||||
: null
|
||||
device.value.isDesktop = useDeviceData.isDesktop
|
||||
device.value.isMobile = useDeviceData.isMobile
|
||||
device.value.isTablet = useDeviceData.isTablet
|
||||
device.value.isIos = useDeviceData.isIos
|
||||
device.value.isAndroid = useDeviceData.isAndroid
|
||||
device.value.isDeviceReady = true
|
||||
}
|
||||
|
||||
const updateIsPassedStoveGnb = () => {
|
||||
isPassedStoveGnb.value = windowY.value >= stoveGnbHeight
|
||||
|
||||
if (isPassedStoveGnb.value) {
|
||||
scrollFixedXValue.value = `-${windowX.value}px`
|
||||
} else {
|
||||
scrollFixedXValue.value = '0px'
|
||||
}
|
||||
}
|
||||
|
||||
const isLoadingComplete = () => {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const scrollLock = () => {
|
||||
isScrollLock.value = !isScrollLock.value
|
||||
}
|
||||
|
||||
const addScrollLock = () => {
|
||||
isScrollLock.value = true
|
||||
}
|
||||
|
||||
const removeScrollLock = () => {
|
||||
isScrollLock.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
device,
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
windowX,
|
||||
windowY,
|
||||
isPassedStoveGnb,
|
||||
scrollFixedXValue,
|
||||
footerRef,
|
||||
isLoading,
|
||||
isScrollLock,
|
||||
|
||||
updateDeviceMode,
|
||||
updateIsPassedStoveGnb,
|
||||
isLoadingComplete,
|
||||
scrollLock,
|
||||
addScrollLock,
|
||||
removeScrollLock
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { StoveJsService } from '@/layers/types/Stove'
|
||||
import type { StoveJsService } from '#layers/types/Stove'
|
||||
|
||||
export type ClassType = HTMLAttributes['class']
|
||||
|
||||
@@ -8,3 +8,58 @@ declare global {
|
||||
stoveJsService?: StoveJsService
|
||||
}
|
||||
}
|
||||
interface CommonRequestType {
|
||||
baseApiUrl: string
|
||||
gameId: string
|
||||
}
|
||||
|
||||
interface CommonResponseType {
|
||||
code?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
interface CommonPeriodType {
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
interface ParsedCustomLinkOptions {
|
||||
tm: (key: string) => { txt: string }
|
||||
query?: Record<string, any>
|
||||
}
|
||||
|
||||
|
||||
// 타입 정의
|
||||
interface FooterMenuItem {
|
||||
title: string
|
||||
url: string
|
||||
target?: string
|
||||
active?: string
|
||||
}
|
||||
|
||||
interface GameRatingInfo {
|
||||
title: string
|
||||
company_name: string
|
||||
rating_grade: string
|
||||
reg_no: string
|
||||
prod_date: string
|
||||
rating_class_no: string
|
||||
rating_type: string
|
||||
content_info: string
|
||||
}
|
||||
|
||||
interface FooterData {
|
||||
use_game_rating: boolean
|
||||
game_rating_info: GameRatingInfo
|
||||
use_dev_ci_url?: boolean
|
||||
dev_ci_url?: string
|
||||
dev_ci_img_path?: string
|
||||
fund_display_yn?: string
|
||||
fund_display_url?: string
|
||||
}
|
||||
|
||||
interface DevCiConfig {
|
||||
dev_ci_yn: boolean
|
||||
dev_ci_img_path: string
|
||||
}
|
||||
|
||||
export type { CommonRequestType, CommonResponseType, CommonPeriodType, ParsedCustomLinkOptions, FooterMenuItem, GameRatingInfo, FooterData, DevCiConfig }
|
||||
|
||||
100
layers/types/DataizationType.ts
Normal file
100
layers/types/DataizationType.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { CommonPeriodType } from '#layers/types/Common'
|
||||
|
||||
// [S] Type in czn_homepage_brand_siteConfig.json ----------------------------------------
|
||||
interface GnbMenuType {
|
||||
id: string
|
||||
title: string
|
||||
link: string
|
||||
target: string
|
||||
displayLocales?: Array<string>
|
||||
}
|
||||
|
||||
interface GnbType extends GnbMenuType {
|
||||
depth2List?: Array<GnbMenuType>
|
||||
}
|
||||
|
||||
interface SnsType {
|
||||
id: string
|
||||
title: string
|
||||
link: string
|
||||
sub: string
|
||||
key?: string
|
||||
log?: object
|
||||
}
|
||||
|
||||
interface LoreType {
|
||||
loreNo: number
|
||||
chapter: number // 1 : 프롤로그, 2 ~ : N장
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface CharacterCardType {
|
||||
id: string
|
||||
}
|
||||
|
||||
interface CharacterType {
|
||||
id: string
|
||||
cardList: Array<CharacterCardType>
|
||||
}
|
||||
|
||||
interface FooterMenuType {
|
||||
id: string
|
||||
title: string
|
||||
link: string
|
||||
target: string
|
||||
active: string
|
||||
}
|
||||
|
||||
interface MediaType {
|
||||
id: string
|
||||
title: string
|
||||
logCode?: string
|
||||
}
|
||||
|
||||
interface MarketType {
|
||||
id: string
|
||||
code: string
|
||||
link: string
|
||||
}
|
||||
|
||||
// [E] Type in czn_homepage_brand_siteConfig.json ----------------------------------------
|
||||
interface ReqGetDataization {
|
||||
baseApiUrl: string
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
interface DataizationType {
|
||||
gnbList?: Array<GnbType>
|
||||
mainVideo: CommonPeriodType
|
||||
promotionList?: Array<PromotionPreregistType>
|
||||
characterList?: Array<CharacterType>
|
||||
loreList?: Array<LoreType>
|
||||
footerMenuList?: Array<FooterMenuType>
|
||||
mediaList?: Array<MediaType>
|
||||
sectionList?: Array<string>
|
||||
marketList?: Array<MarketType>
|
||||
}
|
||||
|
||||
interface ResGetDataization {
|
||||
code: number
|
||||
message: string
|
||||
value?: {
|
||||
dataization?: DataizationType
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
// [S] Type in czn_homepage_brand_siteConfig.json ----------------------------------------
|
||||
GnbType,
|
||||
SnsType,
|
||||
MediaType,
|
||||
LoreType,
|
||||
PromotionPreregistType,
|
||||
FooterMenuType,
|
||||
MarketType,
|
||||
// [E] Type in czn_homepage_brand_siteConfig.json ----------------------------------------
|
||||
DataizationType,
|
||||
ReqGetDataization,
|
||||
ResGetDataization
|
||||
}
|
||||
44
layers/types/GameMaintenanceType.ts
Normal file
44
layers/types/GameMaintenanceType.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { CommonRequestType, CommonResponseType } from './Common'
|
||||
|
||||
/*************************************************************************
|
||||
* 게임 점검
|
||||
************************************************************************/
|
||||
interface ReqGameMaintenance extends CommonRequestType {
|
||||
// Path Variables
|
||||
category: string
|
||||
service_id1: string
|
||||
lang: string
|
||||
}
|
||||
interface Language {
|
||||
lang: string
|
||||
title: string
|
||||
content: string
|
||||
link: string
|
||||
}
|
||||
interface GameMaintenance {
|
||||
maintenance_no: number // 점검 번호
|
||||
category: string // 카테고리
|
||||
service_id1: string // 서비스 ID1
|
||||
service_id2: Array<string> // 서비스 ID2(String Array), service_id1 전체를 설정할 경우 ["*"]로 등록해야 함.
|
||||
type: string // 점검타입(REGULAR / TEMPORARY / URGENT)
|
||||
languages: Array<Language> // 다국어 리스트 정보
|
||||
description: string // 설명
|
||||
start_at: number // UTC기준 점검 시작일(milli-timestamp(13digit))
|
||||
end_at: number // UTC기준 점검 종료일(milli-timestamp(13digit))
|
||||
}
|
||||
interface DtoGameMaintenance {
|
||||
total_count: number
|
||||
list: Array<GameMaintenance>
|
||||
}
|
||||
interface ResGameMaintenance extends CommonResponseType {
|
||||
value?: DtoGameMaintenance
|
||||
error?: string
|
||||
}
|
||||
|
||||
// 게임 점검 데이터
|
||||
interface GameMaintenanceData {
|
||||
ts_start_date: number // 게임 점검 시작 타임스탬프
|
||||
ts_end_date: number // 게임 점검 종료 타임스탬프
|
||||
detail_link?: string // 게임 점검 공지 링크
|
||||
}
|
||||
export type { ReqGameMaintenance, ResGameMaintenance, GameMaintenanceData }
|
||||
39
layers/types/InspectionType.ts
Normal file
39
layers/types/InspectionType.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { CommonRequestType, CommonResponseType } from './Common'
|
||||
|
||||
/*************************************************************************
|
||||
* 웹 점검
|
||||
************************************************************************/
|
||||
interface WebInspectionData {
|
||||
inspection_status: number // 점검 상태 (0: 정상, 1: 점검 중) (단순 운영툴 설정 점검 값)
|
||||
start_date: string // 점검 시작 날짜 (문자열 형식)
|
||||
end_date: string // 점검 종료 날짜 (문자열 형식)
|
||||
ts_start_date: number // 점검 시작 타임스탬프
|
||||
ts_end_date: number // 점검 종료 타임스탬프
|
||||
back_ground_image_type?: string // 배경 이미지 타입 (0: 없음, 기타 값: 특정 타입)
|
||||
back_ground_image_url?: string // 배경 이미지 URL
|
||||
movie_yn?: string // 동영상 사용 여부 ("Y" 또는 "N")
|
||||
movie_url?: string // 동영상 URL
|
||||
inspection_title_type?: string // 점검 제목 타입
|
||||
inspection_title1: string // 점검 제목 1
|
||||
inspection_title2: string // 점검 제목 2
|
||||
inspection_content?: string // 점검 내용
|
||||
|
||||
// Internal -----
|
||||
ip_filter_use_yn?: string // IP 필터 사용 여부 ("Y" 또는 "N")
|
||||
ip_filter_list?: string[] // 허용된 IP 목록
|
||||
launching_status?: number // 런칭 여부 (0: 런칭 전, 1: 런칭 후)
|
||||
}
|
||||
|
||||
interface ReqGetInspectionData extends CommonRequestType {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
interface DtoGetInspectionData {
|
||||
inspection_status?: number // 점검 여부 + 점검 시간 + 화이트 리스트 고려하여 계산된 결과
|
||||
inspection?: WebInspectionData
|
||||
}
|
||||
interface ResGetInspectionData extends CommonResponseType {
|
||||
value?: DtoGetInspectionData
|
||||
}
|
||||
|
||||
export type { WebInspectionData, ReqGetInspectionData, ResGetInspectionData }
|
||||
@@ -4,6 +4,10 @@ export interface GameDataRequest {
|
||||
lang_code: string
|
||||
q?: string
|
||||
qc?: string
|
||||
baseApiUrl: string
|
||||
gameId: string
|
||||
gameDomain: string
|
||||
langCode: string
|
||||
}
|
||||
|
||||
// API 응답 데이터 타입
|
||||
|
||||
168
layers/utils/commonUtil.ts
Normal file
168
layers/utils/commonUtil.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { ParsedCustomLinkOptions } from '#layers/types/Common'
|
||||
|
||||
/**
|
||||
* 페이지 - 유효성 체크
|
||||
*
|
||||
* @param {number} page - 페이지
|
||||
* @param {number} totalPage - 총 페이지 수
|
||||
*/
|
||||
const checkPageValidation = (page: number, totalPage: number) => {
|
||||
// 최소, 최대 범위 체크
|
||||
if (page < 1) {
|
||||
page = 1
|
||||
} else if (page > totalPage) {
|
||||
page = totalPage
|
||||
}
|
||||
return page
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 다운로드 함수
|
||||
*
|
||||
* @param {string} fileUrl - 다운로드할 파일의 URL
|
||||
* @param {string} fileName - 저장할 파일 이름 (옵션)
|
||||
*/
|
||||
const csrDownloadFile = (fileUrl: string, fileName?: string) => {
|
||||
const link = document.createElement('a')
|
||||
link.href = fileUrl
|
||||
|
||||
// 파일 이름이 제공되면 다운로드 이름 설정
|
||||
if (fileName) {
|
||||
link.download = fileName
|
||||
}
|
||||
|
||||
// 링크를 클릭하여 다운로드 트리거
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
// DOM에서 링크 제거
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
/**
|
||||
* 마케팅 코드 조회
|
||||
*/
|
||||
const csrGetMarketingCode = () => {
|
||||
const route = useRoute()
|
||||
const mcode = Number(`${route.query.mcode != null && route.query.mcode !== '' ? route.query.mcode : ''}`)
|
||||
return isNaN(mcode) ? undefined : mcode
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 링크 이동 (새 창)
|
||||
*
|
||||
* @param {string} link - 이동할 외부 링크
|
||||
*/
|
||||
const csrGoExternalLink = (link: string = '') => {
|
||||
window.open(link, '_blank')
|
||||
}
|
||||
|
||||
/**
|
||||
* QA용 국가 코드 조회
|
||||
*/
|
||||
const csrGetQc = () => {
|
||||
const route = useRoute()
|
||||
const qc = `${route.query.qc != null && route.query.qc !== '' ? route.query.qc : ''}`
|
||||
return qc
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열이 숫자인지 확인
|
||||
*
|
||||
* @param {string} str - 확인할 문자열
|
||||
*/
|
||||
const isNumeric = (str: string): boolean => {
|
||||
return /^-?\d+(\.\d+)?$/.test(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* 가공된 링크 파싱
|
||||
*
|
||||
* @param {string} link - 원본 링크
|
||||
* @param {Function} tm - i18n의 tm 함수 (예: (key) => ({ txt: string }))
|
||||
* @param {any} query - 추가 쿼리 파라미터
|
||||
*/
|
||||
const getParsedCustomLink = (link: string, { tm, query = {} }: ParsedCustomLinkOptions) => {
|
||||
const config = useRuntimeConfig()
|
||||
let result = `${link || ''}`
|
||||
|
||||
// @c{key} 패턴 치환 (예: @c{stoveCommunityUrl})
|
||||
if (link.includes('@c')) {
|
||||
result = result.replace(/@c\{(.*?)\}/g, (_, key) => {
|
||||
// config.public에서 해당 key 값을 찾아 치환
|
||||
return typeof config.public[key] === 'string' ? config.public[key] : ''
|
||||
})
|
||||
}
|
||||
|
||||
// @m{key} 패턴 치환 (예: @m{Community_Channel_Key})
|
||||
if (link.includes('@m')) {
|
||||
result = result.replace(/@m\{(.*?)\}/g, (_, key) => {
|
||||
// tm 함수로 변환하여 치환
|
||||
return tm(key)?.txt ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
// @q{key} 패턴 치환 (예: @q{ppid})
|
||||
if (link.includes('@q')) {
|
||||
result = result.replace(/@q\{(.*?)\}/g, (_, key) => {
|
||||
let q = ''
|
||||
if (query[key]) {
|
||||
q += result.includes('?') ? '&' : '?'
|
||||
q += `${key}=${query[key]}`
|
||||
}
|
||||
return q
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠키 설정 - 만료기간 하루 단위 셋팅
|
||||
*
|
||||
* @param {string} name - 쿠키 이름
|
||||
* @param {string} value - 쿠키 값
|
||||
* @param {number} exp - 만료기간 (옵션)
|
||||
*/
|
||||
const setCookieForDay = (name: string, value: string, exp?: number) => {
|
||||
const date = new Date()
|
||||
if (!exp) {
|
||||
exp = 1
|
||||
}
|
||||
date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000)
|
||||
|
||||
const setCookie = useCookie(name, {
|
||||
expires: new Date(date),
|
||||
path: '/'
|
||||
})
|
||||
|
||||
setCookie.value = value
|
||||
}
|
||||
|
||||
// 정적 파일인지 확인하는 함수
|
||||
const isStaticFile = (path: string): boolean => {
|
||||
return /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|scss)$/i.test(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* 기준값이 최솟값 ~ 최댓값에 속하는지 확인
|
||||
*
|
||||
* @param {number} ref - 기준값
|
||||
* @param {number} min - 최솟값
|
||||
* @param {number} max - 최댓값
|
||||
*/
|
||||
const isInRange = (ref: number, min: number, max: number): boolean => {
|
||||
return ref >= min && ref <= max
|
||||
}
|
||||
|
||||
export {
|
||||
checkPageValidation,
|
||||
csrDownloadFile,
|
||||
csrGetMarketingCode,
|
||||
csrGoExternalLink,
|
||||
csrGetQc,
|
||||
isNumeric,
|
||||
getParsedCustomLink,
|
||||
setCookieForDay,
|
||||
isStaticFile,
|
||||
isInRange
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import type { OperateComponents } from '#layers/types/api/resourcesData'
|
||||
export const getLayoutType = (
|
||||
pageData: PageDataValue | null
|
||||
): 'default' | 'promotion' => {
|
||||
return pageData?.page_type === 1 ? 'default' : 'promotion'
|
||||
return pageData?.page_type === 2 ? 'promotion' : 'default'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,6 +145,52 @@ export const getImagePaths = (resourcesData: PageDataResourceGroup) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const formatDateOffset = ({
|
||||
ts,
|
||||
lang,
|
||||
useSeconds,
|
||||
useTimezone
|
||||
}: {
|
||||
ts: number
|
||||
lang: string
|
||||
useSeconds?: boolean
|
||||
useTimezone?: boolean
|
||||
}) => {
|
||||
const offset = { ko: 9, ja: 9, 'zh-tw': 8, en: 0 }[lang] || 0
|
||||
const date = new Date(ts + offset * 3600000)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
|
||||
const year = date.getUTCFullYear()
|
||||
const month = date.getUTCMonth() + 1
|
||||
const day = date.getUTCDate()
|
||||
const hours = date.getUTCHours()
|
||||
const minutes = date.getUTCMinutes()
|
||||
const seconds = date.getUTCSeconds()
|
||||
|
||||
if (lang === 'ko') {
|
||||
let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}`
|
||||
format += useSeconds ? `:${pad(seconds)}` : ''
|
||||
format += useTimezone ? ' (KST)' : ''
|
||||
return `${format}`
|
||||
} else if (lang === 'zh-tw') {
|
||||
let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}`
|
||||
format += useSeconds ? `:${pad(seconds)}` : ''
|
||||
format += useTimezone ? ` (UTC${offset > 0 ? '+' + offset : ''})` : ''
|
||||
return `${format}`
|
||||
} else if (lang === 'ja') {
|
||||
let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}`
|
||||
format += useSeconds ? `:${pad(seconds)}` : ''
|
||||
format += useTimezone ? ' (日本時間)' : ''
|
||||
return `${format}`
|
||||
} else {
|
||||
//= en
|
||||
let format = `${pad(month)}/${pad(day)}/${year} ${pad(hours)}:${pad(minutes)}`
|
||||
format += useSeconds ? `:${pad(seconds)}` : ''
|
||||
format += useTimezone ? ' (UTC)' : ''
|
||||
return `${format}`
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 컴포넌트 그룹의 첫 번째 데이터를 반환합니다.
|
||||
* @param components props.components 또는 group 객체
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -63,7 +63,7 @@ export default defineNuxtConfig({
|
||||
payloadExtraction: false,
|
||||
},
|
||||
typescript: {
|
||||
typeCheck: true,
|
||||
typeCheck: false,
|
||||
strict: false,
|
||||
},
|
||||
nitro: {
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@nuxtjs/device": "^3.2.4",
|
||||
"@nuxtjs/i18n": "^10.0.6",
|
||||
"@pinia/nuxt": "^0.6.1",
|
||||
"@seed-next/date": "^0.0.0",
|
||||
"@splidejs/splide": "^4.1.4",
|
||||
"@splidejs/vue-splide": "^0.6.12",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
@@ -55,6 +56,7 @@
|
||||
"eslint-plugin-nuxt": "^4.0.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"lru-cache": "^11.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
|
||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ importers:
|
||||
'@pinia/nuxt':
|
||||
specifier: ^0.6.1
|
||||
version: 0.6.1(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))
|
||||
'@seed-next/date':
|
||||
specifier: ^0.0.0
|
||||
version: 0.0.0
|
||||
'@splidejs/splide':
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.4
|
||||
@@ -96,6 +99,9 @@ importers:
|
||||
eslint-plugin-vue:
|
||||
specifier: ^10.4.0
|
||||
version: 10.4.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.5.1)))
|
||||
lru-cache:
|
||||
specifier: ^11.1.0
|
||||
version: 11.2.2
|
||||
postcss:
|
||||
specifier: ^8.5.6
|
||||
version: 8.5.6
|
||||
@@ -302,6 +308,12 @@ packages:
|
||||
peerDependencies:
|
||||
postcss-selector-parser: ^7.0.0
|
||||
|
||||
'@date-fns/tz@1.4.1':
|
||||
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
|
||||
|
||||
'@date-fns/utc@2.1.1':
|
||||
resolution: {integrity: sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==}
|
||||
|
||||
'@emnapi/core@1.5.0':
|
||||
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
|
||||
|
||||
@@ -1496,6 +1508,9 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@seed-next/date@0.0.0':
|
||||
resolution: {integrity: sha1-d6+dtjsFjxR4SGWSuBpJrxZCgwc=, tarball: https://git.sginfra.net/api/v4/projects/3708/packages/npm/@seed-next/date/-/@seed-next/date-0.0.0.tgz}
|
||||
|
||||
'@sindresorhus/is@7.0.2':
|
||||
resolution: {integrity: sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2288,6 +2303,9 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
db0@0.3.2:
|
||||
resolution: {integrity: sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw==}
|
||||
peerDependencies:
|
||||
@@ -3246,6 +3264,10 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.2.2:
|
||||
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
@@ -5087,6 +5109,10 @@ snapshots:
|
||||
dependencies:
|
||||
postcss-selector-parser: 7.1.0
|
||||
|
||||
'@date-fns/tz@1.4.1': {}
|
||||
|
||||
'@date-fns/utc@2.1.1': {}
|
||||
|
||||
'@emnapi/core@1.5.0':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.1.0
|
||||
@@ -6253,6 +6279,12 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.50.0':
|
||||
optional: true
|
||||
|
||||
'@seed-next/date@0.0.0':
|
||||
dependencies:
|
||||
'@date-fns/tz': 1.4.1
|
||||
'@date-fns/utc': 2.1.1
|
||||
date-fns: 4.1.0
|
||||
|
||||
'@sindresorhus/is@7.0.2': {}
|
||||
|
||||
'@sindresorhus/merge-streams@2.3.0': {}
|
||||
@@ -7155,6 +7187,8 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
db0@0.3.2: {}
|
||||
|
||||
de-indent@1.0.2: {}
|
||||
@@ -8147,6 +8181,8 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
BIN
public/images/common/img_error.png
Normal file
BIN
public/images/common/img_error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
3
public/images/common/logo-stove.svg
Normal file
3
public/images/common/logo-stove.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.2 KiB |
286
temp/middleware.ts
Normal file
286
temp/middleware.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
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)
|
||||
|
||||
// 허용된 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -14,6 +14,6 @@
|
||||
"types/**/*",
|
||||
"layers/**/*",
|
||||
"app/**/*"
|
||||
],
|
||||
, "temp/inspection.ts", "temp/middleware.ts" ],
|
||||
"exclude": [".nuxt/types/**/*", "node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user