feat: GA/SA 추가 완료

This commit is contained in:
“hyeonggkim”
2025-09-22 19:17:34 +09:00
parent 2ea8b69f7b
commit 1d0b5aa827
12 changed files with 88 additions and 192 deletions

View File

@@ -5,14 +5,6 @@ import LoadingLocal from '#layers/components/blocks/loading/Local.vue'
import type { GameDataMetaTag, GameDataValue } from '#layers/types/api/gameData'
const nuxtApp = useNuxtApp()
const { gtag, initialize } = useGtag()
const { gaId } = useGoogleAnalyticsId()
initialize(gaId.value)
gtag('event', 'screen_view', {
app_name: 'My App',
screen_name: 'Home'
})
const gameDataStore = useGameDataStore()
const { setGameData } = gameDataStore
@@ -116,6 +108,13 @@ if (serverGameData) {
setGameData(serverGameData)
setupMetaData(serverGameData)
}
const { gtag, initialize } = useGtag()
initialize(gameData.value.ga_code)
gtag('event', 'screen_view', {
app_name: 'My App',
screen_name: 'Home'
})
</script>
<template>

View File

@@ -65,6 +65,7 @@ watchEffect(() => {
<component
:is="registry[template.template_code]?.component"
:components="template.components"
:page-ver-tmpl-seq="template.page_ver_tmpl_seq"
/>
</template>
</main>

View File

@@ -2,7 +2,10 @@
import { getResponsiveSrc, getResponsiveClass } from '#layers/utils/dataUtil'
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
const props = defineProps<{ resourcesData: PageDataResourceGroup }>()
const props = defineProps<{ resourcesData: PageDataResourceGroup, pageVerTmplSeq: number }>()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogData } = useAnalytics()
const logData = useAnalyticsLogData(props.resourcesData, props.pageVerTmplSeq)
const bgStyles = getResponsiveSrc(props.resourcesData?.res_path, {
resourcesType: 'bg',
@@ -31,9 +34,9 @@ const handleCloseModal = () => {
<button
v-if="resourcesData"
class="bg-cover bg-center bg-no-repeat w-[66px] h-[66px] lg:w-[100px] lg:h-[100px]"
:class="getResponsiveClass"
:class="getResponsiveClass()"
:style="bgStyles"
@click="handleVideoPlayClick"
@click="() => { handleVideoPlayClick(); sendLog(locale, logData); }"
>
<span class="sr-only">videoPlay</span>
</button>

View File

@@ -1,11 +1,51 @@
import * as amplitude from '@amplitude/analytics-browser'
import type { AnalyticsDetailType } from '../types/AnalyticsType'
import type { AnalyticsDetailType, AnalyticsLogDataTracking } from '../types/AnalyticsType'
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
import type { IdentityInfo, ActionInfo, MarketingInfo } from '../types/Stove81Plug'
declare const svcLog: any
declare const twq: any
declare const ttq: any
/**
* 페이지 데이터와 템플릿 정보를 기반으로 분석용 로그 데이터를 생성하는 composable
* @param resourcesData 페이지 리소스 데이터
* @param pageVerTmplSeq 템플릿 시퀀스 번호
* @returns 분석용 로그 데이터 객체
*/
export const useAnalyticsLogData = (
resourcesData: PageDataResourceGroup,
pageVerTmplSeq: number
) => {
const store = usePageDataStore()
const pageData = store.pageData
if (!pageData) {
return ref({} as AnalyticsDetailType)
}
const pageDataTrack = (typeof resourcesData.tracking === 'object' ? resourcesData.tracking : {}) as AnalyticsLogDataTracking
console.log("🚀 ~ useAnalyticsLogData ~ pageDataTrack:", pageData)
const logData = ref({
actionType: pageDataTrack?.action_type,
// logSourceType:pageDataTrack.logSourceType,
// viewArea:pageDataTrack.viewArea,
// viewType:pageDataTrack.viewType,
clickArea:pageData.page_name_en,
clickSarea: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name_en,
clickItem: `${pageData.templates[pageVerTmplSeq].page_ver_tmpl_name}_${pageDataTrack?.click_item}`,
event: pageData.page_name,
eventCategory: `${pageData.page_name}_${pageDataTrack?.click_item}`,
template_code: pageData.templates[pageVerTmplSeq].template_code,
page_ver_tmpl_name: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name,
page_ver_tmpl_name_en: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name_en,
} as unknown as AnalyticsDetailType)
return logData
}
// target에 {XX1, XX2}와 같은 형태가 포함되어 있을 경우 options.clickItem으로부터 값 추출하여 세팅
const findValueFromOption = (target: string, { options = {} }: any) => {
if (target.includes('{') && target.includes('}')) {
@@ -30,12 +70,13 @@ const findValueFromOption = (target: string, { options = {} }: any) => {
* @param {object} options
*/
const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => {
console.log("🚀 ~ 1111 sendGA ~ analytics:", analytics)
try {
const { gtag } = useGtag()
const eventName = findValueFromOption(analytics.event || '', { options })
const eventCategory = findValueFromOption(analytics.eventCategory || '', { options })
const eventName = analytics.event || ''
const eventLocale = analytics.eventLocale || ''
const eventCategory = `${analytics.eventCategory}`
// GA 클릭 이벤트 명 뒤에 언어 값 추가 노출되도록 개발. ex) GNB_자유게시판_KO
const eventLabel = `${eventCategory}_${eventLocale}`
@@ -58,9 +99,11 @@ const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => {
* @param {object} options
*/
const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options = {} }: any) => {
const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore)
try {
const runtimeConfig = useRuntimeConfig()
const gameNo = runtimeConfig.public.stoveGameNo
const gameNo = gameData.value.game_code
const device = useDevice()
const deviceType = device.isDesktop ? 'pcweb' : 'mobileweb'
@@ -110,6 +153,7 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options = {} }: an
click_area: clickArea,
click_sarea: clickSarea,
click_item: {
click_item: analytics.clickItem,
game_no: gameNo,
lang_cd: eventLocale,
...options?.clickItem
@@ -152,20 +196,21 @@ const sendSA = (analytics: AnalyticsDetailType, { mcode = '', options = {} }: an
* @param {AnalyticsDetailType} analytics
*/
const sendLog = (locale: string, analytics: AnalyticsDetailType) => {
console.log("🚀 ~ sendLog ~ analytics:", analytics)
// 언어 코드 대문자 변환
analytics.eventLocale = locale.toUpperCase()
if (analytics) {
// GA 전송 : eventCategory 유무로 판별
if (analytics.eventCategory && analytics.eventCategory !== '') {
sendGA(analytics, { options: analytics.options })
}
sendGA(analytics, { options: analytics.options })
// SA 전송 : actionType, logSourceType 유무로 판별
if (
analytics.actionType &&
analytics.actionType !== '' &&
analytics.logSourceType &&
analytics.logSourceType !== ''
analytics.actionType &&
analytics.actionType !== ''
// analytics.logSourceType &&
// analytics.logSourceType !== ''
) {
sendSA(analytics, { mcode: analytics.mcode, options: analytics.options })
}
@@ -268,5 +313,5 @@ const sendMarketingScript = ({
}
export default () => {
return { sendGA, sendSA, sendLog, sendMarketingScript }
return { sendGA, sendSA, sendLog, sendMarketingScript, useAnalyticsLogData }
}

View File

@@ -1,86 +0,0 @@
import type { GameDataValue } from '#layers/types/api/gameData'
import { DEFAULT_COVERAGES } from '@/i18n.config'
/**
* gameData.lang_codes를 기반으로 동적으로 언어 제외 설정을 생성하는 컴포저블
*/
export const useDynamicI18nRoutes = () => {
const gameDataStore = useGameDataStore()
/**
* 현재 gameData의 lang_codes를 기반으로 허용된 언어 목록을 반환
*/
const getAllowedLangCodes = (): string[] => {
const gameData = gameDataStore.gameData
// Ensure we only return string values and filter out undefined/null
return Array.isArray(gameData?.lang_codes)
? gameData.lang_codes.filter(
(code): code is string => typeof code === 'string'
)
: []
}
/**
* 특정 언어가 허용되는지 확인
*/
const isLangAllowed = (langCode: string): boolean => {
const allowedLangCodes = getAllowedLangCodes()
return allowedLangCodes.length === 0 || allowedLangCodes.includes(langCode)
}
/**
* defineI18nRoute에서 사용할 수 있는 언어 제외 설정을 생성
* @param pagePath - 페이지 경로 (선택사항)
* @returns 언어 제외 설정 객체
*/
const getI18nRouteConfig = (pagePath?: string) => {
const allowedLangCodes = getAllowedLangCodes()
// 허용된 언어가 없으면 모든 언어 허용
if (allowedLangCodes.length === 0) {
return undefined
}
// 허용된 언어만 포함하는 설정 반환
return {
locales: allowedLangCodes,
}
}
/**
* 특정 언어를 제외하는 설정을 생성
* @param excludedLangCodes - 제외할 언어 코드 배열
* @returns 언어 제외 설정 객체
*/
const getExcludedLangConfig = (excludedLangCodes: string[]) => {
const allowedLangCodes = getAllowedLangCodes()
// 허용된 언어에서 제외할 언어를 제거
const finalAllowedCodes =
allowedLangCodes.length > 0
? allowedLangCodes.filter(code => !excludedLangCodes.includes(code))
: [
'en',
'ja',
'ko',
'zh-tw',
'fr',
'de',
'es',
'pt',
'th',
'zh-cn',
].filter(code => !excludedLangCodes.includes(code))
return {
locales: finalAllowedCodes,
}
}
return {
getAllowedLangCodes,
isLangAllowed,
getI18nRouteConfig,
getExcludedLangConfig,
}
}

View File

@@ -1,19 +0,0 @@
export const useGoogleAnalyticsId = () => {
const nuxtApp = useNuxtApp()
// SSR에서 context에서 가져오기
const getGAIdFromServer = () => {
return import.meta.server
? (nuxtApp.ssrContext?.event.context.googleAnalyticsId ?? null)
: null
}
// useState로 SSR/CSR 상태 동기화
const gaId = useState<string | null>('googleAnalyticsId', () => {
return getGAIdFromServer()
})
return {
gaId
}
}

View File

@@ -1,31 +0,0 @@
export default defineNuxtPlugin(() => {
const { $i18n } = useNuxtApp()
const gameDataStore = useGameDataStore()
// gameData가 로드되면 언어 제외 설정 적용
watchEffect(() => {
const gameData = gameDataStore.gameData
if (gameData && gameData.lang_codes && gameData.lang_codes.length > 0) {
const allowedLangCodes = gameData.lang_codes
// 현재 설정된 locales에서 허용된 언어만 필터링
const availableLocales = $i18n.locales.value.filter((locale: any) =>
allowedLangCodes.includes(locale.code)
)
// locales 업데이트
// 에러로 인해 주석처리
// $i18n.locales.value = availableLocales
// 현재 locale이 허용되지 않은 경우 기본 locale로 변경
if (!allowedLangCodes.includes($i18n.locale.value)) {
const defaultLang = allowedLangCodes.includes(
gameData.default_lang_code
)
? gameData.default_lang_code
: 'ko'
$i18n.locale.value = defaultLang as any
}
}
})
})

View File

@@ -1,30 +0,0 @@
export default defineNuxtPlugin(() => {
const { $i18n } = useNuxtApp()
// SSR에서 gameData를 가져와서 언어 제외 설정 적용
const gameDataFromServer = import.meta.server
? useNuxtApp().ssrContext?.event.context.gameData
: null
if (
gameDataFromServer &&
gameDataFromServer.lang_codes &&
gameDataFromServer.lang_codes.length > 0
) {
const allowedLangCodes = gameDataFromServer.lang_codes
// 현재 설정된 locales에서 허용된 언어만 필터링
const availableLocales = $i18n.locales.value.filter((locale: any) =>
allowedLangCodes.includes(locale.code)
)
// locales 업데이트
// 에러로 인해 주석처리
// $i18n.locales.value = availableLocales
// 현재 locale이 허용되지 않은 경우 기본 locale로 변경
if (!allowedLangCodes.includes($i18n.locale.value)) {
$i18n.locale.value = gameDataFromServer.default_lang_code || 'ko'
}
}
})

View File

@@ -1,9 +1,12 @@
<script setup lang="ts">
interface Props {
components: Record<string, any>
pageVerTmplSeq: number
}
const props = defineProps<Props>()
</script>
<template>
@@ -28,6 +31,7 @@ const props = defineProps<Props>()
<WidgetsVideoPlay
v-if="props.components.videoPlay"
:resources-data="props.components.videoPlay.groups[0]"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
/>
</div>
</section>

View File

@@ -52,8 +52,8 @@ interface AnalyticsDetailType extends AnalyticsType {
opinion?: string
devOption?: string
// 가공 -----
arrViewInfo?: String[]
mapClickItem?: Map<String, Object>
arrViewInfo?: string[]
mapClickItem?: Map<string, object>
eventLocale?: string
// 옵션 -----
mcode?: string
@@ -74,10 +74,19 @@ interface ResGetAnalytics {
}
}
interface AnalyticsLogDataTracking {
view_type: string
click_item: string
action_type: string
click_sarea: string
}
export type {
AnalyticsType,
AnalyticsDetailType,
AnalyticsMetaType,
AnalyticsLogDataTracking,
// [API] Req / Res -----
ReqGetAnalytics,
ResGetAnalytics

View File

@@ -75,6 +75,7 @@ export interface PageDataResourceGroup {
display?: {
text: string
}
tracking: string // JSON 문자열
}
// 컴포넌트 타입