feat: CtLayout01 커스텀 콘텐츠 기능 개선

- fnCustomVideo 유튜브 팝업 함수 추가
- script/link exclude 속성 지원
- 전역 함수 등록 로직 통합 (registerGlobalFunctions)
- CSS Selector injection 보안 취약점 수정
- 사용되지 않는 변수/props 제거
- DOMPurify exclude, defer 속성 허용

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
“hyeonggkim”
2026-02-05 19:21:19 +09:00
parent 3ebf2aa310
commit 87b1ca0db1
12 changed files with 321 additions and 234 deletions

View File

@@ -1,12 +1,7 @@
import { LRUCache } from 'lru-cache'
import { defineEventHandler, createError, type H3Event } from 'h3'
import {
getGameDomain,
getPathLocale,
getIntroRedirectPath,
isExternalUrl,
getPathAfterLanguage,
} from '#layers/utils/urlUtil'
import { ISO_LANGUAGE_CODES } from '@/i18n.config'
import { getGameDomain, getPathLocale, parseUrl } from '#layers/utils/urlUtil'
import { ssrGetFinalLocale } from '#layers/utils/localeUtil'
import { isStaticFile } from '#layers/utils/commonUtil'
import { getTrueClientIp } from '#layers/utils/apiUtil'
@@ -73,41 +68,41 @@ const cache = new LRUCache<string, WebInspectionData>({
/**
* Locale Middleware 역할 함수
*
* @param event - 이벤트 객체
* @param finalLocale - 최종 언어
* URL의 언어 코드를 최종 언어로 변경하거나 추가
*/
function fnLocaleMiddleware(event: H3Event, finalLocale: string) {
function fnLocaleMiddleware(
event: H3Event,
finalLocale: string,
langCodes: string[],
intro: string
): void {
// 이미 응답이 종료되었는지 확인
if (event.node.res.headersSent || event.node.res.writableEnded) {
return
}
const path = event?.node.req.url || ''
const [pathPart, queryString = ''] = path.split('?')
const arrPath = pathPart.split('/')
const finalPath = parseUrl(
path,
finalLocale,
langCodes,
ISO_LANGUAGE_CODES,
intro
)
// 최종 언어 세팅된 경로 생성
const pathLocale = arrPath.length > 1 ? arrPath[1] : ''
// URL에서 현재 언어와 최종 언어가 다르면 리다이렉트
if (pathLocale !== finalLocale) {
let newLocalePath = ''
if (pathLocale === '') {
newLocalePath = `/${finalLocale}`
} else {
arrPath[1] = finalLocale
newLocalePath = arrPath.join('/')
}
if (queryString) {
newLocalePath += `?${queryString}`
}
event.node.res.statusCode = 302
event.node.res.setHeader('Location', newLocalePath)
event.node.res.end()
// finalPath가 유효하지 않으면 리다이렉트하지 않음
if (!finalPath || typeof finalPath !== 'string') {
return
}
// 현재 경로와 최종 경로가 같으면 리다이렉트 불필요
if (path === finalPath) {
return
}
event.node.res.statusCode = 302
event.node.res.setHeader('Location', finalPath)
event.node.res.end()
}
/**
@@ -144,6 +139,7 @@ export default defineEventHandler(async event => {
let gameDataResponse: GameDataResponse | null = null
let gameDataLangCodes: string[] | null = null
let gameDataDefaultLocale: string | null = null
let gameDataIntro: string | null = null
let finalLocale: string
try {
@@ -160,6 +156,7 @@ export default defineEventHandler(async event => {
gameDataResponse = response
gameDataLangCodes = response?.value?.lang_codes || null
gameDataDefaultLocale = response?.value?.default_lang_code || null
gameDataIntro = response?.value?.intro?.page_url || ''
event.context.gameDomain = gameDomain
} catch (error) {
// eslint-disable-next-line no-console
@@ -263,7 +260,12 @@ export default defineEventHandler(async event => {
// -------------------------------------------------------------------------------
// [Locale Middleware]
// -------------------------------------------------------------------------------
fnLocaleMiddleware(event, finalLocale)
fnLocaleMiddleware(
event,
finalLocale,
gameDataLangCodes,
gameDataIntro
)
}
} else {
if (!event.node.res.headersSent && !event.node.res.writableEnded) {
@@ -302,40 +304,7 @@ 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) {
// 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()
}
}
}
fnLocaleMiddleware(event, finalLocale, gameDataLangCodes, gameDataIntro)
}
// 정상 접속 허용