feat: 점검 페이지 추가, 언어 정책 추가
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const currentLayout = computed(() => getLayoutType(pageData.value))
|
||||
|
||||
definePageMeta({
|
||||
layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화
|
||||
middleware: ['inspection']
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ console.log("🚀 ~ currentLayout:", currentLayout)
|
||||
|
||||
definePageMeta({
|
||||
layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화
|
||||
middleware: ['inspection']
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,101 +1,107 @@
|
||||
<template>
|
||||
<section class="inspection-section">
|
||||
<!-- 로고 -->
|
||||
<div v-if="isClient" class="inspection-logo">
|
||||
<img :src="logoImgUrl" alt="logo" class="w-full h-full object-contain" />
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<template v-if="isClient && webInspectionData?.inspection_title1">
|
||||
{{ webInspectionData.inspection_title1 }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="inspection-content">
|
||||
<!-- 점검 메시지 -->
|
||||
|
||||
<h1 class="inspection-title text-lg md:text-2xl">
|
||||
{{ tm('Inspection_Now_Maintenance') }}
|
||||
</template>
|
||||
</h1>
|
||||
</h1>
|
||||
|
||||
<div class="inspection-cards">
|
||||
<!-- 점검 시간 카드 -->
|
||||
<div v-if="isClient && webInspectionData" class="inspection-card inspection-time-card">
|
||||
<h2 class="card-title">{{ tm('Inspection_Maintenance_Time') }}</h2>
|
||||
<div class="inspection-time">
|
||||
<div class="time-row">
|
||||
{{ getLocaleTimezone(locale) }}
|
||||
</div>
|
||||
<div class="time-row">
|
||||
{{ getLocaleTimezone('en', 'US') }}
|
||||
</div>
|
||||
<div class="time-row">
|
||||
{{ getLocaleTimezone('zh-tw', '') }}
|
||||
</div>
|
||||
<div class="time-row">
|
||||
{{ getLocaleTimezone('ja', '') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 온스토브 & 다운로드 카드 -->
|
||||
<div class="inspection-bottom-cards">
|
||||
<!-- 온스토브 카드 -->
|
||||
<div class="inspection-card inspection-stove-card">
|
||||
<h3 v-dompurify-html="tm('Inspection_Game_During_Maintenance') || '홈페이지 점검 중에도 게임과 공식 커뮤니티는 그대로 이용할 수 있어요!'" :class="{ 'text-center': !launchingStatus }" class="card-title"></h3>
|
||||
<div class="button-group justify-center">
|
||||
<a
|
||||
:href="communityUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inspection-btn inspection-btn-outline inspection-btn-community"
|
||||
>
|
||||
<span>{{ tm('Inspection_Community_Btn') || '공식 커뮤니티' }}</span>
|
||||
<AtomsIconsLongArrowRightLine :size="16" color="#1F1F1F" />
|
||||
</a>
|
||||
<AtomsButtonVariant
|
||||
v-if="launchingStatus"
|
||||
type="custom"
|
||||
platform="pc"
|
||||
class="inspection-btn inspection-btn-primary w-full md:w-auto color-black"
|
||||
@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 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('ko', '')" class="time-row"></div>
|
||||
<div v-dompurify-html="getLocaleTimezone('en', 'US')" class="time-row"></div>
|
||||
<div v-dompurify-html="getLocaleTimezone('zh-tw', '')" class="time-row"></div>
|
||||
<div v-dompurify-html="getLocaleTimezone('ja', '')" class="time-row"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 다운로드 카드 -->
|
||||
<div v-if="isClient" class="inspection-card inspection-download-card">
|
||||
<h3 class="card-title">
|
||||
{{ tm('Inspection_Txt_Download') || '게임 다운로드' }}
|
||||
</h3>
|
||||
<div v-if="enabledMarkets.length > 1" class="flex flex-row gap-3">
|
||||
<template v-for="btn in enabledMarkets" :key="btn.platform">
|
||||
<BlocksButtonDownload
|
||||
:class="getButtonClass(btn.platform)"
|
||||
:platform="btn.platform as any"
|
||||
:url="btn.url"
|
||||
type="single"
|
||||
variant="outlined"
|
||||
<!-- 온스토브 & 다운로드 카드 -->
|
||||
<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 }"
|
||||
>
|
||||
{{ getButtonText(btn.platform) }}
|
||||
</BlocksButtonDownload>
|
||||
</template>
|
||||
<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>
|
||||
</div>
|
||||
</clientOnly>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { globalDateFormat } from '@seed-next/date';
|
||||
import { formatDateOffset } from '#layers/utils/dataUtil'
|
||||
import { useCheckGameStart } from '#layers/composables/useGameStart'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
@@ -120,9 +126,6 @@ const { tm, locale } = useI18n({
|
||||
messages: Object(resultGetMultilingual.value.multilingual)
|
||||
})
|
||||
|
||||
console.log("🚀 ~ globalDateFormat(new Date(webInspectionData.value?.ts_start_date || 0), 'ko'):", )
|
||||
|
||||
|
||||
// locale에 따라 뒤에 KST 또는 UTC 추가 ko, en, zh-tw, ja
|
||||
// ko: (KST)
|
||||
// en: (UTC)
|
||||
@@ -134,74 +137,73 @@ const getLocaleTimezone = (localeType: string, region) => {
|
||||
const tsEndDate = webInspectionData.value?.end_date || 0
|
||||
switch (localeType) {
|
||||
case 'ko':
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} ~ ${globalDateFormat(new Date(tsEndDate), localeType, region || 'KR', {useFullDate: true})} (KST)`
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), localeType, region || 'KR', {useFullDate: true})} (KST)`
|
||||
case 'en':
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} ~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (UTC)`
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (UTC)`
|
||||
case 'zh-tw':
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} ~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (台灣時間)`
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (台灣時間)`
|
||||
case 'ja':
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} ~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (JST)`
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (JST)`
|
||||
default:
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} ~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (KST)`
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} <br class="md:hidden">~ ${globalDateFormat(new Date(tsEndDate), localeType, region || '', {useFullDate: true})} (KST)`
|
||||
}
|
||||
}
|
||||
|
||||
const launchingStatus = computed(() => {
|
||||
return !!(webInspectionData.value?.launching_status)
|
||||
return webInspectionData.value?.launching_status
|
||||
})
|
||||
|
||||
|
||||
// market_json에서 use_yn === 1 인 항목만 배열로 변환
|
||||
|
||||
// market_json:
|
||||
// { pc: { url: 'https://apps.apple.com/app/id1234567890', use_yn: 0 },
|
||||
// app_store: { url: 'https://apps.apple.com/app/id1234567890', use_yn: 1 },
|
||||
// google_play: { url: 'https://play.google.com/store/apps/details?id=example', use_yn: 1 } },
|
||||
|
||||
|
||||
// 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(() => {
|
||||
// CSR에서만 처리
|
||||
if (!import.meta.client || !webInspectionData.value) return ''
|
||||
|
||||
const currentLocale = locale.value || 'ko'
|
||||
const localeData = (webInspectionData.value as any)?.[currentLocale]
|
||||
if (localeData?.img_json?.bi_large) {
|
||||
return localeData.img_json.bi_large
|
||||
}
|
||||
return webInspectionData.value.back_ground_image_url || ''
|
||||
return getResolvedHost(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) => {
|
||||
const hasPc = enabledMarkets.value.some(btn => btn.platform === 'pc')
|
||||
|
||||
// pc가 있으면 pc만 flex-1, 나머지는 기본
|
||||
if (hasPc) {
|
||||
return platform === 'pc' ? 'flex-1' : ''
|
||||
const hasPc = enabledMarkets.value.some(btn => btn.platform === 'pc')
|
||||
if(hasPc) {
|
||||
return platform === 'pc' ? `flex-1 btn-platform-pc` : ``
|
||||
}
|
||||
|
||||
// pc가 없으면 모든 버튼에 flex-1
|
||||
return 'flex-1'
|
||||
//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')
|
||||
@@ -213,7 +215,6 @@ const getButtonText = (platform: string) => {
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
const { validateLauncher } = useCheckGameStart()
|
||||
const handleGameStart = () => {
|
||||
validateLauncher()
|
||||
@@ -246,8 +247,7 @@ definePageMeta({
|
||||
}
|
||||
|
||||
.inspection-title {
|
||||
@apply text-center text-[24px] leading-[34px] font-bold tracking-[-0.72px] text-[#1F1F1F];
|
||||
font-family: 'Spoqa Han Sans Neo', sans-serif;
|
||||
@apply text-center font-bold tracking-[-0.72px] text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-cards {
|
||||
@@ -255,27 +255,24 @@ definePageMeta({
|
||||
}
|
||||
|
||||
.inspection-card {
|
||||
@apply bg-white rounded-2xl p-8;
|
||||
@apply bg-white rounded-2xl p-6 md:p-8;
|
||||
}
|
||||
|
||||
.inspection-time-card {
|
||||
@apply flex flex-col items-center gap-4;
|
||||
width: 944px;
|
||||
min-height: 162px;
|
||||
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@apply text-center text-[20px] leading-[30px] font-bold tracking-[-0.6px] text-[#1F1F1F];
|
||||
font-family: 'Spoqa Han Sans Neo', sans-serif;
|
||||
@apply text-center leading-[30px] font-bold tracking-[-0.6px] text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-time {
|
||||
@apply flex flex-col items-center gap-2;
|
||||
@apply flex flex-col items-center gap-0 md:gap-2;
|
||||
}
|
||||
|
||||
.time-row {
|
||||
@apply text-center text-[16px] leading-[26px] font-medium tracking-[-0.48px] text-[#1F1F1F];
|
||||
font-family: 'Spoqa Han Sans Neo', sans-serif;
|
||||
@apply text-center leading-[26px] font-medium tracking-[-0.48px] text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-bottom-cards {
|
||||
@@ -288,16 +285,15 @@ definePageMeta({
|
||||
}
|
||||
|
||||
.inspection-stove-card .card-title {
|
||||
@apply text-left text-[18px] leading-[26px] font-bold tracking-[-0.54px] text-[#1F1F1F];
|
||||
@apply leading-[26px] font-bold tracking-[-0.54px] text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-download-card .card-title {
|
||||
@apply text-left text-[18px] leading-[26px] font-bold tracking-[-0.54px] text-[#1F1F1F];
|
||||
@apply text-left leading-[26px] font-bold tracking-[-0.54px] text-[#1F1F1F];
|
||||
}
|
||||
|
||||
.inspection-content-text {
|
||||
@apply text-left text-[16px] leading-[26px] font-medium tracking-[-0.48px] text-[#1F1F1F];
|
||||
font-family: 'Spoqa Han Sans Neo', sans-serif;
|
||||
@apply text-left leading-[26px] font-medium tracking-[-0.48px] text-[#1F1F1F];
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
@@ -306,15 +302,10 @@ definePageMeta({
|
||||
}
|
||||
|
||||
.inspection-btn {
|
||||
@apply flex items-center justify-center gap-1 px-10 h-12 rounded-lg border border-black/10;
|
||||
font-family: 'Spoqa Han Sans Neo', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.42px;
|
||||
@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;
|
||||
flex: 1;
|
||||
|
||||
}
|
||||
|
||||
.inspection-btn span {
|
||||
@@ -364,6 +355,13 @@ definePageMeta({
|
||||
}
|
||||
}
|
||||
.inspection-btn-community {
|
||||
@apply min-w-[170px] px-4 rounded-lg flex-none;
|
||||
@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>
|
||||
|
||||
@@ -1,24 +1,5 @@
|
||||
<template>
|
||||
<div class="select-language" :class="{ 'language-changing': isChanging }">
|
||||
<!-- <select
|
||||
v-model="selectedLocale"
|
||||
:disabled="isChanging"
|
||||
class="text-black px-2 py-1 rounded-md"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': isChanging }"
|
||||
@change="switchLanguage"
|
||||
>
|
||||
<option
|
||||
v-for="localeOption in availableLanguages"
|
||||
:key="localeOption"
|
||||
:value="localeOption"
|
||||
>
|
||||
{{ localeOption }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="isChanging" class="ml-2 text-sm text-gray-500">
|
||||
변경 중...
|
||||
</span>
|
||||
-->
|
||||
|
||||
<button
|
||||
:disabled="isChanging"
|
||||
@@ -82,6 +63,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const config = useRuntimeConfig()
|
||||
const baseDomain = `${config.public.baseDomain}`
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
|
||||
// 사용 가능한 언어 목록
|
||||
@@ -141,12 +125,21 @@ const switchLanguage = async () => {
|
||||
isChanging.value = true
|
||||
|
||||
try {
|
||||
// URL 경로를 통해 언어 변경
|
||||
const path = switchLocalePath(selectedLocale.value as any)
|
||||
if (path) {
|
||||
// 페이지 데이터 초기화 (새로운 언어로 다시 로드되도록)
|
||||
pageDataStore.clearPageData()
|
||||
window.location.href = path
|
||||
// URL 경로를 통해 언어 변경
|
||||
const path = switchLocalePath(selectedLocale.value as any)
|
||||
if (path) {
|
||||
// 언어 쿠키 설정 (클라이언트 사이드) - 페이지 이동 전에 설정
|
||||
const localeCookie = useCookie('LOCALE', {
|
||||
domain: baseDomain,
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 365, // 1년 (초 단위)
|
||||
sameSite: 'lax'
|
||||
})
|
||||
localeCookie.value = selectedLocale.value.toUpperCase()
|
||||
|
||||
// 페이지 데이터 초기화 (새로운 언어로 다시 로드되도록)
|
||||
pageDataStore.clearPageData()
|
||||
window.location.href = path
|
||||
// 언어 변경 및 라우팅
|
||||
// await setLocale(selectedLocale.value as any)
|
||||
|
||||
|
||||
@@ -119,15 +119,10 @@
|
||||
class="nx3 ml-2.5 md:ml-4"
|
||||
>
|
||||
<img
|
||||
src="https://static-pubcomm.gate8.com/local/template/l9/common/logo_nx3.png"
|
||||
:src="`${staticUrl}${setDevCi.dev_ci_img_path}`"
|
||||
alt="CI"
|
||||
class="w-auto h-auto"
|
||||
/>
|
||||
<!-- <img
|
||||
:src="setDevCi.dev_ci_img_path"
|
||||
alt="CI"
|
||||
class="w-auto h-auto"
|
||||
/> -->
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -144,6 +139,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FooterMenuItem, FooterData, DevCiConfig } from '#layers/types/Common'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const staticUrl = config.public.staticUrl
|
||||
@@ -163,33 +159,7 @@ const { tm } = useI18n({
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
// 타입 정의
|
||||
interface FooterMenuItem {
|
||||
title: string
|
||||
url: string
|
||||
target?: string
|
||||
active?: string
|
||||
}
|
||||
|
||||
interface GameRatingInfo {
|
||||
company_name: string
|
||||
rating_grade: string
|
||||
reg_no: string
|
||||
prod_date: string
|
||||
rating_class_no: string
|
||||
rating_type: string
|
||||
content_info: string
|
||||
}
|
||||
|
||||
interface FooterData {
|
||||
game_rating_info: GameRatingInfo
|
||||
use_dev_ci_url?: string
|
||||
}
|
||||
|
||||
interface DevCiConfig {
|
||||
dev_ci_yn: boolean
|
||||
dev_ci_img_path: string
|
||||
}
|
||||
const path = ref<string>(`${staticUrl}/local/template/${gameData.value.s3_folder_name}`)
|
||||
|
||||
// 공통다국어 data
|
||||
const footerLinks = computed((): FooterMenuItem[] => {
|
||||
@@ -207,42 +177,43 @@ const getGameRatingImage = computed((): string[] => {
|
||||
const contentInfo = footerData.value.game_rating_info.rating_type.split(',')
|
||||
// rating_type 12, 15, 18, 19 에 따라 이미지명을 가져오고 이미지를 반환
|
||||
return contentInfo.map(item => {
|
||||
if (item === '12') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/Type12.svg`
|
||||
} else if (item === '15') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/Type15.svg`
|
||||
} else if (item === '19') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/Type19.svg`
|
||||
} else if (item === 'all') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/TypeAll.svg`
|
||||
} else if (item === 'e') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/TypeExempt.svg`
|
||||
} else {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_age/TypeTest.svg`
|
||||
switch (item) {
|
||||
case '12':
|
||||
return `${path.value}/common/grades_age/Type12.svg`
|
||||
case '15':
|
||||
return `${path.value}/common/grades_age/Type15.svg`
|
||||
case '19':
|
||||
return `${path.value}/common/grades_age/Type19.svg`
|
||||
case 'all':
|
||||
return `${path.value}/common/grades_age/TypeAll.svg`
|
||||
case 'e':
|
||||
return `${path.value}/common/grades_age/TypeExempt.svg`
|
||||
default:
|
||||
return `${path.value}/common/grades_age/TypeTest.svg`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const getContentInfoImage = computed((): string[] => {
|
||||
const contentInfo = footerData.value.game_rating_info.content_info.split(',')
|
||||
contentInfo.pop()
|
||||
|
||||
return contentInfo.map(item => {
|
||||
if (item === '1') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-sexual.svg`
|
||||
} else if (item === '2') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-fear.svg`
|
||||
} else if (item === '3') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-inapposite.svg`
|
||||
} else if (item === '4') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-drug.svg`
|
||||
} else if (item === '5') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-crime.svg`
|
||||
} else if (item === '6') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-speculation.svg`
|
||||
} else if (item === '7') {
|
||||
return `${staticUrl}/local/template/${gameData.value.s3_folder_name}/common/grades_use/Type-violence.svg`
|
||||
switch (item) {
|
||||
case '1':
|
||||
return `${path.value}/common/grades_use/Type-sexual.svg`
|
||||
case '2':
|
||||
return `${path.value}/common/grades_use/Type-fear.svg`
|
||||
case '3':
|
||||
return `${path.value}/common/grades_use/Type-inapposite.svg`
|
||||
case '4':
|
||||
return `${path.value}/common/grades_use/Type-drug.svg`
|
||||
case '5':
|
||||
return `${path.value}/common/grades_use/Type-crime.svg`
|
||||
case '6':
|
||||
return `${path.value}/common/grades_use/Type-speculation.svg`
|
||||
case '7':
|
||||
return `${path.value}/common/grades_use/Type-violence.svg`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
try {
|
||||
if (import.meta.client) {
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
// const baseDomain = `${config.public.baseDomain}`
|
||||
const stoveApiUrl = `${config.public.stoveApiUrl}`
|
||||
console.log("🚀 ~ stoveApiUrl:", stoveApiUrl)
|
||||
// const stoveGameId = `${config.public.stoveGameId}`
|
||||
const stoveGameId = `${gameData.value.game_id}`
|
||||
// const stoveMaintenanceApiUrl = `${config.public.stoveMaintenanceApiUrl}`
|
||||
|
||||
/* const localeCookie = useCookie('LOCALE', {
|
||||
domain: baseDomain
|
||||
}) */
|
||||
// const localeCookie = useCookie('LOCALE', {
|
||||
// domain: baseDomain
|
||||
// })
|
||||
|
||||
// const finalLocale = csrGetFinalLocale(to.path)
|
||||
const finalLocale = csrGetFinalLocale(to.path)
|
||||
console.log("🚀 ~ finalLocale:", finalLocale)
|
||||
// localeCookie.value = finalLocale.toUpperCase()
|
||||
|
||||
// 웹 점검 -----
|
||||
const { isWebInspection, getInspectionDataExternal } = useGetInspectionDataExternal()
|
||||
await getInspectionDataExternal({ baseApiUrl: stoveApiUrl, gameId: 'STOVE_LORD' })
|
||||
await getInspectionDataExternal({ baseApiUrl: stoveApiUrl, gameId: stoveGameId })
|
||||
|
||||
// 게임 점검 -----
|
||||
// const { checkGameMaintenance } = useGetGameMaintenance()
|
||||
@@ -30,10 +33,10 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
|
||||
if (isWebInspection.value && !to.path.includes('inspection') && !to.path.includes('api')) {
|
||||
// 점검 중인 경우
|
||||
// return navigateTo(`/${finalLocale}/inspection`, { external: true })
|
||||
return navigateTo(`/${finalLocale}/inspection`, { external: true })
|
||||
} else if (!isWebInspection.value && to.path?.indexOf('inspection') !== -1) {
|
||||
// 점검이 종료된 후 점검 페이지 접근시 메인으로 리다이렉트
|
||||
// return navigateTo(`/${finalLocale}`, { external: true })
|
||||
return navigateTo(`/${finalLocale}`, { external: true })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
|
||||
|
||||
try {
|
||||
if(to.matched) {
|
||||
if(to.path.includes('inspection')) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
})) as PageDataResponse | null
|
||||
|
||||
console.log("🚀 ~ response?.code:", response?.code)
|
||||
if(response?.code === 91003) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Page not found',
|
||||
})
|
||||
}
|
||||
// if(response?.code === 91003) {
|
||||
// throw createError({
|
||||
// statusCode: 404,
|
||||
// statusMessage: 'Page not found',
|
||||
// })
|
||||
// }
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
store.setPageData(response.value)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import {
|
||||
getHeader,
|
||||
getRequestHost,
|
||||
@@ -7,28 +8,179 @@ import {
|
||||
import { ssrGetFinalLocale } from '../../utils/localeUtil'
|
||||
import type { GameDataResponse } from '../../types/api/gameData'
|
||||
import type { ResGetInspectionData } from '../../types/InspectionType'
|
||||
import { isStaticFile } from '#layers/utils/commonUtil'
|
||||
|
||||
/**
|
||||
* 캐시 제어 헤더를 설정하는 공통 함수
|
||||
*
|
||||
* @param event - 이벤트 객체
|
||||
* @param cacheMode - 캐시 모드 설정 ('no-cache', 'short', 'medium', 'default')
|
||||
* @param customMaxAge - 커스텀 max-age 값 (초 단위)
|
||||
*/
|
||||
function setCacheHeaders(
|
||||
event: { node: { res: { setHeader: (name: string, value: string) => void } } },
|
||||
cacheMode: 'no-cache' | 'short' | 'medium' | 'default',
|
||||
customMaxAge?: number
|
||||
): void {
|
||||
// 원래 setHeader 함수 참조 저장
|
||||
const originalSetHeader = event.node.res.setHeader
|
||||
|
||||
// Cache-Control 헤더 설정값 결정
|
||||
let cacheControl: string
|
||||
switch (cacheMode) {
|
||||
case 'no-cache':
|
||||
cacheControl = 'no-cache, no-store, must-revalidate'
|
||||
// no-cache 모드일 때는 추가 헤더도 설정
|
||||
event.node.res.setHeader('Pragma', 'no-cache')
|
||||
event.node.res.setHeader('Expires', '0')
|
||||
break
|
||||
case 'short':
|
||||
cacheControl = `public, max-age=${customMaxAge || 10}`
|
||||
break
|
||||
case 'medium':
|
||||
cacheControl = `public, max-age=${customMaxAge || 15}`
|
||||
break
|
||||
case 'default':
|
||||
default:
|
||||
cacheControl = `public, max-age=${customMaxAge || 60}`
|
||||
break
|
||||
}
|
||||
|
||||
// Cache-Control 헤더를 강제로 설정하기 위해 setHeader 메소드 오버라이드
|
||||
event.node.res.setHeader = function (name: string, value: string) {
|
||||
if (name.toLowerCase() === 'cache-control') {
|
||||
return originalSetHeader.call(this, name, cacheControl)
|
||||
}
|
||||
return originalSetHeader.call(this, name, value)
|
||||
}
|
||||
|
||||
// 바로 캐시 제어 헤더 적용
|
||||
event.node.res.setHeader('Cache-Control', cacheControl)
|
||||
}
|
||||
|
||||
const cache = new LRUCache({
|
||||
max: 100, // 캐시에 저장할 최대 항목 수
|
||||
ttl: 1000 * 30 // 30초 동안 캐시 유지
|
||||
})
|
||||
|
||||
/**
|
||||
* 최종 언어 쿠키 세팅
|
||||
*
|
||||
* @param event - 이벤트 객체
|
||||
* @param finalLocale - 최종 언어
|
||||
* @param baseDomain - 기본 도메인
|
||||
*/
|
||||
function setFinalLocaleCookie(event: any, finalLocale: string, baseDomain: string) {
|
||||
setCookie(event, 'LOCALE', finalLocale.toUpperCase(), {
|
||||
domain: baseDomain,
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 365 // 1년 (초 단위)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Locale Middleware 역할 함수
|
||||
*
|
||||
* @param event - 이벤트 객체
|
||||
* @param finalLocale - 최종 언어
|
||||
*/
|
||||
function fnLocaleMiddleware(event: any, finalLocale: string) {
|
||||
const path = event?.node.req.url || ''
|
||||
let arrPath = []
|
||||
let queryString = ''
|
||||
|
||||
if (path.includes('?')) {
|
||||
// 쿼리스트링 포함 시 순수 경로만 추출
|
||||
arrPath = path.split('?')[0].split('/')
|
||||
queryString = path.split('?')[1]
|
||||
} else {
|
||||
arrPath = path.split('/')
|
||||
queryString = ''
|
||||
}
|
||||
|
||||
// 최종 언어 세팅된 경로 생성
|
||||
const pathLocale = arrPath.length > 1 ? arrPath[1] : ''
|
||||
|
||||
// URL에서 현재 언어와 최종 언어가 다르면 리다이렉트
|
||||
if (pathLocale !== finalLocale) {
|
||||
let newLocalePath = ''
|
||||
if (pathLocale === '') {
|
||||
newLocalePath = `/${finalLocale}`
|
||||
} else {
|
||||
arrPath[1] = finalLocale
|
||||
newLocalePath = arrPath.join('/')
|
||||
}
|
||||
|
||||
if (queryString !== '') {
|
||||
newLocalePath += `?${queryString}`
|
||||
}
|
||||
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', newLocalePath)
|
||||
event.node.res.end()
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
// const runType = `${config.public.runType}`
|
||||
const iBaseApiUrl = `${config.public.stoveApiUrlServer}`
|
||||
const baseDomain = `${config.public.baseDomain}`
|
||||
console.log("🚀 ~ baseDomain:", config.public.baseDomain)
|
||||
// const url = getRequestURL(event)
|
||||
|
||||
const url = getRequestURL(event)
|
||||
// if (['local', 'local-gate8', 'dev'].includes(runType)) {
|
||||
// Sandbox 이상 환경에서만 동작 및 확인 가능 (local, dev는 통과 처리)
|
||||
// try {
|
||||
// 언어 코드 추출
|
||||
// const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
|
||||
// console.log("🚀 ~ finalLocale:", finalLocale)
|
||||
// setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// [Locale Middleware]
|
||||
// -------------------------------------------------------------------------------
|
||||
// fnLocaleMiddleware(event, finalLocale)
|
||||
// } catch (e) {
|
||||
// console.error('[Exception] /server/middleware/middleware-02-global: ', e)
|
||||
// }
|
||||
// }
|
||||
|
||||
const fullPath = event.path
|
||||
|
||||
// 1-1. 정적 파일 패스
|
||||
if (isStaticFile(event.path)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 1-2. /inspection 패스
|
||||
if (fullPath.includes('/inspection')) {
|
||||
// 리턴 되기 전 언어 쿠키 세팅
|
||||
const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
|
||||
console.log("🚀 0000 ~ finalLocale:", finalLocale)
|
||||
setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
return
|
||||
}
|
||||
|
||||
// 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리
|
||||
if (
|
||||
url.pathname.startsWith('/api/') ||
|
||||
url.pathname.startsWith('/_nuxt/') ||
|
||||
url.pathname.startsWith('/favicon') ||
|
||||
url.pathname.includes('.') ||
|
||||
url.pathname.startsWith('/_')
|
||||
fullPath.startsWith('/api/') ||
|
||||
fullPath.startsWith('/_nuxt/') ||
|
||||
fullPath.startsWith('/favicon') ||
|
||||
fullPath.includes('/assets/') ||
|
||||
fullPath.includes('.') ||
|
||||
fullPath.startsWith('/_')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// 캐시 키 생성
|
||||
const cacheKey = 'inspection'
|
||||
console.log("🚀 11111 ~ cacheKey:", cacheKey)
|
||||
|
||||
const host =
|
||||
(getHeader(event, 'host') || getRequestHost(event)).toString() || ''
|
||||
const baseDomain = process.env.BASE_DOMAIN || '.onstove.com'
|
||||
const isGameDomainExtractable = host.includes(baseDomain)
|
||||
|
||||
if (isGameDomainExtractable) {
|
||||
@@ -41,42 +193,66 @@ export default defineEventHandler(async event => {
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiUrlServer = config.public.stoveApiUrlServer
|
||||
const apiUrl = `${stoveApiUrlServer}/pub-comm/v1.0/template/game`
|
||||
let inspectionData
|
||||
const langCode = ssrGetFinalLocale(
|
||||
event?.node.req.url,
|
||||
event.node.req.headers
|
||||
)
|
||||
|
||||
|
||||
// 2. 언어 코드 추출
|
||||
const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
|
||||
|
||||
|
||||
const queryParams: Record<string, string> = {
|
||||
game_domain: event.context.gameDomain || '',
|
||||
lang_code: langCode,
|
||||
lang_code: finalLocale,
|
||||
}
|
||||
|
||||
const response = (await $fetch(apiUrl, {
|
||||
query: queryParams,
|
||||
})) as GameDataResponse | null
|
||||
|
||||
// 언어패스 쿠키 굽기 - 장기방안에서는 굽지않음
|
||||
// const langCoverages = response?.value?.lang_codes || []
|
||||
// if(langCoverages.includes(finalLocale)) {
|
||||
// setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
// }
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
event.context.gameData = response.value
|
||||
event.context.googleAnalyticsId = response.value?.ga_code
|
||||
|
||||
console.log('🚀 ~ gameData:', response.value)
|
||||
|
||||
|
||||
// 점검 데이터 조회
|
||||
if (response.value.game_id) {
|
||||
const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response.value.game_id}`
|
||||
|
||||
// 직접 $fetch 사용 (composable 사용하지 않음)
|
||||
const inspectionResponse = await $fetch<ResGetInspectionData>(inspectionApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
inspectionData = inspectionResponse?.value?.inspection
|
||||
// console.log("🚀 ~ inspectionData:", inspectionData)
|
||||
let inspectionData
|
||||
if (cache.has(cacheKey)) {
|
||||
inspectionData = cache.get(cacheKey) as WebInspectionData
|
||||
// console.log("🚀 22222 ~ 캐시키 있어 inspectionData:", inspectionData)
|
||||
} else {
|
||||
// 점검 데이터 조회
|
||||
if (response.value.game_id) {
|
||||
const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response.value.game_id}`
|
||||
// console.log("🚀 ~ inspectionApiUrl:", inspectionApiUrl)
|
||||
|
||||
// 직접 $fetch 사용 (composable 사용하지 않음)
|
||||
const inspectionResponse = await $fetch<ResGetInspectionData>(inspectionApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
inspectionData = inspectionResponse?.value?.inspection
|
||||
cache.set(cacheKey, inspectionData) // 캐시에 저장
|
||||
// console.log("🚀 ~ inspectionData:", inspectionData)
|
||||
|
||||
}
|
||||
|
||||
if (inspectionData?.inspection_status === 0) {
|
||||
|
||||
// 4. 현재 시간과 점검 기간 비교
|
||||
const currentTime = Date.now()
|
||||
const tsStartDate = inspectionData?.ts_start_date || 0
|
||||
const tsEndDate = inspectionData?.ts_end_date || 0
|
||||
const timeUntilInspectionSeconds = Math.floor((tsStartDate - currentTime) / 1000)
|
||||
|
||||
|
||||
// 5. 점검 상태별 캐시 설정
|
||||
if (inspectionData?.inspection_status === 1 && currentTime >= tsStartDate && currentTime <= tsEndDate) {
|
||||
/**
|
||||
* 점검 중인 경우
|
||||
* - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ
|
||||
@@ -84,18 +260,78 @@ export default defineEventHandler(async event => {
|
||||
* - 화이트 리스트 체크
|
||||
*/
|
||||
// 현재 경로가 점검 페이지가 아닐 경우 리다이렉트
|
||||
const inspectionPath = `/${langCode}/inspection`
|
||||
|
||||
if (!url.pathname.includes('/inspection')) {
|
||||
const inspectionPath = `/${finalLocale}/inspection`
|
||||
if (fullPath !== inspectionPath) {
|
||||
setCacheHeaders(event, 'no-cache')
|
||||
}
|
||||
|
||||
// 점검 중일 때 IP 필터링 활성화 여부 확인
|
||||
if (inspectionData?.ip_filter_use_yn === 'Y') {
|
||||
const clientIP = getTrueClientIp(event.node.req as any)
|
||||
|
||||
// 허용된 IP 목록 확인
|
||||
if (!inspectionData?.ip_filter_list?.includes(clientIP)) {
|
||||
// 허용되지 않은 IP인 경우 점검 페이지로 이동
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', inspectionPath)
|
||||
event.node.res.end()
|
||||
} else {
|
||||
// 화이트 리스트인 경우
|
||||
// -------------------------------------------------------------------------------
|
||||
// [Locale Middleware]
|
||||
// -------------------------------------------------------------------------------
|
||||
fnLocaleMiddleware(event, finalLocale)
|
||||
}
|
||||
} else {
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', inspectionPath)
|
||||
event.node.res.end()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* 점검이 아닌 경우
|
||||
* - 홈 경로는 no-cache
|
||||
* - 점검 예정 시간에 따른 캐시 설정
|
||||
* - 점검 5분 전: 짧은 캐시 (10초)
|
||||
* - 점검 30분 전: 중간 캐시 (15초)
|
||||
* - 점검 30분 이후: 기본 캐시 (60초)
|
||||
*/
|
||||
// 홈 경로: 캐시 없음
|
||||
const isHomePath = [
|
||||
'',
|
||||
'/'
|
||||
//, ...Object.values(DEFAULT_LOCALE_COVERAGES).flatMap((locale) => [`/${locale}`, `/${locale}/`])
|
||||
].includes(fullPath)
|
||||
|
||||
if (isHomePath) {
|
||||
setCacheHeaders(event, 'no-cache')
|
||||
} else {
|
||||
// 점검 예정 시간에 따른 캐시 설정
|
||||
|
||||
if (tsStartDate > 0 && timeUntilInspectionSeconds > 0) {
|
||||
if (timeUntilInspectionSeconds < 300) {
|
||||
// 점검 5분 전: 짧은 캐시 (10초)
|
||||
setCacheHeaders(event, 'short', 10)
|
||||
} else if (timeUntilInspectionSeconds < 1800) {
|
||||
// 점검 30분 전: 중간 캐시 (15초)
|
||||
setCacheHeaders(event, 'medium', 15)
|
||||
} else {
|
||||
// 점검 30분 이후: 기본 캐시 (60초)
|
||||
setCacheHeaders(event, 'default')
|
||||
}
|
||||
}
|
||||
}
|
||||
// -------------------------------------------------------------------------------
|
||||
// [Locale Middleware]
|
||||
// -------------------------------------------------------------------------------
|
||||
fnLocaleMiddleware(event, finalLocale)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('gameData load error:', error)
|
||||
|
||||
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']
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -27,4 +27,33 @@ interface ParsedCustomLinkOptions {
|
||||
query?: Record<string, any>
|
||||
}
|
||||
|
||||
export type { CommonRequestType, CommonResponseType, CommonPeriodType, ParsedCustomLinkOptions }
|
||||
|
||||
// 타입 정의
|
||||
interface FooterMenuItem {
|
||||
title: string
|
||||
url: string
|
||||
target?: string
|
||||
active?: string
|
||||
}
|
||||
|
||||
interface GameRatingInfo {
|
||||
company_name: string
|
||||
rating_grade: string
|
||||
reg_no: string
|
||||
prod_date: string
|
||||
rating_class_no: string
|
||||
rating_type: string
|
||||
content_info: string
|
||||
}
|
||||
|
||||
interface FooterData {
|
||||
game_rating_info: GameRatingInfo
|
||||
use_dev_ci_url?: string
|
||||
}
|
||||
|
||||
interface DevCiConfig {
|
||||
dev_ci_yn: boolean
|
||||
dev_ci_img_path: string
|
||||
}
|
||||
|
||||
export type { CommonRequestType, CommonResponseType, CommonPeriodType, ParsedCustomLinkOptions, FooterMenuItem, GameRatingInfo, FooterData, DevCiConfig }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user