From ebcb511947c33e824652cf9b030db6e42306a91a Mon Sep 17 00:00:00 2001 From: clkim Date: Fri, 19 Dec 2025 17:28:03 +0900 Subject: [PATCH] =?UTF-8?q?fix.=20=EC=9D=B8=ED=8A=B8=EB=A1=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- layers/middleware/init.route.global.ts | 71 +++++--------------- layers/server/middleware/gameData.ts | 41 +++++++++++- layers/utils/urlUtil.ts | 90 +++++++++++++++++++++----- 3 files changed, 131 insertions(+), 71 deletions(-) diff --git a/layers/middleware/init.route.global.ts b/layers/middleware/init.route.global.ts index f710257..07a3f95 100644 --- a/layers/middleware/init.route.global.ts +++ b/layers/middleware/init.route.global.ts @@ -1,4 +1,7 @@ export default defineNuxtRouteMiddleware(to => { + // server에서는 실행X ----- + if (import.meta.server) return + // error 페이지는 실행X ----- if (to.path.includes('/error')) return @@ -11,73 +14,31 @@ export default defineNuxtRouteMiddleware(to => { // app.vue에서 설정한 스토어 값이 없으면 대기 if (!gameData.value) return + // ------------------------------------------------------------------------------- + // [Home Redirect] + // ------------------------------------------------------------------------------- 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 - ) + const langCode = csrGetFinalLocale(to.path, gameData.value.lang_codes) 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 redirectPath = getIntroRedirectPath( + introPageUrl, + langCode, + to.fullPath + ) // 무한 리다이렉트 방지: 현재 경로와 리다이렉트할 URL 비교 - const normalizedFinalUrl = defaultPath.split('?')[0] // 쿼리스트링 제외 - const currentPath = to.path - const isExternalUrl = - defaultPath.startsWith('http://') || defaultPath.startsWith('https://') - const isSamePath = !isExternalUrl && currentPath === normalizedFinalUrl + const normalizedFinalUrl = redirectPath.split('?')[0] // 리다이렉트 경로 (쿼리스트링 제외) + const isExternal = isExternalUrl(redirectPath) + const isSamePath = !isExternal && to.path === normalizedFinalUrl if (!isSamePath) { // 다른 경로에서 접근한 경우에만 리다이렉트 - const queryString = to.fullPath.includes('?') - ? '?' + to.fullPath.split('?')[1] - : '' - const redirectUrl = - queryString && !defaultPath.includes('?') - ? `${defaultPath}${queryString}` - : defaultPath - return navigateTo(redirectUrl, { external: isExternalUrl }) + return navigateTo(redirectPath, { external: isExternal }) } } }) diff --git a/layers/server/middleware/gameData.ts b/layers/server/middleware/gameData.ts index ce62723..9860daf 100644 --- a/layers/server/middleware/gameData.ts +++ b/layers/server/middleware/gameData.ts @@ -1,6 +1,12 @@ import { LRUCache } from 'lru-cache' import { defineEventHandler, createError, setCookie, type H3Event } from 'h3' -import { getGameDomain, getPathLocale } from '#layers/utils/urlUtil' +import { + getGameDomain, + getPathLocale, + getIntroRedirectPath, + isExternalUrl, + getPathAfterLanguage, +} from '#layers/utils/urlUtil' import { ssrGetFinalLocale } from '#layers/utils/localeUtil' import { isStaticFile } from '#layers/utils/commonUtil' import { getTrueClientIp } from '#layers/utils/apiUtil' @@ -327,6 +333,39 @@ export default defineEventHandler(async event => { // [Locale Middleware] // ------------------------------------------------------------------------------- fnLocaleMiddleware(event, finalLocale) + + // ------------------------------------------------------------------------------- + // [Home Redirect] + // ------------------------------------------------------------------------------- + if (!event.node.res.headersSent && !event.node.res.writableEnded) { + const gamePath = getPathAfterLanguage(fullPath) + const isRootPath = gamePath === '' || gamePath === '/' + + if (isRootPath) { + // gameData.intro.page_url이 있으면 해당 URL로 리다이렉트, 없으면 /home으로 + const introPageUrl = gameDataValue?.intro?.page_url + const currentFullUrl = event.node.req.url || fullPath + const redirectPath = getIntroRedirectPath( + introPageUrl, + finalLocale, + currentFullUrl + ) + + // 무한 리다이렉트 방지: 현재 경로와 리다이렉트할 URL 비교 + const currentPathOnly = fullPath.split('?')[0] // 현재 경로 (쿼리스트링 제외) + const normalizedFinalUrl = redirectPath.split('?')[0] // 리다이렉트 경로 (쿼리스트링 제외) + const isExternal = isExternalUrl(redirectPath) + const isSamePath = + !isExternal && currentPathOnly === normalizedFinalUrl + + if (!isSamePath) { + // 다른 경로에서 접근한 경우에만 리다이렉트 + event.node.res.statusCode = 302 + event.node.res.setHeader('Location', redirectPath) + event.node.res.end() + } + } + } } // 정상 접속 허용 diff --git a/layers/utils/urlUtil.ts b/layers/utils/urlUtil.ts index bceb487..a27c182 100644 --- a/layers/utils/urlUtil.ts +++ b/layers/utils/urlUtil.ts @@ -1,5 +1,14 @@ import { getHeader, getRequestHost, type H3Event } from 'h3' +/** + * URL이 외부 URL인지 확인하는 함수 + * @param url - 확인할 URL 문자열 + * @returns 외부 URL 여부 + */ +export const isExternalUrl = (url: string): boolean => { + return url.startsWith('http://') || url.startsWith('https://') +} + /** * 게임 도메인을 가져오는 컴포저블 함수 * 서버와 클라이언트 환경에서 모두 동작 @@ -48,41 +57,92 @@ export const getGameDomain = (event?: H3Event): string => { * @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 +export const getPathLocale = (url: string): string => { + if (!url) return '' - return cleanTargetUrl.split('/')[1] + const cleanUrl = url.endsWith('/') ? url.slice(0, -1) : url + return cleanUrl.split('/')[1] } /** * URL에서 언어 코드 이후의 경로를 추출하는 함수 - * @param url - 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 +export const getPathAfterLanguage = (url: string): string => { + if (!url) return '' + + const cleanUrl = url.split('?')[0].endsWith('/') ? url.slice(0, -1) : url // URL에서 언어 코드 패턴을 찾아서 그 뒤의 경로를 추출 // 예: /ko/about/story -> /about/story // 예: /ko -> "" (빈 문자열) const languagePattern = /^\/[a-z]{2}(-[a-z]{2})?\/(.+)$/ - const match = cleanTargetUrl.match(languagePattern) + const match = cleanUrl.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)) { + if (languageOnlyPattern.test(cleanUrl)) { return '' } else { // 언어 코드가 없는 경우 원본 경로 그대로 반환 (이미 /로 시작) - return cleanTargetUrl + return cleanUrl } } } + +/** + * intro page_url을 기반으로 리다이렉트 경로를 생성하는 공통 함수 + * @param introPageUrl - intro.page_url 값 + * @param langCode - 현재 언어 코드 + * @param currentUrl - 현재 접근한 URL (쿼리스트링 포함, 옵션) + * @returns 최종 리다이렉트 경로 (없으면 기본값 /home) + */ +export const getIntroRedirectPath = ( + introPageUrl: string | undefined | null, + langCode: string, + currentUrl?: string +): string => { + // 기본값: /langCode/home + let defaultPath = `/${langCode}/home` + + if (introPageUrl && introPageUrl.trim() !== '') { + if (isExternalUrl(introPageUrl)) { + // 외부 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] + } + } + } + } + + // intro에 쿼리스트링이 없고, 현재 URL에 쿼리스트링이 있으면 추가 + if (!defaultPath.includes('?') && currentUrl && currentUrl.includes('?')) { + const queryString = currentUrl.split('?')[1] + if (queryString) { + defaultPath += '?' + queryString + } + } + + return defaultPath +}