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/error.vue b/app/pages/error.vue deleted file mode 100644 index 1068f43..0000000 --- a/app/pages/error.vue +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file diff --git a/app/pages/inspection/index.vue b/app/pages/inspection/index.vue new file mode 100644 index 0000000..afc0268 --- /dev/null +++ b/app/pages/inspection/index.vue @@ -0,0 +1,325 @@ + + + + + diff --git a/error.vue b/error.vue index dc92961..4055e3d 100644 --- a/error.vue +++ b/error.vue @@ -1,221 +1,25 @@ + + - - - - - \ No newline at end of file +
+

{{ error?.statusCode }}

+

{{ error?.statusMessage }}

+ +
+ + + 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..c3a2e46 --- /dev/null +++ b/layers/composables/useGetInspectionDataExternal.ts @@ -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(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 } +} diff --git a/layers/layouts/inspection.vue b/layers/layouts/inspection.vue new file mode 100644 index 0000000..ddd7e8c --- /dev/null +++ b/layers/layouts/inspection.vue @@ -0,0 +1,6 @@ + + + diff --git a/layers/middleware/inspection.ts b/layers/middleware/inspection.ts new file mode 100644 index 0000000..e1ecbd4 --- /dev/null +++ b/layers/middleware/inspection.ts @@ -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) + } +}) diff --git a/layers/middleware/pageData.global.ts b/layers/middleware/pageData.global.ts index af563db..abb60d9 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.matched) { + return + } + const pageUrl = getPathAfterLanguage(to.path) // pageUrl이 빈값이거나 null이면 /brand로 리다이렉트 @@ -36,9 +40,17 @@ export default defineNuxtRouteMiddleware(async (to, _from) => { loading: true, })) as PageDataResponse | null + console.log("🚀 ~ response?.code:", response?.code) + if(response?.code === 91003) { + throw createError({ + statusCode: 404, + statusMessage: 'Page not found', + }) + } + if (response?.code === 0 && 'value' in response) { store.setPageData(response.value) - console.log('🚀 ~ pageData:', response.value) + // console.log('🚀 ~ pageData:', response.value) } else { store.clearPageData() } 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..480b2cc 100644 --- a/layers/server/middleware/gameData.ts +++ b/layers/server/middleware/gameData.ts @@ -6,8 +6,13 @@ import { } from 'h3' import { ssrGetFinalLocale } from '../../utils/localeUtil' import type { GameDataResponse } from '../../types/api/gameData' +import type { ResGetInspectionData } from '../../types/InspectionType' export default defineEventHandler(async event => { + + const config = useRuntimeConfig() + const iBaseApiUrl = `${config.public.stoveApiUrlServer}` + const url = getRequestURL(event) // 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리 @@ -36,24 +41,12 @@ export default defineEventHandler(async event => { const config = useRuntimeConfig() const stoveApiUrlServer = config.public.stoveApiUrlServer const apiUrl = `${stoveApiUrlServer}/pub-comm/v1.0/template/game` - + let inspectionData const langCode = ssrGetFinalLocale( event?.node.req.url, event.node.req.headers ) - // URL의 첫 번째 path를 lang_code로 사용 (파비콘, API 경로 제외) - // const pathSegments = url.pathname - // .split('/') - // .filter( - // segment => - // segment && - // !segment.includes('favicon') && - // !segment.includes('api') && - // !segment.startsWith('_') - // ) - // const langCode = pathSegments[0] || 'ko' - const queryParams: Record = { game_domain: event.context.gameDomain || '', lang_code: langCode, @@ -67,8 +60,43 @@ export default defineEventHandler(async event => { event.context.gameData = response.value event.context.googleAnalyticsId = response.value?.ga_code - console.log('🚀 ~ gameData:', response.value) + // console.log('🚀 ~ gameData:', response.value) + + // 점검 데이터 조회 + if (response.value.game_id) { + const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response.value.game_id}` + + // 직접 $fetch 사용 (composable 사용하지 않음) + const inspectionResponse = await $fetch(inspectionApiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + inspectionData = inspectionResponse?.value?.inspection + // console.log("🚀 ~ inspectionData:", inspectionData) + + if (inspectionData?.inspection_status === 0 ) { + /** + * 점검 중인 경우 + * - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ + * - 점검 URL 경로가 아닐 경우 no-cache 설정 + * - 화이트 리스트 체크 + */ + // 현재 경로가 점검 페이지가 아닐 경우 리다이렉트 + const inspectionPath = `/${langCode}/inspection` + + if (!url.pathname.includes('/inspection')) { + event.node.res.statusCode = 302 + event.node.res.setHeader('Location', inspectionPath) + event.node.res.end() + return + } + } + + } } + } catch (error) { console.error('gameData load error:', error) } 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..4ef5537 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,23 @@ declare global { stoveJsService?: StoveJsService } } +interface CommonRequestType { + baseApiUrl: string + gameId: string +} + +interface CommonResponseType { + code?: number + message?: string +} + +interface CommonPeriodType { + startDate?: string + endDate?: string +} +interface ParsedCustomLinkOptions { + tm: (key: string) => { txt: string } + query?: Record +} + +export type { CommonRequestType, CommonResponseType, CommonPeriodType, ParsedCustomLinkOptions } diff --git a/layers/types/DataizationType.ts b/layers/types/DataizationType.ts new file mode 100644 index 0000000..07faafd --- /dev/null +++ b/layers/types/DataizationType.ts @@ -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 +} + +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..69ed9ae --- /dev/null +++ b/layers/types/InspectionType.ts @@ -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 } diff --git a/layers/utils/commonUtil.ts b/layers/utils/commonUtil.ts new file mode 100644 index 0000000..1d22531 --- /dev/null +++ b/layers/utils/commonUtil.ts @@ -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 +} diff --git a/layers/utils/dataUtil.ts b/layers/utils/dataUtil.ts index 7c3d66d..b9fa47b 100644 --- a/layers/utils/dataUtil.ts +++ b/layers/utils/dataUtil.ts @@ -154,3 +154,50 @@ export const getCurrentTimestamp = (unit: 'ms' | 's' = 'ms'): number => { const now = Date.now() return unit === 's' ? Math.floor(now / 1000) : now } + + +export const formatDateOffset = ({ + ts, + lang, + useSeconds, + useTimezone +}: { + ts: number + lang: string + useSeconds?: boolean + useTimezone?: boolean +}) => { + const offset = { ko: 9, ja: 9, 'zh-tw': 8, en: 0 }[lang] || 0 + const date = new Date(ts + offset * 3600000) + const pad = (n: number) => String(n).padStart(2, '0') + + const year = date.getUTCFullYear() + const month = date.getUTCMonth() + 1 + const day = date.getUTCDate() + const hours = date.getUTCHours() + const minutes = date.getUTCMinutes() + const seconds = date.getUTCSeconds() + + if (lang === 'ko') { + let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}` + format += useSeconds ? `:${pad(seconds)}` : '' + format += useTimezone ? ' (KST)' : '' + return `${format}` + } else if (lang === 'zh-tw') { + let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}` + format += useSeconds ? `:${pad(seconds)}` : '' + format += useTimezone ? ` (UTC${offset > 0 ? '+' + offset : ''})` : '' + return `${format}` + } else if (lang === 'ja') { + let format = `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}` + format += useSeconds ? `:${pad(seconds)}` : '' + format += useTimezone ? ' (日本時間)' : '' + return `${format}` + } else { + //= en + let format = `${pad(month)}/${pad(day)}/${year} ${pad(hours)}:${pad(minutes)}` + format += useSeconds ? `:${pad(seconds)}` : '' + format += useTimezone ? ' (UTC)' : '' + return `${format}` + } +} 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..336f762 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 @@ -106,7 +112,7 @@ importers: specifier: ^3.4.17 version: 3.4.17 typescript: - specifier: ^5.5.0 + specifier: ^5.3.3 version: 5.9.2 vue-tsc: specifier: ^3.0.7 @@ -302,6 +308,12 @@ packages: peerDependencies: postcss-selector-parser: ^7.0.0 + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + + '@date-fns/utc@2.1.1': + resolution: {integrity: sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==} + '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} @@ -1496,6 +1508,9 @@ packages: cpu: [x64] os: [win32] + '@seed-next/date@0.0.0': + resolution: {integrity: sha1-d6+dtjsFjxR4SGWSuBpJrxZCgwc=, tarball: https://git.sginfra.net/api/v4/projects/3708/packages/npm/@seed-next/date/-/@seed-next/date-0.0.0.tgz} + '@sindresorhus/is@7.0.2': resolution: {integrity: sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==} engines: {node: '>=18'} @@ -2288,6 +2303,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + db0@0.3.2: resolution: {integrity: sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw==} peerDependencies: @@ -3246,6 +3264,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -5087,6 +5109,10 @@ snapshots: dependencies: postcss-selector-parser: 7.1.0 + '@date-fns/tz@1.4.1': {} + + '@date-fns/utc@2.1.1': {} + '@emnapi/core@1.5.0': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -6253,6 +6279,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.50.0': optional: true + '@seed-next/date@0.0.0': + dependencies: + '@date-fns/tz': 1.4.1 + '@date-fns/utc': 2.1.1 + date-fns: 4.1.0 + '@sindresorhus/is@7.0.2': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -7155,6 +7187,8 @@ snapshots: csstype@3.1.3: {} + date-fns@4.1.0: {} + db0@0.3.2: {} de-indent@1.0.2: {} @@ -8147,6 +8181,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 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"] }