diff --git a/.gitignore b/.gitignore index a2670c8..1216751 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ logs *.log # Misc -.DS_Store \ No newline at end of file +.DS_Store +CLAUDE.md diff --git a/layers/composables/useAnalytics.ts b/layers/composables/useAnalytics.ts index ed11c75..af0caf0 100644 --- a/layers/composables/useAnalytics.ts +++ b/layers/composables/useAnalytics.ts @@ -147,6 +147,7 @@ const sendSA = ( view_info: { game_no: gameNo, lang_cd: eventLocale, + service_cd: 'official_home', ...options?.viewInfo, }, } @@ -158,6 +159,7 @@ const sendSA = ( pwt_click_item: clickItem, game_no: gameNo, lang_cd: eventLocale, + service_cd: 'official_home', ...options?.clickItem, }, } diff --git a/layers/composables/useTemplateRegistry.ts b/layers/composables/useTemplateRegistry.ts index 422e707..b3451f2 100644 --- a/layers/composables/useTemplateRegistry.ts +++ b/layers/composables/useTemplateRegistry.ts @@ -14,6 +14,7 @@ import FxDownload01 from '#layers/templates/FxDownload01/index.vue' import FxCoupon01 from '#layers/templates/FxCoupon01/index.vue' import FxSecure01 from '#layers/templates/FxSecure01/index.vue' import FxPreregist01 from '#layers/templates/FxPreregist01/index.vue' +import CtLayout01 from '#layers/templates/CtLayout01/index.vue' const templateRegistry = { GR_VISUAL_01: { component: GrVisual01 }, @@ -32,6 +33,7 @@ const templateRegistry = { FX_COUPON_01: { component: FxCoupon01 }, FX_SECURE_01: { component: FxSecure01 }, FX_PREREGIST_01: { component: FxPreregist01 }, + CT_LAYOUT_01: { component: CtLayout01 }, } as const type TemplateKey = keyof typeof templateRegistry diff --git a/layers/middleware/pageData.global.ts b/layers/middleware/pageData.global.ts index f57e710..e54a075 100644 --- a/layers/middleware/pageData.global.ts +++ b/layers/middleware/pageData.global.ts @@ -20,7 +20,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => { const pageDataStore = usePageDataStore() const loadingStore = useLoadingStore() - const { langCodes } = storeToRefs(gameDataStore) + const { langCodes, defaultLangCode } = storeToRefs(gameDataStore) const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl const accessToken = csrGetAccessToken() diff --git a/layers/plugins/dompurify-html.ts b/layers/plugins/dompurify-html.ts index 59277c0..d0f0451 100644 --- a/layers/plugins/dompurify-html.ts +++ b/layers/plugins/dompurify-html.ts @@ -1,55 +1,165 @@ -import VueDOMPurifyHTML from 'vue-dompurify-html' +// @ts-ignore +import DOMPurify from 'dompurify' + +import type { DirectiveBinding } from 'vue' + +// DOMPurify 설정 타입 +interface DOMPurifyConfig { + ALLOWED_TAGS: string[] + ALLOWED_ATTR: string[] +} + +// 공통 허용 태그 +const commonTags = [ + 'br', + 'div', + 'b', + 'strong', + 'i', + 'em', + 'u', + 'a', + 'p', + 'ul', + 'li', + 's', + 'ol', + 'button', + 'span', + 'img', + 'pre', + 'input', + 'dl', + 'dt', + 'dd', + 'table', + 'thead', + 'tbody', + 'tfoot', + 'video', + 'source', + 'tr', + 'th', + 'td', + 'section', +] as const + +// 공통 허용 속성 +const commonAttrs = [ + 'style', + 'id', + 'href', + 'src', + 'target', + 'alt', + 'class', + 'width', + 'height', + 'frameborder', + 'colspan', + 'rowspan', + 'scope', + 'type', + 'autoplay', + 'muted', + 'loop', + 'playsinline', + 'controls', + 'preload', + 'poster', +] as const + +// 기본 설정 (일반 사용자용) +const defaultConfig: DOMPurifyConfig = { + ALLOWED_TAGS: [...commonTags], + ALLOWED_ATTR: [...commonAttrs], +} + +// 관리자용 설정 (확장된 태그 및 속성) +const adminConfig: DOMPurifyConfig = { + ALLOWED_TAGS: [ + ...commonTags, + 'iframe', + 'link', + 'style', + 'script', + 'blockquote', + ], + ALLOWED_ATTR: [ + ...commonAttrs, + 'onclick', + 'rel', + 'media', + 'integrity', + 'crossorigin', + 'referrerpolicy', + 'allowfullscreen', + 'exclude-custom', + 'exclude', + 'defer', + ], +} + +/** + * HTML 문자열을 안전하게 정화하는 헬퍼 함수 + * @param value - 정화할 HTML 문자열 + * @param config - DOMPurify 설정 + * @returns 정화된 HTML 문자열 + */ +function sanitizeHtml(value: unknown, config: DOMPurifyConfig): string { + // SSR 환경에서는 빈 문자열 반환 + if (!import.meta.client) { + return '' + } + + // 값이 없거나 문자열이 아닌 경우 빈 문자열 반환 + if (!value || typeof value !== 'string') { + return '' + } + + try { + return DOMPurify.sanitize(value, config) + } catch (error) { + // 에러 발생 시 원본 값 반환하지 않고 빈 문자열 반환 (보안상 안전) + if (import.meta.dev) { + // eslint-disable-next-line no-console + console.error('[DOMPurify] Sanitization error:', error) + } + return '' + } +} + +/** + * 디렉티브 핸들러 생성 함수 + * @param config - DOMPurify 설정 + * @returns 디렉티브 핸들러 객체 + */ +function createDirectiveHandler(config: DOMPurifyConfig) { + return { + mounted(el: HTMLElement, binding: DirectiveBinding) { + el.innerHTML = sanitizeHtml(binding.value, config) + }, + updated(el: HTMLElement, binding: DirectiveBinding) { + el.innerHTML = sanitizeHtml(binding.value, config) + }, + } +} export default defineNuxtPlugin(nuxtApp => { - nuxtApp.vueApp.use(VueDOMPurifyHTML, { - default: { - ALLOWED_TAGS: [ - 'br', - 'div', - 'b', - 'strong', - 'i', - 'em', - 'u', - 's', - 'a', - 'p', - 'ol', - 'ul', - 'li', - 'span', - 'img', - 'pre', - 'iframe', - 'input', - 'dl', - 'dt', - 'dd', - 'blockquote', - 'table', - 'thead', - 'tbody', - 'tfoot', - 'tr', - 'th', - 'td', - ], - ALLOWED_ATTR: [ - 'style', - 'id', - 'href', - 'src', - 'target', - 'alt', - 'class', - 'width', - 'height', - 'frameborder', - 'allowfullscreen', - 'colspan', - 'rowspan', - 'scope', - ], - }, - }) + // SSR 환경에서는 디렉티브 등록하지 않음 + // if (!import.meta.client) { + // return + // } + + // 기본 디렉티브: v-dompurify-html + nuxtApp.vueApp.directive( + 'dompurify-html', + createDirectiveHandler(defaultConfig) + ) + + // 관리자용 디렉티브: v-dompurify-admin + nuxtApp.vueApp.directive( + 'dompurify-admin', + createDirectiveHandler(adminConfig) + ) }) diff --git a/layers/templates/CtLayout01/index.vue b/layers/templates/CtLayout01/index.vue new file mode 100644 index 0000000..9f0ba6e --- /dev/null +++ b/layers/templates/CtLayout01/index.vue @@ -0,0 +1,732 @@ + + + + + diff --git a/nuxt.config.ts b/nuxt.config.ts index 314d4ba..b227a5a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -116,6 +116,7 @@ export default defineNuxtConfig({ }, experimental: { payloadExtraction: false, + entryImportMap: false, }, nitro: { prerender: { routes: [] },