feat. 일반 템플릿 로그 추가

This commit is contained in:
clkim
2025-12-11 13:38:25 +09:00
parent e77480962d
commit 1d5c9571b7
36 changed files with 750 additions and 735 deletions

View File

@@ -18,27 +18,25 @@
background-color: var(--pagination-active); background-color: var(--pagination-active);
} }
.splide-arrows { .splide__arrows {
@apply hidden md:block; @apply hidden md:block;
} }
.splide-arrow { .splide__arrows .splide-arrow.btn-circle {
@apply absolute top-1/2 w-[40px] h-[40px] bg-cover bg-center bg-no-repeat -translate-y-1/2 cursor-pointer z-[5] @apply absolute top-1/2 w-[40px] h-[40px] bg-cover bg-center bg-no-repeat -translate-y-1/2 cursor-pointer z-[5]
md:w-[48px] md:h-[48px] md:w-[48px] md:h-[48px];
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:rounded-full after:bg-white after:transition-opacity after:duration-300 after:ease-in-out after:opacity-0
hover:after:opacity-10;
} }
.arrow-prev { .splide__arrow--prev {
@apply bg-[image:var(--arrow-prev)]; @apply bg-[image:var(--arrow-prev)];
} }
.arrow-next { .splide__arrow--next {
@apply bg-[image:var(--arrow-next)]; @apply bg-[image:var(--arrow-next)];
} }
.type-full .arrow-prev { .type-full .splide__arrow--prev {
@apply left-10; @apply left-10;
} }
.type-full .arrow-next { .type-full .splide__arrow--next {
@apply right-10; @apply right-10;
} }

View File

@@ -1,11 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
const emit = defineEmits<{ import type { TrackingObject } from '#layers/types/api/common'
(e: 'click'): void
}>() interface Props {
tracking: TrackingObject
}
const props = defineProps<Props>()
const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics()
const handlePlayClick = () => {
sendLog(locale.value, useAnalyticsData(props.tracking))
}
</script> </script>
<template> <template>
<button class="btn-play" @click="emit('click')"> <button class="btn-play" @click="handlePlayClick">
<span class="sr-only">Play</span> <span class="sr-only">Play</span>
</button> </button>
</template> </template>

View File

