From c8cd7f780ee3602e8b8584ae52fec2fae30ec8f1 Mon Sep 17 00:00:00 2001 From: clkim Date: Fri, 16 Jan 2026 14:52:14 +0900 Subject: [PATCH] =?UTF-8?q?feat.=20[SWV-807]=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=ED=8F=B0=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app.vue | 138 ++++++++++++++------- layers/components/blocks/VisualContent.vue | 20 ++- layers/stores/useGameDataStore.ts | 2 + layers/types/api/gameData.ts | 4 +- layers/types/api/pageData.ts | 2 + 5 files changed, 113 insertions(+), 53 deletions(-) diff --git a/app/app.vue b/app/app.vue index ed8813b..a0578f1 100644 --- a/app/app.vue +++ b/app/app.vue @@ -3,87 +3,129 @@ import { useNuxtApp } from 'nuxt/app' import type { GameDataMetaTag, GameDataValue, + GameDataKeyColors, GameDataImg, } from '#layers/types/api/gameData' const nuxtApp = useNuxtApp() const { locale } = useI18n() - const gameDataStore = useGameDataStore() const modalStore = useModalStore() const scrollStore = useScrollStore() const { setGameData } = gameDataStore -const { gameName, gaCode } = storeToRefs(gameDataStore) const { confirm, alert } = modalStore +const { gameName, gaCode } = storeToRefs(gameDataStore) const { scrollGnbPosition } = storeToRefs(scrollStore) -// 통합 메타데이터 설정 -const setupAllMetaData = (data: GameDataValue) => { - const meta = data.meta_tag_json ?? ({} as GameDataMetaTag) - const faviconPath = data.favicon_json ?? ({} as GameDataImg) - const theme = data.design_theme === 1 ? 'light' : 'dark' +// favicon 링크 생성 헬퍼 +const createStyleLinks = (faviconJson: GameDataImg, fontPath: string = '') => { + const links = [] + const iconUrl = faviconJson[0] + const appleTouchIconUrl = faviconJson[1] + const pngIconUrl = faviconJson[2] - // 파비콘 링크 생성 - const faviconLinks = [ - { + if (iconUrl) { + links.push({ rel: 'icon', type: 'image/x-icon', - href: formatPathHost(faviconPath[0]), - }, - { + href: formatPathHost(iconUrl), + }) + } + if (appleTouchIconUrl) { + links.push({ rel: 'apple-touch-icon', - href: formatPathHost(faviconPath[1]), - }, - { + href: formatPathHost(appleTouchIconUrl), + }) + } + if (pngIconUrl) { + links.push({ rel: 'icon', type: 'image/png', - href: formatPathHost(faviconPath[2]), + href: formatPathHost(pngIconUrl), + }) + } + if (fontPath) { + links.push({ + rel: 'stylesheet', + href: formatPathHost(fontPath), + }) + } + + return links +} + +// 메타 태그 생성 헬퍼 +const createMetaTags = (metaTag: Partial = {}) => { + const metaList = [ + { name: 'description', content: metaTag.page_desc }, + { property: 'og:title', content: metaTag.og_title }, + { property: 'og:description', content: metaTag.og_desc }, + { + property: 'og:image', + content: formatPathHost(metaTag.og_image), + }, + { name: 'twitter:title', content: metaTag.x_title }, + { name: 'twitter:description', content: metaTag.x_desc }, + { + name: 'twitter:image', + content: formatPathHost(metaTag.x_image), }, ] - // 색상 CSS 변수 생성 - const cssColorVariables = Object.entries(data.key_color_json ?? {}) + // content가 유효한 메타 태그만 필터링 + return metaList.filter( + meta => meta.content && String(meta.content).trim() !== '' + ) +} + +// CSS 변수 생성 헬퍼 +const createCssVariable = (keyColorJson: GameDataKeyColors) => { + const colorVariables = Object.entries(keyColorJson) + .filter(([key, value]) => key && value != null) .map(([key, value]) => `--${key}: ${value};`) .join('\n ') - const cssContent = ` - :root { - ${cssColorVariables} - } - ` + return `:root {${colorVariables}}` +} - useHead({ - title: meta?.page_title ?? '', - meta: [ - { name: 'description', content: meta.page_desc }, - { property: 'og:title', content: meta.og_title }, - { property: 'og:description', content: meta.og_desc }, - { property: 'og:image', content: formatPathHost(meta.og_image) }, - { name: 'twitter:title', content: meta.x_title }, - { name: 'twitter:description', content: meta.x_desc }, - { name: 'twitter:image', content: formatPathHost(meta.x_image) }, - ], - htmlAttrs: { - 'data-game': data.game_name || '', - 'data-theme': theme, - lang: locale.value ?? data.default_lang_code, - }, - link: faviconLinks, - style: [ - { - innerHTML: cssContent, - id: 'game-css-variables', +// 게임 헤드 설정 +const setupGameHead = (data: GameDataValue) => { + try { + const metaTag: Partial = data.meta_tag_json ?? {} + const designTheme = data.design_theme === 1 ? 'light' : 'dark' + const styleLinks = createStyleLinks( + data.favicon_json + // data?.game_font?.font_path + ) + + useHead({ + title: metaTag.page_title ?? '', + meta: createMetaTags(metaTag), + htmlAttrs: { + 'data-game': data.game_name ?? '', + 'data-theme': designTheme, + lang: locale.value ?? data.default_lang_code ?? 'ko', }, - ], - }) + link: styleLinks, + style: [ + { + innerHTML: createCssVariable(data.key_color_json), + id: 'game-css-variables', + }, + ], + }) + } catch (error) { + // eslint-disable-next-line no-console + console.error('[setupGameHead] Failed to setup game head:', error) + } } if (import.meta.server) { const gameData = nuxtApp.ssrContext?.event?.context?.gameData if (gameData) { setGameData(gameData) - setupAllMetaData(gameData) + setupGameHead(gameData) } } diff --git a/layers/components/blocks/VisualContent.vue b/layers/components/blocks/VisualContent.vue index a63d871..42dcc44 100644 --- a/layers/components/blocks/VisualContent.vue +++ b/layers/components/blocks/VisualContent.vue @@ -12,16 +12,32 @@ const props = withDefaults(defineProps(), { objectFit: 'contain', }) +const gameDataStore = useGameDataStore() +const { fontFamily } = storeToRefs(gameDataStore) + const imagePaths = computed(() => getResourceSrc(props.resourcesData)) const displayText = computed(() => props.resourcesData?.display?.text) const displayColor = computed(() => getColorCodeFromData(props.resourcesData?.display, 'none') ) -// HTML 콘텐츠 정리 (줄바꿈 처리) +// HTML 콘텐츠 줄바꿈 처리 const sanitizedContent = computed(() => { return displayText.value?.replace(/\n/g, '
') || '' }) + +// 텍스트 스타일 +const textStyle = computed(() => { + const style: Record = { + color: displayColor.value, + } + + if (props.resourcesData?.display?.use_game_font === 1) { + style.fontFamily = fontFamily.value + } + + return style +}) diff --git a/layers/stores/useGameDataStore.ts b/layers/stores/useGameDataStore.ts index d63c5fa..e2c7a84 100644 --- a/layers/stores/useGameDataStore.ts +++ b/layers/stores/useGameDataStore.ts @@ -17,6 +17,7 @@ export const useGameDataStore = defineStore('gameData', () => { urlJson: null as GameDataValue['url_json'] | null, gnb: null as GameDataValue['gnb'] | null, eventBanner: null as GameDataValue['event_banner'] | null, + fontFamily: null as GameDataValue['game_font']['font_family'] | null, }) const state = reactive(getInitialState()) @@ -37,6 +38,7 @@ export const useGameDataStore = defineStore('gameData', () => { state.urlJson = data?.url_json state.gnb = data?.gnb state.eventBanner = data?.event_banner + state.fontFamily = data?.game_font?.font_family } const clearGameData = () => { diff --git a/layers/types/api/gameData.ts b/layers/types/api/gameData.ts index 5f9d1f5..93ba96e 100644 --- a/layers/types/api/gameData.ts +++ b/layers/types/api/gameData.ts @@ -29,7 +29,6 @@ export interface GameDataValue { design_theme: number lang_codes: string[] key_color_json: GameDataKeyColors - use_game_font: boolean comm_sns_bg_color_json: { display: ColorObject } @@ -73,8 +72,7 @@ export interface GameDataKeyColors { // 게임 폰트 타입 export interface GameDataGameFont { font_family: string - font_weight: string - font_style: string + font_path: string } // 파비콘 경로 타입 diff --git a/layers/types/api/pageData.ts b/layers/types/api/pageData.ts index 77c7145..65fc9c9 100644 --- a/layers/types/api/pageData.ts +++ b/layers/types/api/pageData.ts @@ -87,11 +87,13 @@ export interface PageDataResourceGroupBtnInfo extends ColorObject { disabled: boolean txt_btn_name: string detail: Record + use_game_font: 0 | 1 } // 리소스 그룹 타입 export interface PageDataResourceGroupDisplay extends ColorObject { text: string + use_game_font: 0 | 1 } export interface PageDataResourceGroup {