diff --git a/.env.dev b/.env.dev
index eb44631..611dd74 100644
--- a/.env.dev
+++ b/.env.dev
@@ -13,7 +13,7 @@ STOVE_API_URL_SERVERL=https://i-api-dev.onstove.com
STOVE_M_API_URL=https://maintenance.gate8.com
# STOVE - GNB
-STOVE_GNB=https://js-cdn-dev.onstove.com/libs/common-gnb/latest/cp-header.js
+STOVE_GNB=https://js-cdn-dev.onstove.com/libs/common-gnb/latest/stove-gnb.js
# STOVE - Client Download
STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/launcher-pack.js
diff --git a/.env.live b/.env.live
index 9b48326..a3fdb0e 100644
--- a/.env.live
+++ b/.env.live
@@ -13,7 +13,7 @@ STOVE_API_URL_SERVERL=https://i-api.onstove.com
STOVE_M_API_URL=https://maintenance.onstove.com
# STOVE - GNB
-STOVE_GNB=https://js-cdn.onstove.com/libs/common-gnb/latest/cp-header.js
+STOVE_GNB=https://js-cdn.onstove.com/libs/common-gnb/latest/stove-gnb.js
# STOVE - Client Download
STOVE_LAUNCHER_SCRIPT=https://js-cdn.onstove.com/libs/stove-js-service/latest/launcher-pack.js
diff --git a/.env.qa b/.env.qa
index 157e09c..fb23f95 100644
--- a/.env.qa
+++ b/.env.qa
@@ -13,7 +13,7 @@ STOVE_API_URL_SERVERL=https://i-api.gate8.com
STOVE_M_API_URL=https://maintenance.gate8.com
# STOVE - GNB
-STOVE_GNB=https://js-cdn-qa.onstove.com/libs/common-gnb/latest/cp-header.js
+STOVE_GNB=https://js-cdn-qa.onstove.com/libs/common-gnb/latest/stove-gnb.js
# STOVE - Client Download
STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/launcher-pack.js
diff --git a/.env.sandbox b/.env.sandbox
index eeb961e..fa3249e 100644
--- a/.env.sandbox
+++ b/.env.sandbox
@@ -13,7 +13,7 @@ STOVE_API_URL_SERVERL=https://i-api.gate8.com
STOVE_M_API_URL=https://maintenance.gate8.com
# STOVE - GNB
-STOVE_GNB=https://js-cdn.gate8.com/libs/common-gnb/latest/cp-header.js
+STOVE_GNB=https://js-cdn.gate8.com/libs/common-gnb/latest/stove-gnb.js
# STOVE - Client Download
STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/launcher-pack.js
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..67673ab
--- /dev/null
+++ b/.npmrc
@@ -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/
diff --git a/.nvmrc b/.nvmrc
index 7af24b7..3bee07e 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1,2 @@
22.11.0
+
diff --git a/app/pages/[d1]/[d2]/[d3].vue b/app/pages/[d1]/[d2]/[d3].vue
index dceae28..6d1ae69 100644
--- a/app/pages/[d1]/[d2]/[d3].vue
+++ b/app/pages/[d1]/[d2]/[d3].vue
@@ -9,6 +9,7 @@ const currentLayout = computed(() => getLayoutType(pageData.value))
definePageMeta({
layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화
+ middleware: ['inspection']
})
diff --git a/app/pages/[d1]/[d2]/index.vue b/app/pages/[d1]/[d2]/index.vue
index dceae28..6d1ae69 100644
--- a/app/pages/[d1]/[d2]/index.vue
+++ b/app/pages/[d1]/[d2]/index.vue
@@ -9,6 +9,7 @@ const currentLayout = computed(() => getLayoutType(pageData.value))
definePageMeta({
layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화
+ middleware: ['inspection']
})
diff --git a/app/pages/error.vue b/app/pages/error.vue
new file mode 100644
index 0000000..6de5f13
--- /dev/null
+++ b/app/pages/error.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errorTitle }}
+
+
+ {{ errorDescription }}
+
+
+
+
+
+
+ {{ homeButtonText }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/pages/index.vue b/app/pages/index.vue
index dceae28..a151a5c 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -7,6 +7,7 @@ const { pageData } = storeToRefs(pageDataStore)
const currentLayout = computed(() => getLayoutType(pageData.value))
+
definePageMeta({
layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화
})
diff --git a/app/pages/inspection/index.vue b/app/pages/inspection/index.vue
new file mode 100644
index 0000000..f3ba325
--- /dev/null
+++ b/app/pages/inspection/index.vue
@@ -0,0 +1,380 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tm('Inspection_Now_Maintenance') }}
+
+
+
+
+
+
{{ tm('Inspection_Maintenance_Time') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ tm('Inspection_Txt_Download') || '게임 다운로드' }}
+
+
+
+ {{ getButtonText(btn.platform) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/error.vue b/error.vue
new file mode 100644
index 0000000..2509015
--- /dev/null
+++ b/error.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errorTitle }}
+
+
+ {{ errorDescription }}
+
+
+
+
+
+
+ {{ homeButtonText }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/i18n/locales/ko.ts b/i18n/locales/ko.ts
index e7f3c28..7074d95 100644
--- a/i18n/locales/ko.ts
+++ b/i18n/locales/ko.ts
@@ -1,10 +1,11 @@
export default defineI18nLocale(async (locale: string) => {
//https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json?20251021185116
- const config = useRuntimeConfig()
- const rootPath = config.public.staticUrl
- const runType = config.public.runType
+ // const config = useRuntimeConfig()
+ // const rootPath = config.public.staticUrl
+ // const runType = config.public.runType
- const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
+ // const translationApi = `${rootPath}/${runType}/test/multilingual/test_common_template.json`
+ const translationApi = `https://static-pubcomm.gate8.com/dev/test/multilingual/test_common_template.json`
try {
const { data } = await useFetch(translationApi, {
@@ -15,7 +16,7 @@ export default defineI18nLocale(async (locale: string) => {
})
// API 데이터에서 locale에 맞는 데이터를 추출
- const apiData = data.value?.[locale] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
+ const apiData = data.value?.['ko'] || {} // locale에 맞는 데이터가 없으면 빈 객체 반환
// API 데이터와 common.json 데이터를 병합 (common.json이 우선순위)
const finalResult = { ...apiData }
diff --git a/layers/components/blocks/LanguageSwitcher.vue b/layers/components/blocks/LanguageSwitcher.vue
index 245d386..c5bcc03 100644
--- a/layers/components/blocks/LanguageSwitcher.vue
+++ b/layers/components/blocks/LanguageSwitcher.vue
@@ -1,66 +1,164 @@
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ isChanging ? '언어 변경 중...' : getLanguageName(selectedLocale) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/layers/components/blocks/StoveGnbNew.vue b/layers/components/blocks/StoveGnbNew.vue
new file mode 100644
index 0000000..996902d
--- /dev/null
+++ b/layers/components/blocks/StoveGnbNew.vue
@@ -0,0 +1,61 @@
+
+
+
+
\ No newline at end of file
diff --git a/layers/components/layouts/Footer.vue b/layers/components/layouts/Footer.vue
index 51c4c30..936d456 100644
--- a/layers/components/layouts/Footer.vue
+++ b/layers/components/layouts/Footer.vue
@@ -24,12 +24,12 @@
- {{ tm('Footer_AgeRating') }}
+ {{ footerAgeRating }}
-
{{ tm('Footer_AgeRating') }}
+
{{ footerAgeRating }}
@@ -55,23 +55,23 @@
- {{ tm('Footer_AgeRating_Info')[0] }}
- {{ footerData.game_rating_info.company_name }}
+ {{ footerAgeRatingInfo[0] }}
+ {{ footerData.game_rating_info.title }}
- {{ tm('Footer_AgeRating_Info')[1] }}
+ {{ footerAgeRatingInfo[1] }}
{{ footerData.game_rating_info.rating_grade }}
- {{ tm('Footer_AgeRating_Info')[2] }}
+ {{ footerAgeRatingInfo[2] }}
{{ footerData.game_rating_info.reg_no }}
- {{ tm('Footer_AgeRating_Info')[3] }}
+ {{ footerAgeRatingInfo[3] }}
{{ footerData.game_rating_info.prod_date }}
- {{ tm('Footer_AgeRating_Info')[4] }}
+ {{ footerAgeRatingInfo[4] }}
{{ footerData.game_rating_info.rating_class_no }}
@@ -85,7 +85,7 @@
@@ -93,7 +93,7 @@
-
{{ tm('Footer_caution') }}
+
{{ footerCaution }}
@@ -114,20 +114,15 @@
-
@@ -144,74 +139,106 @@
+c
\ No newline at end of file
diff --git a/layers/composables/useApiData.ts b/layers/composables/useApiData.ts
new file mode 100644
index 0000000..cc5277c
--- /dev/null
+++ b/layers/composables/useApiData.ts
@@ -0,0 +1,22 @@
+
+interface ReqApiData {
+ baseApiUrl: string
+ url: string
+ }
+
+ export const useApiData = async (req: ReqApiData): Promise => {
+ const dataUrl = `${req.baseApiUrl}/${req.url}` // 정상 URL 경로
+ try {
+ const fetch = await $fetch(dataUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8'
+ }
+ })
+ return fetch
+ } catch (error) {
+ console.log('error', error)
+ return []
+ }
+ }
+
\ No newline at end of file
diff --git a/layers/composables/useGetGameDataExternal.ts b/layers/composables/useGetGameDataExternal.ts
new file mode 100644
index 0000000..63bc0fb
--- /dev/null
+++ b/layers/composables/useGetGameDataExternal.ts
@@ -0,0 +1,54 @@
+import type { GameDataResponse, GameDataRequest } from '#layers/types/api/gameData'
+
+export const useGetGameDataExternal = () => {
+ const { setGameData } = useGameDataStore()
+ const logPrefix = {
+ exception: '[Exception] /composables/useGetGameDataExternal',
+ failure: '[Failure] /composables/useGetGameDataExternal'
+ }
+ const webGameData = ref(null)
+
+ const getGameDataExternal = async (req: GameDataRequest) => {
+ console.log("🚀 ~ getGameDataExternal ~ req:", req)
+ // const config = useRuntimeConfig()
+ const config = useRuntimeConfig()
+ const stoveApiUrl = `${config.public.stoveApiUrl}`
+ const apiUrl = `${stoveApiUrl}/pub-comm/v1.0/template/game?game_domain=${req.gameDomain}&lang_code=${req.langCode}`
+
+ try {
+ const response = (await commonFetch('GET', apiUrl)) as GameDataResponse
+ console.log("🚀 ~ getGameDataExternal ~ response:", response)
+
+ // FIXME: 테스트용 데이터 ---------------------------------------------------
+ /* if (['local', 'local-gate8', 'dev'].includes(`${config.public.runType}`)) {
+ response.value = {
+ inspection_status: 1,
+ inspection: {
+ inspection_status: 1,
+ start_date: '2025-09-19 10:00:00',
+ end_date: '2025-09-19 12:00:00',
+ ts_start_date: new Date().getTime(),
+ ts_end_date: new Date().getTime(),
+ back_ground_image_type: 'image',
+ back_ground_image_url: 'https://www.onstove.com',
+ inspection_title1: '',
+ inspection_title2: ''
+ }
+ }
+ } */
+ // ------------------------------------------------------------------------
+
+
+ if (response?.value) {
+ webGameData.value = response
+
+ setGameData(response.value)
+ }
+ } catch (e) {
+ console.error(`${logPrefix.exception}.getGameDataExternal: `, e)
+ }
+
+ }
+
+ return { webGameData, getGameDataExternal }
+}
diff --git a/layers/composables/useGetGameMaintenance.ts b/layers/composables/useGetGameMaintenance.ts
new file mode 100644
index 0000000..a9835b3
--- /dev/null
+++ b/layers/composables/useGetGameMaintenance.ts
@@ -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 }
diff --git a/layers/composables/useGetInspectionDataExternal.ts b/layers/composables/useGetInspectionDataExternal.ts
new file mode 100644
index 0000000..c487a46
--- /dev/null
+++ b/layers/composables/useGetInspectionDataExternal.ts
@@ -0,0 +1,70 @@
+import type { WebInspectionData, ReqGetInspectionData, ResGetInspectionData } from '#layers/types/InspectionType'
+
+/**
+ * 웹 점검
+ */
+export const useGetInspectionDataExternal = () => {
+ const inspectionStore = useInspectionStore()
+ const logPrefix = {
+ exception: '[Exception] /composables/useGetInspectionDataExternal',
+ failure: '[Failure] /composables/useGetInspectionDataExternal'
+ }
+ const webInspectionData = ref(null)
+ const isWebInspection = ref(false) // 웹 점검 여부
+
+ // [Setter] 웹 점검 여부 세팅
+ const setIsWebInspection = (status: boolean) => {
+ isWebInspection.value = status
+ }
+
+ /**
+ * 웹 점검 여부
+ *
+ * @param {ReqGetInspectionData} req
+ * @description https://wiki.smilegate.net/pages/viewpage.action?pageId=563198067
+ */
+ const getInspectionDataExternal = async (req: ReqGetInspectionData) => {
+ // const config = useRuntimeConfig()
+ const apiUrl = `${req.baseApiUrl}/pub-comm/v3.0/inspection/${req.gameId}`
+
+ try {
+ const response = (await commonFetch('GET', apiUrl)) as ResGetInspectionData
+ console.log("🚀 ~ getInspectionDataExternal ~ response:", response)
+
+ // FIXME: 테스트용 데이터 ---------------------------------------------------
+ /* if (['local', 'local-gate8', 'dev'].includes(`${config.public.runType}`)) {
+ response.value = {
+ inspection_status: 1,
+ inspection: {
+ inspection_status: 1,
+ start_date: '2025-09-19 10:00:00',
+ end_date: '2025-09-19 12:00:00',
+ ts_start_date: new Date().getTime(),
+ ts_end_date: new Date().getTime(),
+ back_ground_image_type: 'image',
+ back_ground_image_url: 'https://www.onstove.com',
+ inspection_title1: '',
+ inspection_title2: ''
+ }
+ }
+ } */
+ // ------------------------------------------------------------------------
+
+ if (response?.value && response.value.inspection) {
+ webInspectionData.value = response.value.inspection
+ isWebInspection.value = response.value.inspection_status === 1
+
+ inspectionStore.setWebInspectionData(webInspectionData.value)
+ inspectionStore.setWebInspectionStatus(isWebInspection.value)
+ }
+ } catch (e) {
+ console.error(`${logPrefix.exception}.getInspectionDataExternal: `, e)
+ }
+
+ if (webInspectionData.value !== null) {
+ setIsWebInspection(isWebInspection.value)
+ }
+ }
+
+ return { webInspectionData, isWebInspection, getInspectionDataExternal }
+}
diff --git a/layers/layouts/onlyStove.vue b/layers/layouts/onlyStove.vue
new file mode 100644
index 0000000..33b1bcd
--- /dev/null
+++ b/layers/layouts/onlyStove.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/layers/middleware/init.route.global.ts b/layers/middleware/init.route.global.ts
index e61a5bd..ebd64ae 100644
--- a/layers/middleware/init.route.global.ts
+++ b/layers/middleware/init.route.global.ts
@@ -1,4 +1,4 @@
-import { useGameDataStore } from '#layers/stores/useGameDataStore'
+import type { GameDataRequest, GameDataValue } from '#layers/types/api/gameData'
export default defineNuxtRouteMiddleware(async (to, _from) => {
// 서버 사이드에서는 스킵
@@ -6,13 +6,6 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
return
}
- const gameDataStore = useGameDataStore()
-
- // gameData가 로드되지 않았으면 스킵 (다른 미들웨어에서 로드됨)
- if (!gameDataStore.gameData) {
- return
- }
-
// 현재 경로에서 언어 코드 추출
// 예: /ko/about/story -> ko
// 예: /en/test/page -> en
@@ -20,10 +13,23 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
const match = to.path.match(languagePattern)
const currentLangCode = match ? match[1] : null
- // 허용된 언어 코드 목록
- const allowedLangCodes = gameDataStore.gameData.lang_codes || []
+ //현재 url에서 게임 도메인만 추출
+ const currentDomain = window.location.hostname;
+ const req: GameDataRequest = {
+ gameDomain: `${currentDomain}`,
+ langCode: `${currentLangCode}`,
+ }
+ const { getGameDataExternal } = useGetGameDataExternal()
+ await getGameDataExternal(req)
- // 현재 언어가 허용된 언어 목록에 없으면 404로 리다이렉트
+ const gameDataStore = useGameDataStore()
+ const gameData = gameDataStore.gameData as GameDataValue
+ const langCodes = gameData?.lang_codes
+
+ // 허용된 언어 코드 목록
+ const allowedLangCodes = langCodes || []
+
+ // 현재 언어가 허용된 언어 목록에 없으면 에러 페이지로 이동
if (currentLangCode && !allowedLangCodes.includes(currentLangCode)) {
throw createError({
statusCode: 404,
diff --git a/layers/middleware/inspection.ts b/layers/middleware/inspection.ts
new file mode 100644
index 0000000..006409a
--- /dev/null
+++ b/layers/middleware/inspection.ts
@@ -0,0 +1,46 @@
+export default defineNuxtRouteMiddleware(async (to) => {
+ try {
+ if (import.meta.client) {
+ const gameDataStore = useGameDataStore()
+ const { gameData } = storeToRefs(gameDataStore)
+ console.log("🚀 ~ 00000 gameData:", gameData.value)
+
+ const config = useRuntimeConfig()
+ // const baseDomain = `${config.public.baseDomain}`
+ const stoveApiUrl = `${config.public.stoveApiUrl}`
+ const stoveGameId = `${gameData.value.game_id}`
+ // const stoveMaintenanceApiUrl = `${config.public.stoveMaintenanceApiUrl}`
+
+ // const localeCookie = useCookie('LOCALE', {
+ // domain: baseDomain
+ // })
+
+ const finalLocale = csrGetFinalLocale(to.path)
+ // localeCookie.value = finalLocale.toUpperCase()
+
+ // 웹 점검 -----
+ const { isWebInspection, getInspectionDataExternal } = useGetInspectionDataExternal()
+ await getInspectionDataExternal({ baseApiUrl: stoveApiUrl, gameId: stoveGameId })
+
+
+ // 게임 점검 -----
+ // const { checkGameMaintenance } = useGetGameMaintenance()
+ // await checkGameMaintenance({
+ // baseApiUrl: stoveMaintenanceApiUrl,
+ // category: 'GAME',
+ // service_id1: stoveGameId,
+ // lang: `${finalLocale}`.toLowerCase()
+ // })
+
+ if (isWebInspection.value && !to.path.includes('inspection') && !to.path.includes('api')) {
+ // 점검 중인 경우
+ return navigateTo(`/${finalLocale}/inspection`, { external: true })
+ } else if (!isWebInspection.value && to.path?.indexOf('inspection') !== -1) {
+ // 점검이 종료된 후 점검 페이지 접근시 메인으로 리다이렉트
+ return navigateTo(`/${finalLocale}`, { external: true })
+ }
+ }
+ } catch (e) {
+ console.error('[Exception] /middleware/inspection: ', e)
+ }
+})
diff --git a/layers/middleware/pageData.global.ts b/layers/middleware/pageData.global.ts
index af563db..fae74cd 100644
--- a/layers/middleware/pageData.global.ts
+++ b/layers/middleware/pageData.global.ts
@@ -16,6 +16,10 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
try {
+ if(to.path.includes('inspection')) {
+ return
+ }
+
const pageUrl = getPathAfterLanguage(to.path)
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
@@ -36,9 +40,17 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
loading: true,
})) as PageDataResponse | null
+ console.log("🚀 ~ response?.code:", response?.code)
+ // if(response?.code === 91003) {
+ // throw createError({
+ // statusCode: 404,
+ // statusMessage: 'Page not found',
+ // })
+ // }
+
if (response?.code === 0 && 'value' in response) {
store.setPageData(response.value)
- console.log('🚀 ~ pageData:', response.value)
+ // console.log('🚀 ~ pageData:', response.value)
} else {
store.clearPageData()
}
diff --git a/layers/plugins/error-handler.ts b/layers/plugins/error-handler.ts
new file mode 100644
index 0000000..54a402a
--- /dev/null
+++ b/layers/plugins/error-handler.ts
@@ -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
+ })
+ })
\ No newline at end of file
diff --git a/layers/server/api/clientIp.ts b/layers/server/api/clientIp.ts
new file mode 100644
index 0000000..25a2958
--- /dev/null
+++ b/layers/server/api/clientIp.ts
@@ -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 || ''
+})
diff --git a/layers/server/middleware/gameData.ts b/layers/server/middleware/gameData.ts
index 3857a86..dd5ed2a 100644
--- a/layers/server/middleware/gameData.ts
+++ b/layers/server/middleware/gameData.ts
@@ -1,3 +1,4 @@
+import { LRUCache } from 'lru-cache'
import {
getHeader,
getRequestHost,
@@ -6,24 +7,179 @@ import {
} from 'h3'
import { ssrGetFinalLocale } from '../../utils/localeUtil'
import type { GameDataResponse } from '../../types/api/gameData'
+import type { ResGetInspectionData } from '../../types/InspectionType'
+import { isStaticFile } from '#layers/utils/commonUtil'
+
+/**
+ * 캐시 제어 헤더를 설정하는 공통 함수
+ *
+ * @param event - 이벤트 객체
+ * @param cacheMode - 캐시 모드 설정 ('no-cache', 'short', 'medium', 'default')
+ * @param customMaxAge - 커스텀 max-age 값 (초 단위)
+ */
+function setCacheHeaders(
+ event: { node: { res: { setHeader: (name: string, value: string) => void } } },
+ cacheMode: 'no-cache' | 'short' | 'medium' | 'default',
+ customMaxAge?: number
+): void {
+ // 원래 setHeader 함수 참조 저장
+ const originalSetHeader = event.node.res.setHeader
+
+ // Cache-Control 헤더 설정값 결정
+ let cacheControl: string
+ switch (cacheMode) {
+ case 'no-cache':
+ cacheControl = 'no-cache, no-store, must-revalidate'
+ // no-cache 모드일 때는 추가 헤더도 설정
+ event.node.res.setHeader('Pragma', 'no-cache')
+ event.node.res.setHeader('Expires', '0')
+ break
+ case 'short':
+ cacheControl = `public, max-age=${customMaxAge || 10}`
+ break
+ case 'medium':
+ cacheControl = `public, max-age=${customMaxAge || 15}`
+ break
+ case 'default':
+ default:
+ cacheControl = `public, max-age=${customMaxAge || 60}`
+ break
+ }
+
+ // Cache-Control 헤더를 강제로 설정하기 위해 setHeader 메소드 오버라이드
+ event.node.res.setHeader = function (name: string, value: string) {
+ if (name.toLowerCase() === 'cache-control') {
+ return originalSetHeader.call(this, name, cacheControl)
+ }
+ return originalSetHeader.call(this, name, value)
+ }
+
+ // 바로 캐시 제어 헤더 적용
+ event.node.res.setHeader('Cache-Control', cacheControl)
+}
+
+const cache = new LRUCache({
+ max: 100, // 캐시에 저장할 최대 항목 수
+ ttl: 1000 * 30 // 30초 동안 캐시 유지
+})
+
+/**
+ * 최종 언어 쿠키 세팅
+ *
+ * @param event - 이벤트 객체
+ * @param finalLocale - 최종 언어
+ * @param baseDomain - 기본 도메인
+ */
+function setFinalLocaleCookie(event: any, finalLocale: string, baseDomain: string) {
+ setCookie(event, 'LOCALE', finalLocale.toUpperCase(), {
+ domain: baseDomain,
+ path: '/',
+ maxAge: 60 * 60 * 24 * 365 // 1년 (초 단위)
+ })
+}
+
+/**
+ * Locale Middleware 역할 함수
+ *
+ * @param event - 이벤트 객체
+ * @param finalLocale - 최종 언어
+ */
+function fnLocaleMiddleware(event: any, finalLocale: string) {
+ const path = event?.node.req.url || ''
+ let arrPath = []
+ let queryString = ''
+
+ if (path.includes('?')) {
+ // 쿼리스트링 포함 시 순수 경로만 추출
+ arrPath = path.split('?')[0].split('/')
+ queryString = path.split('?')[1]
+ } else {
+ arrPath = path.split('/')
+ queryString = ''
+ }
+
+ // 최종 언어 세팅된 경로 생성
+ const pathLocale = arrPath.length > 1 ? arrPath[1] : ''
+
+ // URL에서 현재 언어와 최종 언어가 다르면 리다이렉트
+ if (pathLocale !== finalLocale) {
+ let newLocalePath = ''
+ if (pathLocale === '') {
+ newLocalePath = `/${finalLocale}`
+ } else {
+ arrPath[1] = finalLocale
+ newLocalePath = arrPath.join('/')
+ }
+
+ if (queryString !== '') {
+ newLocalePath += `?${queryString}`
+ }
+
+ event.node.res.statusCode = 302
+ event.node.res.setHeader('Location', newLocalePath)
+ event.node.res.end()
+ }
+}
export default defineEventHandler(async event => {
- const url = getRequestURL(event)
+
+ const config = useRuntimeConfig()
+ // const runType = `${config.public.runType}`
+ const iBaseApiUrl = `${config.public.stoveApiUrlServer}`
+ const baseDomain = `${config.public.baseDomain}`
+ // console.log("🚀 ~ baseDomain:", config.public.baseDomain)
+ // const url = getRequestURL(event)
+
+ // if (['local', 'local-gate8', 'dev'].includes(runType)) {
+ // Sandbox 이상 환경에서만 동작 및 확인 가능 (local, dev는 통과 처리)
+ // try {
+ // 언어 코드 추출
+ // const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
+ // console.log("🚀 ~ finalLocale:", finalLocale)
+ // setFinalLocaleCookie(event, finalLocale, baseDomain)
+
+ // -------------------------------------------------------------------------------
+ // [Locale Middleware]
+ // -------------------------------------------------------------------------------
+ // fnLocaleMiddleware(event, finalLocale)
+ // } catch (e) {
+ // console.error('[Exception] /server/middleware/middleware-02-global: ', e)
+ // }
+ // }
+
+ const fullPath = event.path
+
+ // 1-1. 정적 파일 패스
+ if (isStaticFile(event.path)) {
+ return
+ }
+
+ // 1-2. /inspection 패스
+ if (fullPath.includes('/inspection')) {
+ // 리턴 되기 전 언어 쿠키 세팅
+ const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
+ setFinalLocaleCookie(event, finalLocale, baseDomain)
+ return
+ }
// 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리
if (
- url.pathname.startsWith('/api/') ||
- url.pathname.startsWith('/_nuxt/') ||
- url.pathname.startsWith('/favicon') ||
- url.pathname.includes('.') ||
- url.pathname.startsWith('/_')
+ fullPath.startsWith('/api/') ||
+ fullPath.startsWith('/_nuxt/') ||
+ fullPath.startsWith('/favicon') ||
+ fullPath.includes('/assets/') ||
+ fullPath.includes('.') ||
+ fullPath.startsWith('/_')
) {
return
}
+ // 캐시 키 생성
+ const cacheKey = 'inspection'
+ // console.log("🚀 11111 ~ cacheKey:", cacheKey)
+
const host =
(getHeader(event, 'host') || getRequestHost(event)).toString() || ''
- const baseDomain = process.env.BASE_DOMAIN || '.onstove.com'
const isGameDomainExtractable = host.includes(baseDomain)
if (isGameDomainExtractable) {
@@ -36,39 +192,140 @@ export default defineEventHandler(async event => {
const config = useRuntimeConfig()
const stoveApiUrlServer = config.public.stoveApiUrlServer
const apiUrl = `${stoveApiUrlServer}/pub-comm/v1.0/template/game`
+
+ // console.log("🚀 ~ apiUrl:", apiUrl)
- 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'
-
+ // 2. 언어 코드 추출
+ const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
+
+
const queryParams: Record = {
game_domain: event.context.gameDomain || '',
- lang_code: langCode,
+ lang_code: finalLocale,
}
-
const response = (await $fetch(apiUrl, {
query: queryParams,
})) as GameDataResponse | null
+ // 언어패스 쿠키 굽기 - 장기방안에서는 굽지않음
+ // const langCoverages = response?.value?.lang_codes || []
+ // if(langCoverages.includes(finalLocale)) {
+ // setFinalLocaleCookie(event, finalLocale, baseDomain)
+ // }
+
if (response?.code === 0 && 'value' in response) {
event.context.gameData = response.value
event.context.googleAnalyticsId = response.value?.ga_code
- console.log('🚀 ~ gameData:', response.value)
+ // console.log('🚀 ~ gameData:', response.value)
+
+ // 점검 데이터 조회
+ let inspectionData
+ if (cache.has(cacheKey)) {
+ inspectionData = cache.get(cacheKey) as WebInspectionData
+ } else {
+ // 점검 데이터 조회
+ if (response?.value?.game_id) {
+ const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response?.value?.game_id}`
+ // 직접 $fetch 사용 (composable 사용하지 않음)
+ const inspectionResponse = await $fetch(inspectionApiUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ inspectionData = inspectionResponse?.value?.inspection
+ cache.set(cacheKey, inspectionData) // 캐시에 저장
+ // console.log("🚀 ~ inspectionData:", inspectionData)
+
+ }
+ }
+
+
+ // 4. 현재 시간과 점검 기간 비교
+ const currentTime = Date.now()
+ const tsStartDate = inspectionData?.ts_start_date || 0
+ const tsEndDate = inspectionData?.ts_end_date || 0
+ const timeUntilInspectionSeconds = Math.floor((tsStartDate - currentTime) / 1000)
+
+ // 5. 점검 상태별 캐시 설정
+ if (inspectionData?.inspection_status === 1 && currentTime >= tsStartDate && currentTime <= tsEndDate) {
+ /**
+ * 점검 중인 경우
+ * - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ
+ * - 점검 URL 경로가 아닐 경우 no-cache 설정
+ * - 화이트 리스트 체크
+ */
+ // 현재 경로가 점검 페이지가 아닐 경우 리다이렉트
+ const inspectionPath = `/${finalLocale}/inspection`
+ if (fullPath !== inspectionPath) {
+ setCacheHeaders(event, 'no-cache')
+ }
+
+ // 점검 중일 때 IP 필터링 활성화 여부 확인
+ if (inspectionData?.ip_filter_use_yn === 'Y') {
+ const clientIP = getTrueClientIp(event.node.req as any)
+
+ // 허용된 IP 목록 확인
+ if (!inspectionData?.ip_filter_list?.includes(clientIP)) {
+ // 허용되지 않은 IP인 경우 점검 페이지로 이동
+ event.node.res.statusCode = 302
+ event.node.res.setHeader('Location', inspectionPath)
+ event.node.res.end()
+ } else {
+ // 화이트 리스트인 경우
+ // -------------------------------------------------------------------------------
+ // [Locale Middleware]
+ // -------------------------------------------------------------------------------
+ fnLocaleMiddleware(event, finalLocale)
+ }
+ } else {
+ event.node.res.statusCode = 302
+ event.node.res.setHeader('Location', inspectionPath)
+ event.node.res.end()
+ }
+ } else {
+ /**
+ * 점검이 아닌 경우
+ * - 홈 경로는 no-cache
+ * - 점검 예정 시간에 따른 캐시 설정
+ * - 점검 5분 전: 짧은 캐시 (10초)
+ * - 점검 30분 전: 중간 캐시 (15초)
+ * - 점검 30분 이후: 기본 캐시 (60초)
+ */
+ // 홈 경로: 캐시 없음
+ const isHomePath = [
+ '',
+ '/'
+ //, ...Object.values(DEFAULT_LOCALE_COVERAGES).flatMap((locale) => [`/${locale}`, `/${locale}/`])
+ ].includes(fullPath)
+
+ if (isHomePath) {
+ setCacheHeaders(event, 'no-cache')
+ } else {
+ // 점검 예정 시간에 따른 캐시 설정
+
+ if (tsStartDate > 0 && timeUntilInspectionSeconds > 0) {
+ if (timeUntilInspectionSeconds < 300) {
+ // 점검 5분 전: 짧은 캐시 (10초)
+ setCacheHeaders(event, 'short', 10)
+ } else if (timeUntilInspectionSeconds < 1800) {
+ // 점검 30분 전: 중간 캐시 (15초)
+ setCacheHeaders(event, 'medium', 15)
+ } else {
+ // 점검 30분 이후: 기본 캐시 (60초)
+ setCacheHeaders(event, 'default')
+ }
+ }
+ }
+ // -------------------------------------------------------------------------------
+ // [Locale Middleware]
+ // -------------------------------------------------------------------------------
+ fnLocaleMiddleware(event, finalLocale)
+ }
+
}
+
} catch (error) {
console.error('gameData load error:', error)
}
diff --git a/layers/server/plugins/nitroPlugin.ts b/layers/server/plugins/nitroPlugin.ts
new file mode 100644
index 0000000..59e1c72
--- /dev/null
+++ b/layers/server/plugins/nitroPlugin.ts
@@ -0,0 +1,68 @@
+import type { RenderResponse } from 'nitropack'
+import type { H3Event } from 'h3'
+import { defineNitroPlugin } from 'nitropack/runtime'
+import { getTrueClientIp } from '#layers/utils/apiUtil'
+
+function generateRequestId(): string {
+ return Date.now().toString(36) + Math.random().toString(36).substring(2)
+}
+
+function getIpAddress(event: H3Event): string {
+ return getTrueClientIp(event.node.req as any) || 'unknown'
+}
+
+export default defineNitroPlugin((nitroApp) => {
+ // 정적 파일 체크 함수 추가
+ const isStaticFile = (path: string): boolean => {
+ return /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i.test(path)
+ }
+
+ // 헬스체크 경로 체크 함수 추가
+ const isHealthCheck = (path: string): boolean => {
+ return path === '/health'
+ }
+
+ nitroApp.hooks.hook('request', (event) => {
+ // 정적 파일 요청은 로깅 제외
+ if (isStaticFile(event.path) || isHealthCheck(event.path)) {
+ return
+ }
+ // 상세 로깅을 위한 정보 수집
+ const startTime = Date.now()
+ const userAgent = event.node.req.headers['user-agent'] || ''
+ const method = event.method || ''
+ const headers = JSON.stringify(event.node.req.headers, null, 2)
+ const requestId = generateRequestId()
+
+ if (process.env.NODE_ENV !== 'development') {
+ console.log(
+ `Request Info {"requestId":"${requestId}", "type":"request","method":"${method}","url":"${event.path}","userIp":"${getIpAddress(event)}","userAgent":"${userAgent}", "headers" : "${headers}" }`
+ )
+
+ // 요청 완료 후 응답 상태 코드 로깅
+ event.node.res.on('finish', () => {
+ console.log(
+ `Response Info {"requestId":"${requestId}","type":"response","method":"${method}","url":"${event.path}","statusCode":${event.node.res.statusCode},"responseTime":"${Date.now() - startTime}ms","userIp":"${getIpAddress(event)}","userAgent":"${userAgent}","statusMessage":"${event.node.res.statusMessage}","responseHeader": ${JSON.stringify(event.node.res.getHeaders(), null, 2)}}`
+ )
+ console.log(
+ '==========================================================================================================================================================================================================================================================='
+ )
+ })
+ }
+ })
+
+ nitroApp.hooks.hook('error', (error) => {
+ console.error('[Nitro Error]', {
+ message: error.message,
+ stack: error.stack,
+ timestamp: new Date().toISOString()
+ })
+ })
+
+ // 응답 헤더에서 'x-powered-by' 제거
+ nitroApp.hooks.hook('render:response', (response: Partial) => {
+ if (response?.headers) {
+ delete response.headers['x-powered-by']
+ }
+ })
+})
diff --git a/layers/stores/inspectionStore.ts b/layers/stores/inspectionStore.ts
new file mode 100644
index 0000000..12b3aa2
--- /dev/null
+++ b/layers/stores/inspectionStore.ts
@@ -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(null) // 웹 점검 정보
+ const webInspectionStatus = ref(null) // 웹 점검 상태
+ const gameMaintenanceData = ref(null) // 게임 점검 정보
+ const gameMaintenanceStatus = ref(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
+ }
+})
diff --git a/layers/stores/useCallerInfoStore.ts b/layers/stores/useCallerInfoStore.ts
new file mode 100644
index 0000000..368b3dd
--- /dev/null
+++ b/layers/stores/useCallerInfoStore.ts
@@ -0,0 +1,13 @@
+export const useCallerInfoStore = defineStore('callerInfoStore', () => {
+ const callerId = ref('')
+ const callerDetail = ref('')
+
+ const setCallerId = (paramCallerId: string | null) => {
+ callerId.value = paramCallerId
+ }
+ const setCallerDetail = (paramCalleDetail: string | null) => {
+ callerDetail.value = paramCalleDetail
+ }
+
+ return { callerId, callerDetail, setCallerId, setCallerDetail }
+})
diff --git a/layers/stores/useCommonStore.ts b/layers/stores/useCommonStore.ts
new file mode 100644
index 0000000..d19df92
--- /dev/null
+++ b/layers/stores/useCommonStore.ts
@@ -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({
+ 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(null)
+ const isLoading = ref(true)
+ const isScrollLock = ref(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
+ }
+})
diff --git a/layers/types/Common.ts b/layers/types/Common.ts
index 42d5031..5b60274 100644
--- a/layers/types/Common.ts
+++ b/layers/types/Common.ts
@@ -1,5 +1,5 @@
import type { HTMLAttributes } from 'vue'
-import type { StoveJsService } from '@/layers/types/Stove'
+import type { StoveJsService } from '#layers/types/Stove'
export type ClassType = HTMLAttributes['class']
@@ -8,3 +8,58 @@ declare global {
stoveJsService?: StoveJsService
}
}
+interface CommonRequestType {
+ baseApiUrl: string
+ gameId: string
+}
+
+interface CommonResponseType {
+ code?: number
+ message?: string
+}
+
+interface CommonPeriodType {
+ startDate?: string
+ endDate?: string
+}
+interface ParsedCustomLinkOptions {
+ tm: (key: string) => { txt: string }
+ query?: Record
+}
+
+
+// 타입 정의
+interface FooterMenuItem {
+ title: string
+ url: string
+ target?: string
+ active?: string
+}
+
+interface GameRatingInfo {
+ title: string
+ company_name: string
+ rating_grade: string
+ reg_no: string
+ prod_date: string
+ rating_class_no: string
+ rating_type: string
+ content_info: string
+}
+
+interface FooterData {
+ use_game_rating: boolean
+ game_rating_info: GameRatingInfo
+ use_dev_ci_url?: boolean
+ dev_ci_url?: string
+ dev_ci_img_path?: string
+ fund_display_yn?: string
+ fund_display_url?: string
+}
+
+interface DevCiConfig {
+ dev_ci_yn: boolean
+ dev_ci_img_path: string
+}
+
+export type { CommonRequestType, CommonResponseType, CommonPeriodType, ParsedCustomLinkOptions, FooterMenuItem, GameRatingInfo, FooterData, DevCiConfig }
diff --git a/layers/types/DataizationType.ts b/layers/types/DataizationType.ts
new file mode 100644
index 0000000..a667182
--- /dev/null
+++ b/layers/types/DataizationType.ts
@@ -0,0 +1,100 @@
+import type { CommonPeriodType } from '#layers/types/Common'
+
+// [S] Type in czn_homepage_brand_siteConfig.json ----------------------------------------
+interface GnbMenuType {
+ id: string
+ title: string
+ link: string
+ target: string
+ displayLocales?: Array
+}
+
+interface GnbType extends GnbMenuType {
+ depth2List?: Array
+}
+
+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
+}
+
+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
+ mainVideo: CommonPeriodType
+ promotionList?: Array
+ characterList?: Array
+ loreList?: Array
+ footerMenuList?: Array
+ mediaList?: Array
+ sectionList?: Array
+ marketList?: Array
+}
+
+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
+}
diff --git a/layers/types/GameMaintenanceType.ts b/layers/types/GameMaintenanceType.ts
new file mode 100644
index 0000000..c7dae68
--- /dev/null
+++ b/layers/types/GameMaintenanceType.ts
@@ -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 // 서비스 ID2(String Array), service_id1 전체를 설정할 경우 ["*"]로 등록해야 함.
+ type: string // 점검타입(REGULAR / TEMPORARY / URGENT)
+ languages: Array // 다국어 리스트 정보
+ description: string // 설명
+ start_at: number // UTC기준 점검 시작일(milli-timestamp(13digit))
+ end_at: number // UTC기준 점검 종료일(milli-timestamp(13digit))
+}
+interface DtoGameMaintenance {
+ total_count: number
+ list: Array
+}
+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 }
diff --git a/layers/types/InspectionType.ts b/layers/types/InspectionType.ts
new file mode 100644
index 0000000..846c84d
--- /dev/null
+++ b/layers/types/InspectionType.ts
@@ -0,0 +1,39 @@
+import type { CommonRequestType, CommonResponseType } from './Common'
+
+/*************************************************************************
+ * 웹 점검
+ ************************************************************************/
+interface WebInspectionData {
+ inspection_status: number // 점검 상태 (0: 정상, 1: 점검 중) (단순 운영툴 설정 점검 값)
+ start_date: string // 점검 시작 날짜 (문자열 형식)
+ end_date: string // 점검 종료 날짜 (문자열 형식)
+ ts_start_date: number // 점검 시작 타임스탬프
+ ts_end_date: number // 점검 종료 타임스탬프
+ back_ground_image_type?: string // 배경 이미지 타입 (0: 없음, 기타 값: 특정 타입)
+ back_ground_image_url?: string // 배경 이미지 URL
+ movie_yn?: string // 동영상 사용 여부 ("Y" 또는 "N")
+ movie_url?: string // 동영상 URL
+ inspection_title_type?: string // 점검 제목 타입
+ inspection_title1: string // 점검 제목 1
+ inspection_title2: string // 점검 제목 2
+ inspection_content?: string // 점검 내용
+
+ // Internal -----
+ ip_filter_use_yn?: string // IP 필터 사용 여부 ("Y" 또는 "N")
+ ip_filter_list?: string[] // 허용된 IP 목록
+ launching_status?: number // 런칭 여부 (0: 런칭 전, 1: 런칭 후)
+}
+
+interface ReqGetInspectionData extends CommonRequestType {
+ // do nothing
+}
+
+interface DtoGetInspectionData {
+ inspection_status?: number // 점검 여부 + 점검 시간 + 화이트 리스트 고려하여 계산된 결과
+ inspection?: WebInspectionData
+}
+interface ResGetInspectionData extends CommonResponseType {
+ value?: DtoGetInspectionData
+}
+
+export type { WebInspectionData, ReqGetInspectionData, ResGetInspectionData }
diff --git a/layers/types/api/gameData.ts b/layers/types/api/gameData.ts
index 9bf3831..450e020 100644
--- a/layers/types/api/gameData.ts
+++ b/layers/types/api/gameData.ts
@@ -4,6 +4,10 @@ export interface GameDataRequest {
lang_code: string
q?: string
qc?: string
+ baseApiUrl: string
+ gameId: string
+ gameDomain: string
+ langCode: string
}
// API 응답 데이터 타입
diff --git a/layers/utils/commonUtil.ts b/layers/utils/commonUtil.ts
new file mode 100644
index 0000000..fb42881
--- /dev/null
+++ b/layers/utils/commonUtil.ts
@@ -0,0 +1,168 @@
+import type { ParsedCustomLinkOptions } from '#layers/types/Common'
+
+/**
+ * 페이지 - 유효성 체크
+ *
+ * @param {number} page - 페이지
+ * @param {number} totalPage - 총 페이지 수
+ */
+const checkPageValidation = (page: number, totalPage: number) => {
+ // 최소, 최대 범위 체크
+ if (page < 1) {
+ page = 1
+ } else if (page > totalPage) {
+ page = totalPage
+ }
+ return page
+}
+
+/**
+ * 파일 다운로드 함수
+ *
+ * @param {string} fileUrl - 다운로드할 파일의 URL
+ * @param {string} fileName - 저장할 파일 이름 (옵션)
+ */
+const csrDownloadFile = (fileUrl: string, fileName?: string) => {
+ const link = document.createElement('a')
+ link.href = fileUrl
+
+ // 파일 이름이 제공되면 다운로드 이름 설정
+ if (fileName) {
+ link.download = fileName
+ }
+
+ // 링크를 클릭하여 다운로드 트리거
+ document.body.appendChild(link)
+ link.click()
+
+ // DOM에서 링크 제거
+ document.body.removeChild(link)
+}
+
+/**
+ * 마케팅 코드 조회
+ */
+const csrGetMarketingCode = () => {
+ const route = useRoute()
+ const mcode = Number(`${route.query.mcode != null && route.query.mcode !== '' ? route.query.mcode : ''}`)
+ return isNaN(mcode) ? undefined : mcode
+}
+
+/**
+ * 외부 링크 이동 (새 창)
+ *
+ * @param {string} link - 이동할 외부 링크
+ */
+const csrGoExternalLink = (link: string = '') => {
+ window.open(link, '_blank')
+}
+
+/**
+ * QA용 국가 코드 조회
+ */
+const csrGetQc = () => {
+ const route = useRoute()
+ const qc = `${route.query.qc != null && route.query.qc !== '' ? route.query.qc : ''}`
+ return qc
+}
+
+/**
+ * 문자열이 숫자인지 확인
+ *
+ * @param {string} str - 확인할 문자열
+ */
+const isNumeric = (str: string): boolean => {
+ return /^-?\d+(\.\d+)?$/.test(str)
+}
+
+/**
+ * 가공된 링크 파싱
+ *
+ * @param {string} link - 원본 링크
+ * @param {Function} tm - i18n의 tm 함수 (예: (key) => ({ txt: string }))
+ * @param {any} query - 추가 쿼리 파라미터
+ */
+const getParsedCustomLink = (link: string, { tm, query = {} }: ParsedCustomLinkOptions) => {
+ const config = useRuntimeConfig()
+ let result = `${link || ''}`
+
+ // @c{key} 패턴 치환 (예: @c{stoveCommunityUrl})
+ if (link.includes('@c')) {
+ result = result.replace(/@c\{(.*?)\}/g, (_, key) => {
+ // config.public에서 해당 key 값을 찾아 치환
+ return typeof config.public[key] === 'string' ? config.public[key] : ''
+ })
+ }
+
+ // @m{key} 패턴 치환 (예: @m{Community_Channel_Key})
+ if (link.includes('@m')) {
+ result = result.replace(/@m\{(.*?)\}/g, (_, key) => {
+ // tm 함수로 변환하여 치환
+ return tm(key)?.txt ?? ''
+ })
+ }
+
+ // @q{key} 패턴 치환 (예: @q{ppid})
+ if (link.includes('@q')) {
+ result = result.replace(/@q\{(.*?)\}/g, (_, key) => {
+ let q = ''
+ if (query[key]) {
+ q += result.includes('?') ? '&' : '?'
+ q += `${key}=${query[key]}`
+ }
+ return q
+ })
+ }
+ return result
+}
+
+/**
+ * 쿠키 설정 - 만료기간 하루 단위 셋팅
+ *
+ * @param {string} name - 쿠키 이름
+ * @param {string} value - 쿠키 값
+ * @param {number} exp - 만료기간 (옵션)
+ */
+const setCookieForDay = (name: string, value: string, exp?: number) => {
+ const date = new Date()
+ if (!exp) {
+ exp = 1
+ }
+ date.setTime(date.getTime() + exp * 24 * 60 * 60 * 1000)
+
+ const setCookie = useCookie(name, {
+ expires: new Date(date),
+ path: '/'
+ })
+
+ setCookie.value = value
+}
+
+// 정적 파일인지 확인하는 함수
+const isStaticFile = (path: string): boolean => {
+ return /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|scss)$/i.test(path)
+}
+
+/**
+ * 기준값이 최솟값 ~ 최댓값에 속하는지 확인
+ *
+ * @param {number} ref - 기준값
+ * @param {number} min - 최솟값
+ * @param {number} max - 최댓값
+ */
+const isInRange = (ref: number, min: number, max: number): boolean => {
+ return ref >= min && ref <= max
+}
+
+export {
+ checkPageValidation,
+ csrDownloadFile,
+ csrGetMarketingCode,
+ csrGoExternalLink,
+ csrGetQc,
+ isNumeric,
+ getParsedCustomLink,
+ setCookieForDay,
+ isStaticFile,
+ isInRange
+}
diff --git a/layers/utils/dataUtil.ts b/layers/utils/dataUtil.ts
index 59e3bd5..dd0ae0e 100644
--- a/layers/utils/dataUtil.ts
+++ b/layers/utils/dataUtil.ts
@@ -20,7 +20,7 @@ import type { OperateComponents } from '#layers/types/api/resourcesData'
export const getLayoutType = (
pageData: PageDataValue | null
): 'default' | 'promotion' => {
- return pageData?.page_type === 1 ? 'default' : 'promotion'
+ return pageData?.page_type === 2 ? 'promotion' : 'default'
}
/**
@@ -145,6 +145,52 @@ export const getImagePaths = (resourcesData: PageDataResourceGroup) => {
}
}
+
+export const formatDateOffset = ({
+ ts,
+ lang,
+ useSeconds,
+ useTimezone
+}: {
+ ts: number
+ lang: string
+ useSeconds?: boolean
+ useTimezone?: boolean
+}) => {
+ const offset = { ko: 9, ja: 9, 'zh-tw': 8, en: 0 }[lang] || 0
+ const date = new Date(ts + offset * 3600000)
+ const pad = (n: number) => String(n).padStart(2, '0')
+
+ const year = date.getUTCFullYear()
+ const month = date.getUTCMonth() + 1
+ const day = date.getUTCDate()
+ const hours = date.getUTCHours()
+ const minutes = date.getUTCMinutes()
+ const seconds = date.getUTCSeconds()
+
+ if (lang === 'ko') {
+ let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}`
+ format += useSeconds ? `:${pad(seconds)}` : ''
+ format += useTimezone ? ' (KST)' : ''
+ return `${format}`
+ } else if (lang === 'zh-tw') {
+ let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}`
+ format += useSeconds ? `:${pad(seconds)}` : ''
+ format += useTimezone ? ` (UTC${offset > 0 ? '+' + offset : ''})` : ''
+ return `${format}`
+ } else if (lang === 'ja') {
+ let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}`
+ format += useSeconds ? `:${pad(seconds)}` : ''
+ format += useTimezone ? ' (日本時間)' : ''
+ return `${format}`
+ } else {
+ //= en
+ let format = `${pad(month)}/${pad(day)}/${year} ${pad(hours)}:${pad(minutes)}`
+ format += useSeconds ? `:${pad(seconds)}` : ''
+ format += useTimezone ? ' (UTC)' : ''
+ return `${format}`
+ }
+}
/**
* 컴포넌트 그룹의 첫 번째 데이터를 반환합니다.
* @param components props.components 또는 group 객체
@@ -158,4 +204,4 @@ export const getComponentGroup = (
if (!components) return null
return components[componentName]?.groups?.[0] || null
-}
+}
\ No newline at end of file
diff --git a/layers/utils/localeUtil.ts b/layers/utils/localeUtil.ts
index 7272e7d..7b7a89b 100644
--- a/layers/utils/localeUtil.ts
+++ b/layers/utils/localeUtil.ts
@@ -1,6 +1,6 @@
-import { DEFAULT_LOCALE_CODE, DEFAULT_COVERAGES } from '@/i18n.config'
+import { DEFAULT_LOCALE_CODE } from '../../i18n.config'
-// 사용자 선호 언어 가져오기
+// 사용자 선호 언어 조회
export const getPreferredLanguage = (acceptLanguageHeader = '') => {
const languages = acceptLanguageHeader
.split(',')
@@ -13,58 +13,18 @@ export const getPreferredLanguage = (acceptLanguageHeader = '') => {
return languages.length > 0 ? languages[0].code : null
}
-export const getFinalLanguage = (path = '', defaultLocale: string, coverages: string[]) => {
- // const nuxtApp = useNuxtApp()
- let finalLocale = ''
- let requestedLocale
- let acceptLanguage: string
- let defaultLang = 'en'
- let defaultLangEn: string
- if (defaultLocale) {
- defaultLangEn = defaultLocale
- } else {
- defaultLangEn = 'en'
+// 쿠키 파싱 유틸리티 함수
+const parseCookies = (cookieHeader: string) => {
+ const cookies: Record = {}
+ if (cookieHeader) {
+ cookieHeader.split(';').forEach((cookie) => {
+ const [name, value] = cookie.trim().split('=')
+ if (name && value) {
+ cookies[name] = decodeURIComponent(value)
+ }
+ })
}
-
- requestedLocale = path?.split('/')[1]?.toLowerCase() ?? 'undefined'
-
- if (import.meta.server) {
- const headers = useRequestHeaders(['accept-language'])
- acceptLanguage = headers['accept-language'] || defaultLangEn
-
- defaultLang =
- coverages.find((locale: string) => getPreferredLanguage(acceptLanguage)?.startsWith(locale)) || defaultLangEn
- }
-
- // const DEFAULT_COVERAGES = i18n.locales.map((locale) => locale.code)
- const DEFAULT_COVERAGES = coverages
- const requestedPage = path?.split('/')[2]?.toLowerCase() ?? undefined
-
- const localeMap: Record = {
- 'zh-tw': 'zh-TW',
- 'zh-cn': 'zh-CN'
- }
-
- if (localeMap[requestedLocale]) {
- requestedLocale = localeMap[requestedLocale]
- }
-
- if (requestedLocale !== undefined && DEFAULT_COVERAGES.includes(requestedLocale)) {
- finalLocale = requestedLocale
- } else if (
- requestedLocale === undefined ||
- requestedLocale === '' ||
- path !== '' ||
- (requestedLocale !== undefined && !DEFAULT_COVERAGES.includes(requestedLocale) && requestedPage !== undefined)
- ) {
- // 요청된 언어가 없을 때 or 잘못된 언어코드로 요청 시 브라우저 언어로 설정
- finalLocale = defaultLang
- } else {
- // 그 외의 경우 기본 언어로 설정 (중국어 번체)
- finalLocale = defaultLangEn
- }
-
- return finalLocale.toLowerCase()
+ return cookies
}
/**
@@ -73,34 +33,38 @@ export const getFinalLanguage = (path = '', defaultLocale: string, coverages: st
* @param {string} path - 현재 URL 경로
*/
export const csrGetFinalLocale = (path = '') => {
+ const config = useRuntimeConfig()
+ const baseDomain = `${config.public.baseDomain}`
+
let finalLocale = DEFAULT_LOCALE_CODE // 기본값 설정
- const localeMap: Record = {
- 'zh-tw': 'zh-TW',
- 'zh-cn': 'zh-CN'
- }
// 1. URL 패스에 포함된 언어
if (path && path !== '' && path.split('/').length > 1) {
- const pathLocal = path.split('/')[1]
+ // 쿼리스트링 제거한 순수 path 검사
+ if (path.includes('?')) {
+ path = path.split('?')[0]
+ }
+ const pathLocale = `${path.split('/')[1]}`.toLowerCase()
// URL 패스에 포함된 언어가 지원하는 언어인지 체크
- if (pathLocal && pathLocal !== '' && DEFAULT_COVERAGES.includes(pathLocal)) {
- finalLocale = pathLocal // .toLowerCase()
-
- if (localeMap[pathLocal]) {
- finalLocale = localeMap[pathLocal]
- }
+ if (pathLocale && pathLocale !== '') {
+ finalLocale = pathLocale
+ return finalLocale
}
return finalLocale
}
- // 2. 브라우저 언어
- const browserLanguage = navigator.language || navigator.languages[0]
- if (browserLanguage && browserLanguage !== '' && DEFAULT_COVERAGES.includes(browserLanguage)) {
- finalLocale = browserLanguage // .toLowerCase()
- if (localeMap[browserLanguage]) {
- finalLocale = localeMap[browserLanguage]
- }
+ // 2. LOCALE 쿠키 언어
+ const cookieLanguage = `${useCookie('LOCALE', { domain: baseDomain }).value}`.toLowerCase()
+ if (cookieLanguage && cookieLanguage !== '') {
+ finalLocale = cookieLanguage
+ return finalLocale
+ }
+
+ // 3. 브라우저 언어
+ const browserLanguage = `${navigator.language || navigator.languages[0]}`.toLowerCase()
+ if (browserLanguage && browserLanguage !== '' && coveragesLocales.includes(browserLanguage)) {
+ finalLocale = browserLanguage
return finalLocale
}
@@ -118,22 +82,31 @@ export const csrGetFinalLocale = (path = '') => {
*/
export const ssrGetFinalLocale = (path = '', headers: any) => {
let finalLocale = DEFAULT_LOCALE_CODE // 기본값 설정
-
-
try {
// 1. URL path에 포함된 언어 정보
if (path && path !== '' && path.split('/').length > 1) {
- const pathLocale = path.split('/')[1]
-
+ // 쿼리스트링 제거한 순수 path 검사
+ if (path.includes('?')) {
+ path = path.split('?')[0]
+ }
+ const pathLocalee = `${path.split('/')[1]}`.toLowerCase()
// URL path에 포함된 언어 정보가 지원하는 언어인지 체크
- if (pathLocale && pathLocale !== '' && DEFAULT_COVERAGES.includes(pathLocale)) {
- finalLocale = pathLocale // .toLowerCase()
-
+ if (pathLocalee && pathLocalee !== '') {
+ finalLocale = pathLocalee
return finalLocale
}
}
- // 2. 요청 헤더의 브라우저 언어 (accept-language)
+ // 2. LOCALE 쿠키 언어 (SSR에서는 headers에서 직접 파싱)
+ const cookieHeader = headers.cookie || ''
+ const cookies = parseCookies(cookieHeader)
+ const cookieLanguage = cookies.LOCALE ? `${cookies.LOCALE}`.toLowerCase() : ''
+ if (cookieLanguage && cookieLanguage !== '' && coveragesLocales.includes(cookieLanguage)) {
+ finalLocale = cookieLanguage
+ return finalLocale
+ }
+
+ // 3. 요청 헤더의 브라우저 언어 (accept-language)
if (headers && headers['accept-language']) {
const acceptLanguage = Array.isArray(headers['accept-language'])
? headers['accept-language'][0]
@@ -143,13 +116,11 @@ export const ssrGetFinalLocale = (path = '', headers: any) => {
const preferredLocale = getPreferredLanguage(acceptLanguage)
if (preferredLocale) {
// 선호 언어의 기본 코드와 일치하는 지원 로케일 찾기
- const matchedLocale = DEFAULT_COVERAGES.find((locale: string) =>
+ const matchedLocale = coveragesLocales.find((locale: string) =>
preferredLocale.toLowerCase().startsWith(locale.toLowerCase())
)
if (matchedLocale) {
- finalLocale = matchedLocale
- // return matchedLocale.toLowerCase()
-
+ finalLocale = matchedLocale.toLowerCase()
return finalLocale
}
}
@@ -159,7 +130,6 @@ export const ssrGetFinalLocale = (path = '', headers: any) => {
// 3. 서비스 기본 언어
finalLocale = DEFAULT_LOCALE_CODE
} catch (e) {
- console.error('[Exception] localeUtil.ssrGetFinalLocale: ', e)
finalLocale = DEFAULT_LOCALE_CODE
}
return finalLocale
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 47f66cf..fa1040c 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -63,7 +63,7 @@ export default defineNuxtConfig({
payloadExtraction: false,
},
typescript: {
- typeCheck: true,
+ typeCheck: false,
strict: false,
},
nitro: {
diff --git a/package.json b/package.json
index 6f980b3..b3ad0ff 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 92b9f42..b3f4e6b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,9 @@ importers:
'@pinia/nuxt':
specifier: ^0.6.1
version: 0.6.1(magicast@0.3.5)(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))
+ '@seed-next/date':
+ specifier: ^0.0.0
+ version: 0.0.0
'@splidejs/splide':
specifier: ^4.1.4
version: 4.1.4
@@ -96,6 +99,9 @@ importers:
eslint-plugin-vue:
specifier: ^10.4.0
version: 10.4.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1))(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.5.1)))
+ lru-cache:
+ specifier: ^11.1.0
+ version: 11.2.2
postcss:
specifier: ^8.5.6
version: 8.5.6
@@ -302,6 +308,12 @@ packages:
peerDependencies:
postcss-selector-parser: ^7.0.0
+ '@date-fns/tz@1.4.1':
+ resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
+
+ '@date-fns/utc@2.1.1':
+ resolution: {integrity: sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==}
+
'@emnapi/core@1.5.0':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
@@ -1496,6 +1508,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@seed-next/date@0.0.0':
+ resolution: {integrity: sha1-d6+dtjsFjxR4SGWSuBpJrxZCgwc=, tarball: https://git.sginfra.net/api/v4/projects/3708/packages/npm/@seed-next/date/-/@seed-next/date-0.0.0.tgz}
+
'@sindresorhus/is@7.0.2':
resolution: {integrity: sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==}
engines: {node: '>=18'}
@@ -2288,6 +2303,9 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+ date-fns@4.1.0:
+ resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
+
db0@0.3.2:
resolution: {integrity: sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw==}
peerDependencies:
@@ -3246,6 +3264,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+ lru-cache@11.2.2:
+ resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
+ engines: {node: 20 || >=22}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -5087,6 +5109,10 @@ snapshots:
dependencies:
postcss-selector-parser: 7.1.0
+ '@date-fns/tz@1.4.1': {}
+
+ '@date-fns/utc@2.1.1': {}
+
'@emnapi/core@1.5.0':
dependencies:
'@emnapi/wasi-threads': 1.1.0
@@ -6253,6 +6279,12 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.50.0':
optional: true
+ '@seed-next/date@0.0.0':
+ dependencies:
+ '@date-fns/tz': 1.4.1
+ '@date-fns/utc': 2.1.1
+ date-fns: 4.1.0
+
'@sindresorhus/is@7.0.2': {}
'@sindresorhus/merge-streams@2.3.0': {}
@@ -7155,6 +7187,8 @@ snapshots:
csstype@3.1.3: {}
+ date-fns@4.1.0: {}
+
db0@0.3.2: {}
de-indent@1.0.2: {}
@@ -8147,6 +8181,8 @@ snapshots:
lru-cache@10.4.3: {}
+ lru-cache@11.2.2: {}
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
diff --git a/public/images/common/img_error.png b/public/images/common/img_error.png
new file mode 100644
index 0000000..c3dbc4a
Binary files /dev/null and b/public/images/common/img_error.png differ
diff --git a/public/images/common/logo-stove.svg b/public/images/common/logo-stove.svg
new file mode 100644
index 0000000..208d0a5
--- /dev/null
+++ b/public/images/common/logo-stove.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/temp/middleware.ts b/temp/middleware.ts
new file mode 100644
index 0000000..03cd7f3
--- /dev/null
+++ b/temp/middleware.ts
@@ -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(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)
+ }
+ }
+})
diff --git a/tsconfig.json b/tsconfig.json
index 51f3aea..698edbb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -14,6 +14,6 @@
"types/**/*",
"layers/**/*",
"app/**/*"
- ],
+, "temp/inspection.ts", "temp/middleware.ts" ],
"exclude": [".nuxt/types/**/*", "node_modules"]
}