@@ -3,6 +3,7 @@ interface Props {
title: string title: string
description: string | number description: string | number
imgPath: string | null imgPath: string | null
analyticsSarea: string
linkTarget?: '_blank' | '_self' linkTarget?: '_blank' | '_self'
url?: string url?: string
alt?: string alt?: string
@@ -10,8 +11,9 @@ interface Props {
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const { locale } = useI18n() const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics() const { sendLog, useAnalyticsData } = useAnalytics()
const isNoImage = computed(() => { const isNoImage = computed(() => {
return !props.imgPath || props.imgPath === null return !props.imgPath || props.imgPath === null
@@ -21,14 +23,12 @@ const isShowOverlay = computed(() => {
}) })
const handleLinkClick = (title: string) => { const handleLinkClick = (title: string) => {
const trackingData = { const analytics = {
tracking: {
click_item: title, click_item: title,
action_type: 'click', action_type: 'click',
click_sarea: '', click_sarea: props.analyticsSarea,
},
} }
sendLog(locale.value, useAnalyticsLogDataDirect(trackingData, 1)) sendLog(locale.value, useAnalyticsData(analytics))
} }
</script> </script>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import type { PageDataResourceGroups } from '#layers/types/api/pageData'
interface Props {
arrowsData?: PageDataResourceGroups
}
const props = defineProps<Props>()
const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics()
const handleArrowClick = (direction: 'prev' | 'next') => {
if (props.arrowsData) {
const arrowIndex = direction === 'prev' ? 0 : 1
sendLog(
locale.value,
useAnalyticsData(props.arrowsData[arrowIndex]?.tracking)
)
}
}
</script>
<template>
<div class="splide__arrows">
<AtomsButtonCircle
sr-only="Previous"
class="splide-arrow splide__arrow--prev"
@click="handleArrowClick('prev')"
/>
<AtomsButtonCircle
sr-only="Next"
class="splide-arrow splide__arrow--next"
@click="handleArrowClick('next')"
/>
</div>
</template>

View File

@@ -9,15 +9,16 @@ import type {
} from '#layers/types/api/gameData' } from '#layers/types/api/gameData'
const MORE_WIDTH = 72 const MORE_WIDTH = 72
const START_WIDTH_MARGIN = 40 const START_MARGIN = 40
const route = useRoute() const route = useRoute()
const { tm } = useI18n() const { locale, tm } = useI18n()
const { width } = useWindowSize() const { width } = useWindowSize()
const gameDataStore = useGameDataStore() const gameDataStore = useGameDataStore()
const pageDataStore = usePageDataStore() const pageDataStore = usePageDataStore()
const scrollStore = useScrollStore() const scrollStore = useScrollStore()
const breakpoints = useResponsiveBreakpoints() const breakpoints = useResponsiveBreakpoints()
const { sendLog, useAnalyticsData } = useAnalytics()
const { gameData } = storeToRefs(gameDataStore) const { gameData } = storeToRefs(gameDataStore)
const { pageLayoutType } = storeToRefs(pageDataStore) const { pageLayoutType } = storeToRefs(pageDataStore)
@@ -95,7 +96,7 @@ const calculateOverflow = () => {
} }
const screenWidth = width.value const screenWidth = width.value
const totalNavWidth = navWidth.value + startWidth.value + START_WIDTH_MARGIN const totalNavWidth = navWidth.value + startWidth.value + START_MARGIN
// 해상도가 navWidth + startWidth보다 작은 경우 // 해상도가 navWidth + startWidth보다 작은 경우
if (screenWidth < totalNavWidth) { if (screenWidth < totalNavWidth) {
@@ -120,18 +121,6 @@ const calculateOverflow = () => {
// 100ms마다 최대 1회 실행 // 100ms마다 최대 1회 실행
const throttledCalculateOverflow = useThrottleFn(calculateOverflow, 100) const throttledCalculateOverflow = useThrottleFn(calculateOverflow, 100)
const handleMenuOpen = () => {
isMenuOpen.value = true
scrollStore.controlScrollLock(true)
}
const handleMenuClose = (isPassing: boolean = false) => {
if (isPassing) return
isMenuOpen.value = false
scrollStore.controlScrollLock(false)
}
const isNotClickable = (gnbItem: GameDataMenu) => { const isNotClickable = (gnbItem: GameDataMenu) => {
return gnbItem.click_action_type === 0 return gnbItem.click_action_type === 0
} }
@@ -140,6 +129,32 @@ const has2depthButton = (gnbItem: GameDataMenu) => {
return gnbItem.children && Object.keys(gnbItem.children).length > 0 return gnbItem.children && Object.keys(gnbItem.children).length > 0
} }
const handleMenuOpen = () => {
isMenuOpen.value = true
scrollStore.controlScrollLock(true)
}
const handleMenuClose = () => {
isMenuOpen.value = false
scrollStore.controlScrollLock(false)
}
const handleSendLog = (item: string) => {
const analytics = {
action_type: 'click',
click_item: item,
click_sarea: 'GNB',
}
sendLog(locale.value, useAnalyticsData(analytics))
}
const handleGnbItemClick = (gnbItem: GameDataMenu) => {
if (isNotClickable(gnbItem)) return
handleMenuClose()
sendLog(locale.value, useAnalyticsData(gnbItem.tracking_json))
}
onMounted(() => { onMounted(() => {
overflowCount.value = 0 overflowCount.value = 0
isMounted.value = true isMounted.value = true
@@ -170,7 +185,11 @@ onMounted(() => {
v-if="gnbData" v-if="gnbData"
:class="['game-wrap', { 'is-fixed': isPassedStoveGnb }]" :class="['game-wrap', { 'is-fixed': isPassedStoveGnb }]"
> >
<AtomsLocaleLink to="/home" class="mx-auto md:hidden"> <AtomsLocaleLink
to="/home"
class="mx-auto md:hidden"
@click="handleSendLog('BI')"
>
<img <img
:src="formatPathHost(gnbData?.bi_path)" :src="formatPathHost(gnbData?.bi_path)"
:alt="gameData?.game_name" :alt="gameData?.game_name"
@@ -183,11 +202,14 @@ onMounted(() => {
</button> </button>
<div <div
:class="['nav-wrap', { 'is-open': isMenuOpen }]" :class="['nav-wrap', { 'is-open': isMenuOpen }]"
@click="handleMenuClose()" @click="handleMenuClose"
> >
<div ref="navAreaRef" class="nav-area" @click.stop> <div ref="navAreaRef" class="nav-area" @click.stop>
<div class="nav-logo"> <div class="nav-logo">
<AtomsLocaleLink to="/home" @click="handleMenuClose"> <AtomsLocaleLink
to="/home"
@click="[handleMenuClose(), handleSendLog('BI')]"
>
<img <img
:src="formatPathHost(gnbData?.bi_path)" :src="formatPathHost(gnbData?.bi_path)"
:alt="gameData?.game_name" :alt="gameData?.game_name"
@@ -219,7 +241,7 @@ onMounted(() => {
'router-link-active': hasActiveChild(gnbItem.children), 'router-link-active': hasActiveChild(gnbItem.children),
}, },
]" ]"
@click="handleMenuClose(isNotClickable(gnbItem))" @click="handleGnbItemClick(gnbItem)"
> >
<span>{{ gnbItem.menu_name }}</span> <span>{{ gnbItem.menu_name }}</span>
<AtomsIconsWebLinkLine <AtomsIconsWebLinkLine
@@ -248,7 +270,7 @@ onMounted(() => {
:to="child.url_path" :to="child.url_path"
:target="child.link_target" :target="child.link_target"
class="item-link" class="item-link"
@click="handleMenuClose(isNotClickable(child))" @click="handleGnbItemClick(child)"
> >
<span>{{ child.menu_name }}</span> <span>{{ child.menu_name }}</span>
<AtomsIconsWebLinkLine <AtomsIconsWebLinkLine
@@ -293,7 +315,7 @@ onMounted(() => {
), ),
}, },
]" ]"
@click="handleMenuClose(isNotClickable(gnbItem))" @click="handleGnbItemClick(gnbItem)"
> >
<span>{{ gnbItem.menu_name }}</span> <span>{{ gnbItem.menu_name }}</span>
</component> </component>
@@ -340,7 +362,7 @@ onMounted(() => {
'nav-1depth', 'nav-1depth',
{ 'router-link-active': pageLayoutType === 'promotion' }, { 'router-link-active': pageLayoutType === 'promotion' },
]" ]"
@click="handleMenuClose" @click="[handleMenuClose(), handleSendLog('이벤트')]"
> >
<span <span
class="flex items-center gap-1 flex-1 text-gradient-pink" class="flex items-center gap-1 flex-1 text-gradient-pink"
@@ -367,6 +389,9 @@ onMounted(() => {
:text-color=" :text-color="
getColorCodeFromData(start1depthData?.btn_info, 'txt') getColorCodeFromData(start1depthData?.btn_info, 'txt')
" "
@click="
sendLog(locale, useAnalyticsData(start1depthData.tracking))
"
> >
{{ start1depthData?.btn_info?.txt_btn_name }} {{ start1depthData?.btn_info?.txt_btn_name }}
</BlocksButtonLauncher> </BlocksButtonLauncher>
@@ -376,7 +401,13 @@ onMounted(() => {
> >
<ul> <ul>
<li v-for="(item, key) in start2depthData" :key="key"> <li v-for="(item, key) in start2depthData" :key="key">
<BlocksButtonLauncher type="custom" :platform="key"> <BlocksButtonLauncher
type="custom"
:platform="key"
@click="
sendLog(locale, useAnalyticsData(item.tracking))
"
>
{{ item.btn_info?.txt_btn_name }} {{ item.btn_info?.txt_btn_name }}
</BlocksButtonLauncher> </BlocksButtonLauncher>
</li> </li>
@@ -385,7 +416,7 @@ onMounted(() => {
</template> </template>
</div> </div>
</ClientOnly> </ClientOnly>
<button class="btn-close" @click="handleMenuClose()"> <button class="btn-close" @click="handleMenuClose">
<AtomsIconsCloseLine <AtomsIconsCloseLine
size="24" size="24"
color="var(--foreground-reversal)" color="var(--foreground-reversal)"

View File

@@ -75,7 +75,7 @@ watch(isPAssApiLoading, newVal => {
onMounted(() => { onMounted(() => {
const { sendLog } = useAnalytics() const { sendLog } = useAnalytics()
sendLog(locale.value, useAnalyticsLogDataDirect('view', 1)) sendLog(locale.value, useAnalyticsData('view'))
if (!hasApiCallStarted.value) { if (!hasApiCallStarted.value) {
loadingStore.stopFullLoading() loadingStore.stopFullLoading()
@@ -94,6 +94,7 @@ onMounted(() => {
:id="template.page_ver_tmpl_name_en" :id="template.page_ver_tmpl_name_en"
:components="template.page_ver_tmpl_json" :components="template.page_ver_tmpl_json"
:page-ver-tmpl-seq="template.page_ver_tmpl_seq" :page-ver-tmpl-seq="template.page_ver_tmpl_seq"
:page-ver-tmpl-name-en="template.page_ver_tmpl_name_en"
/> />
</template> </template>
</div> </div>

View File

@@ -7,7 +7,6 @@ import type { ButtonType } from '#layers/types/components/button'
interface Props { interface Props {
resourcesData: PageDataResourceGroup[] resourcesData: PageDataResourceGroup[]
pageVerTmplSeq: number
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -16,7 +15,7 @@ const { locale } = useI18n()
const modalStore = useModalStore() const modalStore = useModalStore()
const scrollStore = useScrollStore() const scrollStore = useScrollStore()
const breakpoints = useResponsiveBreakpoints() const breakpoints = useResponsiveBreakpoints()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics() const { sendLog, useAnalyticsData } = useAnalytics()
const { tm } = useI18n() const { tm } = useI18n()
const device = useDevice() const device = useDevice()
@@ -92,8 +91,7 @@ const downloadFile = async (url: string = '', osType: number = 0) => {
} }
const handleButtonClick = (button: PageDataResourceGroup) => { const handleButtonClick = (button: PageDataResourceGroup) => {
// 로그 sendLog(locale.value, useAnalyticsData(button.tracking))
sendLog(locale.value, useAnalyticsLogDataDirect(button, props.pageVerTmplSeq))
const btnDetail = button.btn_info?.detail const btnDetail = button.btn_info?.detail

View File

@@ -3,27 +3,22 @@ import type { PageDataResourceGroup } from '#layers/types/api/pageData'
const props = defineProps<{ const props = defineProps<{
resourcesData: PageDataResourceGroup resourcesData: PageDataResourceGroup
pageVerTmplSeq: number
}>() }>()
const modalStore = useModalStore() const modalStore = useModalStore()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
// 비디오 플레이 버튼 클릭 핸들러 // 비디오 플레이 버튼 클릭 핸들러
const handleVideoPlayClick = () => { const handleVideoPlayClick = () => {
const youtubeUrl = props.resourcesData?.display?.text ?? '' const youtubeUrl = props.resourcesData?.display?.text ?? ''
if (youtubeUrl) { if (youtubeUrl) {
modalStore.handleOpenYoutube({ youtubeUrl }) modalStore.handleOpenYoutube({ youtubeUrl })
sendLog(
locale.value,
useAnalyticsLogDataDirect(props.resourcesData, props.pageVerTmplSeq)
)
} }
} }
</script> </script>
<template> <template>
<AtomsButtonPlay @click="handleVideoPlayClick" /> <AtomsButtonPlay
:tracking="props.resourcesData.tracking"
@click="handleVideoPlayClick"
/>
</template> </template>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { Splide } from '@splidejs/vue-splide' import { Splide, SplideTrack } from '@splidejs/vue-splide'
import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide' import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide'
import type { SlideItemSize } from '#layers/types/components/slide' import type { SlideItemSize } from '#layers/types/components/slide'
import { useSplideArrow } from '#layers/composables/useSplideArrow' import type { PageDataResourceGroups } from '#layers/types/api/pageData'
interface Props { interface Props {
slideItemSize: SlideItemSize slideItemSize: SlideItemSize
@@ -10,6 +10,7 @@ interface Props {
autoplay?: boolean autoplay?: boolean
interval?: number interval?: number
arrows?: boolean arrows?: boolean
arrowsData?: PageDataResourceGroups
pagination?: boolean pagination?: boolean
class?: string class?: string
} }
@@ -21,13 +22,9 @@ const props = withDefaults(defineProps<Props>(), {
pagination: true, pagination: true,
}) })
const emit = defineEmits(['mounted', 'move', 'arrowClick']) const emit = defineEmits(['mounted', 'move'])
const splideIndex = defineModel<number>('index', { required: false }) const splideIndex = defineModel<number>('index', { required: false })
// Splide composable
const { addArrowClickListeners } = useSplideArrow()
const isMultipleItems = computed(() => (props.slideItemLength ?? 0) > 1) const isMultipleItems = computed(() => (props.slideItemLength ?? 0) > 1)
const options = computed((): ResponsiveOptions => { const options = computed((): ResponsiveOptions => {
@@ -45,10 +42,6 @@ const options = computed((): ResponsiveOptions => {
interval: props.interval, interval: props.interval,
flickPower: 300, flickPower: 300,
classes: { classes: {
arrows: 'splide-arrows',
arrow: 'splide-arrow',
prev: 'arrow-prev',
next: 'arrow-next',
pagination: 'splide-pagination-bullets', pagination: 'splide-pagination-bullets',
page: 'splide-pagination-bullet', page: 'splide-pagination-bullet',
}, },
@@ -95,13 +88,6 @@ const handleSplideMounted = (splide: SplideType) => {
if (splideIndex.value !== undefined) { if (splideIndex.value !== undefined) {
splideIndex.value = splide.index splideIndex.value = splide.index
} }
//
nextTick(() => {
addArrowClickListeners(splide, (direction, targetIndex) => {
emit('arrowClick', direction, targetIndex)
})
})
} }
const handleMove = ( const handleMove = (
@@ -122,10 +108,14 @@ const handleMove = (
<div :class="`center-focus ${props.class || ''}`" :style="style"> <div :class="`center-focus ${props.class || ''}`" :style="style">
<Splide <Splide
:options="options" :options="options"
:has-track="false"
@splide:mounted="handleSplideMounted" @splide:mounted="handleSplideMounted"
@splide:move="handleMove" @splide:move="handleMove"
> >
<SplideTrack>
<slot /> <slot />
</SplideTrack>
<BlocksSlideArrows v-if="props.arrows" :arrows-data="props.arrowsData" />
</Splide> </Splide>
</div> </div>
</template> </template>
@@ -192,10 +182,10 @@ const handleMove = (
width: var(--banner-width-pc-active); width: var(--banner-width-pc-active);
height: var(--banner-height-pc-active); height: var(--banner-height-pc-active);
} }
.center-focus:deep(.arrow-prev) { .center-focus:deep(.splide__arrow--prev) {
left: 28px; left: 28px;
} }
.center-focus:deep(.arrow-next) { .center-focus:deep(.splide__arrow--next) {
right: 28px; right: 28px;
} }
} }

View File

@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { Splide } from '@splidejs/vue-splide' import { Splide, SplideTrack } from '@splidejs/vue-splide'
import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide' import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide'
import type { SlideItemSize } from '#layers/types/components/slide' import type { SlideItemSize } from '#layers/types/components/slide'
import type { PageDataResourceGroups } from '#layers/types/api/pageData'
interface Props { interface Props {
slideItemSize: SlideItemSize slideItemSize: SlideItemSize
@@ -9,6 +10,7 @@ interface Props {
autoplay?: boolean autoplay?: boolean
interval?: number interval?: number
arrows?: boolean arrows?: boolean
arrowsData?: PageDataResourceGroups
pagination?: boolean pagination?: boolean
class?: string class?: string
} }
@@ -20,13 +22,9 @@ const props = withDefaults(defineProps<Props>(), {
pagination: true, pagination: true,
}) })
const emit = defineEmits(['mounted', 'move', 'arrowClick']) const emit = defineEmits(['mounted', 'move'])
const splideIndex = defineModel<number>('index', { required: false }) const splideIndex = defineModel<number>('index', { required: false })
// Splide composable
const { addArrowClickListeners } = useSplideArrow()
const isMultipleItems = computed(() => (props.slideItemLength ?? 0) > 1) const isMultipleItems = computed(() => (props.slideItemLength ?? 0) > 1)
const options = computed((): ResponsiveOptions => { const options = computed((): ResponsiveOptions => {
@@ -44,10 +42,6 @@ const options = computed((): ResponsiveOptions => {
interval: props.interval, interval: props.interval,
flickPower: 300, flickPower: 300,
classes: { classes: {
arrows: 'splide-arrows',
arrow: 'splide-arrow',
prev: 'arrow-prev',
next: 'arrow-next',
pagination: 'splide-pagination-bullets', pagination: 'splide-pagination-bullets',
page: 'splide-pagination-bullet', page: 'splide-pagination-bullet',
}, },
@@ -97,13 +91,6 @@ const handleSplideMounted = (splide: SplideType) => {
if (splideIndex.value !== undefined) { if (splideIndex.value !== undefined) {
splideIndex.value = splide.index splideIndex.value = splide.index
} }
//
nextTick(() => {
addArrowClickListeners(splide, (direction, targetIndex) => {
emit('arrowClick', direction, targetIndex)
})
})
} }
const handleMove = ( const handleMove = (
@@ -124,10 +111,14 @@ const handleMove = (
<div :class="`center-highlight ${props.class || ''}`" :style="style"> <div :class="`center-highlight ${props.class || ''}`" :style="style">
<Splide <Splide
:options="options" :options="options"
:has-track="false"
@splide:mounted="handleSplideMounted" @splide:mounted="handleSplideMounted"
@splide:move="handleMove" @splide:move="handleMove"
> >
<SplideTrack>
<slot /> <slot />
</SplideTrack>
<BlocksSlideArrows v-if="props.arrows" :arrows-data="props.arrowsData" />
</Splide> </Splide>
</div> </div>
</template> </template>
@@ -179,10 +170,10 @@ const handleMove = (
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.center-highlight:deep(.arrow-prev) { .center-highlight:deep(.splide__arrow--prev) {
margin-left: calc(-1 * var(--banner-arrow-pc)); margin-left: calc(-1 * var(--banner-arrow-pc));
} }
.center-highlight:deep(.arrow-next) { .center-highlight:deep(.splide__arrow--next) {
margin-left: var(--banner-arrow-pc); margin-left: var(--banner-arrow-pc);
} }
} }

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Splide } from '@splidejs/vue-splide' import { Splide, SplideTrack } from '@splidejs/vue-splide'
import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide' import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide'
import type { PageDataResourceGroups } from '#layers/types/api/pageData' import type { PageDataResourceGroups } from '#layers/types/api/pageData'
@@ -12,6 +12,7 @@ interface Props {
perPage?: number perPage?: number
drag?: boolean drag?: boolean
arrows?: boolean arrows?: boolean
arrowsData?: PageDataResourceGroups
pagination?: boolean pagination?: boolean
paginationData?: PageDataResourceGroups paginationData?: PageDataResourceGroups
destroy?: boolean destroy?: boolean
@@ -29,13 +30,10 @@ const props = withDefaults(defineProps<Props>(), {
destroy: false, destroy: false,
}) })
const emit = defineEmits(['mounted', 'move', 'arrowClick']) const emit = defineEmits(['mounted', 'move'])
const splideIndex = defineModel<number>('index', { required: false }) const splideIndex = defineModel<number>('index', { required: false })
// Splide composable
const { addArrowClickListeners } = useSplideArrow()
const slideType = computed(() => { const slideType = computed(() => {
if (props.type) return props.type if (props.type) return props.type
@@ -60,10 +58,6 @@ const options = computed((): ResponsiveOptions => {
destroy: props.destroy, destroy: props.destroy,
flickPower: 300, flickPower: 300,
classes: { classes: {
arrows: 'splide-arrows',
arrow: 'splide-arrow',
prev: 'arrow-prev',
next: 'arrow-next',
pagination: 'splide-pagination-bullets', pagination: 'splide-pagination-bullets',
page: 'splide-pagination-bullet', page: 'splide-pagination-bullet',
}, },
@@ -77,13 +71,6 @@ const handleSplideMounted = (splide: SplideType) => {
if (splideIndex.value !== undefined) { if (splideIndex.value !== undefined) {
splideIndex.value = splide.index splideIndex.value = splide.index
} }
//
nextTick(() => {
addArrowClickListeners(splide, (direction, targetIndex) => {
emit('arrowClick', direction, targetIndex)
})
})
} }
const handleMove = ( const handleMove = (
@@ -103,10 +90,14 @@ const handleMove = (
<template> <template>
<Splide <Splide
:options="options" :options="options"
:has-track="false"
:style="getPaginationClass(props?.paginationData)" :style="getPaginationClass(props?.paginationData)"
@splide:mounted="handleSplideMounted" @splide:mounted="handleSplideMounted"
@splide:move="handleMove" @splide:move="handleMove"
> >
<SplideTrack>
<slot /> <slot />
</SplideTrack>
<BlocksSlideArrows v-if="props.arrows" :arrows-data="props.arrowsData" />
</Splide> </Splide>
</template> </template>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Splide } from '@splidejs/vue-splide' import { Splide, SplideTrack } from '@splidejs/vue-splide'
import { useSplideArrow } from '#layers/composables/useSplideArrow'
import { getPaginationClass } from '#layers/utils/styleUtil' import { getPaginationClass } from '#layers/utils/styleUtil'
import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide' import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide'
import type { PageDataResourceGroups } from '#layers/types/api/pageData' import type { PageDataResourceGroups } from '#layers/types/api/pageData'
@@ -10,6 +9,7 @@ interface Props {
autoplay?: boolean autoplay?: boolean
interval?: number interval?: number
arrows?: boolean arrows?: boolean
arrowsData?: PageDataResourceGroups
pagination?: boolean pagination?: boolean
paginationData?: PageDataResourceGroups paginationData?: PageDataResourceGroups
} }
@@ -22,13 +22,11 @@ const props = withDefaults(defineProps<Props>(), {
pagination: true, pagination: true,
}) })
const emit = defineEmits(['mounted', 'move', 'arrowClick']) const emit = defineEmits(['mounted', 'move'])
const splideIndex = defineModel<number>('index', { required: false }) const splideIndex = defineModel<number>('index', { required: false })
const splideRef = ref() const splideRef = ref()
// Splide composable
const { addArrowClickListeners } = useSplideArrow()
const options = computed((): ResponsiveOptions => { const options = computed((): ResponsiveOptions => {
return { return {
@@ -48,10 +46,6 @@ const options = computed((): ResponsiveOptions => {
pagination: props.pagination, pagination: props.pagination,
flickPower: 300, flickPower: 300,
classes: { classes: {
arrows: 'splide-arrows type-full',
arrow: 'splide-arrow',
prev: 'arrow-prev',
next: 'arrow-next',
pagination: 'splide-pagination-bullets type-full', pagination: 'splide-pagination-bullets type-full',
page: 'splide-pagination-bullet', page: 'splide-pagination-bullet',
}, },
@@ -64,13 +58,6 @@ const handleSplideMounted = (splide: SplideType) => {
if (splideIndex.value !== undefined) { if (splideIndex.value !== undefined) {
splideIndex.value = splide.index splideIndex.value = splide.index
} }
//
nextTick(() => {
addArrowClickListeners(splide, (direction, targetIndex) => {
emit('arrowClick', direction, targetIndex)
})
})
} }
const handleMove = ( const handleMove = (
@@ -95,12 +82,16 @@ defineExpose({
<Splide <Splide
ref="splideRef" ref="splideRef"
:options="options" :options="options"
:has-track="false"
class="h-full" class="h-full"
:style="getPaginationClass(props.paginationData)" :style="getPaginationClass(props.paginationData)"
@splide:mounted="handleSplideMounted" @splide:mounted="handleSplideMounted"
@splide:move="handleMove" @splide:move="handleMove"
> >
<SplideTrack>
<slot /> <slot />
</SplideTrack>
<BlocksSlideArrows v-if="props.arrows" :arrows-data="props.arrowsData" />
</Splide> </Splide>
</template> </template>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Splide, SplideSlide } from '@splidejs/vue-splide' import { Splide, SplideTrack, SplideSlide } from '@splidejs/vue-splide'
import { useSplideArrow } from '#layers/composables/useSplideArrow'
import type { Splide as SplideType, Options } from '@splidejs/splide' import type { Splide as SplideType, Options } from '@splidejs/splide'
import type { import type {
PageDataResourceGroups, PageDataResourceGroups,
@@ -8,10 +7,12 @@ import type {
} from '#layers/types/api/pageData' } from '#layers/types/api/pageData'
interface Props { interface Props {
drag?: boolean
thumbnailData: PageDataResourceGroup[] thumbnailData: PageDataResourceGroup[]
paginationData?: PageDataResourceGroups paginationData: PageDataResourceGroups
analyticsSarea: string
drag?: boolean
arrows?: boolean arrows?: boolean
arrowsData?: PageDataResourceGroups
variant?: 'default' | 'media' variant?: 'default' | 'media'
} }
@@ -21,13 +22,13 @@ const props = withDefaults(defineProps<Props>(), {
variant: 'default', variant: 'default',
}) })
const emit = defineEmits(['mounted', 'move', 'arrowClick']) const emit = defineEmits(['mounted', 'move'])
const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics()
const splideIndex = defineModel<number>('index', { required: false }) const splideIndex = defineModel<number>('index', { required: false })
// Splide composable
const { addArrowClickListeners } = useSplideArrow()
let mainInst: SplideType | null = null let mainInst: SplideType | null = null
let thumbsInst: SplideType | null = null let thumbsInst: SplideType | null = null
let removeArrowListeners: (() => void) | null = null let removeArrowListeners: (() => void) | null = null
@@ -62,12 +63,6 @@ const thumbOptions = computed<Options>(() => ({
isNavigation: true, isNavigation: true,
updateOnMove: true, updateOnMove: true,
flickPower: 300, flickPower: 300,
classes: {
arrows: 'splide-arrows',
arrow: 'splide-arrow',
prev: 'arrow-prev',
next: 'arrow-next',
},
breakpoints: { breakpoints: {
[BREAKPOINTS.md - 1]: { [BREAKPOINTS.md - 1]: {
padding: { padding: {
@@ -113,21 +108,22 @@ const handleMove = (
} }
} }
const handleThumbnailClick = (title: string) => {
const paginationAnalytics = {
action_type: 'click',
click_item: title,
click_sarea: props.analyticsSarea,
}
sendLog(locale.value, useAnalyticsData(paginationAnalytics))
}
onMounted(() => { onMounted(() => {
mainInst = mainRef.value?.splide ?? null mainInst = mainRef.value?.splide ?? null
thumbsInst = thumbsRef.value?.splide ?? null thumbsInst = thumbsRef.value?.splide ?? null
if (mainInst && thumbsInst) { if (mainInst && thumbsInst) {
mainInst.sync(thumbsInst) mainInst.sync(thumbsInst)
//
nextTick(() => {
removeArrowListeners = addArrowClickListeners(
thumbsInst,
(direction, targetIndex) => {
emit('arrowClick', direction, targetIndex)
}
)
})
} }
}) })
@@ -163,13 +159,17 @@ onBeforeUnmount(() => {
v-if="props.thumbnailData.length > 1" v-if="props.thumbnailData.length > 1"
ref="thumbsRef" ref="thumbsRef"
:options="thumbOptions" :options="thumbOptions"
:arrows-data="props.arrowsData"
:has-track="false"
class="thumbnail-splide" class="thumbnail-splide"
:style="getPaginationClass(paginationData)" :style="getPaginationClass(paginationData)"
> >
<SplideTrack>
<SplideSlide <SplideSlide
v-for="(item, index) in props.thumbnailData" v-for="(item, index) in props.thumbnailData"
:key="index" :key="index"
class="thumbnail-slide" class="thumbnail-slide"
@click="handleThumbnailClick(item?.group_label ?? `${index + 1}`)"
> >
<AtomsImg <AtomsImg
:src="getThumbnailSrc(item)" :src="getThumbnailSrc(item)"
@@ -177,6 +177,8 @@ onBeforeUnmount(() => {
class="slide-image" class="slide-image"
/> />
</SplideSlide> </SplideSlide>
</SplideTrack>
<BlocksSlideArrows v-if="props.arrows" :arrows-data="props.arrowsData" />
</Splide> </Splide>
</div> </div>
</template> </template>
@@ -226,10 +228,10 @@ onBeforeUnmount(() => {
.thumbnail-carousel.thumbnail-default .thumbnail-splide { .thumbnail-carousel.thumbnail-default .thumbnail-splide {
@apply absolute bottom-[32px] left-1/2 -translate-x-1/2 max-w-[100%] md:bottom-[48px] md:max-w-[896px] md:px-[64px]; @apply absolute bottom-[32px] left-1/2 -translate-x-1/2 max-w-[100%] md:bottom-[48px] md:max-w-[896px] md:px-[64px];
} }
.thumbnail-carousel.thumbnail-default:deep(.arrow-prev) { .thumbnail-carousel.thumbnail-default:deep(.splide__arrow--prev) {
@apply left-0; @apply left-0;
} }
.thumbnail-carousel.thumbnail-default:deep(.arrow-next) { .thumbnail-carousel.thumbnail-default:deep(.splide__arrow--next) {
@apply right-0; @apply right-0;
} }
.thumbnail-carousel.thumbnail-default .thumbnail-slide { .thumbnail-carousel.thumbnail-default .thumbnail-slide {
@@ -253,10 +255,10 @@ onBeforeUnmount(() => {
sm:max-w-[calc(100%+80px)] sm:mx-[-40px] sm:max-w-[calc(100%+80px)] sm:mx-[-40px]
md:max-w-[100%] md:mt-[28px] md:mx-auto md:px-[64px]; md:max-w-[100%] md:mt-[28px] md:mx-auto md:px-[64px];
} }
.thumbnail-carousel.thumbnail-media:deep(.arrow-prev) { .thumbnail-carousel.thumbnail-media:deep(.splide__arrow--prev) {
@apply left-[0]; @apply left-[0];
} }
.thumbnail-carousel.thumbnail-media:deep(.arrow-next) { .thumbnail-carousel.thumbnail-media:deep(.splide__arrow--next) {
@apply right-[0]; @apply right-[0];
} }
.thumbnail-carousel.thumbnail-media .thumbnail-slide { .thumbnail-carousel.thumbnail-media .thumbnail-slide {

View File

@@ -1,5 +1,5 @@
import * as amplitude from '@amplitude/analytics-browser' import * as amplitude from '@amplitude/analytics-browser'
import type { PageDataResourceGroup } from '#layers/types/api/pageData' import type { TrackingObject } from '#layers/types/api/common'
import type { AnalyticsDetailType } from '#layers/types/AnalyticsType' import type { AnalyticsDetailType } from '#layers/types/AnalyticsType'
import type { import type {
IdentityInfo, IdentityInfo,
@@ -11,63 +11,9 @@ declare const svcLog: any
declare const twq: any declare const twq: any
declare const ttq: any declare const ttq: any
/** // ============================================================================
* 페이지 데이터와 템플릿 정보를 기반으로 분석용 로그 데이터를 생성하는 composable (직접 객체 반환) // 유틸 함수
* @param resourcesData 페이지 리소스 데이터 // ============================================================================
* @param pageVerTmplSeq 템플릿 시퀀스 번호
* @returns 분석용 로그 데이터 객체 (ref 없이)
*/
export const useAnalyticsLogDataDirect = (
resourcesData: PageDataResourceGroup | string,
pageVerTmplSeq: number | null
) => {
const store = usePageDataStore()
const pageData = store.pageData
if (!pageData) {
return {} as AnalyticsDetailType
}
// resourcesData가 문자열인 경우 (예: 'view')
if (typeof resourcesData === 'string') {
const logData = {
actionType: resourcesData,
event: pageData.page_name,
eventCategory: pageData.page_name,
} as unknown as AnalyticsDetailType
if (resourcesData === 'view') {
logData.viewArea = pageData.page_name_en
logData.viewType = 'pageView'
}
return logData
}
// resourcesData가 객체인 경우 (기존 로직)
if (!resourcesData || !resourcesData.tracking) {
return {} as AnalyticsDetailType
}
const pageDataTrack = resourcesData.tracking
const logData = {
actionType: pageDataTrack?.action_type,
event: pageData.page_name,
eventCategory: `${pageData.page_name}_${pageDataTrack?.click_sarea}_${pageDataTrack?.click_item}`,
} as unknown as AnalyticsDetailType
if (pageDataTrack.action_type === 'click') {
logData.clickArea = pageData.page_name_en
logData.clickSarea = pageDataTrack.click_sarea
logData.clickItem = pageDataTrack.click_item
} else if (pageDataTrack.action_type === 'view') {
logData.viewArea = pageData.page_name_en
logData.viewType = 'view_frame'
}
return logData
}
// target에 {XX1, XX2}와 같은 형태가 포함되어 있을 경우 options.clickItem으로부터 값 추출하여 세팅 // target에 {XX1, XX2}와 같은 형태가 포함되어 있을 경우 options.clickItem으로부터 값 추출하여 세팅
const findValueFromOption = (target: string, { options = {} }: any) => { const findValueFromOption = (target: string, { options = {} }: any) => {
@@ -92,69 +38,112 @@ const findValueFromOption = (target: string, { options = {} }: any) => {
return target return target
} }
/** 브라우저 환경인지 체크 */
const isClient = () => typeof window !== 'undefined' && import.meta.client
/** Analytics 객체가 비어있는지 체크 */
const isEmptyAnalytics = (analytics?: Partial<AnalyticsDetailType>) =>
!analytics || Object.keys(analytics).length === 0
// ============================================================================
// Analytics 데이터 생성
// ============================================================================
/** /**
* Google Analytics 전송 * 로그 데이터를 생성하는 composable (직접 객체 반환)
* * @param analytics 트래킹 데이터
* @param {AnalyticsDetailType} analytics * @param type 'page' | 'template'
* @param {object} options * @returns 분석용 로그 데이터 객체 (ref 없이)
*/ */
const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => { export const useAnalyticsData = (
if (!import.meta.client) return analytics: string | TrackingObject,
type: 'page' | 'template' = 'template'
): Partial<AnalyticsDetailType> => {
const pageDataStore = usePageDataStore()
const { pageName, pageNameEn } = storeToRefs(pageDataStore)
try { if (!analytics || !pageName.value || !pageNameEn.value) {
const { gtag } = useGtag() return {}
}
const eventName = analytics.event || '' const baseEvent = pageName.value
const eventLocale = analytics.eventLocale || '' const baseViewArea = pageNameEn.value
const eventCategory = `${analytics.eventCategory}` let logData = {} as AnalyticsDetailType
// GA 클릭 이벤트 명 뒤에 언어 값 추가 노출되도록 개발. ex) GNB_자유게시판_KO // 문자열 'view'만 들어오는 경우 (페이지뷰)
const eventLabel = `${eventCategory}_${eventLocale}` if (analytics === 'view') {
logData = {
actionType: analytics,
event: baseEvent,
eventCategory: baseEvent,
viewArea: baseViewArea,
viewType: 'pageView',
} as AnalyticsDetailType
gtag('set', 'cookie_domain', `${window?.location?.hostname || ''}`) // env 값으로 설정 시 쿠키 생성 안 돼서 window.location.hostname으로 설정 return logData
gtag('set', 'cookie_expires', '0') // 0으로 설정 시 쿠키가 Session 기반 쿠키로 전환 }
gtag('event', `${eventName}`, {
event_category: eventLabel, // 객체인 경우
}) if (typeof analytics === 'object') {
} catch (e) { const { action_type, click_sarea, click_item } = analytics
console.error('[Exception] useAnalytics.sendGA: ', e)
logData = {
actionType: action_type,
event: baseEvent,
eventCategory: `${baseEvent}_${click_sarea}_${click_item}`,
} as AnalyticsDetailType
if (action_type === 'click') {
logData.clickArea = baseViewArea
logData.clickSarea = click_sarea
logData.clickItem = click_item
} else if (action_type === 'view') {
logData.viewArea = baseViewArea
logData.viewType = 'view_frame'
} }
} }
return logData
}
// ============================================================================
// Stove Analytics(81 Plug) + Amplitude
// ============================================================================
/** /**
* Stove Analytics(81 Plug) 전송 * Stove Analytics(81 Plug) 전송
* *
* @param {AnalyticsDetailType} analytics * @param analytics Partial<AnalyticsDetailType>
* @param {string} mcode * @param param1 mcode, options
* @param {object} options
*/ */
const sendSA = ( const sendSA = (
analytics: AnalyticsDetailType, analytics: Partial<AnalyticsDetailType>,
{ mcode = '', options = {} }: any { mcode = '', options = {} }: { mcode?: string; options?: any } = {}
) => { ) => {
if (!import.meta.client) return if (!isClient()) return
if (isEmptyAnalytics(analytics)) return
const gameDataStore = useGameDataStore() const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore) const { gameData } = storeToRefs(gameDataStore)
try { try {
const gameNo = gameData.value.game_code
const device = useDevice() const device = useDevice()
const deviceType = device.isDesktop ? 'pcweb' : 'mobileweb'
const gameNo = gameData.value?.game_code
const deviceType = device.isDesktop ? 'pcweb' : 'mobileweb'
const country = `${csrGetCountry()}` const country = `${csrGetCountry()}`
const memberNo = `${csrGetStoveMemberNo()}` const memberNo = `${csrGetStoveMemberNo()}`
const actionType = analytics.actionType || '' const {
const logSourceType = analytics.logSourceType || '' actionType = '',
const viewArea = analytics.viewArea || '' logSourceType = '',
const viewType = analytics.viewType || '' viewArea = '',
const clickArea = analytics.clickArea || '' viewType = '',
const clickSarea = findValueFromOption(analytics.clickSarea || '', { clickArea = '',
options, clickSarea = '',
}) eventLocale = '',
const eventLocale = analytics.eventLocale || '' clickItem,
} = analytics
const identityInfo: IdentityInfo = { const identityInfo: IdentityInfo = {
app_id: 'stove', app_id: 'stove',
@@ -173,7 +162,7 @@ const sendSA = (
media_page: '', media_page: '',
} }
let actionParam = {} let actionParam: ActionInfo['action_param'] = {}
if (actionType === 'view') { if (actionType === 'view') {
actionParam = { actionParam = {
@@ -190,7 +179,7 @@ const sendSA = (
click_area: clickArea, click_area: clickArea,
click_sarea: clickSarea, click_sarea: clickSarea,
click_item: { click_item: {
pwt_click_item: analytics.clickItem, pwt_click_item: clickItem,
game_no: gameNo, game_no: gameNo,
lang_cd: eventLocale, lang_cd: eventLocale,
...options?.clickItem, ...options?.clickItem,
@@ -206,8 +195,8 @@ const sendSA = (
const amplitudeActionInfo = { const amplitudeActionInfo = {
...actionInfo, ...actionInfo,
url: import.meta.client ? `${location?.href || ''}` : '', url: isClient() ? `${location?.href || ''}` : '',
agent: import.meta.client ? `${navigator?.userAgent || ''}` : '', agent: isClient() ? `${navigator?.userAgent || ''}` : '',
} }
const amplitudeActionParams: { const amplitudeActionParams: {
@@ -218,66 +207,107 @@ const sendSA = (
event_properties: amplitudeActionInfo, event_properties: amplitudeActionInfo,
} }
// 81plug
svcLog.identity(identityInfo) svcLog.identity(identityInfo)
svcLog.action(actionInfo, {}, {}) // 81plug warning log 제거를 위해 2번째 인자부터 빈 객체 세팅 // 81plug warning log 제거를 위해 2번째 인자부터 빈 객체 세팅
svcLog.action(actionInfo, {}, {})
// Amplitude
amplitude.track(amplitudeActionParams) amplitude.track(amplitudeActionParams)
} catch (e) { } catch (e) {
console.error('[Exception] useAnalytics.sendSA: ', e) console.error('[Exception] useAnalytics.sendSA: ', e)
} }
} }
// ============================================================================
// 기본 로그 일괄 전송
// ============================================================================
/** /**
* 기본 로그 일괄 전송 * 기본 로그 일괄 전송
* *
* @param {string} locale * @param locale 언어 코드
* @param {AnalyticsDetailType} analytics * @param analytics Partial<AnalyticsDetailType>
*/ */
const sendLog = (locale: string, analytics: AnalyticsDetailType) => { const sendLog = (locale: string, analytics: Partial<AnalyticsDetailType>) => {
if (isEmptyAnalytics(analytics)) return
// 언어 코드 대문자 변환 // 언어 코드 대문자 변환
analytics.eventLocale = locale.toUpperCase() analytics.eventLocale = locale.toUpperCase()
if (analytics) { // SA 전송 : actionType, logSourceType 유무로 판별 (logSourceType 체크는 주석 처리 되어있던 로직 유지)
// GA 전송 : eventCategory 유무로 판별 if (analytics.actionType && analytics.actionType !== '') {
sendGA(analytics, { options: analytics.options })
// SA 전송 : actionType, logSourceType 유무로 판별
if (
analytics.actionType &&
analytics.actionType !== ''
// analytics.logSourceType &&
// analytics.logSourceType !== ''
) {
sendSA(analytics, { mcode: analytics.mcode, options: analytics.options }) sendSA(analytics, { mcode: analytics.mcode, options: analytics.options })
} }
} }
// ============================================================================
// Google Analytics
// ============================================================================
/** GA 공통 설정 래퍼 */
const withGA = (callback: (gtag: any) => void) => {
if (!isClient()) return
try {
const { gtag } = useGtag()
const hostname = window?.location?.hostname || ''
gtag('set', 'cookie_domain', hostname) // env 값으로 설정 시 쿠키 생성 안 돼서 window.location.hostname으로 설정
gtag('set', 'cookie_expires', '0') // 0으로 설정 시 쿠키가 Session 기반 쿠키로 전환
callback(gtag)
} catch (e) {
console.error('[Exception] useAnalytics.withGA: ', e)
}
} }
/** /**
* Google Analytics 전송 (기본 이벤트만 전송) * Google Analytics 전송 (기본 이벤트만 전송)
* *
* @param {string} gaEventName * @param gaEventName 이벤트명
*/ */
const sendGAEventOnly = (gaEventName: string) => { const sendGAEventOnly = (gaEventName: string) => {
if (!import.meta.client) return if (!gaEventName) return
try { withGA(gtag => {
const { gtag } = useGtag()
gtag('set', 'cookie_domain', `${window?.location?.hostname || ''}`) // env 값으로 설정 시 쿠키 생성 안 돼서 window.location.hostname으로 설정
gtag('set', 'cookie_expires', '0') // 0으로 설정 시 쿠키가 Session 기반 쿠키로 전환
gtag('event', `${gaEventName}`) gtag('event', `${gaEventName}`)
} catch (e) { })
console.error('[Exception] useAnalytics.sendGAEventOnly: ', e)
} }
/**
* Google Analytics 전송
*
* @param analytics Partial<AnalyticsDetailType>
*/
const sendGA = (analytics: Partial<AnalyticsDetailType>) => {
if (isEmptyAnalytics(analytics)) return
withGA(gtag => {
const eventName = analytics.event || ''
const eventLocale = analytics.eventLocale || ''
const eventCategory = analytics.eventCategory || ''
// GA 클릭 이벤트 명 뒤에 언어 값 추가 노출되도록 개발. ex) GNB_자유게시판_KO
const eventLabel = `${eventCategory}_${eventLocale}`
gtag('event', `${eventName}`, {
event_category: eventLabel,
})
})
} }
// ============================================================================
// Meta / Twitter / TikTok Pixel
// ============================================================================
/** /**
* 메타 픽셀 전송 * 메타 픽셀 전송
* *
* @param {string} fbEventName * @param fbEventName 이벤트명
* @description 수집 대상 페이지에 useHead({ meta: [loadMetaPixelMeta()] }) 선언 * @description 수집 대상 페이지에 useHead({ meta: [loadMetaPixelMeta()] }) 선언
*/ */
const sendMetaPixel = (fbEventName: string) => { const sendMetaPixel = (fbEventName?: string) => {
if (!import.meta.client) return if (!isClient() || !fbEventName) return
try { try {
const { $fbq } = useNuxtApp() const { $fbq } = useNuxtApp()
@@ -292,11 +322,11 @@ const sendMetaPixel = (fbEventName: string) => {
/** /**
* X(트위터) 픽셀 전송 * X(트위터) 픽셀 전송
* *
* @param {string} twEventName * @param twEventName 이벤트명
* @description 수집 대상 페이지에 useHead({ script: [loadTwitterPixelScript()] }) 선언 * @description 수집 대상 페이지에 useHead({ script: [loadTwitterPixelScript()] }) 선언
*/ */
const sendTwitterPixel = (twEventName: string) => { const sendTwitterPixel = (twEventName?: string) => {
if (!import.meta.client) return if (!isClient() || !twEventName) return
try { try {
twq('event', twEventName, {}) twq('event', twEventName, {})
@@ -308,11 +338,11 @@ const sendTwitterPixel = (twEventName: string) => {
/** /**
* 틱톡 픽셀 전송 * 틱톡 픽셀 전송
* *
* @param {string} ttEventName * @param ttEventName 이벤트명
* @description 수집 대상 페이지에 onMounted(() => { loadTikTokPixelScript() }) 선언 * @description 수집 대상 페이지에 onMounted(() => { loadTikTokPixelScript() }) 선언
*/ */
const sendTiktokPixel = (ttEventName: string) => { const sendTiktokPixel = (ttEventName?: string) => {
if (!import.meta.client) return if (!isClient() || !ttEventName) return
try { try {
ttq.track(ttEventName) ttq.track(ttEventName)
@@ -321,45 +351,61 @@ const sendTiktokPixel = (ttEventName: string) => {
} }
} }
// ============================================================================
// 마케팅 스크립트 일괄 전송
// ============================================================================
/** /**
* 마케팅 인텔리전스 팀 요청 마케팅 스크립트 일괄 전송 * 마케팅 인텔리전스 팀 요청 마케팅 스크립트 일괄 전송
* *
* @param {string} gaEventName * @param logName 'ga' | 'meta' | 'twitter' | 'tiktok'
* @param {string} fbEventName * @param eventName 이벤트명 (ga: eventOnly, meta/twitter/tiktok: 필수)
* @param {string} twEventName * @param analytics GA용 analytics 데이터 (eventName 없을 때만 사용)
* @param {string} ttEventName
*/ */
const sendMarketingScript = ({ const sendMarketingLog = ({
gaEventName, logName,
fbEventName, eventName,
twEventName, analytics,
ttEventName,
}: { }: {
gaEventName?: string logName: 'ga' | 'meta' | 'twitter' | 'tiktok'
fbEventName?: string eventName?: string
twEventName?: string analytics?: Partial<AnalyticsDetailType>
ttEventName?: string
}) => { }) => {
if (gaEventName) { switch (logName) {
sendGAEventOnly(gaEventName) case 'ga': {
if (eventName) {
sendGAEventOnly(eventName)
} else if (analytics) {
sendGA(analytics)
} }
if (fbEventName) { break
sendMetaPixel(fbEventName)
} }
if (twEventName) { case 'meta': {
sendTwitterPixel(twEventName) sendMetaPixel(eventName)
break
} }
if (ttEventName) { case 'twitter': {
sendTiktokPixel(ttEventName) sendTwitterPixel(eventName)
break
}
case 'tiktok': {
sendTiktokPixel(eventName)
break
}
default:
break
} }
} }
// ============================================================================
// export
// ============================================================================
export default () => { export default () => {
return { return {
sendGA,
sendSA, sendSA,
sendLog, sendLog,
sendMarketingScript, sendMarketingLog,
useAnalyticsLogDataDirect, useAnalyticsData,
} }
} }

View File

@@ -1,83 +0,0 @@
import type { Splide as SplideType } from '@splidejs/splide'
/**
* Splide 슬라이더의 화살표 클릭 로직을 처리하는 composable
*/
export const useSplideArrow = () => {
/**
* 화살표 클릭 시 슬라이드 인덱스를 계산하는 함수
* @param direction - 이동 방향 ('prev' | 'next')
* @param splide - Splide 인스턴스
* @returns 다음 슬라이드 인덱스
*/
const calculateTargetIndex = (direction: 'prev' | 'next', splide: SplideType): number => {
const currentIndex = splide.index
const totalSlides = splide.length
if (direction === 'next') {
return currentIndex + 1 >= totalSlides ? 0 : currentIndex + 1
} else {
return currentIndex - 1 < 0 ? totalSlides - 1 : currentIndex - 1
}
}
/**
* 화살표 클릭 핸들러
* @param direction - 이동 방향
* @param splide - Splide 인스턴스
* @param onArrowClick - 화살표 클릭 시 실행될 콜백 함수
*/
const handleArrowClick = (
direction: 'prev' | 'next',
splide: SplideType,
onArrowClick?: (direction: 'prev' | 'next', targetIndex: number) => void
) => {
const targetIndex = calculateTargetIndex(direction, splide)
if (onArrowClick) {
onArrowClick(direction, targetIndex)
}
}
/**
* 화살표 버튼에 클릭 이벤트 리스너를 추가하는 함수
* @param splide - Splide 인스턴스
* @param onArrowClick - 화살표 클릭 시 실행될 콜백 함수
* @returns 이벤트 리스너 제거 함수
*/
const addArrowClickListeners = (
splide: SplideType,
onArrowClick?: (direction: 'prev' | 'next', targetIndex: number) => void
) => {
const prevArrow = splide.root.querySelector('.arrow-prev')
const nextArrow = splide.root.querySelector('.arrow-next')
const prevHandler = () => handleArrowClick('prev', splide, onArrowClick)
const nextHandler = () => handleArrowClick('next', splide, onArrowClick)
if (prevArrow) {
prevArrow.addEventListener('click', prevHandler)
}
if (nextArrow) {
nextArrow.addEventListener('click', nextHandler)
}
// 이벤트 리스너 제거 함수 반환
return () => {
if (prevArrow) {
prevArrow.removeEventListener('click', prevHandler)
}
if (nextArrow) {
nextArrow.removeEventListener('click', nextHandler)
}
}
}
return {
calculateTargetIndex,
handleArrowClick,
addArrowClickListeners
}
}

View File

@@ -14,16 +14,17 @@ import {
import type { PageDataTemplateComponents } from '#layers/types/api/pageData' import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
import type { ReqCouponList } from '#layers/types/api/couponData' import type { ReqCouponList } from '#layers/types/api/couponData'
// Props
interface Props { interface Props {
id?: string
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
// Configuration provide('pageVerTmplNameEn', props.pageVerTmplNameEn)
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const stoveApiUrl = runtimeConfig.public.stoveApiUrl as string const stoveApiUrl = runtimeConfig.public.stoveApiUrl as string
const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_coupon.json' const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_coupon.json'
@@ -486,7 +487,6 @@ onMounted(async () => {
<template> <template>
<WidgetsFixMainTitle <WidgetsFixMainTitle
:id="props.id"
:title="tm('Coupon_Page_Title')" :title="tm('Coupon_Page_Title')"
:resources-data="backgroundData" :resources-data="backgroundData"
/> />

View File

@@ -5,14 +5,15 @@ import type { Platform } from '#layers/types/components/button'
// Props // Props
interface Props { interface Props {
id?: string
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
// Configuration // Configuration
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_download.json' const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_download.json'
const stoveClientDownloadUrl = runtimeConfig.public const stoveClientDownloadUrl = runtimeConfig.public
@@ -127,7 +128,6 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
<template> <template>
<WidgetsFixMainTitle <WidgetsFixMainTitle
:id="props.id"
:title="tm('Download_Page_Title')" :title="tm('Download_Page_Title')"
:resources-data="backgroundData" :resources-data="backgroundData"
/> />

View File

@@ -8,6 +8,7 @@ import type { Platform } from '#layers/types/components/button'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -362,7 +363,7 @@ const handlePreregistClick = () => {
</li> </li>
</ul> </ul>
<BlocksSlideDefault v-bind="splideOptions"> <WidgetsSlideDefault v-bind="splideOptions">
<SplideSlide <SplideSlide
v-for="item in rewardImages" v-for="item in rewardImages"
:key="item.id" :key="item.id"
@@ -380,7 +381,7 @@ const handlePreregistClick = () => {
class="w-full h-full object-contain" class="w-full h-full object-contain"
/> />
</SplideSlide> </SplideSlide>
</BlocksSlideDefault> </WidgetsSlideDefault>
</ClientOnly> </ClientOnly>
</div> </div>
<WidgetsDescription <WidgetsDescription

View File

@@ -4,9 +4,9 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
// Props // Props
interface Props { interface Props {
id?: string
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -148,7 +148,6 @@ onMounted(() => {
<template> <template>
<WidgetsFixMainTitle <WidgetsFixMainTitle
:id="props.id"
:title="tm('Secure_Page_Title') || '보안 강화 캠페인'" :title="tm('Secure_Page_Title') || '보안 강화 캠페인'"
:resources-data="backgroundData" :resources-data="backgroundData"
class="mx-auto" class="mx-auto"
@@ -156,7 +155,9 @@ onMounted(() => {
<div class="section-container static"> <div class="section-container static">
<section class="section-secure bg-[#F0F0F0] pb-50"> <section class="section-secure bg-[#F0F0F0] pb-50">
<div class="section-static content-standa flex-wrap md:max-w-[1300px] mx-auto"> <div
class="section-static content-standa flex-wrap md:max-w-[1300px] mx-auto"
>
<!-- Title Section (Non-Korean only) --> <!-- Title Section (Non-Korean only) -->
<div <div
v-if="locale !== 'ko'" v-if="locale !== 'ko'"
@@ -170,7 +171,10 @@ onMounted(() => {
<p <p
class="text-[#666666] text-[13px] md:text-[14px] font-normal leading-[22px] md:leading-[24px] tracking-[-0.39px] md:tracking-[-0.42px]" class="text-[#666666] text-[13px] md:text-[14px] font-normal leading-[22px] md:leading-[24px] tracking-[-0.39px] md:tracking-[-0.42px]"
> >
*{{ tm('Secure_Global_Desc') || 'Set up the STOVE Authenticator to better protect your account.' }} *{{
tm('Secure_Global_Desc') ||
'Set up the STOVE Authenticator to better protect your account.'
}}
</p> </p>
</div> </div>
@@ -282,7 +286,9 @@ onMounted(() => {
> >
<img <img
:src=" :src="
formatPathHost(card.benefitIcon, { imageType: 'common' }) formatPathHost(card.benefitIcon, {
imageType: 'common',
})
" "
:alt="card.benefitTitle" :alt="card.benefitTitle"
:class="[ :class="[

View File

@@ -6,13 +6,17 @@ import {
getComponentContainer, getComponentContainer,
} from '#layers/utils/dataUtil' } from '#layers/utils/dataUtil'
import { getYouTubeThumbnail } from '#layers/utils/youtubeUtil' import { getYouTubeThumbnail } from '#layers/utils/youtubeUtil'
import type { PageDataTemplateComponents } from '#layers/types/api/pageData' import type {
PageDataTemplateComponents,
PageDataResourceGroups,
} from '#layers/types/api/pageData'
import type { OperateGroupItem } from '#layers/types/api/operateResources' import type { OperateGroupItem } from '#layers/types/api/operateResources'
interface Props { interface Props {
id?: string id?: string
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -49,6 +53,23 @@ const officialUrlData = computed(
() => getComponentGroup(props.components, 'officialUrl')?.display?.text ?? '' () => getComponentGroup(props.components, 'officialUrl')?.display?.text ?? ''
) )
const arrowsData: PageDataResourceGroups = [
{
tracking: {
action_type: 'click',
click_item: '1. 컨텐츠 리스트 - 네비게이션(좌)',
click_sarea: 'Home_GameInfo__arrow',
},
},
{
tracking: {
action_type: 'click',
click_item: '1. 컨텐츠 리스트 - 네비게이션(우)',
click_sarea: 'Home_GameInfo__arrow',
},
},
]
const { data: slideData } = await useAsyncData( const { data: slideData } = await useAsyncData(
`fx-video-01-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`, `fx-video-01-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`,
async () => { async () => {
@@ -137,11 +158,12 @@ const handleLoadMoreRecent = () => {
<div <div
class="relative content-static bg-[#fff] rounded-[12px] md:rounded-[16px]" class="relative content-static bg-[#fff] rounded-[12px] md:rounded-[16px]"
> >
<BlocksSlideFade <WidgetsSlideFade
v-model:index="currentRecommendedIndex" v-model:index="currentRecommendedIndex"
:autoplay="recommendedVideos.length > 1" :autoplay="recommendedVideos.length > 1"
:interval="3000" :interval="3000"
:arrows="recommendedVideos.length > 1" :arrows="recommendedVideos.length > 1"
:arrows-data="arrowsData"
:pagination="false" :pagination="false"
:drag="false" :drag="false"
> >
@@ -185,7 +207,7 @@ const handleLoadMoreRecent = () => {
</p> </p>
</div> </div>
</SplideSlide> </SplideSlide>
</BlocksSlideFade> </WidgetsSlideFade>
<div v-if="recommendedVideos.length > 1" class="splide-pagination"> <div v-if="recommendedVideos.length > 1" class="splide-pagination">
<span class="font-[700] text-[#1F1F1F]"> <span class="font-[700] text-[#1F1F1F]">
{{ currentRecommendedIndex + 1 }} {{ currentRecommendedIndex + 1 }}
@@ -253,20 +275,20 @@ const handleLoadMoreRecent = () => {
.splide { .splide {
@apply pb-[68px] sm:pb-[0]; @apply pb-[68px] sm:pb-[0];
} }
.splide:deep(.splide-arrows) { .splide:deep(.splide__arrows) {
@apply block; @apply block;
} }
.splide:deep(.splide-arrows) .splide-arrow { .splide:deep(.splide__arrows) .splide-arrow {
@apply block top-[unset] bottom-[20px] translate-y-0 bg-cover bg-center bg-no-repeat @apply block top-[unset] bottom-[20px] translate-y-0 bg-cover bg-center bg-no-repeat
sm:bottom-[24px] md:bottom-[36px] lg:bottom-[60px]; sm:bottom-[24px] md:bottom-[36px] lg:bottom-[60px];
} }
.splide:deep(.splide-arrows) .arrow-prev { .splide:deep(.splide__arrows) .splide__arrow--prev {
@apply left-[20px] bg-[image:url('/images/common/btn_system_arrow_prev.png')] @apply left-[20px] bg-[image:url('/images/common/btn_system_arrow_prev.png')]
sm:left-[calc(60.3%+21px)] sm:left-[calc(60.3%+21px)]
md:left-[calc(56%+39px)] md:left-[calc(56%+39px)]
lg:left-[790px]; lg:left-[790px];
} }
.splide:deep(.splide-arrows) .arrow-next { .splide:deep(.splide__arrows) .splide__arrow--next {
@apply right-[20px] bg-[image:url('/images/common/btn_system_arrow_next.png')] @apply right-[20px] bg-[image:url('/images/common/btn_system_arrow_next.png')]
sm:right-[28px] sm:right-[28px]
md:right-[unset] md:left-[calc(56%+99px)] md:right-[unset] md:left-[calc(56%+99px)]

View File

@@ -8,6 +8,7 @@ import type { CwmsArticleItem } from '#layers/types/api/cwmsArticle'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -15,7 +16,6 @@ const props = defineProps<Props>()
const gameDataStore = useGameDataStore() const gameDataStore = useGameDataStore()
const pageDataStore = usePageDataStore() const pageDataStore = usePageDataStore()
const { getCwmsArticle } = useCwmsArticle() const { getCwmsArticle } = useCwmsArticle()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const { locale } = useI18n() const { locale } = useI18n()
const { gameData } = storeToRefs(gameDataStore) const { gameData } = storeToRefs(gameDataStore)
@@ -24,19 +24,23 @@ const { pageData } = storeToRefs(pageDataStore)
const boardId = computed( const boardId = computed(
() => getComponentGroup(props.components, 'boardId')?.display?.text () => getComponentGroup(props.components, 'boardId')?.display?.text
) )
const paginationData = computed(() => {
return getComponentGroupAry(props.components, 'pagination')
})
const backgroundData = computed(() => const backgroundData = computed(() =>
getComponentGroup(props.components, 'background') getComponentGroup(props.components, 'background')
) )
const mainTitleData = computed(() => const mainTitleData = computed(() =>
getComponentGroup(props.components, 'mainTitle') getComponentGroup(props.components, 'mainTitle')
) )
const buttonListData = ref(getComponentGroupAry(props.components, 'buttonList'))
const descriptionData = computed(() => const descriptionData = computed(() =>
getComponentGroup(props.components, 'description') getComponentGroup(props.components, 'description')
) )
const arrowsData = computed(() =>
getComponentGroupAry(props.components, 'arrow')
)
const paginationData = computed(() => {
return getComponentGroupAry(props.components, 'pagination')
})
const buttonListData = ref(getComponentGroupAry(props.components, 'buttonList'))
const { data: slideData } = await useAsyncData( const { data: slideData } = await useAsyncData(
`gr-board-01-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`, `gr-board-01-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`,
@@ -103,12 +107,6 @@ const getArticleUrl = (articleId: string) => {
} }
return `${communityUrl}/view/${articleId}` return `${communityUrl}/view/${articleId}`
} }
const onArrowClick = (direction, targetIndex) => {
const arrowGroupAry = getComponentGroupAry(props.components, 'arrow')
const logTracking = arrowGroupAry?.[direction === 'prev' ? 0 : 1]
sendLog(locale.value, useAnalyticsLogDataDirect(logTracking, 1))
}
</script> </script>
<template> <template>
@@ -124,12 +122,12 @@ const onArrowClick = (direction, targetIndex) => {
class="title-md max-w-[944px] mx-[20px] sm:mx-[40px]" class="title-md max-w-[944px] mx-[20px] sm:mx-[40px]"
/> />
<ClientOnly> <ClientOnly>
<BlocksSlideDefault <WidgetsSlideDefault
v-if="slideLength > 0" v-if="slideLength > 0"
:slide-item-length="slideLength"
v-bind="splideOptions" v-bind="splideOptions"
:slide-item-length="slideLength"
:arrows-data="arrowsData"
class="w-full" class="w-full"
@arrow-click="onArrowClick"
> >
<SplideSlide <SplideSlide
v-for="(item, index) in slideData" v-for="(item, index) in slideData"
@@ -141,16 +139,16 @@ const onArrowClick = (direction, targetIndex) => {
:description="globalDateFormat(item.create_datetime, locale)" :description="globalDateFormat(item.create_datetime, locale)"
:img-path="item.media_thumbnail_url" :img-path="item.media_thumbnail_url"
:url="getArticleUrl(item.article_id)" :url="getArticleUrl(item.article_id)"
:analytics-sarea="pageVerTmplNameEn"
link-target="_blank" link-target="_blank"
/> />
</div> </div>
</SplideSlide> </SplideSlide>
</BlocksSlideDefault> </WidgetsSlideDefault>
</ClientOnly> </ClientOnly>
<WidgetsButtonList <WidgetsButtonList
v-if="buttonListData" v-if="buttonListData"
:resources-data="buttonListData" :resources-data="buttonListData"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
class="mt-[48px] md:mt-[64px]" class="mt-[48px] md:mt-[64px]"
/> />
<WidgetsDescription <WidgetsDescription
@@ -166,10 +164,10 @@ const onArrowClick = (direction, targetIndex) => {
.splide { .splide {
@apply mt-[24px] md:max-w-[776px] md:mt-[48px] md:mx-auto md:px-[72px] lg:max-w-[1428px]; @apply mt-[24px] md:max-w-[776px] md:mt-[48px] md:mx-auto md:px-[72px] lg:max-w-[1428px];
} }
.splide:deep(.arrow-prev) { .splide:deep(.arsplide__arrow--prev) {
@apply top-[calc(50%-28px)] left-[0]; @apply top-[calc(50%-28px)] left-[0];
} }
.splide:deep(.arrow-next) { .splide:deep(.splide__arrow--next) {
@apply top-[calc(50%-28px)] right-[0]; @apply top-[calc(50%-28px)] right-[0];
} }
.slide-inner { .slide-inner {

View File

@@ -5,6 +5,7 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -56,7 +57,6 @@ const buttonListData = computed(() => {
<WidgetsButtonList <WidgetsButtonList
v-if="buttonListData" v-if="buttonListData"
:resources-data="buttonListData" :resources-data="buttonListData"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
class="mt-[56px]" class="mt-[56px]"
/> />
<WidgetsDescription <WidgetsDescription

View File

@@ -9,10 +9,14 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics()
const splideRef = ref<SplideSlide | null>(null) const splideRef = ref<SplideSlide | null>(null)
const currentSlideIndex = ref<number | null>(null) const currentSlideIndex = ref<number | null>(null)
@@ -23,17 +27,24 @@ const paginationData = computed(() => {
return getComponentGroupAry(props.components, 'pagination') return getComponentGroupAry(props.components, 'pagination')
}) })
const goToSlide = (index: number) => { const goToSlide = (index: number, title: string) => {
const splide = splideRef.value?.splide const splide = splideRef.value?.splide
const paginationAnalytics = {
action_type: 'click',
click_item: title,
click_sarea: props.pageVerTmplNameEn,
}
if (splide) { if (splide) {
splide.go(index) splide.go(index)
sendLog(locale.value, useAnalyticsData(paginationAnalytics))
} }
} }
</script> </script>
<template> <template>
<section class="section-standard"> <section class="section-standard">
<BlocksSlideFade <WidgetsSlideFade
v-if="slideData" v-if="slideData"
ref="splideRef" ref="splideRef"
v-model:index="currentSlideIndex" v-model:index="currentSlideIndex"
@@ -66,7 +77,7 @@ const goToSlide = (index: number) => {
/> />
</div> </div>
</SplideSlide> </SplideSlide>
</BlocksSlideFade> </WidgetsSlideFade>
<div <div
v-if="slideData && slideData.length > 1" v-if="slideData && slideData.length > 1"
class="splide-pagination" class="splide-pagination"
@@ -88,7 +99,12 @@ const goToSlide = (index: number) => {
'btn-pagination', 'btn-pagination',
{ 'is-active': currentSlideIndex === index }, { 'is-active': currentSlideIndex === index },
]" ]"
@click="goToSlide(index)" @click="
goToSlide(
index,
getComponentGroup(item, 'pagenaviTitle')?.display?.text || ''
)
"
> >
<span class="item-bullet"></span> <span class="item-bullet"></span>
<span class="item-title"> <span class="item-title">
@@ -105,7 +121,7 @@ const goToSlide = (index: number) => {
<style scoped> <style scoped>
.splide-pagination { .splide-pagination {
@apply overflow-hidden flex items-center justify-center absolute bottom-10 left-1/2 max-w-full -translate-x-1/2 z-[5] md:bottom-[96px]; @apply flex items-center justify-center absolute bottom-10 left-1/2 max-w-full -translate-x-1/2 z-[5] md:bottom-[96px];
} }
.btn-pagination { .btn-pagination {
@apply relative; @apply relative;
@@ -122,7 +138,7 @@ const goToSlide = (index: number) => {
color: var(--pagination-disabled); color: var(--pagination-disabled);
} }
.progress-bar { .progress-bar {
@apply relative w-[68px] h-[1px] overflow-hidden md:w-[184px]; @apply relative w-[62px] h-[1px] overflow-hidden xs:w-[68px] md:w-[184px];
background-color: var(--pagination-disabled); background-color: var(--pagination-disabled);
} }
.progress-fill { .progress-fill {

View File

@@ -9,6 +9,7 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -23,6 +24,9 @@ const thumbnailData = computed(() => {
.map(item => item?.pagenaviThumbnail?.groups?.[0]) .map(item => item?.pagenaviThumbnail?.groups?.[0])
.filter((group): group is NonNullable<typeof group> => group != null) .filter((group): group is NonNullable<typeof group> => group != null)
}) })
const arrowsData = computed(() => {
return getComponentGroupAry(props.components, 'arrow')
})
const paginationData = computed(() => { const paginationData = computed(() => {
return getComponentGroupAry(props.components, 'pagination') return getComponentGroupAry(props.components, 'pagination')
}) })
@@ -30,10 +34,12 @@ const paginationData = computed(() => {
<template> <template>
<section class="section-standard"> <section class="section-standard">
<BlocksSlideThumbnail <WidgetsSlideThumbnail
v-model:index="currentSlideIndex" v-model:index="currentSlideIndex"
:thumbnail-data="thumbnailData" :thumbnail-data="thumbnailData"
:pagination-data="paginationData" :pagination-data="paginationData"
:arrows-data="arrowsData"
:analytics-sarea="props.pageVerTmplNameEn"
> >
<SplideSlide v-for="(item, index) in slideData" :key="index"> <SplideSlide v-for="(item, index) in slideData" :key="index">
<WidgetsBackground <WidgetsBackground
@@ -59,6 +65,6 @@ const paginationData = computed(() => {
/> />
</div> </div>
</SplideSlide> </SplideSlide>
</BlocksSlideThumbnail> </WidgetsSlideThumbnail>
</section> </section>
</template> </template>

View File

@@ -14,6 +14,7 @@ import type {
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -32,6 +33,9 @@ const thumbnailData = computed(() => {
.map(item => item?.pagenaviThumbnail?.groups?.[0]) .map(item => item?.pagenaviThumbnail?.groups?.[0])
.filter((group): group is NonNullable<typeof group> => group != null) .filter((group): group is NonNullable<typeof group> => group != null)
}) })
const arrowsData = computed(() => {
return getComponentGroupAry(props.components, 'arrow')
})
const paginationData = computed(() => { const paginationData = computed(() => {
return getComponentGroupAry(props.components, 'pagination') return getComponentGroupAry(props.components, 'pagination')
}) })
@@ -44,10 +48,12 @@ const getVideoSrc = (item: PageDataTemplateComponent) => {
<template> <template>
<section class="section-standard"> <section class="section-standard">
<BlocksSlideThumbnail <WidgetsSlideThumbnail
v-model:index="currentSlideIndex" v-model:index="currentSlideIndex"
:thumbnail-data="thumbnailData" :thumbnail-data="thumbnailData"
:pagination-data="paginationData" :pagination-data="paginationData"
:arrows-data="arrowsData"
:analytics-sarea="props.pageVerTmplNameEn"
> >
<SplideSlide v-for="(item, index) in slideData" :key="index"> <SplideSlide v-for="(item, index) in slideData" :key="index">
<WidgetsBackground <WidgetsBackground
@@ -91,6 +97,6 @@ const getVideoSrc = (item: PageDataTemplateComponent) => {
/> />
</div> </div>
</SplideSlide> </SplideSlide>
</BlocksSlideThumbnail> </WidgetsSlideThumbnail>
</section> </section>
</template> </template>

View File

@@ -12,13 +12,11 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const playingSlideIndex = ref<number | null>(null) const playingSlideIndex = ref<number | null>(null)
let stopVideoTimeoutId: ReturnType<typeof setTimeout> | null = null let stopVideoTimeoutId: ReturnType<typeof setTimeout> | null = null
@@ -36,32 +34,26 @@ const slideData = computed(() => {
.map(item => item.media?.groups?.[0]) .map(item => item.media?.groups?.[0])
.filter((group): group is NonNullable<typeof group> => group != null) .filter((group): group is NonNullable<typeof group> => group != null)
}) })
const arrowsData = computed(() => {
return getComponentGroupAry(props.components, 'arrow')
})
const paginationData = computed(() => { const paginationData = computed(() => {
return getComponentGroupAry(props.components, 'pagination') return getComponentGroupAry(props.components, 'pagination')
}) })
const getVideoPlayTracking = (item: string) => {
return {
action_type: 'click',
click_item: `${item}_영상재생`,
click_sarea: props.pageVerTmplNameEn,
}
}
const handleVideoClick = (index: number) => { const handleVideoClick = (index: number) => {
playingSlideIndex.value = index playingSlideIndex.value = index
const group = getComponentGroup(props.components, 'videoPlay') const group = getComponentGroup(props.components, 'videoPlay')
if (!group || !group.tracking) return if (!group || !group.tracking) return
const base = group.tracking.click_item || ''
const next = base
? base.replace(/(^.*_)(\d+)$/, `$1${index}`) === base
? `${base}_${index}`
: base.replace(/(^.*_)(\d+)$/, `$1${index}`)
: `${index}`
const sendingGroup = {
...group,
tracking: { ...group.tracking, click_item: next },
}
sendLog(
locale.value,
useAnalyticsLogDataDirect(sendingGroup, props.pageVerTmplSeq)
)
} }
const handleSplideMove = () => { const handleSplideMove = () => {
@@ -78,12 +70,6 @@ const handleSplideMove = () => {
}, 600) }, 600)
} }
const onArrowClick = (direction, _targetIndex) => {
const arrowGroupAry = getComponentGroupAry(props.components, 'arrow')
const logTracking = arrowGroupAry?.[direction === 'prev' ? 0 : 1]
sendLog(locale.value, useAnalyticsLogDataDirect(logTracking, 1))
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
// 타이머 정리 // 타이머 정리
if (stopVideoTimeoutId) { if (stopVideoTimeoutId) {
@@ -102,14 +88,15 @@ onBeforeUnmount(() => {
:resources-data="mainTitleData" :resources-data="mainTitleData"
class="title-md max-w-[944px]" class="title-md max-w-[944px]"
/> />
<BlocksSlideThumbnail <WidgetsSlideThumbnail
:drag="false"
:thumbnail-data="slideData" :thumbnail-data="slideData"
:pagination-data="paginationData"
:drag="false"
variant="media" variant="media"
class="mt-[24px] md:mt-[32px]" class="mt-[24px] md:mt-[32px]"
:pagination-data="paginationData"
:arrows="slideData.length > 5" :arrows="slideData.length > 5"
@arrow-click="onArrowClick" :arrows-data="arrowsData"
:analytics-sarea="props.pageVerTmplNameEn"
@move="handleSplideMove" @move="handleSplideMove"
> >
<SplideSlide <SplideSlide
@@ -128,6 +115,7 @@ onBeforeUnmount(() => {
<AtomsButtonPlay <AtomsButtonPlay
v-if="playingSlideIndex !== index" v-if="playingSlideIndex !== index"
class="btn-play" class="btn-play"
:tracking="getVideoPlayTracking(item?.group_label)"
@click="handleVideoClick(index)" @click="handleVideoClick(index)"
/> />
<transition name="fade"> <transition name="fade">
@@ -142,7 +130,7 @@ onBeforeUnmount(() => {
</transition> </transition>
</template> </template>
</SplideSlide> </SplideSlide>
</BlocksSlideThumbnail> </WidgetsSlideThumbnail>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -10,6 +10,7 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -23,6 +24,9 @@ const mainTitleData = computed(() =>
const slideData = computed(() => { const slideData = computed(() => {
return getComponentContainer(props.components, 'group_sets', { minLength: 4 }) return getComponentContainer(props.components, 'group_sets', { minLength: 4 })
}) })
const arrowsData = computed(() =>
getComponentGroupAry(props.components, 'arrow')
)
const buttonListData = ref( const buttonListData = ref(
getComponentGroupAry(slideData?.value[0], 'buttonList') getComponentGroupAry(slideData?.value[0], 'buttonList')
@@ -49,13 +53,6 @@ const handleSplideMove = (_splide: SplideType, newIndex: number) => {
'buttonList' 'buttonList'
) )
} }
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const onArrowClick = (direction, targetIndex) => {
const arrowGroupAry = getComponentGroupAry(props.components, 'arrow')
const logTracking = arrowGroupAry?.[direction === 'prev' ? 0 : 1]
sendLog(locale.value, useAnalyticsLogDataDirect(logTracking, 1))
}
</script> </script>
<template> <template>
@@ -71,14 +68,14 @@ const onArrowClick = (direction, targetIndex) => {
:resources-data="mainTitleData" :resources-data="mainTitleData"
class="title-md max-w-[944px] mx-[20px] sm:mx-[40px]" class="title-md max-w-[944px] mx-[20px] sm:mx-[40px]"
/> />
<BlocksSlideCenterFocus <WidgetsSlideCenterFocus
v-if="slideData" v-if="slideData"
:slide-item-size="slideItemSize" :slide-item-size="slideItemSize"
:slide-item-length="slideData?.length" :slide-item-length="slideData?.length"
:arrows-data="arrowsData"
:pagination="false" :pagination="false"
class="mt-[24px] md:mt-[48px]" class="mt-[24px] md:mt-[48px]"
@move="handleSplideMove" @move="handleSplideMove"
@arrow-click="onArrowClick"
> >
<SplideSlide v-for="(item, index) in slideData" :key="index"> <SplideSlide v-for="(item, index) in slideData" :key="index">
<div class="slide-inner border-line mt-auto"> <div class="slide-inner border-line mt-auto">
@@ -89,11 +86,10 @@ const onArrowClick = (direction, targetIndex) => {
/> />
</div> </div>
</SplideSlide> </SplideSlide>
</BlocksSlideCenterFocus> </WidgetsSlideCenterFocus>
<WidgetsButtonList <WidgetsButtonList
v-if="buttonListData" v-if="buttonListData"
:resources-data="buttonListData" :resources-data="buttonListData"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
class="mt-[40px] md:mt-[56px]" class="mt-[40px] md:mt-[56px]"
/> />
</div> </div>

View File

@@ -10,6 +10,7 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -23,6 +24,10 @@ const mainTitleData = computed(() =>
const slideData = computed(() => const slideData = computed(() =>
getComponentContainer(props.components, 'group_sets', { minLength: 4 }) getComponentContainer(props.components, 'group_sets', { minLength: 4 })
) )
const arrowsData = computed(() =>
getComponentGroupAry(props.components, 'arrow')
)
const imgTitleData = ref(getComponentGroup(slideData?.value[0], 'imgTitle')) const imgTitleData = ref(getComponentGroup(slideData?.value[0], 'imgTitle'))
const descriptionData = ref( const descriptionData = ref(
getComponentGroup(slideData?.value[0], 'description') getComponentGroup(slideData?.value[0], 'description')
@@ -60,13 +65,6 @@ const handleSplideMove = (
'buttonList' 'buttonList'
) )
} }
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const onArrowClick = (direction, targetIndex) => {
const arrowGroupAry = getComponentGroupAry(props.components, 'arrow')
const logTracking = arrowGroupAry?.[direction === 'prev' ? 0 : 1]
sendLog(locale.value, useAnalyticsLogDataDirect(logTracking, 1))
}
</script> </script>
<template> <template>
@@ -80,14 +78,14 @@ const onArrowClick = (direction, targetIndex) => {
:resources-data="mainTitleData" :resources-data="mainTitleData"
class="title-md max-w-[944px] mx-[20px] sm:mx-[40px]" class="title-md max-w-[944px] mx-[20px] sm:mx-[40px]"
/> />
<BlocksSlideCenterHighlight <WidgetsSlideCenterHighlight
v-if="slideData" v-if="slideData"
:slide-item-size="slideItemSize" :slide-item-size="slideItemSize"
:slide-item-length="slideData?.length" :slide-item-length="slideData?.length"
:arrows-data="arrowsData"
:pagination="false" :pagination="false"
class="mt-[24px] md:mt-[48px]" class="mt-[24px] md:mt-[48px]"
@move="handleSplideMove" @move="handleSplideMove"
@arrow-click="onArrowClick"
> >
<SplideSlide v-for="(item, index) in slideData" :key="index"> <SplideSlide v-for="(item, index) in slideData" :key="index">
<div class="slide-inner border-line"> <div class="slide-inner border-line">
@@ -98,7 +96,7 @@ const onArrowClick = (direction, targetIndex) => {
/> />
</div> </div>
</SplideSlide> </SplideSlide>
</BlocksSlideCenterHighlight> </WidgetsSlideCenterHighlight>
<WidgetsSubTitle <WidgetsSubTitle
v-if="imgTitleData" v-if="imgTitleData"
:resources-data="imgTitleData" :resources-data="imgTitleData"
@@ -112,7 +110,6 @@ const onArrowClick = (direction, targetIndex) => {
<WidgetsButtonList <WidgetsButtonList
v-if="buttonListData" v-if="buttonListData"
:resources-data="buttonListData" :resources-data="buttonListData"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
class="mt-[32px] mx-[20px] sm:mx-[40px]" class="mt-[32px] mx-[20px] sm:mx-[40px]"
/> />
</div> </div>

View File

@@ -5,6 +5,7 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -44,15 +45,10 @@ const buttonListData = computed(() =>
:resources-data="descriptionData" :resources-data="descriptionData"
class="w-full max-w-[355px] md:max-w-[944px]" class="w-full max-w-[355px] md:max-w-[944px]"
/> />
<WidgetsVideoPlay <WidgetsVideoPlay v-if="videoPlayData" :resources-data="videoPlayData" />
v-if="videoPlayData"
:resources-data="videoPlayData"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
/>
<WidgetsButtonList <WidgetsButtonList
v-if="buttonListData" v-if="buttonListData"
:resources-data="buttonListData" :resources-data="buttonListData"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
class="mt-[22px] md:mt-[52px]" class="mt-[22px] md:mt-[52px]"
/> />
</div> </div>

View File

@@ -12,6 +12,7 @@ import type { OperateGroupItem } from '#layers/types/api/operateResources'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -19,7 +20,6 @@ const props = defineProps<Props>()
const pageDataStore = usePageDataStore() const pageDataStore = usePageDataStore()
const { getOperateResources } = useOperateResources() const { getOperateResources } = useOperateResources()
const { locale } = useI18n() const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const { pageData } = storeToRefs(pageDataStore) const { pageData } = storeToRefs(pageDataStore)
@@ -35,6 +35,9 @@ const descriptionData = computed(() =>
const videoPlayData = computed(() => const videoPlayData = computed(() =>
getComponentGroup(props.components, 'videoPlay') getComponentGroup(props.components, 'videoPlay')
) )
const arrowsData = computed(() =>
getComponentGroupAry(props.components, 'arrow')
)
const { data: slideData } = await useAsyncData( const { data: slideData } = await useAsyncData(
`gr-visual-02-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`, `gr-visual-02-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`,
@@ -75,12 +78,6 @@ const slideItemSize = {
gap: 32, gap: 32,
}, },
} }
const onArrowClick = direction => {
const arrowGroupAry = getComponentGroupAry(props.components, 'arrow')
const logTracking = arrowGroupAry?.[direction === 'prev' ? 0 : 1]
sendLog(locale.value, useAnalyticsLogDataDirect(logTracking, 1))
}
</script> </script>
<template> <template>
@@ -101,18 +98,14 @@ const onArrowClick = direction => {
:resources-data="descriptionData" :resources-data="descriptionData"
class="w-full max-w-[355px] mx-[20px] sm:mx-[40px] md:max-w-[944px]" class="w-full max-w-[355px] mx-[20px] sm:mx-[40px] md:max-w-[944px]"
/> />
<WidgetsVideoPlay <WidgetsVideoPlay v-if="videoPlayData" :resources-data="videoPlayData" />
v-if="videoPlayData" <WidgetsSlideCenterHighlight
:resources-data="videoPlayData"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
/>
<BlocksSlideCenterHighlight
v-if="slideData && slideData.length > 0" v-if="slideData && slideData.length > 0"
:slide-item-size="slideItemSize" :slide-item-size="slideItemSize"
:slide-item-length="slideData.length" :slide-item-length="slideData.length"
:arrows-data="arrowsData"
:pagination="false" :pagination="false"
class="mt-[36px] md:mt-[60px]" class="mt-[36px] md:mt-[60px]"
@arrow-click="onArrowClick"
> >
<SplideSlide v-for="(item, index) in slideData" :key="index"> <SplideSlide v-for="(item, index) in slideData" :key="index">
<BlocksCardNews <BlocksCardNews
@@ -121,10 +114,11 @@ const onArrowClick = direction => {
:img-path="formatPathHost(item.img_path)" :img-path="formatPathHost(item.img_path)"
:url="item.url" :url="item.url"
:link-target="item.link_target" :link-target="item.link_target"
:analytics-sarea="`${pageVerTmplNameEn}_bannerList`"
class="slide-inner" class="slide-inner"
/> />
</SplideSlide> </SplideSlide>
</BlocksSlideCenterHighlight> </WidgetsSlideCenterHighlight>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -11,39 +11,34 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props { interface Props {
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const currentSlideIndex = ref<number | null>(null) const currentSlideIndex = ref<number | null>(null)
const slideData = computed(() => { const slideData = computed(() => {
return getComponentContainer(props.components, 'group_sets') return getComponentContainer(props.components, 'group_sets')
}) })
const arrowsData = computed(() => {
return getComponentGroupAry(props.components, 'arrow')
})
const paginationData = computed(() => { const paginationData = computed(() => {
return getComponentGroupAry(props.components, 'pagination') return getComponentGroupAry(props.components, 'pagination')
}) })
const onArrowClick = direction => {
const arrowGroupAry = getComponentGroupAry(props.components, 'arrow')
const logTracking = arrowGroupAry?.[direction === 'prev' ? 0 : 1]
sendLog(locale.value, useAnalyticsLogDataDirect(logTracking, 1))
}
</script> </script>
<template> <template>
<section class="section-standard"> <section class="section-standard">
<BlocksSlideFade <WidgetsSlideFade
v-if="slideData" v-if="slideData"
v-model:index="currentSlideIndex" v-model:index="currentSlideIndex"
:arrows="slideData.length > 1" :arrows="slideData.length > 1"
:pagination="slideData.length > 1" :pagination="slideData.length > 1"
class="h-full"
:pagination-data="paginationData" :pagination-data="paginationData"
@arrow-click="onArrowClick" :arrows-data="arrowsData"
class="h-full"
> >
<SplideSlide v-for="(item, index) in slideData" :key="index"> <SplideSlide v-for="(item, index) in slideData" :key="index">
<WidgetsBackground <WidgetsBackground
@@ -70,11 +65,10 @@ const onArrowClick = direction => {
<WidgetsButtonList <WidgetsButtonList
v-if="hasComponentGroup(item, 'buttonList')" v-if="hasComponentGroup(item, 'buttonList')"
:resources-data="getComponentGroupAry(item, 'buttonList')" :resources-data="getComponentGroupAry(item, 'buttonList')"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
class="mt-[28px] md:mt-[52px]" class="mt-[28px] md:mt-[52px]"
/> />
</div> </div>
</SplideSlide> </SplideSlide>
</BlocksSlideFade> </WidgetsSlideFade>
</section> </section>
</template> </template>

View File

@@ -14,7 +14,7 @@ interface AnalyticsMetaType {
interface AnalyticsType { interface AnalyticsType {
analyticsNo: number analyticsNo: number
gameId: string gameId?: string
analyticsType?: AnalyticsMetaType analyticsType?: AnalyticsMetaType
analyticsVersion?: AnalyticsMetaType analyticsVersion?: AnalyticsMetaType
analyticsCode?: string analyticsCode?: string
@@ -25,8 +25,7 @@ interface AnalyticsType {
} }
interface AnalyticsDetailType extends AnalyticsType { interface AnalyticsDetailType extends AnalyticsType {
detailNo: number detailNo?: number
// analyticsNo in AnalyticsType
sbNo?: string sbNo?: string
areaNo?: string areaNo?: string
event?: string event?: string
@@ -81,7 +80,6 @@ interface AnalyticsLogDataTracking {
click_sarea: string click_sarea: string
} }
export type { export type {
AnalyticsType, AnalyticsType,
AnalyticsDetailType, AnalyticsDetailType,
@@ -89,5 +87,5 @@ export type {
AnalyticsLogDataTracking, AnalyticsLogDataTracking,
// [API] Req / Res ----- // [API] Req / Res -----
ReqGetAnalytics, ReqGetAnalytics,
ResGetAnalytics ResGetAnalytics,
} }

View File

@@ -43,3 +43,9 @@ export interface RequestObject {
headers: Record<string, string> headers: Record<string, string>
socket: { remoteAddress?: string } socket: { remoteAddress?: string }
} }
export interface TrackingObject {
action_type: string
click_item: string
click_sarea: string
}

View File

@@ -1,3 +1,5 @@
import type { TrackingObject } from '#layers/types/api/common'
// API 요청 파라미터 타입 // API 요청 파라미터 타입
export interface GameDataRequest { export interface GameDataRequest {
game_alias: string game_alias: string
@@ -142,16 +144,9 @@ export interface GameDataResourceGroupBtnInfo {
detail: Record<string, any> detail: Record<string, any>
} }
// 트래킹 타입
export interface GameDataTracking {
viewType: string
actionType: string
clickSarea: string
}
export interface GameDataResourceGroup { export interface GameDataResourceGroup {
btn_info?: GameDataResourceGroupBtnInfo btn_info?: GameDataResourceGroupBtnInfo
tracking: GameDataTracking tracking: TrackingObject
} }
type MarketPlatform = 'pc' | 'app_store' | 'google_play' type MarketPlatform = 'pc' | 'app_store' | 'google_play'
@@ -181,7 +176,7 @@ export interface GameDataMenu {
url_path: string url_path: string
link_target: string link_target: string
children: GameDataMenuChildren children: GameDataMenuChildren
tracking_json: string | GameDataTracking // JSON 문자열 또는 객체로 변경 tracking_json: string | TrackingObject // JSON 문자열 또는 객체로 변경
} }
// GNB 설정 타입 // GNB 설정 타입

View File

@@ -1,3 +1,5 @@
import type { TrackingObject } from '#layers/types/api/common'
// API 요청 파라미터 타입 // API 요청 파라미터 타입
export interface PageDataRequest { export interface PageDataRequest {
game_alias: string game_alias: string
@@ -51,7 +53,7 @@ export interface PageDataLnbMenu {
menu_name: string menu_name: string
target_type: number target_type: number
page_ver_tmpl_name_en: string page_ver_tmpl_name_en: string
tracking_json: Record<string, PageDataTracking> tracking_json: Record<string, TrackingObject>
children?: Record<string, PageDataLnbMenu> children?: Record<string, PageDataLnbMenu>
} }
@@ -93,6 +95,7 @@ export interface PageDataResourceGroupBtnInfo {
// 리소스 그룹 타입 // 리소스 그룹 타입
export interface PageDataResourceGroup { export interface PageDataResourceGroup {
group_label?: string
resource_type?: PageDataResourceGroupType resource_type?: PageDataResourceGroupType
res_path?: PageDataResourceGroupResPath res_path?: PageDataResourceGroupResPath
btn_info?: PageDataResourceGroupBtnInfo btn_info?: PageDataResourceGroupBtnInfo
@@ -101,7 +104,7 @@ export interface PageDataResourceGroup {
color_code?: string color_code?: string
color_name?: string color_name?: string
} }
tracking?: PageDataTracking tracking?: TrackingObject
} }
export type PageDataResourceGroups = PageDataResourceGroup[] export type PageDataResourceGroups = PageDataResourceGroup[]
@@ -171,15 +174,8 @@ export interface PageDataApiResult {
data: PageDataResponse | null data: PageDataResponse | null
error: string | null error: string | null
} }
// Tracking 타입
export interface PageDataTracking {
click_item: string
action_type: string
click_sarea: string
}
// Arrow 컴포넌트 타입 // Arrow 컴포넌트 타입
export type PageDataArrowComponent = PageDataTemplateComponent & { export type PageDataArrowComponent = PageDataTemplateComponent & {
groups: PageDataResourceGroups groups: PageDataResourceGroups
} }

View File

@@ -5,6 +5,7 @@ export default {
theme: { theme: {
extend: { extend: {
screens: { screens: {
xxs: '0px', // 0px ~ 359px
xs: '360px', // Mobile: 360px ~ 767px xs: '360px', // Mobile: 360px ~ 767px
sm: '768px', // Tablet: 768px ~ 1023px sm: '768px', // Tablet: 768px ~ 1023px
md: '1024px', // PC: 1024px ~ 1439px md: '1024px', // PC: 1024px ~ 1439px