diff --git a/layers/components/blocks/LanguageSwitcher.vue b/layers/components/blocks/LanguageSwitcher.vue index 28a5834..6cd0c53 100644 --- a/layers/components/blocks/LanguageSwitcher.vue +++ b/layers/components/blocks/LanguageSwitcher.vue @@ -193,7 +193,7 @@ const getLanguageName = (localeCode: string) => { const { locale, setLocale } = useI18n() const switchLocalePath = useSwitchLocalePath() -const pageDataStore = usePageDataStore() +const { loadGameData } = useGameDataLoader() const selectedLocale = ref(locale.value) const isChanging = ref(false) @@ -233,25 +233,16 @@ const switchLanguage = async () => { }) localeCookie.value = selectedLocale.value.toLowerCase() - window.location.href = path - // 언어 변경 및 라우팅 - // await setLocale(selectedLocale.value as any) - - // 전체 페이지에 페이드 아웃 효과 적용 - // document.body.style.transition = 'opacity 0.1s ease-out' - // document.body.style.opacity = '0' - - // // 페이드 아웃 완료 후 페이지 이동 - // await new Promise(resolve => setTimeout(resolve, 100)) - - // 서버 미드웨어를 통해 gameData 갱신을 위해 페이지 새로고침 - // 이렇게 하면 서버 미드웨어가 새로운 언어로 gameData를 다시 가져옴 + // i18n locale 변경 (SPA) + await setLocale(selectedLocale.value as any) + // gameData가 언어별로 달라지는 영역(Header/GNB 등) 즉시 갱신 + await loadGameData(selectedLocale.value) + // Nuxt SPA 라우팅으로 페이지 이동 + await navigateTo(path) } } catch { // 오류 발생 시 이전 언어로 복원 selectedLocale.value = locale.value - // 페이드 효과 복원 - document.body.style.opacity = '1' } finally { isChanging.value = false } diff --git a/layers/composables/useGameDataLoader.ts b/layers/composables/useGameDataLoader.ts new file mode 100644 index 0000000..74b047a --- /dev/null +++ b/layers/composables/useGameDataLoader.ts @@ -0,0 +1,31 @@ +import type { GameDataResponse } from '#layers/types/api/gameData' +import { getGameDomain } from '#layers/utils/urlUtil' + +export const useGameDataLoader = () => { + const runtimeConfig = useRuntimeConfig() + const gameDataStore = useGameDataStore() + + const loadGameData = async (langCode: string) => { + const normalizedLangCode = `${langCode}`.toLowerCase() + + const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl + const gameDomain = getGameDomain() + + const response = await $fetch( + `${stoveApiBaseUrl}/pub-comm/v1.0/template/game`, + { + query: { + game_domain: gameDomain || '', + lang_code: normalizedLangCode, + }, + } + ) + + if (response?.code === 0 && 'value' in response) { + gameDataStore.setGameData(response.value) + gameDataStore.setCurrentLangCode(normalizedLangCode) + } + } + + return { loadGameData } +} diff --git a/layers/middleware/init.route.global.ts b/layers/middleware/init.route.global.ts index 7357c27..c381b6e 100644 --- a/layers/middleware/init.route.global.ts +++ b/layers/middleware/init.route.global.ts @@ -2,7 +2,9 @@ import { DEFAULT_LOCALE_CODE, ISO_LANGUAGE_CODES } from '@/i18n.config' import { csrGetFinalLocale } from '#layers/utils/localeUtil' import { parseUrl, isExternalUrl } from '#layers/utils/urlUtil' -export default defineNuxtRouteMiddleware(to => { +let pending: Promise | null = null + +export default defineNuxtRouteMiddleware(async to => { // 서버에서는 실행하지 않음 (서버 미들웨어에서 이미 처리) if (import.meta.server) return @@ -12,7 +14,8 @@ export default defineNuxtRouteMiddleware(to => { } const gameDataStore = useGameDataStore() - const { langCodes, defaultLangCode, intro } = storeToRefs(gameDataStore) + const { langCodes, defaultLangCode, intro, currentLangCode, gameData } = + storeToRefs(gameDataStore) // 게임 데이터 스토어가 초기화되지 않았으면 대기 if (!langCodes.value || !defaultLangCode.value || !intro.value) { @@ -39,7 +42,33 @@ export default defineNuxtRouteMiddleware(to => { // 현재 경로와 최종 경로가 같으면 리다이렉트 불필요 if (fullPath.split('?')[0] === finalPath.split('?')[0]) { - return + // gameData가 없으면(초기 진입 등) 여기서 강제 호출하지 않음 + // - 초기 gameData는 서버 미들웨어에서 주입되는 구조 + if (!gameData.value) return + + // 초기 hydration에서 currentLangCode가 비어있으면 현재 로케일로 동기화만 + if (!currentLangCode.value) { + gameDataStore.setCurrentLangCode(finalLocale) + return + } + + // 로케일이 같으면 불필요한 재호출 방지 + if ( + `${currentLangCode.value}`.toLowerCase() === + `${finalLocale}`.toLowerCase() + ) { + return + } + + // 동시에 여러 번 호출되는 것 방지 (라우트 훅/미들웨어 중복 등) + if (pending) return pending + + const { loadGameData } = useGameDataLoader() + pending = loadGameData(finalLocale).finally(() => { + pending = null + }) + + return pending } // 외부 URL인지 확인 diff --git a/layers/stores/useGameDataStore.ts b/layers/stores/useGameDataStore.ts index 839bc2f..66a8072 100644 --- a/layers/stores/useGameDataStore.ts +++ b/layers/stores/useGameDataStore.ts @@ -9,6 +9,7 @@ export const useGameDataStore = defineStore('gameData', () => { stoveGnbJson: null as GameDataValue['stove_gnb_json'] | null, langCodes: null as GameDataValue['lang_codes'] | null, defaultLangCode: null as GameDataValue['default_lang_code'] | null, + currentLangCode: null as string | null, gaCode: null as GameDataValue['ga_code'] | null, platformType: null as GameDataValue['platform_type'] | null, osType: null as GameDataValue['os_type'] | null, @@ -55,6 +56,10 @@ export const useGameDataStore = defineStore('gameData', () => { state.eventBanner = data?.event_banner } + const setCurrentLangCode = (langCode: string | null) => { + state.currentLangCode = langCode ? `${langCode}`.toLowerCase() : null + } + const clearGameData = () => { Object.assign(state, getInitialState()) } @@ -62,6 +67,7 @@ export const useGameDataStore = defineStore('gameData', () => { return { ...toRefs(state), setGameData, + setCurrentLangCode, clearGameData, } })