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: [] },