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`
|
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if(to.matched) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const pageUrl = getPathAfterLanguage(to.path)
|
const pageUrl = getPathAfterLanguage(to.path)
|
||||||
|
|
||||||
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
|
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
|
||||||
@@ -36,9 +40,17 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|||||||
loading: true,
|
loading: true,
|
||||||
})) as PageDataResponse | null
|
})) 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) {
|
if (response?.code === 0 && 'value' in response) {
|
||||||
store.setPageData(response.value)
|
store.setPageData(response.value)
|
||||||
console.log('🚀 ~ pageData:', response.value)
|
// console.log('🚀 ~ pageData:', response.value)
|
||||||
} else {
|
} else {
|
||||||
store.clearPageData()
|
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'
|
} from 'h3'
|
||||||
import { ssrGetFinalLocale } from '../../utils/localeUtil'
|
import { ssrGetFinalLocale } from '../../utils/localeUtil'
|
||||||
import type { GameDataResponse } from '../../types/api/gameData'
|
import type { GameDataResponse } from '../../types/api/gameData'
|
||||||
|
import type { ResGetInspectionData } from '../../types/InspectionType'
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const iBaseApiUrl = `${config.public.stoveApiUrlServer}`
|
||||||
|
|
||||||
const url = getRequestURL(event)
|
const url = getRequestURL(event)
|
||||||
|
|
||||||
// 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리
|
// 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리
|
||||||
@@ -36,24 +41,12 @@ export default defineEventHandler(async event => {
|
|||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const stoveApiUrlServer = config.public.stoveApiUrlServer
|
const stoveApiUrlServer = config.public.stoveApiUrlServer
|
||||||
const apiUrl = `${stoveApiUrlServer}/pub-comm/v1.0/template/game`
|
const apiUrl = `${stoveApiUrlServer}/pub-comm/v1.0/template/game`
|
||||||
|
let inspectionData
|
||||||
const langCode = ssrGetFinalLocale(
|
const langCode = ssrGetFinalLocale(
|
||||||
event?.node.req.url,
|
event?.node.req.url,
|
||||||
event.node.req.headers
|
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> = {
|
const queryParams: Record<string, string> = {
|
||||||
game_domain: event.context.gameDomain || '',
|
game_domain: event.context.gameDomain || '',
|
||||||
lang_code: langCode,
|
lang_code: langCode,
|
||||||
@@ -67,8 +60,43 @@ export default defineEventHandler(async event => {
|
|||||||
event.context.gameData = response.value
|
event.context.gameData = response.value
|
||||||
event.context.googleAnalyticsId = response.value?.ga_code
|
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) {
|
} catch (error) {
|
||||||
console.error('gameData load error:', 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 { HTMLAttributes } from 'vue'
|
||||||
import type { StoveJsService } from '@/layers/types/Stove'
|
import type { StoveJsService } from '#layers/types/Stove'
|
||||||
|
|
||||||
export type ClassType = HTMLAttributes['class']
|
export type ClassType = HTMLAttributes['class']
|
||||||
|
|
||||||
@@ -8,3 +8,23 @@ declare global {
|
|||||||
stoveJsService?: StoveJsService
|
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()
|
const now = Date.now()
|
||||||
return unit === 's' ? Math.floor(now / 1000) : 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/device": "^3.2.4",
|
||||||
"@nuxtjs/i18n": "^10.0.6",
|
"@nuxtjs/i18n": "^10.0.6",
|
||||||
"@pinia/nuxt": "^0.6.1",
|
"@pinia/nuxt": "^0.6.1",
|
||||||
|
"@seed-next/date": "^0.0.0",
|
||||||
"@splidejs/splide": "^4.1.4",
|
"@splidejs/splide": "^4.1.4",
|
||||||
"@splidejs/vue-splide": "^0.6.12",
|
"@splidejs/vue-splide": "^0.6.12",
|
||||||
"@vueuse/core": "^13.6.0",
|
"@vueuse/core": "^13.6.0",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"eslint-plugin-nuxt": "^4.0.0",
|
"eslint-plugin-nuxt": "^4.0.0",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"eslint-plugin-vue": "^10.4.0",
|
"eslint-plugin-vue": "^10.4.0",
|
||||||
|
"lru-cache": "^11.1.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
|
|||||||
38
pnpm-lock.yaml
generated
38
pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ importers:
|
|||||||
'@pinia/nuxt':
|
'@pinia/nuxt':
|
||||||
specifier: ^0.6.1
|
specifier: ^0.6.1
|
||||||
version: 0.6.1(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))
|
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':
|
'@splidejs/splide':
|
||||||
specifier: ^4.1.4
|
specifier: ^4.1.4
|
||||||
version: 4.1.4
|
version: 4.1.4
|
||||||
@@ -96,6 +99,9 @@ importers:
|
|||||||
eslint-plugin-vue:
|
eslint-plugin-vue:
|
||||||
specifier: ^10.4.0
|
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)))
|
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:
|
postcss:
|
||||||
specifier: ^8.5.6
|
specifier: ^8.5.6
|
||||||
version: 8.5.6
|
version: 8.5.6
|
||||||
@@ -106,7 +112,7 @@ importers:
|
|||||||
specifier: ^3.4.17
|
specifier: ^3.4.17
|
||||||
version: 3.4.17
|
version: 3.4.17
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.5.0
|
specifier: ^5.3.3
|
||||||
version: 5.9.2
|
version: 5.9.2
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^3.0.7
|
specifier: ^3.0.7
|
||||||
@@ -302,6 +308,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
postcss-selector-parser: ^7.0.0
|
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':
|
'@emnapi/core@1.5.0':
|
||||||
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
|
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
|
||||||
|
|
||||||
@@ -1496,6 +1508,9 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
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':
|
'@sindresorhus/is@7.0.2':
|
||||||
resolution: {integrity: sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==}
|
resolution: {integrity: sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2288,6 +2303,9 @@ packages:
|
|||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
|
date-fns@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||||
|
|
||||||
db0@0.3.2:
|
db0@0.3.2:
|
||||||
resolution: {integrity: sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw==}
|
resolution: {integrity: sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3246,6 +3264,10 @@ packages:
|
|||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
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:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
@@ -5087,6 +5109,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss-selector-parser: 7.1.0
|
postcss-selector-parser: 7.1.0
|
||||||
|
|
||||||
|
'@date-fns/tz@1.4.1': {}
|
||||||
|
|
||||||
|
'@date-fns/utc@2.1.1': {}
|
||||||
|
|
||||||
'@emnapi/core@1.5.0':
|
'@emnapi/core@1.5.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.1.0
|
'@emnapi/wasi-threads': 1.1.0
|
||||||
@@ -6253,6 +6279,12 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.50.0':
|
'@rollup/rollup-win32-x64-msvc@4.50.0':
|
||||||
optional: true
|
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/is@7.0.2': {}
|
||||||
|
|
||||||
'@sindresorhus/merge-streams@2.3.0': {}
|
'@sindresorhus/merge-streams@2.3.0': {}
|
||||||
@@ -7155,6 +7187,8 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
|
date-fns@4.1.0: {}
|
||||||
|
|
||||||
db0@0.3.2: {}
|
db0@0.3.2: {}
|
||||||
|
|
||||||
de-indent@1.0.2: {}
|
de-indent@1.0.2: {}
|
||||||
@@ -8147,6 +8181,8 @@ snapshots:
|
|||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
|
lru-cache@11.2.2: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
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/**/*",
|
"types/**/*",
|
||||||
"layers/**/*",
|
"layers/**/*",
|
||||||
"app/**/*"
|
"app/**/*"
|
||||||
],
|
, "temp/inspection.ts", "temp/middleware.ts" ],
|
||||||
"exclude": [".nuxt/types/**/*", "node_modules"]
|
"exclude": [".nuxt/types/**/*", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user