feat: 점검 작업 중
This commit is contained in:
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/
|
||||
@@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<div>
|
||||
<h1>Dang</h1>
|
||||
<p>It looks like something broke.</p>
|
||||
<p>Sorry about that.</p>
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
325
app/pages/inspection/index.vue
Normal file
325
app/pages/inspection/index.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<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>
|
||||
|
||||
<div class="inspection-content">
|
||||
<!-- 점검 메시지 -->
|
||||
<h1 class="inspection-title">
|
||||
<template v-if="isClient && webInspectionData?.inspection_title1">
|
||||
{{ webInspectionData.inspection_title1 }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ tm('Inspection_Now_Maintenance') }}
|
||||
</template>
|
||||
</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="card-title"></h3>
|
||||
<div class="button-group">
|
||||
<a
|
||||
:href="communityUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inspection-btn inspection-btn-outline"
|
||||
>
|
||||
<span>{{ tm('Inspection_Community_Btn') || '공식 커뮤니티' }}</span>
|
||||
<AtomsIconsLongArrowRightLine :size="16" color="#1F1F1F" />
|
||||
</a>
|
||||
<button
|
||||
class="inspection-btn inspection-btn-primary"
|
||||
@click="handleGameStart"
|
||||
>
|
||||
<span>{{ tm('game_start_btn') || '게임 실행' }}</span>
|
||||
<AtomsIconsLongArrowRightLine :size="16" color="#FFFFFF" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 다운로드 카드 -->
|
||||
<div v-if="isClient" class="inspection-card inspection-download-card">
|
||||
<h3 class="card-title">
|
||||
{{ tm('Inspection_Txt_Download') || '게임 다운로드' }}
|
||||
</h3>
|
||||
<div v-if="webInspectionData?.inspection_content" class="inspection-content-text">
|
||||
{{ webInspectionData.inspection_content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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()
|
||||
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 resultGetMultilingual = await useGetMultilingual({
|
||||
baseApiUrl: translationApi,
|
||||
fileName: 'test_common_inspection.json'
|
||||
})
|
||||
const { tm, locale } = useI18n({
|
||||
useScope: 'local',
|
||||
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)
|
||||
// zh-tw: 台灣時間 (KST)
|
||||
// ja: (JST)
|
||||
// 나머지: (KST)
|
||||
const getLocaleTimezone = (localeType: string, region) => {
|
||||
const tsStartDate = webInspectionData.value?.start_date || 0
|
||||
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)`
|
||||
case 'en':
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} ~ ${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})} (台灣時間)`
|
||||
case 'ja':
|
||||
return `${globalDateFormat(new Date(tsStartDate), localeType, region || '', {useFullDate: true})} ~ ${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)`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 날짜 포맷팅 함수 (CSR에서만 실행)
|
||||
const formatInspectionTime = (timestamp: number, isKST: boolean): string => {
|
||||
if (!import.meta.client || !timestamp) return ''
|
||||
|
||||
const lang = locale.value || 'ko'
|
||||
const formatted = formatDateOffset({
|
||||
ts: timestamp,
|
||||
lang: isKST ? lang : 'en',
|
||||
useSeconds: true,
|
||||
useTimezone: true
|
||||
})
|
||||
|
||||
// formatDateOffset의 결과를 YYYY.MM.DD, HH:MM:SS 형식으로 변환
|
||||
// KST의 경우: YYYY-MM-DD HH:MM:SS (KST) -> YYYY.MM.DD, HH:MM:SS (KST)
|
||||
// UTC의 경우: MM/DD/YYYY HH:MM:SS (UTC) -> YYYY.MM.DD, HH:MM:SS (UTC)
|
||||
if (isKST) {
|
||||
return formatted.replace(/-/g, '.').replace(' ', ', ')
|
||||
} else {
|
||||
// UTC 형식 변환: MM/DD/YYYY HH:MM:SS (UTC) -> YYYY.MM.DD, HH:MM:SS (UTC)
|
||||
const parts = formatted.match(/(\d{2})\/(\d{2})\/(\d{4}) (.+)/)
|
||||
if (parts) {
|
||||
return `${parts[3]}.${parts[1]}.${parts[2]}, ${parts[4]}`
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
}
|
||||
|
||||
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 || ''
|
||||
})
|
||||
|
||||
// 커뮤니티 URL
|
||||
const communityUrl = computed(() => {
|
||||
return '#'
|
||||
})
|
||||
|
||||
// 게임 시작
|
||||
const { validateLauncher } = useCheckGameStart()
|
||||
const handleGameStart = () => {
|
||||
validateLauncher()
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['inspection'],
|
||||
layout: 'inspection',
|
||||
showLoading: false
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.inspection-section {
|
||||
@apply flex flex-col items-center gap-8 px-10 py-[120px] pb-[200px] min-h-screen;
|
||||
background-color: #F0F0F0;
|
||||
}
|
||||
|
||||
.inspection-logo {
|
||||
@apply w-[944px] h-[150px] 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 text-[24px] leading-[34px] font-bold tracking-[-0.72px] text-[#1F1F1F];
|
||||
font-family: 'Spoqa Han Sans Neo', sans-serif;
|
||||
}
|
||||
|
||||
.inspection-cards {
|
||||
@apply flex flex-col gap-5 w-full;
|
||||
}
|
||||
|
||||
.inspection-card {
|
||||
@apply bg-white rounded-2xl 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;
|
||||
}
|
||||
|
||||
.inspection-time {
|
||||
@apply flex flex-col items-center 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;
|
||||
}
|
||||
|
||||
.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 text-left text-[18px] 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];
|
||||
}
|
||||
|
||||
.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;
|
||||
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-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;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.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-[#C7AE8B] border-[#C7AE8B];
|
||||
}
|
||||
|
||||
.inspection-btn-primary span {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
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 }
|
||||
69
layers/composables/useGetInspectionDataExternal.ts
Normal file
69
layers/composables/useGetInspectionDataExternal.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
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
|
||||
|
||||
// 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/inspection.vue
Normal file
6
layers/layouts/inspection.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LayoutsHeader />
|
||||
<slot />
|
||||
</template>
|
||||
42
layers/middleware/inspection.ts
Normal file
42
layers/middleware/inspection.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
try {
|
||||
if (import.meta.client) {
|
||||
const config = useRuntimeConfig()
|
||||
// const baseDomain = `${config.public.baseDomain}`
|
||||
const stoveApiUrl = `${config.public.stoveApiUrl}`
|
||||
console.log("🚀 ~ stoveApiUrl:", stoveApiUrl)
|
||||
// const stoveGameId = `${config.public.stoveGameId}`
|
||||
// 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: 'STOVE_LORD' })
|
||||
|
||||
// 게임 점검 -----
|
||||
// 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.matched) {
|
||||
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 || ''
|
||||
})
|
||||
@@ -6,8 +6,13 @@ import {
|
||||
} from 'h3'
|
||||
import { ssrGetFinalLocale } from '../../utils/localeUtil'
|
||||
import type { GameDataResponse } from '../../types/api/gameData'
|
||||
import type { ResGetInspectionData } from '../../types/InspectionType'
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const iBaseApiUrl = `${config.public.stoveApiUrlServer}`
|
||||
|
||||
const url = getRequestURL(event)
|
||||
|
||||
// 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리
|
||||
@@ -36,24 +41,12 @@ 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
|
||||
)
|
||||
|
||||
// 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,
|
||||
@@ -67,8 +60,43 @@ export default defineEventHandler(async event => {
|
||||
event.context.gameData = response.value
|
||||
event.context.googleAnalyticsId = response.value?.ga_code
|
||||
|
||||
console.log('🚀 ~ gameData:', response.value)
|
||||
// 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)
|
||||
|
||||
if (inspectionData?.inspection_status === 0 ) {
|
||||
/**
|
||||
* 점검 중인 경우
|
||||
* - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ
|
||||
* - 점검 URL 경로가 아닐 경우 no-cache 설정
|
||||
* - 화이트 리스트 체크
|
||||
*/
|
||||
// 현재 경로가 점검 페이지가 아닐 경우 리다이렉트
|
||||
const inspectionPath = `/${langCode}/inspection`
|
||||
|
||||
if (!url.pathname.includes('/inspection')) {
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', inspectionPath)
|
||||
event.node.res.end()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('gameData load error:', error)
|
||||
}
|
||||
|
||||
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,23 @@ 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>
|
||||
}
|
||||
|
||||
export type { CommonRequestType, CommonResponseType, CommonPeriodType, ParsedCustomLinkOptions }
|
||||
|
||||
101
layers/types/DataizationType.ts
Normal file
101
layers/types/DataizationType.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { PromotionPreregistType } from '@/types/promotion/PreregistType'
|
||||
import type { CommonPeriodType } from '@/types/CommonType'
|
||||
|
||||
// [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 }
|
||||
38
layers/types/InspectionType.ts
Normal file
38
layers/types/InspectionType.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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 목록
|
||||
}
|
||||
|
||||
interface ReqGetInspectionData extends CommonRequestType {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
interface DtoGetInspectionData {
|
||||
inspection_status?: number // 점검 여부 + 점검 시간 + 화이트 리스트 고려하여 계산된 결과
|
||||
inspection?: WebInspectionData
|
||||
}
|
||||
interface ResGetInspectionData extends CommonResponseType {
|
||||
value?: DtoGetInspectionData
|
||||
}
|
||||
|
||||
export type { WebInspectionData, ReqGetInspectionData, ResGetInspectionData }
|
||||
168
layers/utils/commonUtil.ts
Normal file
168
layers/utils/commonUtil.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { ParsedCustomLinkOptions } from '@/types/CommonType'
|
||||
|
||||
/**
|
||||
* 페이지 - 유효성 체크
|
||||
*
|
||||
* @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
|
||||
}
|
||||
@@ -154,3 +154,50 @@ export const getCurrentTimestamp = (unit: 'ms' | 's' = 'ms'): number => {
|
||||
const now = Date.now()
|
||||
return unit === 's' ? Math.floor(now / 1000) : now
|
||||
}
|
||||
|
||||
|
||||
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}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
38
pnpm-lock.yaml
generated
38
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
|
||||
@@ -106,7 +112,7 @@ importers:
|
||||
specifier: ^3.4.17
|
||||
version: 3.4.17
|
||||
typescript:
|
||||
specifier: ^5.5.0
|
||||
specifier: ^5.3.3
|
||||
version: 5.9.2
|
||||
vue-tsc:
|
||||
specifier: ^3.0.7
|
||||
@@ -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
|
||||
|
||||
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