fix. 서버 미들웨어, 미들웨어 수정
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
# 타입 변경 검증 리포트
|
||||
|
||||
## 변경 사항
|
||||
|
||||
`NodeJS.Timeout` 타입을 `ReturnType<typeof setTimeout>`으로 변경하여 더 범용적으로 사용 가능하도록 개선했습니다.
|
||||
|
||||
## 변경된 파일
|
||||
|
||||
1. `layers/stores/useModalStore.ts` - `toastTimeoutId` 타입 변경
|
||||
2. `layers/stores/useLoadingStore.ts` - `apiLoadingTimeoutId` 타입 변경
|
||||
3. `layers/components/atoms/Video.vue` - `pauseTimeoutId` 타입 변경
|
||||
4. `layers/templates/GrGallery01/index.vue` - `stopVideoTimeoutId` 타입 변경
|
||||
5. `layers/composables/useGameStart.ts` - `launcherTimeoutId` 타입 변경
|
||||
|
||||
## 타입 호환성 검증
|
||||
|
||||
### ✅ 타입 체크 통과
|
||||
- `pnpm typecheck` 명령어 실행 결과: **성공** (에러 없음)
|
||||
- 모든 파일에서 타입 에러 없음
|
||||
|
||||
### ✅ clearTimeout 호환성
|
||||
- `clearTimeout`은 `ReturnType<typeof setTimeout>` 타입을 정상적으로 받을 수 있음
|
||||
- 브라우저 환경: `clearTimeout(number)` ✅
|
||||
- Node.js 환경: `clearTimeout(NodeJS.Timeout)` ✅
|
||||
|
||||
### ✅ 사용 패턴 검증
|
||||
모든 파일에서 다음 패턴이 정상적으로 작동함:
|
||||
```typescript
|
||||
// 타입 선언
|
||||
const timeoutId = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
// setTimeout 사용
|
||||
timeoutId.value = setTimeout(() => {
|
||||
// ...
|
||||
}, delay)
|
||||
|
||||
// clearTimeout 사용
|
||||
if (timeoutId.value) {
|
||||
clearTimeout(timeoutId.value) // ✅ 정상 작동
|
||||
timeoutId.value = null
|
||||
}
|
||||
```
|
||||
|
||||
## 타입 비교
|
||||
|
||||
### 이전: `NodeJS.Timeout`
|
||||
- **장점**: Node.js 환경에서 명확한 타입
|
||||
- **단점**: 브라우저 환경에서 타입 불일치 가능성
|
||||
- **문제**: 브라우저에서는 `setTimeout`이 `number`를 반환하므로 타입 에러 발생 가능
|
||||
|
||||
### 변경 후: `ReturnType<typeof setTimeout>`
|
||||
- **장점**:
|
||||
- 환경에 따라 자동으로 적절한 타입 선택
|
||||
- 브라우저: `number`
|
||||
- Node.js: `NodeJS.Timeout`
|
||||
- 범용적이고 유연함
|
||||
- **단점**: 없음
|
||||
- **결과**: 모든 환경에서 정상 작동
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### ✅ 타입 안정성
|
||||
- TypeScript 컴파일러가 모든 타입을 정상적으로 추론
|
||||
- 타입 에러 없음
|
||||
|
||||
### ✅ 런타임 호환성
|
||||
- `clearTimeout`이 모든 환경에서 정상 작동
|
||||
- 브라우저와 Node.js 모두 지원
|
||||
|
||||
### ✅ 코드 품질
|
||||
- 더 범용적이고 유지보수하기 쉬운 타입
|
||||
- 환경 의존성 제거
|
||||
|
||||
## 결론
|
||||
|
||||
**✅ 변경 완료 및 검증 통과**
|
||||
|
||||
`ReturnType<typeof setTimeout>` 타입으로 변경해도 문제가 없으며, 오히려 더 범용적이고 안전한 타입입니다. 모든 타입 체크를 통과했고, `clearTimeout`과의 호환성도 확인되었습니다.
|
||||
|
||||
@@ -23,7 +23,7 @@ const metaData = ref<GameDataMetaTag | null>(null)
|
||||
// SSR에서 게임 데이터 가져오기
|
||||
const getGameDataFromServer = (): GameDataValue | null => {
|
||||
return import.meta.server
|
||||
? (nuxtApp.ssrContext?.event.context.gameData ?? null)
|
||||
? nuxtApp.ssrContext?.event?.context?.gameData
|
||||
: null
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { usePageDataStore } from '#layers/stores/usePageDataStore'
|
||||
import type { PageDataValue } from '#layers/types/api/pageData'
|
||||
|
||||
const currentLayout = ref<'default' | 'promotion' | null>('default')
|
||||
const currentPageData = ref<PageDataValue | null>(null)
|
||||
const currentLayout = ref<'default' | 'promotion' | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
const pageDataStore = usePageDataStore()
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { usePageDataStore } from '#layers/stores/usePageDataStore'
|
||||
import type { PageDataValue } from '#layers/types/api/pageData'
|
||||
|
||||
const currentLayout = ref<'default' | 'promotion' | null>('default')
|
||||
const currentPageData = ref<PageDataValue | null>(null)
|
||||
const currentLayout = ref<'default' | 'promotion' | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
const pageDataStore = usePageDataStore()
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { usePageDataStore } from '#layers/stores/usePageDataStore'
|
||||
import type { PageDataValue } from '#layers/types/api/pageData'
|
||||
|
||||
const currentLayout = ref<'default' | 'promotion' | null>('default')
|
||||
const currentPageData = ref<PageDataValue | null>(null)
|
||||
const currentLayout = ref<'default' | 'promotion' | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
const pageDataStore = usePageDataStore()
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { usePageDataStore } from '#layers/stores/usePageDataStore'
|
||||
import type { PageDataValue } from '#layers/types/api/pageData'
|
||||
|
||||
const currentLayout = ref<'default' | 'promotion' | null>('default')
|
||||
const currentPageData = ref<PageDataValue | null>(null)
|
||||
const currentLayout = ref<'default' | 'promotion' | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
const pageDataStore = usePageDataStore()
|
||||
|
||||
@@ -79,13 +79,17 @@
|
||||
|
||||
<div v-if="launchingStatus" class="flex flex-1">
|
||||
<BlocksButtonLauncher
|
||||
v-if="Number(gameData?.platform_type) !== 2 && device.isDesktop || Number(gameData?.platform_type) === 1"
|
||||
v-if="
|
||||
(Number(gameData?.platform_type) !== 2 &&
|
||||
device.isDesktop) ||
|
||||
Number(gameData?.platform_type) === 1
|
||||
"
|
||||
type="custom"
|
||||
platform="pc"
|
||||
class="inspection-btn inspection-btn-primary w-full color-black text-sm md:text-base flex-1"
|
||||
>
|
||||
<span>{{ tm('Txt_Game_Start') }}</span>
|
||||
|
||||
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
@@ -103,7 +107,7 @@
|
||||
</BlocksButtonLauncher>
|
||||
|
||||
<AtomsButtonVariant
|
||||
v-else-if="Number(gameData?.platform_type) !== 1"
|
||||
v-else-if="Number(gameData?.platform_type) !== 1"
|
||||
type="custom"
|
||||
class="inspection-btn inspection-btn-primary w-full color-black text-sm md:text-base flex-1"
|
||||
@click="handleGameStart"
|
||||
@@ -125,10 +129,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</AtomsButtonVariant>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -139,7 +140,11 @@
|
||||
:class="`platform-type-${gameData?.platform_type}`"
|
||||
>
|
||||
<h3 class="card-title text-base md:text-lg">
|
||||
{{ t('Inspection_Txt_Download', { gameName: gameData?.game_name }) }}
|
||||
{{
|
||||
t('Inspection_Txt_Download', {
|
||||
gameName: gameData?.game_name,
|
||||
})
|
||||
}}
|
||||
</h3>
|
||||
<div class="flex flex-row gap-3 flex-wrap">
|
||||
<BlocksButtonLauncher
|
||||
@@ -166,7 +171,6 @@
|
||||
<script setup lang="ts">
|
||||
import { globalDateFormat } from '@seed-next/date'
|
||||
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiUrl = config.public.stoveApiUrl as string
|
||||
|
||||
@@ -185,7 +189,8 @@ const { t, tm, locale }: any = useI18n({
|
||||
|
||||
const device = useDevice()
|
||||
const loadingStore = useLoadingStore()
|
||||
const { webInspectionData, getInspectionDataExternal } = useGetInspectionDataExternal()
|
||||
const { webInspectionData, getInspectionDataExternal } =
|
||||
useGetInspectionDataExternal()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
await getInspectionDataExternal({
|
||||
@@ -244,10 +249,12 @@ const enabledMarkets = computed(() => {
|
||||
platform,
|
||||
url: info.url as string,
|
||||
}))
|
||||
|
||||
|
||||
// platform_type이 1이면 app_store, google_play 제외하고 pc 추가
|
||||
if (platformType === 1) {
|
||||
const filteredMarkets = markets.filter(m => m.platform !== 'app_store' && m.platform !== 'google_play')
|
||||
const filteredMarkets = markets.filter(
|
||||
m => m.platform !== 'app_store' && m.platform !== 'google_play'
|
||||
)
|
||||
const hasPc = filteredMarkets.some(m => m.platform === 'pc')
|
||||
if (!hasPc) {
|
||||
filteredMarkets.unshift({
|
||||
@@ -257,12 +264,12 @@ const enabledMarkets = computed(() => {
|
||||
}
|
||||
return filteredMarkets
|
||||
}
|
||||
|
||||
|
||||
// platform_type이 2이면 pc 제외
|
||||
if (platformType === 2) {
|
||||
return markets.filter(m => m.platform !== 'pc')
|
||||
}
|
||||
|
||||
|
||||
// platform_type이 3이면 pc 항목 추가
|
||||
if (platformType === 3) {
|
||||
const hasPc = markets.some(m => m.platform === 'pc')
|
||||
@@ -273,7 +280,7 @@ const enabledMarkets = computed(() => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return markets
|
||||
})
|
||||
|
||||
@@ -310,15 +317,18 @@ const getButtonText = (platform: string) => {
|
||||
google_play: 'platform_google_play',
|
||||
app_store: 'platform_app_store',
|
||||
}
|
||||
|
||||
|
||||
const key = platformKeyMap[platform]
|
||||
return key ? tm(key) : ''
|
||||
}
|
||||
|
||||
const handleGameStart = () => {
|
||||
|
||||
const os = device.isAndroid ? 'google_play' : device.isApple ? 'app_store' : 'google_play'
|
||||
|
||||
const os = device.isAndroid
|
||||
? 'google_play'
|
||||
: device.isApple
|
||||
? 'app_store'
|
||||
: 'google_play'
|
||||
|
||||
//const os = device.isAndroid ? 'google_play' : device.isApple ? 'app_store' : 'pc'
|
||||
|
||||
// platform_type이 2이면서 device.isDesktop이면 window.open(gameData.value.market_json[os].url, '_blank')
|
||||
@@ -328,11 +338,8 @@ const handleGameStart = () => {
|
||||
|
||||
onMounted(() => {
|
||||
loadingStore.stopFullLoading()
|
||||
|
||||
console.log("🚀 ~ 3333 onMounted ~ enabledMarkets:", enabledMarkets.value)
|
||||
})
|
||||
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['inspection'],
|
||||
layout: 'only-stove',
|
||||
@@ -480,11 +487,11 @@ definePageMeta({
|
||||
@apply w-full flex-none hidden md:flex md:flex-1;
|
||||
}
|
||||
|
||||
|
||||
.inspection-download-card .btn-platform-app_store {
|
||||
@apply flex-1 px-2 md:px-4;
|
||||
}
|
||||
.inspection-download-card.platform-type-3 .btn-platform-app_store, .inspection-download-card.platform-type-3 .btn-platform-google_play {
|
||||
.inspection-download-card.platform-type-3 .btn-platform-app_store,
|
||||
.inspection-download-card.platform-type-3 .btn-platform-google_play {
|
||||
@apply flex-1 px-2 md:px-4 md:flex-none;
|
||||
}
|
||||
:deep(.inspection-download-card .btn-platform-app_store .text) {
|
||||
@@ -499,7 +506,9 @@ definePageMeta({
|
||||
:deep(.inspection-download-card .btn-platform-google_play .text) {
|
||||
@apply block md:hidden;
|
||||
}
|
||||
:deep(.inspection-download-card.platform-type-2 .btn-platform-google_play .text) {
|
||||
:deep(
|
||||
.inspection-download-card.platform-type-2 .btn-platform-google_play .text
|
||||
) {
|
||||
@apply block md:block;
|
||||
}
|
||||
:deep(.inspection-download-card .btn-base.single .icon-platform) {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
@import url('https://static-cdn.onstove.com/resources/stds/stds-font-kr/stds-font-kr.css');
|
||||
@import url('https://static-cdn.onstove.com/resources/stds/stds-font-global/stds-font-global.css');
|
||||
/* @import url(https://static-cdn.onstove.com/0.0.4/font-icon/StoveFont-Icon.css); */
|
||||
|
||||
:lang(ko) {
|
||||
font-family:
|
||||
stds-font-kr,
|
||||
|
||||
@@ -24,8 +24,9 @@ const componentTag = computed((): string => {
|
||||
switch (props.type) {
|
||||
case 'external':
|
||||
case 'link':
|
||||
case 'download':
|
||||
return 'a'
|
||||
case 'download':
|
||||
return props.href ? 'a' : 'button'
|
||||
case 'internal':
|
||||
return 'AtomsLocaleLink'
|
||||
default:
|
||||
@@ -47,11 +48,14 @@ const componentProps = computed(() => {
|
||||
}
|
||||
|
||||
if (props.type === 'download') {
|
||||
return {
|
||||
href: props.href,
|
||||
target: '_self',
|
||||
download: props.href?.split('/').pop() ?? 'download',
|
||||
if (props.href) {
|
||||
return {
|
||||
href: props.href,
|
||||
target: '_self',
|
||||
download: props.href?.split('/').pop() ?? 'download',
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
@@ -41,8 +41,23 @@ const imagePaths = computed(() => {
|
||||
<picture v-if="isResponsiveMode">
|
||||
<source media="(min-width: 1024px)" :srcset="imagePaths.pc" />
|
||||
<source media="(max-width: 1023px)" :srcset="imagePaths.mo" />
|
||||
<img :src="imagePaths.pc" :alt="alt" v-bind="$attrs" loading="lazy" />
|
||||
<img
|
||||
:src="imagePaths.pc"
|
||||
:alt="alt"
|
||||
v-bind="$attrs"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
fetchpriority="auto"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
<img v-else :src="imagePaths.mo" :alt="alt" v-bind="$attrs" loading="lazy" />
|
||||
<img
|
||||
v-else
|
||||
:src="imagePaths.mo"
|
||||
:alt="alt"
|
||||
v-bind="$attrs"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
fetchpriority="auto"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -6,9 +6,9 @@ import type {
|
||||
} from '#layers/types/api/eventNavigation'
|
||||
|
||||
const { locale } = useI18n()
|
||||
const gameDomain = useGetGameDomain()
|
||||
const { sendLog } = useAnalytics()
|
||||
|
||||
const gameDomain = getGameDomain()
|
||||
const analytics = {
|
||||
action_type: 'click',
|
||||
click_sarea: 'EventNavigation',
|
||||
|
||||
@@ -14,11 +14,12 @@ const props = defineProps<Props>()
|
||||
|
||||
const mainContentRef = ref<HTMLElement>()
|
||||
|
||||
const { locale } = useI18n()
|
||||
const { tm, locale } = useI18n()
|
||||
const { height: viewportH } = useWindowSize()
|
||||
const { bottom: mainBottom } = useElementBounding(mainContentRef)
|
||||
const { getTemplateComponent } = useTemplateRegistry()
|
||||
const loadingStore = useLoadingStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const { isPAssApiLoading, hasApiCallStarted } = storeToRefs(loadingStore)
|
||||
|
||||
@@ -80,6 +81,17 @@ onMounted(() => {
|
||||
if (!hasApiCallStarted.value) {
|
||||
loadingStore.stopFullLoading()
|
||||
}
|
||||
|
||||
// 페이지 접근 권한 설정(로그인 유무)
|
||||
if (props.pageData?.is_login_required === 1 && !csrGetAccessToken()) {
|
||||
modalStore.handleOpenConfirm({
|
||||
contentText: tm('Alert_StoveLogin'),
|
||||
confirmButtonText: tm('Text_StoveLogin'),
|
||||
confirmButtonEvent: () => {
|
||||
csrGoStoveLogin()
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { getHeader, getRequestHost } from 'h3'
|
||||
import { useRequestEvent } from 'nuxt/app'
|
||||
|
||||
/**
|
||||
* 게임 도메인을 가져오는 컴포저블 함수
|
||||
* 서버와 클라이언트 환경에서 모두 동작
|
||||
* @returns 게임 도메인 문자열
|
||||
*/
|
||||
export const useGetGameDomain = (): string => {
|
||||
try {
|
||||
if (import.meta.client) {
|
||||
const host = window.location.host || ''
|
||||
return host.split(':')[0]
|
||||
}
|
||||
|
||||
const event = useRequestEvent()
|
||||
if (!event) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 미들웨어에서 설정한 gameDomain가 있다면 우선 사용
|
||||
if (event.context.gameDomain) {
|
||||
return event.context.gameDomain
|
||||
}
|
||||
|
||||
const host =
|
||||
(getHeader(event, 'host') || getRequestHost(event)).toString() || ''
|
||||
const cleanHost = host.split(':')[0]
|
||||
|
||||
return cleanHost || ''
|
||||
} catch (error) {
|
||||
console.error('useGetGameDomain error:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
export const usePathResolver = () => {
|
||||
const getPathAfterLanguage = (url?: string): string => {
|
||||
// URL이 제공되지 않으면 현재 URL 사용
|
||||
const targetUrl =
|
||||
url || (import.meta.client ? window.location.pathname : '')
|
||||
|
||||
// URL에서 언어 코드 패턴을 찾아서 그 뒤의 경로를 추출
|
||||
// 예: /ko/about/story -> /about/story
|
||||
// 예: /en/test/page -> /test/page
|
||||
// 예: /zh-tw/about/story -> /about/story
|
||||
// 예: /zh-cn/test/page -> /test/page
|
||||
// 예: /ko -> "" (빈 문자열)
|
||||
const languagePattern = /^\/[a-z]{2}(-[a-z]{2})?\/(.+)$/
|
||||
const match = targetUrl.match(languagePattern)
|
||||
|
||||
if (match && match[2]) {
|
||||
return `/${match[2]}`
|
||||
}
|
||||
|
||||
// 언어 코드만 있고 뒤에 아무것도 없는 경우 (예: /ko, /en, /zh-tw, /zh-cn)
|
||||
const languageOnlyPattern = /^\/[a-z]{2}(-[a-z]{2})?$/
|
||||
if (languageOnlyPattern.test(targetUrl)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 언어 코드가 없는 경우 원본 경로 그대로 반환 (이미 /로 시작)
|
||||
return targetUrl
|
||||
}
|
||||
|
||||
const getCurrentPath = (): string => {
|
||||
if (import.meta.client) {
|
||||
return getPathAfterLanguage()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
return {
|
||||
getPathAfterLanguage,
|
||||
getCurrentPath,
|
||||
}
|
||||
}
|
||||
@@ -1,92 +1,83 @@
|
||||
import type { GameDataValue } from '#layers/types/api/gameData'
|
||||
export default defineNuxtRouteMiddleware(to => {
|
||||
// error 페이지는 실행X -----
|
||||
if (to.path.includes('/error')) return
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const getGameDataFromServer = (): GameDataValue | null => {
|
||||
return import.meta.server
|
||||
? (nuxtApp.ssrContext?.event.context.gameData ?? null)
|
||||
: null
|
||||
}
|
||||
// inspection 페이지는 실행X -----
|
||||
if (to.path.includes('inspection')) return
|
||||
|
||||
const serverGameData = getGameDataFromServer()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
const { setGameData } = gameDataStore
|
||||
// app.vue에서 설정한 스토어 값이 없으면 대기
|
||||
if (!gameData.value) return
|
||||
|
||||
if (serverGameData) {
|
||||
setGameData(serverGameData)
|
||||
}
|
||||
try {
|
||||
// 서버 사이드에서는 스킵
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
// 현재 경로에서 언어 코드 추출
|
||||
const gameData = gameDataStore.gameData as GameDataValue
|
||||
const langCodes = gameData?.lang_codes
|
||||
const currentLangCode = csrGetFinalLocale(to.path, langCodes)
|
||||
const { getPathAfterLanguage } = usePathResolver()
|
||||
const pageUrl = getPathAfterLanguage(to.path)
|
||||
|
||||
//현재 url에서 게임 도메인만 추출
|
||||
// const currentDomain = window.location.hostname
|
||||
// const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
// 쿼리스트링에서 f 파라미터 값 추출 (CSR용)
|
||||
// const fValue = (to.query.f as string) || ''
|
||||
|
||||
// 미리보기 API 호출 처리
|
||||
// let finalGameDomain = currentDomain
|
||||
// if (fValue === 'preview') {
|
||||
// finalGameDomain = 'samplegame.onstove.com'
|
||||
// }
|
||||
|
||||
// const req: GameDataRequest = {
|
||||
// gameDomain: `${finalGameDomain}`,
|
||||
// langCode: `${currentLangCode}`,
|
||||
// game_alias: '',
|
||||
// lang_code: `${currentLangCode}`,
|
||||
// baseApiUrl: `${runtimeConfig.public.stoveApiUrl}`,
|
||||
// gameId: '',
|
||||
// }
|
||||
// const { getGameDataExternal } = useGetGameDataExternal()
|
||||
// await getGameDataExternal(req)
|
||||
|
||||
// error 페이지는 API 호출하지 않음
|
||||
if (
|
||||
pageUrl === '/error' ||
|
||||
to.path.includes('/error')
|
||||
) {
|
||||
console.log('🚀 ~ init.route.global error 페이지는 API 호출하지 않음')
|
||||
showError(
|
||||
createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Internal Server Error',
|
||||
fatal: false, // 즉시 에러 페이지로
|
||||
data: { reason: 'post-not-found' },
|
||||
})
|
||||
const gamePath = getPathAfterLanguage(to.path)
|
||||
const langCode = import.meta.client
|
||||
? csrGetFinalLocale(to.path, gameData.value.lang_codes)
|
||||
: ssrGetFinalLocale(
|
||||
to.path,
|
||||
useRequestHeaders(['accept-language']),
|
||||
gameData.value.lang_codes,
|
||||
gameData.value.default_lang_code
|
||||
)
|
||||
return
|
||||
|
||||
const isRootPath = gamePath === '' || gamePath === '/'
|
||||
|
||||
if (isRootPath) {
|
||||
// gameData.intro.page_url이 있으면 해당 URL로 리다이렉트, 없으면 /home으로
|
||||
const introPageUrl = gameData.value?.intro?.page_url
|
||||
let defaultPath = `/${langCode}/home`
|
||||
|
||||
if (introPageUrl && introPageUrl.trim() !== '') {
|
||||
// 외부 URL인지 확인
|
||||
const isExternalUrl =
|
||||
introPageUrl.startsWith('http://') ||
|
||||
introPageUrl.startsWith('https://')
|
||||
|
||||
if (isExternalUrl) {
|
||||
// 외부 URL인 경우 그대로 사용
|
||||
defaultPath = introPageUrl
|
||||
} else {
|
||||
// 내부 경로인 경우 언어 코드 패턴 확인
|
||||
const normalizedIntroUrl = introPageUrl.split('?')[0] // 쿼리스트링 제외
|
||||
const languagePattern = /^\/[a-z]{2}(-[a-z]{2})?(\/|$)/
|
||||
const hasLanguageCode = languagePattern.test(normalizedIntroUrl)
|
||||
|
||||
if (hasLanguageCode) {
|
||||
// 이미 언어 코드가 있으면 그대로 사용
|
||||
defaultPath = introPageUrl
|
||||
} else {
|
||||
// 언어 코드가 없으면 추가
|
||||
const pathWithSlash = normalizedIntroUrl.startsWith('/')
|
||||
? normalizedIntroUrl
|
||||
: `/${normalizedIntroUrl}`
|
||||
defaultPath = `/${langCode}${pathWithSlash}`
|
||||
|
||||
// 쿼리스트링이 있으면 다시 추가
|
||||
if (introPageUrl.includes('?')) {
|
||||
defaultPath += '?' + introPageUrl.split('?')[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 허용된 언어 코드 목록≈≈
|
||||
const allowedLangCodes = langCodes || []
|
||||
// 무한 리다이렉트 방지: 현재 경로와 리다이렉트할 URL 비교
|
||||
const normalizedFinalUrl = defaultPath.split('?')[0] // 쿼리스트링 제외
|
||||
const currentPath = to.path
|
||||
const isExternalUrl =
|
||||
defaultPath.startsWith('http://') || defaultPath.startsWith('https://')
|
||||
const isSamePath = !isExternalUrl && currentPath === normalizedFinalUrl
|
||||
|
||||
// 현재 언어가 허용된 언어 목록에 없으면 에러 페이지로 이동
|
||||
if (currentLangCode && !allowedLangCodes.includes(currentLangCode)) {
|
||||
return navigateTo(`/${currentLangCode}/error`, { external: true })
|
||||
if (!isSamePath) {
|
||||
// 다른 경로에서 접근한 경우에만 리다이렉트
|
||||
const queryString = to.fullPath.includes('?')
|
||||
? '?' + to.fullPath.split('?')[1]
|
||||
: ''
|
||||
const redirectUrl =
|
||||
queryString && !defaultPath.includes('?')
|
||||
? `${defaultPath}${queryString}`
|
||||
: defaultPath
|
||||
return navigateTo(redirectUrl, { external: isExternalUrl })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
showError(
|
||||
createError({
|
||||
statusCode: error?.statusCode || error?.status || 500,
|
||||
statusMessage:
|
||||
error?.statusMessage || error?.message || 'Internal Server Error',
|
||||
fatal: false, // 즉시 에러 페이지로
|
||||
data: { reason: 'post-not-found' },
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
export default defineNuxtRouteMiddleware(async to => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
// server에서는 실행X -----
|
||||
if (import.meta.server) return
|
||||
|
||||
// error 페이지는 실행X -----
|
||||
if (to.path.includes('/error')) return
|
||||
|
||||
try {
|
||||
//error 발생시에는 미들웨어 실행하지 않음
|
||||
//error 객체 조회
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
const { getPathAfterLanguage } = usePathResolver()
|
||||
const pageUrl = getPathAfterLanguage(to.path)
|
||||
|
||||
// error 페이지는 API 호출하지 않음
|
||||
if (pageUrl === '/error' || to.path.includes('/error')) {
|
||||
console.log("🚀 ~ inspection error 페이지는 API 호출하지 않음")
|
||||
return
|
||||
}
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
// const baseDomain = `${runtimeConfig.public.baseDomain}`
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const stoveGameId = gameData.value.game_id
|
||||
// const stoveMaintenanceApiUrl = `${runtimeConfig.public.stoveMaintenanceApiUrl}`
|
||||
|
||||
// const localeCookie = useCookie('LOCALE', {
|
||||
// domain: baseDomain
|
||||
// })
|
||||
// app.vue에서 설정한 스토어 값이 없으면 대기
|
||||
if (!gameData.value) return
|
||||
|
||||
const stoveApiBaseUrl = `${runtimeConfig.public.stoveApiUrl}`
|
||||
// const baseDomain = `${runtimeConfig.public.baseDomain}`
|
||||
// const stoveMaintenanceApiUrl = `${runtimeConfig.public.stoveMaintenanceApiUrl}`
|
||||
const stoveGameId = gameData.value.game_id
|
||||
|
||||
/* const localeCookie = useCookie('LOCALE', {
|
||||
domain: baseDomain
|
||||
}) */
|
||||
|
||||
const finalLocale = csrGetFinalLocale(to.path, gameData.value.lang_codes)
|
||||
// localeCookie.value = finalLocale.toUpperCase()
|
||||
|
||||
// 웹 점검 -----
|
||||
const { isWebInspection, getInspectionDataExternal } = useGetInspectionDataExternal()
|
||||
|
||||
const { isWebInspection, getInspectionDataExternal } =
|
||||
useGetInspectionDataExternal()
|
||||
await getInspectionDataExternal({
|
||||
baseApiUrl: stoveApiBaseUrl,
|
||||
gameId: stoveGameId,
|
||||
@@ -48,24 +43,8 @@ export default defineNuxtRouteMiddleware(async to => {
|
||||
// lang: `${finalLocale}`.toLowerCase()
|
||||
// })
|
||||
|
||||
// 테스트 수정
|
||||
// isWebInspection.value ===
|
||||
// if (
|
||||
// !isWebInspection.value &&
|
||||
// !to.path.includes('inspection') &&
|
||||
// !to.path.includes('api')
|
||||
// ) {
|
||||
// console.log("🚀 ~ 점검 중인 경우")
|
||||
// // 점검 중인 경우
|
||||
// return navigateTo(`/${finalLocale}/inspection`, { external: true })
|
||||
// } else if ( isWebInspection.value && !to.path.includes('inspection') ) {
|
||||
// // 점검이 종료된 후 점검 페이지 접근시 메인으로 리다이렉트
|
||||
// console.log("🚀 ~ 점검이 종료된 후 점검 페이지 접근시 메인으로 리다이렉트")
|
||||
// return navigateTo(`/${finalLocale}`, { external: true })
|
||||
// }
|
||||
|
||||
if (
|
||||
isWebInspection.value === true &&
|
||||
isWebInspection.value &&
|
||||
!to.path.includes('inspection') &&
|
||||
!to.path.includes('api')
|
||||
) {
|
||||
@@ -78,20 +57,6 @@ export default defineNuxtRouteMiddleware(async to => {
|
||||
// 점검이 종료된 후 점검 페이지 접근시 메인으로 리다이렉트
|
||||
return navigateTo(`/${finalLocale}`, { external: true })
|
||||
}
|
||||
|
||||
//이동한 페이지는 에러 페이지로 리다이렉트
|
||||
|
||||
const error = useError()
|
||||
if(error.value?.statusCode){
|
||||
return showError(createError({
|
||||
statusCode: error.value?.statusCode,
|
||||
statusMessage: error.value?.message,
|
||||
fatal: true,
|
||||
data: { path: to.path }
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Exception] /middleware/inspection: ', e)
|
||||
}
|
||||
|
||||
@@ -1,130 +1,58 @@
|
||||
import { commonFetch } from '#layers/utils/apiUtil'
|
||||
import { usePageDataStore } from '#layers/stores/usePageDataStore'
|
||||
import { useLoadingStore } from '#layers/stores/useLoadingStore'
|
||||
import { useGetGameDomain } from '#layers/composables/useGetGameDomain'
|
||||
import { usePathResolver } from '#layers/composables/usePathResolver'
|
||||
import { commonFetch } from '#layers/utils/apiUtil'
|
||||
import { getGameDomain, getPathAfterLanguage } from '#layers/utils/urlUtil'
|
||||
import type { PageDataResponse } from '#layers/types/api/pageData'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
// client에서만 동작되도록 처리
|
||||
if (!import.meta.client) return
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
|
||||
const gameDomain = useGetGameDomain()
|
||||
// server에서는 실행X -----
|
||||
if (import.meta.server) return
|
||||
|
||||
// error 페이지는 실행X -----
|
||||
if (to.path.includes('/error')) return
|
||||
|
||||
// inspection 페이지는 실행X -----
|
||||
if (to.path.includes('inspection')) return
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
const pageDataStore = usePageDataStore()
|
||||
const loadingStore = useLoadingStore()
|
||||
const { getPathAfterLanguage } = usePathResolver()
|
||||
|
||||
const langCode = csrGetFinalLocale(to.path, gameData.value?.lang_codes)
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const accessToken = csrGetAccessToken()
|
||||
const gameDomain = getGameDomain()
|
||||
const gamePath = getPathAfterLanguage(to.path)
|
||||
const langCode =
|
||||
csrGetFinalLocale(to.path, gameData.value?.lang_codes) || 'ko'
|
||||
|
||||
let pageDataResponse: PageDataResponse | null = null
|
||||
|
||||
try {
|
||||
if (to.path.includes('inspection')) {
|
||||
console.log('🚀 ~ 점검페이지 접근 pageData.global')
|
||||
return
|
||||
}
|
||||
|
||||
const pageUrl = getPathAfterLanguage(to.path)
|
||||
|
||||
// 루트 경로(언어 코드만 있는 경로)로 접근할 때만 intro.page_url로 리다이렉트
|
||||
const isRootPath =
|
||||
!pageUrl ||
|
||||
pageUrl === '' ||
|
||||
pageUrl === '/' ||
|
||||
pageUrl === `/${langCode}/`
|
||||
console.log('🚀 ~ isRootPath:', isRootPath)
|
||||
|
||||
if (isRootPath) {
|
||||
// gameData.intro.page_url이 있으면 해당 URL로 리다이렉트
|
||||
const introPageUrl = gameData.value?.intro?.page_url
|
||||
if (introPageUrl && introPageUrl.trim() !== '') {
|
||||
// 외부 URL인지 확인
|
||||
const isExternalUrl =
|
||||
introPageUrl.startsWith('http://') ||
|
||||
introPageUrl.startsWith('https://')
|
||||
|
||||
// 내부 경로인 경우 언어 코드 추가
|
||||
let finalIntroUrl = introPageUrl
|
||||
if (!isExternalUrl) {
|
||||
const normalizedIntroUrl = introPageUrl.split('?')[0] // 쿼리스트링 제외
|
||||
|
||||
// 언어 코드 패턴 확인 (예: /ko, /en, /zh-tw 등)
|
||||
const languagePattern = /^\/[a-z]{2}(-[a-z]{2})?(\/|$)/
|
||||
const hasLanguageCode = languagePattern.test(normalizedIntroUrl)
|
||||
|
||||
// 언어 코드가 없으면 추가
|
||||
if (!hasLanguageCode) {
|
||||
// 경로가 /로 시작하지 않으면 / 추가
|
||||
const pathWithSlash = normalizedIntroUrl.startsWith('/')
|
||||
? normalizedIntroUrl
|
||||
: `/${normalizedIntroUrl}`
|
||||
finalIntroUrl = `/${langCode}${pathWithSlash}`
|
||||
|
||||
// 쿼리스트링이 있으면 다시 추가
|
||||
if (introPageUrl.includes('?')) {
|
||||
finalIntroUrl += '?' + introPageUrl.split('?')[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 무한 리다이렉트 방지: 현재 경로와 리다이렉트할 URL 비교
|
||||
const normalizedFinalUrl = finalIntroUrl.split('?')[0] // 쿼리스트링 제외
|
||||
const currentPath = to.path
|
||||
const isSamePath = !isExternalUrl && currentPath === normalizedFinalUrl
|
||||
|
||||
if (!isSamePath) {
|
||||
// 다른 경로에서 접근한 경우에만 리다이렉트
|
||||
const queryString = to.fullPath.includes('?')
|
||||
? '?' + to.fullPath.split('?')[1]
|
||||
: ''
|
||||
const redirectUrl =
|
||||
queryString && !finalIntroUrl.includes('?')
|
||||
? `${finalIntroUrl}${queryString}`
|
||||
: finalIntroUrl
|
||||
console.log('🚀 ~ pageData.global redirectUrl:', redirectUrl)
|
||||
return navigateTo(redirectUrl, { external: isExternalUrl })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pageUrl이 빈값이거나 null이면 /home로 리다이렉트
|
||||
if (
|
||||
!pageUrl ||
|
||||
pageUrl === '' ||
|
||||
pageUrl === '/' ||
|
||||
pageUrl === `/${langCode}/`
|
||||
) {
|
||||
console.log('🚀 ~ pageData.global /home 리다이렉트')
|
||||
return navigateTo(`/${langCode}/home`, { external: false })
|
||||
}
|
||||
|
||||
// error 페이지는 API 호출하지 않음
|
||||
if (pageUrl === '/error' || to.path.includes('/error')) return
|
||||
|
||||
// 페이지 이동 시 로딩 상태 시작
|
||||
loadingStore.startFullLoading()
|
||||
|
||||
const accessToken = csrGetAccessToken()
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
}
|
||||
|
||||
// 미리보기 쿼리스트링에서 파라미터 값 추출
|
||||
// preview?page_seq=1&page_ver=1&lang_code=ko
|
||||
const queryString = to.fullPath.includes('?')
|
||||
? to.fullPath.split('?')[1]
|
||||
: ''
|
||||
const urlParams = new URLSearchParams(queryString)
|
||||
const pageSeq = urlParams.get('page_seq') || ''
|
||||
const pageVer = urlParams.get('page_ver') || ''
|
||||
const queryLangCode = urlParams.get('lang_code') || langCode
|
||||
|
||||
let queryParams: Record<string, string>
|
||||
let apiUrl: string
|
||||
if (pageUrl === '/preview') {
|
||||
|
||||
if (gamePath === '/preview') {
|
||||
// 미리보기 쿼리스트링에서 파라미터 값 추출
|
||||
// preview?page_seq=1&page_ver=1&lang_code=ko
|
||||
const queryString = to.fullPath.includes('?')
|
||||
? to.fullPath.split('?')[1]
|
||||
: ''
|
||||
const urlParams = new URLSearchParams(queryString)
|
||||
const pageSeq = urlParams.get('page_seq') || ''
|
||||
const pageVer = urlParams.get('page_ver') || ''
|
||||
const queryLangCode = urlParams.get('lang_code') || langCode
|
||||
|
||||
apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/page/preview`
|
||||
queryParams = {
|
||||
lang_code: queryLangCode,
|
||||
@@ -137,79 +65,20 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
queryParams = {
|
||||
game_domain: gameDomain,
|
||||
lang_code: langCode,
|
||||
page_url: pageUrl,
|
||||
page_url: gamePath,
|
||||
_t: Date.now().toString(), // 캐시 무효화를 위한 타임스탬프
|
||||
}
|
||||
}
|
||||
|
||||
const response = (await commonFetch('GET', apiUrl, {
|
||||
pageDataResponse = (await commonFetch('GET', apiUrl, {
|
||||
headers,
|
||||
query: queryParams,
|
||||
})) as PageDataResponse | null
|
||||
console.log('🚀 ~ pageData.global response:', response.value)
|
||||
|
||||
// 페이지 접근 권한 설정(로그인 유무)
|
||||
if (response?.value?.is_login_required === 1 && !accessToken) {
|
||||
// 로그인 레이어 팝업 띄워주기
|
||||
const nuxtApp = useNuxtApp()
|
||||
const modalStore = useModalStore()
|
||||
const $i18n = nuxtApp.$i18n as any
|
||||
const { tm } = $i18n
|
||||
modalStore.handleOpenConfirm({
|
||||
contentText: tm('Alert_StoveLogin'),
|
||||
confirmButtonText: tm('Text_StoveLogin'),
|
||||
confirmButtonEvent: () => {
|
||||
csrGoStoveLogin()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 404 에러 코드 체크
|
||||
const isNotFoundError =
|
||||
(response?.code === 91002 && response?.message === 'Invalid LangCode') ||
|
||||
response?.code === 91003 ||
|
||||
response?.code === 90004
|
||||
|
||||
if (isNotFoundError) {
|
||||
//클릭한 주소는 주소표시줄에 표시하도록 수정
|
||||
if (import.meta.client) {
|
||||
window.history.replaceState({}, '', to.path)
|
||||
}
|
||||
// 뒤로가기 이동 시 이전 페이지로 이동되도록 수정
|
||||
showError(
|
||||
createError({
|
||||
statusCode: 404,
|
||||
statusMessage: response?.message,
|
||||
fatal: false, // 즉시 에러 페이지로
|
||||
data: { reason: response?.message },
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (response?.code === 90002) {
|
||||
//클릭한 주소는 주소표시줄에 표시하도록 수정
|
||||
if (import.meta.client) {
|
||||
window.history.replaceState({}, '', to.path)
|
||||
}
|
||||
// 뒤로가기 이동 시 이전 페이지로 이동되도록 수정
|
||||
showError(
|
||||
createError({
|
||||
statusCode: 500,
|
||||
statusMessage: response?.message,
|
||||
fatal: false, // 즉시 에러 페이지로
|
||||
data: { reason: response?.message },
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
pageDataStore.setPageData(response.value)
|
||||
} else {
|
||||
pageDataStore.clearPageData()
|
||||
}
|
||||
console.log('🚀 ~ pageData.global response:', pageDataResponse?.value)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
pageDataStore.clearPageData()
|
||||
console.error(error)
|
||||
|
||||
showError(
|
||||
createError({
|
||||
@@ -220,4 +89,41 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (pageDataResponse?.code === 0 && 'value' in pageDataResponse) {
|
||||
pageDataStore.setPageData(pageDataResponse.value)
|
||||
return
|
||||
} else {
|
||||
// 90001 (API Respond 4xx status): API 응답 4xx 에러
|
||||
// 91001 (Invalid GameCode): 게임 코드 없음
|
||||
// 91003 (Invalid PageUrl): 페이지 주소 없음
|
||||
if (
|
||||
pageDataResponse?.code === 90001 ||
|
||||
pageDataResponse?.code === 91001 ||
|
||||
pageDataResponse?.code === 91003
|
||||
) {
|
||||
showError(
|
||||
createError({
|
||||
statusCode: 404,
|
||||
statusMessage: pageDataResponse?.message,
|
||||
fatal: false, // 즉시 에러 페이지로
|
||||
data: { reason: pageDataResponse?.message },
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
// 91002 (Invalid LangCode): 미지원 언어로 접근
|
||||
if (pageDataResponse?.code === 91002) {
|
||||
navigateTo(`/${langCode}/home`)
|
||||
return
|
||||
}
|
||||
|
||||
// [TODO]
|
||||
// 90004 (Not found user): 사용자 없음
|
||||
// if (pageDataResponse?.code === 90043) {
|
||||
// return navigateTo(`/${langCode}/home`)
|
||||
// }
|
||||
}
|
||||
|
||||
pageDataStore.clearPageData()
|
||||
})
|
||||
|
||||
10
layers/plugins/router-state.client.ts
Normal file
10
layers/plugins/router-state.client.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export default defineNuxtPlugin(() => {
|
||||
const router = useRouter()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.path !== from.path) {
|
||||
modalStore.handleResetModalAll()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,20 +1,14 @@
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import {
|
||||
getHeader,
|
||||
getRequestHost,
|
||||
defineEventHandler,
|
||||
createError,
|
||||
setCookie,
|
||||
type H3Event,
|
||||
} from 'h3'
|
||||
import { ssrGetFinalLocale } from '../../utils/localeUtil'
|
||||
import type { GameDataResponse } from '../../types/api/gameData'
|
||||
import { defineEventHandler, createError, setCookie, type H3Event } from 'h3'
|
||||
import { getGameDomain, getPathLocale } from '#layers/utils/urlUtil'
|
||||
import { ssrGetFinalLocale } from '#layers/utils/localeUtil'
|
||||
import { isStaticFile } from '#layers/utils/commonUtil'
|
||||
import { getTrueClientIp } from '#layers/utils/apiUtil'
|
||||
import type { GameDataResponse } from '#layers/types/api/gameData'
|
||||
import type {
|
||||
ResGetInspectionData,
|
||||
WebInspectionData,
|
||||
} from '../../types/InspectionType'
|
||||
import { isStaticFile } from '#layers/utils/commonUtil'
|
||||
import { getTrueClientIp } from '#layers/utils/apiUtil'
|
||||
} from '#layers/types/InspectionType'
|
||||
|
||||
/**
|
||||
* 캐시 제어 헤더를 설정하는 공통 함수
|
||||
@@ -66,7 +60,7 @@ function setCacheHeaders(
|
||||
event.node.res.setHeader('Cache-Control', cacheControl)
|
||||
}
|
||||
|
||||
const cache = new LRUCache({
|
||||
const cache = new LRUCache<string, WebInspectionData>({
|
||||
max: 100, // 캐시에 저장할 최대 항목 수
|
||||
ttl: 1000 * 30, // 30초 동안 캐시 유지
|
||||
})
|
||||
@@ -103,17 +97,8 @@ function fnLocaleMiddleware(event: H3Event, 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 [pathPart, queryString = ''] = path.split('?')
|
||||
const arrPath = pathPart.split('/')
|
||||
|
||||
// 최종 언어 세팅된 경로 생성
|
||||
const pathLocale = arrPath.length > 1 ? arrPath[1] : ''
|
||||
@@ -128,7 +113,7 @@ function fnLocaleMiddleware(event: H3Event, finalLocale: string) {
|
||||
newLocalePath = arrPath.join('/')
|
||||
}
|
||||
|
||||
if (queryString !== '') {
|
||||
if (queryString) {
|
||||
newLocalePath += `?${queryString}`
|
||||
}
|
||||
|
||||
@@ -138,13 +123,26 @@ function fnLocaleMiddleware(event: H3Event, finalLocale: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
// HMR 요청 필터링 (개발 모드에서 중복 실행 방지)
|
||||
if (event.path.startsWith('/_nuxt/') || event.path.startsWith('/__nuxt')) {
|
||||
return
|
||||
}
|
||||
/**
|
||||
* 특정 경로를 건너뛰어야 하는지 확인
|
||||
*/
|
||||
function shouldSkipPath(path: string): boolean {
|
||||
return (
|
||||
path.startsWith('/_nuxt/') ||
|
||||
path.startsWith('/__nuxt') ||
|
||||
path.startsWith('/api/') ||
|
||||
path.includes('/assets/') ||
|
||||
path.includes('favicon') ||
|
||||
path === '/robots.txt' ||
|
||||
path === '/sitemap.xml'
|
||||
)
|
||||
}
|
||||
|
||||
if (event.path.includes('/error')) {
|
||||
export default defineEventHandler(async event => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
// HMR 요청 필터링 (개발 모드에서 중복 실행 방지)
|
||||
if (shouldSkipPath(event.path)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -153,166 +151,97 @@ export default defineEventHandler(async event => {
|
||||
return
|
||||
}
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const iBaseApiUrl = runtimeConfig.public.stoveApiUrlServer
|
||||
const stoveApiServerBaseUrl = runtimeConfig.public.stoveApiUrlServer
|
||||
const baseDomain = runtimeConfig.public.baseDomain
|
||||
const apiUrl = `${iBaseApiUrl}/pub-comm/v1.0/template/game`
|
||||
|
||||
let initLangCodes: string[] | null = null
|
||||
let gameDataResponse: GameDataResponse | null = null
|
||||
let gameDataLangCodes: string[] | null = null
|
||||
let gameDataDefaultLocale: string | null = null
|
||||
let finalLocale: string
|
||||
let cleanHost: string | undefined
|
||||
let initDefaultLocale: string | null = null
|
||||
|
||||
const host =
|
||||
(getHeader(event, 'host') || getRequestHost(event)).toString() || ''
|
||||
const isGameDomainExtractable = host.includes(baseDomain)
|
||||
|
||||
if (isGameDomainExtractable) {
|
||||
cleanHost = host.split(':')[0]
|
||||
event.context.gameDomain = cleanHost
|
||||
}
|
||||
|
||||
try {
|
||||
const gameApiUrl = `${stoveApiServerBaseUrl}/pub-comm/v1.0/template/game`
|
||||
const gameDomain = getGameDomain(event)
|
||||
const queryParams: Record<string, string> = {
|
||||
game_domain: cleanHost || '',
|
||||
lang_code: '',
|
||||
game_domain: gameDomain || '',
|
||||
lang_code: getPathLocale(event?.node.req.url),
|
||||
}
|
||||
const initResponse = await $fetch<GameDataResponse>(apiUrl, {
|
||||
const response = await $fetch<GameDataResponse>(gameApiUrl, {
|
||||
query: queryParams,
|
||||
})
|
||||
|
||||
initLangCodes = initResponse?.value?.lang_codes || null
|
||||
initDefaultLocale = initResponse?.value?.default_lang_code || null
|
||||
gameDataResponse = response
|
||||
gameDataLangCodes = response?.value?.lang_codes || null
|
||||
gameDataDefaultLocale = response?.value?.default_lang_code || null
|
||||
event.context.gameDomain = gameDomain
|
||||
} catch (error) {
|
||||
console.error('init gameData load error:', error)
|
||||
|
||||
return
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('gameData load error:', error)
|
||||
}
|
||||
|
||||
const fullPath = event.path
|
||||
if (gameDataResponse?.code === 0 && 'value' in gameDataResponse) {
|
||||
// ### 정상 응답 처리 -------------------------------------------------------------
|
||||
const gameDataValue = gameDataResponse.value
|
||||
event.context.gameData = gameDataValue
|
||||
event.context.googleAnalyticsId = gameDataValue?.ga_code
|
||||
console.log('🚀 ~ gameData response:', event.context.gameData)
|
||||
|
||||
// 1-1. 정적 파일 패스
|
||||
if (isStaticFile(event.path)) {
|
||||
return
|
||||
}
|
||||
// -------------------------------------------------------------------------------
|
||||
// [Inspection Middleware]
|
||||
// -------------------------------------------------------------------------------
|
||||
const fullPath = event.path
|
||||
|
||||
// 1-2. /inspection 패스
|
||||
if (fullPath.includes('/inspection')) {
|
||||
// 리턴 되기 전 언어 쿠키 세팅
|
||||
finalLocale = ssrGetFinalLocale(
|
||||
event?.node.req.url,
|
||||
event.node.req.headers,
|
||||
initLangCodes,
|
||||
initDefaultLocale
|
||||
)
|
||||
setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
|
||||
// 점검 페이지에서도 gameData를 가져와서 context에 설정
|
||||
try {
|
||||
const queryParams: Record<string, string> = {
|
||||
game_domain: cleanHost || '',
|
||||
lang_code: finalLocale,
|
||||
}
|
||||
const response = await $fetch<GameDataResponse>(apiUrl, {
|
||||
query: queryParams,
|
||||
})
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
event.context.gameData = response.value
|
||||
event.context.googleAnalyticsId = response.value?.ga_code
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('inspection path gameData load error:', error)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
// 1-1. 정적 파일 패스
|
||||
if (isStaticFile(event.path)) return
|
||||
|
||||
// 정적 자산, API, 파비콘 등은 제외하고 페이지 요청만 처리
|
||||
if (
|
||||
fullPath.startsWith('/api/') ||
|
||||
fullPath.startsWith('/_nuxt/') ||
|
||||
fullPath.startsWith('/favicon') ||
|
||||
fullPath.includes('/assets/') ||
|
||||
fullPath.includes('.') ||
|
||||
fullPath.startsWith('/_')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// 캐시 키 생성
|
||||
const cacheKey = 'inspection'
|
||||
|
||||
try {
|
||||
// 이미 응답이 종료되었는지 확인
|
||||
if (event.node.res.headersSent || event.node.res.writableEnded) {
|
||||
// 1-2. /inspection 패스
|
||||
if (fullPath.includes('/inspection')) {
|
||||
// 리턴 되기 전 언어 쿠키 세팅
|
||||
const finalLocale = ssrGetFinalLocale(
|
||||
event?.node.req.url,
|
||||
event.node.req.headers,
|
||||
gameDataLangCodes,
|
||||
gameDataDefaultLocale
|
||||
)
|
||||
setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 언어 코드 추출
|
||||
finalLocale = ssrGetFinalLocale(
|
||||
event?.node.req.url,
|
||||
event.node.req.headers,
|
||||
initLangCodes,
|
||||
initDefaultLocale
|
||||
)
|
||||
const queryParams: Record<string, string> = {
|
||||
game_domain: cleanHost || '',
|
||||
lang_code: finalLocale,
|
||||
}
|
||||
// 1-3. 특정 경로 패스 (API, 리소스)
|
||||
if (shouldSkipPath(fullPath)) return
|
||||
|
||||
const response = await $fetch<GameDataResponse>(apiUrl, {
|
||||
query: queryParams,
|
||||
})
|
||||
// 캐시 키 생성 (게임 ID 포함하여 충돌 방지)
|
||||
const gameId = gameDataValue?.game_id || 'default'
|
||||
const cacheKey = `inspection:${gameId}`
|
||||
|
||||
console.log('🚀 ~ gameData response:', response)
|
||||
|
||||
// 언어패스 쿠키 굽기 - 장기방안에서는 굽지않음
|
||||
if (initLangCodes?.includes(finalLocale)) {
|
||||
try {
|
||||
// 2. 언어 코드 추출
|
||||
finalLocale = ssrGetFinalLocale(
|
||||
event?.node.req.url,
|
||||
event.node.req.headers,
|
||||
gameDataLangCodes,
|
||||
gameDataDefaultLocale
|
||||
)
|
||||
setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
}
|
||||
|
||||
if (response?.code === 91001) {
|
||||
// 91001 에러 발생 시 바로 /error 페이지로 리다이렉트
|
||||
if (!event.node.res.headersSent && !event.node.res.writableEnded) {
|
||||
const errorPath = `/${finalLocale || 'ko'}/error`
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', errorPath)
|
||||
event.node.res.end()
|
||||
return
|
||||
}
|
||||
}
|
||||
// 초기화
|
||||
let inspectionData
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
event.context.gameData = response.value
|
||||
event.context.googleAnalyticsId = response.value?.ga_code
|
||||
|
||||
// 점검 데이터 조회
|
||||
let inspectionData: WebInspectionData | undefined
|
||||
if (cache.has(cacheKey)) {
|
||||
inspectionData = cache.get(cacheKey) as WebInspectionData
|
||||
// 3. 캐시된 데이터가 없거나 만료되었을 때만 API 호출
|
||||
const cachedData = cache.get(cacheKey)
|
||||
if (cachedData) {
|
||||
inspectionData = cachedData
|
||||
} else {
|
||||
// 점검 데이터 조회
|
||||
if (response?.value?.game_id) {
|
||||
try {
|
||||
const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response?.value?.game_id}`
|
||||
// 직접 $fetch 사용 (composable 사용하지 않음)
|
||||
const inspectionResponse = await $fetch<ResGetInspectionData>(
|
||||
inspectionApiUrl,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
inspectionData = inspectionResponse?.value?.inspection
|
||||
console.log("🚀 ~ inspectionData:", inspectionData)
|
||||
cache.set(cacheKey, inspectionData) // 캐시에 저장
|
||||
} catch (error) {
|
||||
console.error('inspection data load error:', error)
|
||||
// 에러 발생 시 inspectionData는 undefined로 유지
|
||||
}
|
||||
const inspectionApiUrl = `${stoveApiServerBaseUrl}/pub-comm/v3.0/inspection/${gameDataValue?.game_id}`
|
||||
const response = await $fetch<ResGetInspectionData>(inspectionApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
inspectionData = response?.value?.inspection as WebInspectionData
|
||||
if (inspectionData) {
|
||||
cache.set(cacheKey, inspectionData) // 캐시에 저장
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,38 +265,36 @@ export default defineEventHandler(async event => {
|
||||
* - 점검 URL 경로가 아닐 경우 no-cache 설정
|
||||
* - 화이트 리스트 체크
|
||||
*/
|
||||
// 점검 url path 가 아닐 경우, no-cache 설정
|
||||
const inspectionPath = `/${finalLocale}/inspection`
|
||||
const isInspectionPath = fullPath === inspectionPath
|
||||
|
||||
// 현재 경로가 점검 페이지가 아닐 경우 캐시 헤더 설정
|
||||
if (!isInspectionPath) {
|
||||
if (fullPath !== inspectionPath) {
|
||||
setCacheHeaders(event, 'no-cache')
|
||||
}
|
||||
|
||||
// 응답이 이미 종료되었는지 확인
|
||||
if (event.node.res.headersSent || event.node.res.writableEnded) {
|
||||
return
|
||||
}
|
||||
|
||||
// 점검 중일 때 IP 필터링 활성화 여부 확인
|
||||
if (inspectionData?.ip_filter_use_yn === 'Y') {
|
||||
const clientIP = getTrueClientIp(event.node.req as any)
|
||||
|
||||
// 허용된 IP 목록 확인
|
||||
if (inspectionData?.ip_filter_list?.includes(clientIP)) {
|
||||
// 화이트 리스트인 경우 Locale Middleware 실행
|
||||
fnLocaleMiddleware(event, finalLocale)
|
||||
} else {
|
||||
const allowedIPs = inspectionData?.ip_filter_list || []
|
||||
if (!clientIP || !allowedIPs.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 {
|
||||
// IP 필터링이 비활성화된 경우 모든 사용자를 점검 페이지로 이동
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', inspectionPath)
|
||||
event.node.res.end()
|
||||
if (!event.node.res.headersSent && !event.node.res.writableEnded) {
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', inspectionPath)
|
||||
event.node.res.end()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
@@ -379,127 +306,51 @@ export default defineEventHandler(async event => {
|
||||
* - 점검 30분 이후: 기본 캐시 (60초)
|
||||
*/
|
||||
// 홈 경로: 캐시 없음
|
||||
const isHomePath = [
|
||||
'',
|
||||
'/',
|
||||
//, ...Object.values(DEFAULT_LOCALE_COVERAGES).flatMap((locale) => [`/${locale}`, `/${locale}/`])
|
||||
console.log("🚀 ~ isHomePath: 여기야??", fullPath)
|
||||
].includes(fullPath)
|
||||
const isHomePath = fullPath === '' || fullPath === '/'
|
||||
|
||||
if (isHomePath) {
|
||||
setCacheHeaders(event, 'no-cache')
|
||||
} else {
|
||||
} else if (tsStartDate > 0 && timeUntilInspectionSeconds > 0) {
|
||||
// 점검 예정 시간에 따른 캐시 설정
|
||||
|
||||
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')
|
||||
}
|
||||
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')
|
||||
}
|
||||
}
|
||||
// -------------------------------------------------------------------------------
|
||||
// [Root Path Redirect to /home or intro.page_url]
|
||||
// 언어 코드만 있는 경로(예: /ko, /ko/)를 /home 또는 intro.page_url로 리다이렉트
|
||||
// -------------------------------------------------------------------------------
|
||||
const normalizedPath = fullPath.endsWith('/')
|
||||
? fullPath.slice(0, -1)
|
||||
: fullPath
|
||||
const localePath = `/${finalLocale}`
|
||||
if (normalizedPath === localePath) {
|
||||
// intro.page_url이 있으면 해당 값 사용, 없으면 /home 사용
|
||||
const introPageUrl = response.value?.intro?.page_url
|
||||
let defaultPath = `/${finalLocale}/home`
|
||||
|
||||
if (introPageUrl && introPageUrl.trim() !== '') {
|
||||
// 외부 URL인지 확인
|
||||
const isExternalUrl = introPageUrl.startsWith('http://') || introPageUrl.startsWith('https://')
|
||||
|
||||
if (isExternalUrl) {
|
||||
// 외부 URL인 경우 그대로 사용
|
||||
defaultPath = introPageUrl
|
||||
} else {
|
||||
// 내부 경로인 경우 언어 코드 패턴 확인
|
||||
const normalizedIntroUrl = introPageUrl.split('?')[0] // 쿼리스트링 제외
|
||||
const languagePattern = /^\/[a-z]{2}(-[a-z]{2})?(\/|$)/
|
||||
const hasLanguageCode = languagePattern.test(normalizedIntroUrl)
|
||||
|
||||
if (hasLanguageCode) {
|
||||
// 이미 언어 코드가 있으면 그대로 사용
|
||||
defaultPath = introPageUrl
|
||||
} else {
|
||||
// 언어 코드가 없으면 추가
|
||||
const pathWithSlash = normalizedIntroUrl.startsWith('/')
|
||||
? normalizedIntroUrl
|
||||
: `/${normalizedIntroUrl}`
|
||||
defaultPath = `/${finalLocale}${pathWithSlash}`
|
||||
|
||||
// 쿼리스트링이 있으면 다시 추가
|
||||
if (introPageUrl.includes('?')) {
|
||||
defaultPath += '?' + introPageUrl.split('?')[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = event?.node.req.url?.includes('?')
|
||||
? '?' + event.node.req.url.split('?')[1]
|
||||
: ''
|
||||
|
||||
if (!event.node.res.headersSent && !event.node.res.writableEnded) {
|
||||
// 쿼리스트링 처리
|
||||
let redirectUrl = defaultPath
|
||||
if (queryString) {
|
||||
// defaultPath에 이미 쿼리스트링이 있는지 확인
|
||||
if (defaultPath.includes('?')) {
|
||||
redirectUrl = `${defaultPath}&${queryString.substring(1)}`
|
||||
} else {
|
||||
redirectUrl = `${defaultPath}${queryString}`
|
||||
}
|
||||
}
|
||||
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', redirectUrl)
|
||||
event.node.res.end()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// [Locale Middleware]
|
||||
// -------------------------------------------------------------------------------
|
||||
fnLocaleMiddleware(event, finalLocale)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('gameData load error:', error)
|
||||
|
||||
// 응답이 이미 종료되었는지 확인
|
||||
if (event.node.res.headersSent || event.node.res.writableEnded) {
|
||||
return
|
||||
// 정상 접속 허용
|
||||
} catch (error) {
|
||||
console.error('gameData inspection error:', error)
|
||||
}
|
||||
|
||||
} else {
|
||||
// ### 에러 응답 처리 -------------------------------------------------------------
|
||||
// 언어 코드 추출 시도
|
||||
let errorLocale = 'ko' // 기본값
|
||||
try {
|
||||
errorLocale = ssrGetFinalLocale(
|
||||
event?.node.req.url,
|
||||
event.node.req.headers,
|
||||
initLangCodes,
|
||||
initDefaultLocale
|
||||
gameDataLangCodes,
|
||||
gameDataDefaultLocale
|
||||
)
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Locale extraction error:', e)
|
||||
}
|
||||
|
||||
// 91001 에러인 경우 바로 리다이렉트
|
||||
if (error?.statusCode === 91001 || error?.cause?.statusCode === 91001) {
|
||||
if (gameDataResponse?.code === 91001) {
|
||||
const errorPath = `/${errorLocale}/error`
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', errorPath)
|
||||
@@ -509,8 +360,8 @@ export default defineEventHandler(async event => {
|
||||
|
||||
// 다른 에러는 기존대로 throw
|
||||
throw createError({
|
||||
statusCode: error?.statusCode || 500,
|
||||
statusMessage: error?.statusMessage,
|
||||
statusCode: gameDataResponse?.code || 500,
|
||||
statusMessage: gameDataResponse?.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,19 +2,25 @@ import type { GameDataValue } from '#layers/types/api/gameData'
|
||||
|
||||
export const useGameDataStore = defineStore('gameData', () => {
|
||||
const gameData = ref<GameDataValue | null>(null)
|
||||
const langCode = ref<string | null>(null)
|
||||
const langCodes = ref<string[] | null>(null)
|
||||
const defaultLangCode = ref<string | null>(null)
|
||||
|
||||
const setGameData = (data: GameDataValue) => {
|
||||
gameData.value = data
|
||||
langCodes.value = data.lang_codes
|
||||
defaultLangCode.value = data.default_lang_code
|
||||
}
|
||||
|
||||
const clearGameData = () => {
|
||||
gameData.value = null
|
||||
langCodes.value = null
|
||||
defaultLangCode.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
langCode,
|
||||
gameData,
|
||||
langCodes,
|
||||
defaultLangCode,
|
||||
setGameData,
|
||||
clearGameData,
|
||||
}
|
||||
|
||||
@@ -154,6 +154,14 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
content.storeTabActiveIndex.value = tabActiveIndex
|
||||
}
|
||||
|
||||
const handleResetModalAll = () => {
|
||||
alert.storeIsOpen.value = false
|
||||
confirm.storeIsOpen.value = false
|
||||
youtube.storeIsOpen.value = false
|
||||
toast.storeIsOpen.value = false
|
||||
content.storeIsOpen.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
alert,
|
||||
confirm,
|
||||
@@ -166,5 +174,6 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
handleOpenToast,
|
||||
handleOpenContent,
|
||||
handleControlDimmed,
|
||||
handleResetModalAll,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_LOCALE_CODE } from '../../i18n.config'
|
||||
import { DEFAULT_LOCALE_CODE } from '@/i18n.config'
|
||||
|
||||
// 사용자 선호 언어 조회
|
||||
export const getPreferredLanguage = (acceptLanguageHeader = '') => {
|
||||
@@ -97,7 +97,12 @@ export const csrGetFinalLocale = (path = '', coveragesLocales: string[]) => {
|
||||
* @param {string} path - 현재 URL 경로
|
||||
* @param {any} headers - 요청 헤더
|
||||
*/
|
||||
export const ssrGetFinalLocale = (path, headers: any, coveragesLocales: string[], defaultLocale: string) => {
|
||||
export const ssrGetFinalLocale = (
|
||||
path,
|
||||
headers: any,
|
||||
coveragesLocales: string[],
|
||||
defaultLocale: string
|
||||
) => {
|
||||
let finalLocale = defaultLocale // 기본값 설정
|
||||
|
||||
try {
|
||||
@@ -109,7 +114,11 @@ export const ssrGetFinalLocale = (path, headers: any, coveragesLocales: string[]
|
||||
}
|
||||
const pathLocalee = `${path.split('/')[1]}`.toLowerCase()
|
||||
// URL path에 포함된 언어 정보가 지원하는 언어인지 체크
|
||||
if (pathLocalee && pathLocalee !== '' && coveragesLocales.includes(pathLocalee)) {
|
||||
if (
|
||||
pathLocalee &&
|
||||
pathLocalee !== '' &&
|
||||
coveragesLocales.includes(pathLocalee)
|
||||
) {
|
||||
finalLocale = pathLocalee
|
||||
|
||||
return finalLocale
|
||||
@@ -119,8 +128,14 @@ export const ssrGetFinalLocale = (path, headers: any, coveragesLocales: string[]
|
||||
// 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)) {
|
||||
const cookieLanguage = cookies.LOCALE
|
||||
? `${cookies.LOCALE}`.toLowerCase()
|
||||
: ''
|
||||
if (
|
||||
cookieLanguage &&
|
||||
cookieLanguage !== '' &&
|
||||
coveragesLocales.includes(cookieLanguage)
|
||||
) {
|
||||
finalLocale = cookieLanguage
|
||||
return finalLocale
|
||||
}
|
||||
|
||||
88
layers/utils/urlUtil.ts
Normal file
88
layers/utils/urlUtil.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { getHeader, getRequestHost, type H3Event } from 'h3'
|
||||
|
||||
/**
|
||||
* 게임 도메인을 가져오는 컴포저블 함수
|
||||
* 서버와 클라이언트 환경에서 모두 동작
|
||||
* @param event - H3 이벤트 객체 (서버 미들웨어에서 직접 전달 가능)
|
||||
* @returns 게임 도메인 문자열
|
||||
*/
|
||||
export const getGameDomain = (event?: H3Event): string => {
|
||||
try {
|
||||
let host = ''
|
||||
|
||||
// 클라이언트 환경에서는 window.location.host를 사용
|
||||
if (import.meta.client) {
|
||||
host = (window.location.host || '').split(':')[0]
|
||||
}
|
||||
|
||||
// 서버 환경에서는 event 객체를 사용
|
||||
if (import.meta.server) {
|
||||
if (!event) return ''
|
||||
|
||||
// 미들웨어에서 설정한 gameDomain이 있다면 우선 사용
|
||||
if (event.context.gameDomain) {
|
||||
host = event.context.gameDomain
|
||||
} else {
|
||||
const serverHost =
|
||||
getHeader(event, 'host') || getRequestHost(event) || ''
|
||||
host = serverHost.split(':')[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (!host) return ''
|
||||
|
||||
// dev2 호스트명인 경우 l9-dev.onstove.com을 사용
|
||||
if (host === 'samplegame-dev2.onstove.com') {
|
||||
return 'l9-dev.onstove.com'
|
||||
}
|
||||
|
||||
return host
|
||||
} catch (error) {
|
||||
console.error('getGameDomain error:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL에서 언어 코드를 추출하는 함수
|
||||
* @param url - URL 문자열
|
||||
* @returns 언어 코드 문자열
|
||||
*/
|
||||
export const getPathLocale = (url?: string): string => {
|
||||
const targetUrl = url || (import.meta.client ? window.location.pathname : '')
|
||||
const cleanTargetUrl = targetUrl.endsWith('/')
|
||||
? targetUrl.slice(0, -1)
|
||||
: targetUrl
|
||||
|
||||
return cleanTargetUrl.split('/')[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* URL에서 언어 코드 이후의 경로를 추출하는 함수
|
||||
* @param url - URL 문자열
|
||||
* @returns 언어 코드 이후의 경로 문자열
|
||||
*/
|
||||
export const getPathAfterLanguage = (url?: string): string => {
|
||||
const targetUrl = url || (import.meta.client ? window.location.pathname : '')
|
||||
const cleanTargetUrl = targetUrl.endsWith('/')
|
||||
? targetUrl.slice(0, -1)
|
||||
: targetUrl
|
||||
|
||||
// URL에서 언어 코드 패턴을 찾아서 그 뒤의 경로를 추출
|
||||
// 예: /ko/about/story -> /about/story
|
||||
// 예: /ko -> "" (빈 문자열)
|
||||
const languagePattern = /^\/[a-z]{2}(-[a-z]{2})?\/(.+)$/
|
||||
const match = cleanTargetUrl.match(languagePattern)
|
||||
if (match && match[2]) {
|
||||
return `/${match[2]}`
|
||||
} else {
|
||||
// 언어 코드만 있고 뒤에 아무것도 없는 경우 (예: /ko, /en, /zh-tw, /zh-cn)
|
||||
const languageOnlyPattern = /^\/[a-z]{2}(-[a-z]{2})?$/
|
||||
if (languageOnlyPattern.test(cleanTargetUrl)) {
|
||||
return ''
|
||||
} else {
|
||||
// 언어 코드가 없는 경우 원본 경로 그대로 반환 (이미 /로 시작)
|
||||
return cleanTargetUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,55 @@ export default defineNuxtConfig({
|
||||
app: {
|
||||
head: {
|
||||
viewport: 'width=device-width, initial-scale=1, maximum-scale=5',
|
||||
link: [
|
||||
// 폰트 CSS 비동기 로딩
|
||||
{
|
||||
rel: 'preload',
|
||||
href: 'https://static-cdn.onstove.com/resources/stds/stds-font-kr/stds-font-kr.css',
|
||||
as: 'style',
|
||||
onload: "this.onload=null;this.rel='stylesheet'",
|
||||
},
|
||||
{
|
||||
rel: 'preload',
|
||||
href: 'https://static-cdn.onstove.com/resources/stds/stds-font-global/stds-font-global.css',
|
||||
as: 'style',
|
||||
onload: "this.onload=null;this.rel='stylesheet'",
|
||||
},
|
||||
// {
|
||||
// rel: 'preload',
|
||||
// href: 'https://static-cdn.onstove.com/0.0.4/font-icon/StoveFont-Icon.css',
|
||||
// as: 'style',
|
||||
// onload: "this.onload=null;this.rel='stylesheet'",
|
||||
// },
|
||||
],
|
||||
noscript: [
|
||||
// JavaScript 비활성화 시 폰트 CSS 로드
|
||||
{
|
||||
tag: 'link',
|
||||
rel: 'stylesheet',
|
||||
href: 'https://static-cdn.onstove.com/resources/stds/stds-font-kr/stds-font-kr.css',
|
||||
},
|
||||
{
|
||||
tag: 'link',
|
||||
rel: 'stylesheet',
|
||||
href: 'https://static-cdn.onstove.com/resources/stds/stds-font-global/stds-font-global.css',
|
||||
},
|
||||
// {
|
||||
// tag: 'link',
|
||||
// rel: 'stylesheet',
|
||||
// href: 'https://static-cdn.onstove.com/0.0.4/font-icon/StoveFont-Icon.css',
|
||||
// },
|
||||
],
|
||||
script: [
|
||||
{
|
||||
type: 'text/javascript',
|
||||
src: process.env.STOVE_GNB,
|
||||
defer: true,
|
||||
},
|
||||
{
|
||||
type: 'text/javascript',
|
||||
src: process.env.STOVE_81PLUG,
|
||||
defer: true,
|
||||
},
|
||||
{
|
||||
type: 'text/javascript',
|
||||
@@ -59,16 +100,10 @@ export default defineNuxtConfig({
|
||||
|
||||
// i18n 설정 - 런타임에 동적으로 설정됨
|
||||
i18n: getI18n(),
|
||||
experimental: {
|
||||
payloadExtraction: false,
|
||||
},
|
||||
typescript: {
|
||||
typeCheck: true,
|
||||
strict: false,
|
||||
},
|
||||
nitro: {
|
||||
prerender: { routes: [] },
|
||||
},
|
||||
// [test] Nuxt가 pages 스캔하도록 명시
|
||||
pages: true,
|
||||
|
||||
@@ -113,4 +148,25 @@ export default defineNuxtConfig({
|
||||
},
|
||||
base: '/',
|
||||
},
|
||||
experimental: {
|
||||
payloadExtraction: false,
|
||||
},
|
||||
build: {
|
||||
extractCSS: true, // CSS 추출
|
||||
analyze: false, // 트리 쉐이킹 활성화
|
||||
},
|
||||
nitro: {
|
||||
prerender: { routes: [] },
|
||||
compressPublicAssets: true,
|
||||
minify: true,
|
||||
// 캐시 헤더 최적화
|
||||
routeRules: {
|
||||
// 폰트: 1년 캐시
|
||||
'**/*.{woff,woff2,ttf,eot}': {
|
||||
headers: {
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,6 +2,22 @@ import type { Config } from 'tailwindcss'
|
||||
|
||||
export default {
|
||||
content: ['./app/**/*.{js,vue,ts}', './layers/**/*.{js,vue,ts}'],
|
||||
// 사용하지 않는 CSS 제거 (PurgeCSS)
|
||||
purge: {
|
||||
enabled: process.env.NODE_ENV === 'production',
|
||||
content: ['./app/**/*.{js,vue,ts}', './layers/**/*.{js,vue,ts}'],
|
||||
// 안전한 클래스 보호
|
||||
safelist: [
|
||||
/^theme-/,
|
||||
/^bg-/,
|
||||
/^text-/,
|
||||
/^border-/,
|
||||
/^hover:/,
|
||||
/^md:/,
|
||||
/^lg:/,
|
||||
/^sm:/,
|
||||
],
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
|
||||
Reference in New Issue
Block a user