- fnCustomVideo 유튜브 팝업 함수 추가 - script/link exclude 속성 지원 - 전역 함수 등록 로직 통합 (registerGlobalFunctions) - CSS Selector injection 보안 취약점 수정 - 사용되지 않는 변수/props 제거 - DOMPurify exclude, defer 속성 허용 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
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://')
|
|
}
|
|
|
|
/**
|
|
* 경로에 쿼리스트링을 추가하는 함수
|
|
*/
|
|
export const addQueryString = (path: string, queryString: string): string => {
|
|
return queryString ? `${path}?${queryString}` : path
|
|
}
|
|
|
|
/**
|
|
* 게임 도메인을 추출하는 함수
|
|
* 서버와 클라이언트 환경에서 모두 동작
|
|
* @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 => {
|
|
if (!url) return ''
|
|
|
|
const cleanUrl = url.endsWith('/') ? url.slice(0, -1) : url
|
|
return cleanUrl.split('/')[1] || ''
|
|
}
|
|
|
|
/**
|
|
* URL에서 언어 코드 이후의 경로를 추출하는 함수
|
|
* 서버와 클라이언트 환경에서 모두 동작
|
|
* @param url - URL 문자열
|
|
* @returns 언어 코드 이후의 경로 문자열
|
|
*/
|
|
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 = 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(cleanUrl)) {
|
|
return ''
|
|
} else {
|
|
// 언어 코드가 없는 경우 원본 경로 그대로 반환 (이미 /로 시작)
|
|
return cleanUrl
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* URL을 파싱하여 최종 경로를 반환하는 함수
|
|
* 경로가 '/' 또는 ''이고 인트로 URL이 있으면 인트로로 리다이렉트,
|
|
* 그렇지 않으면 언어 코드를 추가한 URL을 반환
|
|
* SSR과 CSR 모두에서 작동
|
|
* @param url - URL 문자열
|
|
* @param finalLocale - 최종 언어 코드
|
|
* @param langCodes - 지원하는 언어 코드 배열
|
|
* @param ISO_LANGUAGE_CODES - ISO 언어 코드 배열
|
|
* @param introPageUrl - 인트로 페이지 URL (선택)
|
|
* @returns 최종 URL 문자열
|
|
*/
|
|
export const parseUrl = (
|
|
url: string,
|
|
finalLocale: string,
|
|
langCodes: string[],
|
|
ISO_LANGUAGE_CODES: string[],
|
|
introPageUrl?: string
|
|
): string => {
|
|
const [pathPart, queryString = ''] = url.split('?')
|
|
const pathSegments = pathPart.split('/').filter(Boolean)
|
|
const currentLocale = pathSegments[0]
|
|
const isKnownLocale =
|
|
langCodes.includes(currentLocale) ||
|
|
ISO_LANGUAGE_CODES.includes(currentLocale)
|
|
const isEmptyPath =
|
|
pathSegments.length === 0 || (pathSegments.length === 1 && isKnownLocale)
|
|
|
|
// 경로가 '/' 또는 ''인 경우 인트로 URL 처리
|
|
if (isEmptyPath) {
|
|
const hasIntroUrl = introPageUrl?.trim()
|
|
if (hasIntroUrl) {
|
|
// 외부 URL인 경우 그대로 반환
|
|
if (isExternalUrl(introPageUrl)) {
|
|
return introPageUrl
|
|
}
|
|
// 내부 경로인 경우 언어 코드 추가
|
|
// 인트로 URL이 이미 언어 코드로 시작하는지 확인
|
|
const introPathSegments = introPageUrl.split('/').filter(Boolean)
|
|
const introFirstSegment = introPathSegments[0]
|
|
const introHasLocale =
|
|
introFirstSegment &&
|
|
(langCodes.includes(introFirstSegment) ||
|
|
ISO_LANGUAGE_CODES.includes(introFirstSegment))
|
|
|
|
let introPath: string
|
|
if (introHasLocale) {
|
|
// 이미 언어 코드가 있으면 언어 코드만 교체
|
|
introPath = '/' + [finalLocale, ...introPathSegments.slice(1)].join('/')
|
|
} else {
|
|
// 언어 코드가 없으면 추가
|
|
introPath = `/${finalLocale}${introPageUrl}`
|
|
}
|
|
return addQueryString(introPath, queryString)
|
|
}
|
|
// 인트로 URL이 없으면 기본 홈 경로 반환
|
|
return addQueryString(`/${finalLocale}/home`, queryString)
|
|
}
|
|
|
|
// 리다이렉트 경로 생성
|
|
const remainingPath = pathSegments.slice(1)
|
|
const hasRemainingPath = remainingPath.length > 0
|
|
let newPath: string
|
|
|
|
if (isKnownLocale) {
|
|
// 현재 언어 코드와 최종 언어가 같으면 리다이렉트 불필요
|
|
if (currentLocale === finalLocale) return url
|
|
|
|
// 유효한 언어 코드가 있지만 다른 언어인 경우
|
|
if (hasRemainingPath) {
|
|
// 경로가 있으면 언어 코드만 교체 (/vi/story -> /finalLocale/story)
|
|
newPath = '/' + [finalLocale, ...remainingPath].join('/')
|
|
}
|
|
} else {
|
|
// 언어 코드가 없거나 유효하지 않은 경우: 언어 코드를 앞에 추가
|
|
const pathWithoutSlash = pathPart === '/' ? '' : pathPart
|
|
newPath = `/${finalLocale}${pathWithoutSlash}`
|
|
}
|
|
|
|
return addQueryString(newPath, queryString)
|
|
}
|