258 lines
6.6 KiB
Vue
258 lines
6.6 KiB
Vue
<script setup lang="ts">
|
|
const route = useRoute()
|
|
const requestURL = useRequestURL()
|
|
const { locale } = useI18n()
|
|
const gameDataStore = useGameDataStore()
|
|
const modalStore = useModalStore()
|
|
const scrollStore = useScrollStore()
|
|
|
|
const { confirm, alert } = modalStore
|
|
const {
|
|
gameName,
|
|
gaCode,
|
|
gameTheme,
|
|
gameMetaTag,
|
|
faviconJson,
|
|
defaultLangCode,
|
|
gameFontJson,
|
|
keyColorJson,
|
|
} = storeToRefs(gameDataStore)
|
|
const { scrollGnbPosition } = storeToRefs(scrollStore)
|
|
|
|
// favicon 링크 생성 헬퍼
|
|
const createStyleLinks = () => {
|
|
const links = []
|
|
const iconUrl = faviconJson.value?.[0]
|
|
const appleTouchIconUrl = faviconJson.value?.[1]
|
|
const pngIconUrl = faviconJson.value?.[2]
|
|
const fontPath = gameFontJson.value?.font_path
|
|
|
|
if (iconUrl) {
|
|
links.push({
|
|
rel: 'icon',
|
|
type: 'image/x-icon',
|
|
href: formatPathHost(iconUrl),
|
|
})
|
|
}
|
|
if (appleTouchIconUrl) {
|
|
links.push({
|
|
rel: 'apple-touch-icon',
|
|
href: formatPathHost(appleTouchIconUrl),
|
|
})
|
|
}
|
|
if (pngIconUrl) {
|
|
links.push({
|
|
rel: 'icon',
|
|
type: 'image/png',
|
|
href: formatPathHost(pngIconUrl),
|
|
})
|
|
}
|
|
if (fontPath) {
|
|
links.push({
|
|
rel: 'stylesheet',
|
|
href: formatPathHost(fontPath),
|
|
})
|
|
}
|
|
|
|
return links
|
|
}
|
|
|
|
// CSS 변수 생성 헬퍼
|
|
const createStyleCss = () => {
|
|
const colorVariables = Object.entries(keyColorJson.value || {})
|
|
.filter(([key, value]) => key && value != null)
|
|
.map(([key, value]) => `--${key}: ${value};`)
|
|
.join('\n ')
|
|
|
|
return `:root {${colorVariables}}`
|
|
}
|
|
|
|
// 게임 헤드 설정
|
|
const setupGameHead = () => {
|
|
if (!gameMetaTag.value) return
|
|
try {
|
|
const styleCss = createStyleCss()
|
|
|
|
useHead({
|
|
htmlAttrs: {
|
|
'data-game': gameName.value ?? '',
|
|
'data-theme': gameTheme.value,
|
|
lang: locale.value ?? defaultLangCode.value ?? 'ko',
|
|
},
|
|
link: createStyleLinks(),
|
|
style: [
|
|
{
|
|
innerHTML: styleCss,
|
|
id: 'game-css-variables',
|
|
},
|
|
],
|
|
})
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('[setupGameHead] Failed to setup game head:', error)
|
|
}
|
|
}
|
|
|
|
// Schema.org 구조화된 데이터 설정
|
|
const setupStructuredData = () => {
|
|
if (!gameName.value) return
|
|
|
|
try {
|
|
// SEO용 URL은 설정 도메인이 아닌 실제 요청 호스트 기준 (SSR/CSR 모두 useRequestURL)
|
|
const baseUrl = requestURL.origin
|
|
const currentLocale = locale.value ?? defaultLangCode.value ?? 'ko'
|
|
|
|
// Organization Schema
|
|
const organizationSchema: Record<string, any> = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Organization',
|
|
name: gameName.value,
|
|
url: baseUrl,
|
|
}
|
|
|
|
// 로고 이미지 추가 (favicon 중 PNG 우선)
|
|
if (faviconJson.value) {
|
|
const logoUrl = faviconJson.value[2] || faviconJson.value[0]
|
|
if (logoUrl) {
|
|
organizationSchema.logo = formatPathHost(logoUrl)
|
|
}
|
|
}
|
|
|
|
// SNS 링크 추가
|
|
const snsData = gameDataStore.snsJson
|
|
if (snsData) {
|
|
const sameAs: string[] = []
|
|
if (snsData.youtube?.use_yn === 1 && snsData.youtube?.url) {
|
|
sameAs.push(snsData.youtube.url)
|
|
}
|
|
if (snsData.twitter?.use_yn === 1 && snsData.twitter?.url) {
|
|
sameAs.push(snsData.twitter.url)
|
|
}
|
|
if (snsData.facebook?.use_yn === 1 && snsData.facebook?.url) {
|
|
sameAs.push(snsData.facebook.url)
|
|
}
|
|
if (snsData.discord?.use_yn === 1 && snsData.discord?.url) {
|
|
sameAs.push(snsData.discord.url)
|
|
}
|
|
if (snsData.instagram?.use_yn === 1 && snsData.instagram?.url) {
|
|
sameAs.push(snsData.instagram.url)
|
|
}
|
|
if (snsData.tiktok?.use_yn === 1 && snsData.tiktok?.url) {
|
|
sameAs.push(snsData.tiktok.url)
|
|
}
|
|
|
|
if (sameAs.length > 0) {
|
|
organizationSchema.sameAs = sameAs
|
|
}
|
|
}
|
|
|
|
// WebSite Schema - 사이트 정보
|
|
const websiteSchema: Record<string, any> = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'WebSite',
|
|
name: gameName.value,
|
|
url: baseUrl,
|
|
inLanguage: currentLocale,
|
|
}
|
|
|
|
// 설명 추가 (있는 경우)
|
|
if (gameMetaTag.value?.page_desc) {
|
|
websiteSchema.description = gameMetaTag.value.page_desc
|
|
}
|
|
|
|
useHead({
|
|
script: [
|
|
{
|
|
type: 'application/ld+json',
|
|
innerHTML: JSON.stringify(organizationSchema),
|
|
},
|
|
{
|
|
type: 'application/ld+json',
|
|
innerHTML: JSON.stringify(websiteSchema),
|
|
},
|
|
],
|
|
link: [
|
|
{
|
|
rel: 'canonical',
|
|
href: `${baseUrl}${route.path}`,
|
|
},
|
|
],
|
|
})
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('[setupStructuredData] Failed to setup schema:', error)
|
|
}
|
|
}
|
|
|
|
setupGameHead()
|
|
setupStructuredData()
|
|
|
|
let rafId: number | null = null
|
|
onMounted(() => {
|
|
useEventListener('scroll', scrollStore.updateScrollValue, { passive: true })
|
|
|
|
const { gtag, initialize } = useGtag()
|
|
initialize(gaCode.value)
|
|
gtag('event', 'screen_view', {
|
|
app_name: 'My App',
|
|
screen_name: 'Home',
|
|
})
|
|
|
|
watch(
|
|
scrollGnbPosition,
|
|
newValue => {
|
|
if (rafId) cancelAnimationFrame(rafId)
|
|
|
|
rafId = requestAnimationFrame(() => {
|
|
document.documentElement.style.setProperty(
|
|
'--scroll-stove-position',
|
|
`${newValue}px`
|
|
)
|
|
rafId = null
|
|
})
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
// requestAnimationFrame 정리
|
|
if (rafId) cancelAnimationFrame(rafId)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<h1 class="sr-only">{{ gameName }}</h1>
|
|
<NuxtPage />
|
|
|
|
<!-- 공통 모달 컴포넌트 -->
|
|
<WidgetsModalClient />
|
|
<BlocksModalYouTube />
|
|
<BlocksModalContent />
|
|
<BlocksModalConfirm
|
|
v-model:is-open="confirm.storeIsOpen"
|
|
:is-show-dimmed="confirm.storeIsShowDimmed"
|
|
:content-text="confirm.storeContentText"
|
|
:confirm-button-text="confirm.storeConfirmButtonText"
|
|
:cancel-button-text="confirm.storeCancelButtonText"
|
|
:is-outside-close="confirm.storeIsOutsideClose"
|
|
:modal-name="confirm.storeModalName"
|
|
@confirm-button-event="confirm.storeConfirmButtonEvent"
|
|
@cancel-button-event="confirm.storeCancelButtonEvent"
|
|
/>
|
|
<BlocksModalAlert
|
|
v-model:is-open="alert.storeIsOpen"
|
|
:is-show-dimmed="alert.storeIsShowDimmed"
|
|
:content-text="alert.storeContentText"
|
|
:confirm-button-text="alert.storeConfirmButtonText"
|
|
:is-outside-close="alert.storeIsOutsideClose"
|
|
:modal-name="alert.storeModalName"
|
|
@confirm-button-event="alert.storeConfirmButtonEvent"
|
|
/>
|
|
<BlocksModalToast />
|
|
|
|
<!-- 로딩 컴포넌트 -->
|
|
<AtomsLoadingFull />
|
|
<AtomsLoadingLocal />
|
|
</template>
|