diff --git a/DYNAMIC_I18N_ROUTES.md b/DYNAMIC_I18N_ROUTES.md deleted file mode 100644 index 056b74d..0000000 --- a/DYNAMIC_I18N_ROUTES.md +++ /dev/null @@ -1,120 +0,0 @@ -# Dynamic I18n Routes - gameData.lang_codes 기반 언어 제외 설정 - -이 문서는 `gameDataFromServer.lang_codes`를 기반으로 특정 언어들을 제외하는 Ignoring Localized Routes 기능의 사용법을 설명합니다. - -## 구현된 기능 - -### 1. 동적 언어 제외 설정 -- `gameDataFromServer.lang_codes`에 포함된 언어만 허용 -- 포함되지 않은 언어는 자동으로 제외 처리 -- 런타임에 동적으로 언어 설정 변경 - -### 2. 구현된 파일들 - -#### `i18n.config.ts` -- `getI18n()` 함수에 `allowedLangCodes` 매개변수 추가 -- `generatePageExclusions()` 함수로 언어 제외 설정 생성 -- `customRoutes: 'config'` 설정으로 페이지별 언어 제외 지원 - -#### `layers/composables/useDynamicI18nRoutes.ts` -- `getAllowedLangCodes()`: 허용된 언어 목록 반환 -- `isLangAllowed()`: 특정 언어 허용 여부 확인 -- `getI18nRouteConfig()`: `defineI18nRoute`용 설정 생성 -- `getExcludedLangConfig()`: 특정 언어 제외 설정 생성 - -#### `layers/plugins/dynamic-i18n-routes.client.ts` -- 클라이언트 사이드에서 gameData 변경 감지 -- 언어 설정 동적 업데이트 - -#### `layers/plugins/dynamic-i18n-routes.server.ts` -- 서버 사이드에서 gameData 기반 언어 설정 적용 - -## 사용법 - -### 1. 페이지에서 동적 언어 제외 설정 - -```vue - -``` - -### 2. 특정 언어만 허용하는 경우 - -```vue - -``` - -### 3. 특정 언어를 제외하는 경우 - -```vue - -``` - -### 4. 언어 허용 여부 확인 - -```vue - -``` - -## 동작 원리 - -1. **서버 사이드**: `gameDataFromServer.lang_codes`를 기반으로 초기 언어 설정 적용 -2. **클라이언트 사이드**: gameData 변경 시 언어 설정 동적 업데이트 -3. **페이지 레벨**: `defineI18nRoute`를 통해 페이지별 언어 제외 설정 -4. **자동 리다이렉트**: 허용되지 않은 언어로 접근 시 기본 언어로 리다이렉트 - -## 예시 시나리오 - -### 시나리오 1: 게임별 언어 제한 -```typescript -// gameData.lang_codes = ['ko', 'en', 'ja'] -// 결과: 한국어, 영어, 일본어만 허용, 나머지 언어는 자동 제외 -``` - -### 시나리오 2: 특정 언어 제외 -```typescript -// gameData.lang_codes = ['ko', 'en', 'ja', 'zh-tw', 'fr', 'de', 'es', 'pt', 'th', 'zh-cn'] -// getExcludedLangConfig(['fr', 'de']) 호출 -// 결과: 프랑스어, 독일어 제외, 나머지 언어 허용 -``` - -## 주의사항 - -1. `defineI18nRoute`는 컴파일 타임에 실행되므로, 동적 설정이 필요한 경우 `watchEffect`나 `watch`를 사용 -2. SSR과 클라이언트 사이드 모두에서 일관된 동작을 위해 플러그인 사용 권장 -3. 언어 변경 시 사용자 경험을 고려한 적절한 리다이렉트 처리 필요 - -## 참고 문서 - -- [Nuxt i18n - Ignoring Localized Routes](https://i18n.nuxtjs.org/docs/guide/ignoring-localized-routes) -- [Nuxt i18n - defineI18nRoute](https://i18n.nuxtjs.org/docs/compiler-macros/define-i18n-route) diff --git a/app/error.vue b/app/error.vue new file mode 100644 index 0000000..e2f4873 --- /dev/null +++ b/app/error.vue @@ -0,0 +1,105 @@ + + + + \ No newline at end of file diff --git a/app/pages/error/index.vue b/app/pages/error/index.vue deleted file mode 100644 index 4549382..0000000 --- a/app/pages/error/index.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - \ No newline at end of file diff --git a/error.vue b/error.vue index 2509015..c1b40a7 100644 --- a/error.vue +++ b/error.vue @@ -1,14 +1,16 @@ + - \ No newline at end of file + \ No newline at end of file diff --git a/i18n.config.ts b/i18n.config.ts index 476a277..3e2847d 100644 --- a/i18n.config.ts +++ b/i18n.config.ts @@ -66,10 +66,10 @@ const getI18n = (allowedLangCodes?: string[]): NuxtI18nOptions => { debug: false, // 동적으로 언어 제외 설정을 위한 pages 설정 customRoutes: 'config', - pages: - allowedLangCodes && allowedLangCodes.length > 0 - ? generatePageExclusions(allowedLangCodes) - : {}, + // pages: + // allowedLangCodes && allowedLangCodes.length > 0 + // ? generatePageExclusions(allowedLangCodes) + // : {}, // 추가적인 설정이 필요하다면 여기에 포함시킬 수 있습니다. } } diff --git a/layers/middleware/init.route.global.ts b/layers/middleware/init.route.global.ts index f6e37a1..61734f2 100644 --- a/layers/middleware/init.route.global.ts +++ b/layers/middleware/init.route.global.ts @@ -7,24 +7,29 @@ export default defineNuxtRouteMiddleware(async (to, _from) => { } // 현재 경로에서 언어 코드 추출 - // 예: /ko/about/story -> ko - // 예: /en/test/page -> en - const gameDataStore = useGameDataStore() 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) - // const languagePattern = /^\/([a-z]{2})(?:\/|$)/ - // const match = to.path.match(languagePattern) - // const currentLangCode = match ? match[1] : null - console.log("🚀 777777 ~ currentLangCode:", currentLangCode) //현재 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: `${currentDomain}`, + gameDomain: `${finalGameDomain}`, langCode: `${currentLangCode}`, game_alias: '', lang_code: `${currentLangCode}`, @@ -34,16 +39,19 @@ export default defineNuxtRouteMiddleware(async (to, _from) => { const { getGameDataExternal } = useGetGameDataExternal() await getGameDataExternal(req) - - // 허용된 언어 코드 목록≈≈ const allowedLangCodes = langCodes || [] + // error 페이지는 API 호출하지 않음 + if (pageUrl === '/error' || to.path.includes('/error')) { + return + } // 현재 언어가 허용된 언어 목록에 없으면 에러 페이지로 이동 if (currentLangCode && !allowedLangCodes.includes(currentLangCode)) { - throw createError({ - statusCode: 404, - statusMessage: 'Language not supported', - }) + return navigateTo(`/${currentLangCode}/error`, { external: false }) + // throw createError({ + // statusCode: 404, + // statusMessage: 'Language not supported11', + // }) } }) diff --git a/layers/middleware/inspection.ts b/layers/middleware/inspection.ts index 77ef40e..715c08f 100644 --- a/layers/middleware/inspection.ts +++ b/layers/middleware/inspection.ts @@ -1,6 +1,19 @@ export default defineNuxtRouteMiddleware(async to => { try { + //error 발생시에는 미들웨어 실행하지 않음 + //error 객체 조회 if (import.meta.client) { + + const error = useError() + if(error.value?.statusCode){ + return showError(createError({ + statusCode: error.value?.statusCode, + statusMessage: error.value?.message, + fatal: true, + // data: { path: to.path } + })) + } + const gameDataStore = useGameDataStore() const { gameData } = storeToRefs(gameDataStore) console.log('🚀 ~ 00000 gameData:', gameData.value) @@ -40,6 +53,7 @@ export default defineNuxtRouteMiddleware(async to => { !to.path.includes('inspection') && !to.path.includes('api') ) { + console.log('e111111111 eee') // 점검 중인 경우 return navigateTo(`/${finalLocale}/inspection`, { external: true }) } else if ( @@ -47,8 +61,11 @@ export default defineNuxtRouteMiddleware(async to => { to.path?.indexOf('inspection') !== -1 ) { // 점검이 종료된 후 점검 페이지 접근시 메인으로 리다이렉트 + console.log('ddddd 222222') return navigateTo(`/${finalLocale}`, { external: true }) } + + console.log('33333333 eee') } } catch (e) { console.error('[Exception] /middleware/inspection: ', e) diff --git a/layers/middleware/pageData.global.ts b/layers/middleware/pageData.global.ts index c16a295..9383c07 100644 --- a/layers/middleware/pageData.global.ts +++ b/layers/middleware/pageData.global.ts @@ -6,7 +6,7 @@ import type { PageDataResponse } from '#layers/types/api/pageData' import type { GameDataValue } from '#layers/types/api/gameData' export default defineNuxtRouteMiddleware(async (to, _from) => { - // [TODO] 하이드레이션 에러 처리 + // client에서만 동작되도록 처리 if (!import.meta.client) return const runtimeConfig = useRuntimeConfig() @@ -28,10 +28,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => { ) try { - // 서버 사이드에서는 스킵 - if (import.meta.server) { - return - } + if (to.path.includes('inspection')) { return } @@ -45,11 +42,26 @@ export default defineNuxtRouteMiddleware(async (to, _from) => { // pageUrl이 빈값이거나 null이면 /brand로 리다이렉트 if (!pageUrl || pageUrl === '' || pageUrl === '/' || pageUrl === `/${langCode}/`) { - return navigateTo(`/${langCode}/brand`, { replace: true }) + return navigateTo(`/${langCode}/brand`, { external: false }) + } + + const accessToken = csrGetAccessToken() + + const headers = { + Authorization: `Bearer ${accessToken}`, + } + + // 쿼리스트링에서 f 파라미터 값 추출 (CSR용) + const fValue = (to.query.f as string) || '' + + // 미리보기 API 호출 처리 + let finalGameDomain = gameDomain + if (fValue === 'preview') { + finalGameDomain = 'samplegame.onstove.com' } const queryParams: Record = { - game_domain: gameDomain, + game_domain: finalGameDomain, lang_code: langCode, page_url: pageUrl, _t: Date.now().toString(), // 캐시 무효화를 위한 타임스탬프 @@ -57,13 +69,37 @@ export default defineNuxtRouteMiddleware(async (to, _from) => { // console.log('🚀 ~ queryParams:', queryParams) const response = (await commonFetch('GET', apiUrl, { + headers, query: queryParams, loading: true, })) as PageDataResponse | null console.log('🚀 ~ response?.code:', response?.code) + // 페이지 접근 권한 설정(로그인 유무) + 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() + }, + }) + } + if(response?.code === 91003) { - return navigateTo(`/${langCode}/error`, { external: false }) + // return navigateTo(`/${langCode}/error`, { external: false }) + showError(createError({ + statusCode: 404, + statusMessage: '페이지를 찾을 수 없어요.', + fatal: false, // 즉시 에러 페이지로 + data: { reason: 'post-not-found' } + })) + } if (response?.code === 0 && 'value' in response) { @@ -74,5 +110,12 @@ export default defineNuxtRouteMiddleware(async (to, _from) => { } catch (error) { console.error(error) store.clearPageData() + + showError(createError({ + statusCode: error.statusCode, + statusMessage: error.message, + fatal: false, // 즉시 에러 페이지로 + data: { reason: 'post-not-found' } + })) } }) diff --git a/layers/plugins/error-handler.ts b/layers/plugins/error-handler.ts deleted file mode 100644 index 54a402a..0000000 --- a/layers/plugins/error-handler.ts +++ /dev/null @@ -1,12 +0,0 @@ -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/middleware/gameData.ts b/layers/server/middleware/gameData.ts index 4293d9d..d18aa9f 100644 --- a/layers/server/middleware/gameData.ts +++ b/layers/server/middleware/gameData.ts @@ -96,6 +96,7 @@ function fnLocaleMiddleware(event: any, finalLocale: string) { // 쿼리스트링 포함 시 순수 경로만 추출 arrPath = path.split('?')[0].split('/') queryString = path.split('?')[1] + } else { arrPath = path.split('/') queryString = '' @@ -238,6 +239,33 @@ export default defineEventHandler(async event => { // 2. 언어 코드 추출 finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers, initLangCodes, initDefaultLocale) + + const path = event?.node.req.url || '' + let queryStringF = '' + let fValue = '' + let test500 = false + if (path.includes('?')) { + queryStringF = path.split('?')[1] + // 쿼리스트링에서 f 파라미터 값 추출 + try { + const urlParams = new URLSearchParams(queryStringF) + fValue = urlParams.get('f') || '' + // 테스트용 500 에러 발생 (예: ?test500=true) + test500 = urlParams.get('test500') === 'true' + } catch (e) { + console.error('쿼리스트링 파싱 에러:', e) + } + } + + // 테스트용 500 에러 발생 + if (test500) { + throw new Error('테스트용 500 에러 발생') + } + + // 미리보기 API 호출 처리 + if (fValue === 'preview') { + cleanHost = 'samplegame.onstove.com' + } const queryParams: Record = { game_domain: cleanHost || '', @@ -385,5 +413,35 @@ export default defineEventHandler(async event => { } } catch (error) { console.error('gameData load error:', error) + + // 500 에러 발생 시 /error 페이지로 리다이렉트 + if (!event.node.res.headersSent && !event.node.res.writableEnded) { + // 언어 코드 추출 시도 + let finalLocale = 'ko' // 기본값 + try { + finalLocale = ssrGetFinalLocale( + event?.node.req.url, + event.node.req.headers, + initLangCodes, + initDefaultLocale + ) + } catch (e) { + console.error('Locale extraction error:', e) + } + // finalLocale이 undefined인 경우 기본값으로 'ko' 설정 + + console.log("🚀 ~ 여기도 타? error:", error) + throw createError({ + statusCode: error.statusCode, + statusMessage: error.statusMessage, + }) + // if (!finalLocale) { + // finalLocale = 'ko' + // } + // const errorPath = `/${finalLocale}/error?message=${error.message}` + // event.node.res.statusCode = 302 + // event.node.res.setHeader('Location', errorPath) + // event.node.res.end() + } } }) diff --git a/layers/templates/FxSecure01/index.vue b/layers/templates/FxSecure01/index.vue index 5dd640e..e629c3f 100644 --- a/layers/templates/FxSecure01/index.vue +++ b/layers/templates/FxSecure01/index.vue @@ -47,6 +47,7 @@ const checkLoginValidation = async () => { const validateTokenResult = await handleTokenValidation( accessToken.value || '' ) + console.log("🚀 ~ checkLoginValidation ~ validateTokenResult:", validateTokenResult) isLogin.value = validateTokenResult } diff --git a/layers/types/api/pageData.ts b/layers/types/api/pageData.ts index 7db7b39..d14edfb 100644 --- a/layers/types/api/pageData.ts +++ b/layers/types/api/pageData.ts @@ -28,6 +28,7 @@ export interface PageDataValue { page_name: string page_name_en: string page_ver: string + is_login_required: number meta_tag_type: number fit_page_height: boolean use_top_btn: boolean