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: props.analyticsSarea,
click_sarea: '',
},
} }
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"
> >
<slot /> <SplideTrack>
<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"
> >
<slot /> <SplideTrack>
<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"
> >
<slot /> <SplideTrack>
<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"
> >
<slot /> <SplideTrack>
<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,20 +159,26 @@ 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)"
> >
<SplideSlide <SplideTrack>
v-for="(item, index) in props.thumbnailData" <SplideSlide
:key="index" v-for="(item, index) in props.thumbnailData"
class="thumbnail-slide" :key="index"
> class="thumbnail-slide"
<AtomsImg @click="handleThumbnailClick(item?.group_label ?? `${index + 1}`)"
:src="getThumbnailSrc(item)" >
alt="thumbnail image" <AtomsImg
class="slide-image" :src="getThumbnailSrc(item)"
/> alt="thumbnail image"
</SplideSlide> class="slide-image"
/>
</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 eventLocale = analytics.eventLocale || ''
const eventCategory = `${analytics.eventCategory}`
// GA 클릭 이벤트 명 뒤에 언어 값 추가 노출되도록 개발. ex) GNB_자유게시판_KO
const eventLabel = `${eventCategory}_${eventLocale}`
gtag('set', 'cookie_domain', `${window?.location?.hostname || ''}`) // env 값으로 설정 시 쿠키 생성 안 돼서 window.location.hostname으로 설정
gtag('set', 'cookie_expires', '0') // 0으로 설정 시 쿠키가 Session 기반 쿠키로 전환
gtag('event', `${eventName}`, {
event_category: eventLabel,
})
} catch (e) {
console.error('[Exception] useAnalytics.sendGA: ', e)
} }
const baseEvent = pageName.value
const baseViewArea = pageNameEn.value
let logData = {} as AnalyticsDetailType
// 문자열 'view'만 들어오는 경우 (페이지뷰)
if (analytics === 'view') {
logData = {
actionType: analytics,
event: baseEvent,
eventCategory: baseEvent,
viewArea: baseViewArea,
viewType: 'pageView',
} as AnalyticsDetailType
return logData
}
// 객체인 경우
if (typeof analytics === 'object') {
const { action_type, click_sarea, click_item } = analytics
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 }) sendSA(analytics, { mcode: analytics.mcode, options: analytics.options })
// SA 전송 : actionType, logSourceType 유무로 판별 }
if ( }
analytics.actionType &&
analytics.actionType !== '' // ============================================================================
// analytics.logSourceType && // Google Analytics
// analytics.logSourceType !== '' // ============================================================================
) {
sendSA(analytics, { mcode: analytics.mcode, options: analytics.options }) /** 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) {
if (fbEventName) { sendGAEventOnly(eventName)
sendMetaPixel(fbEventName) } else if (analytics) {
} sendGA(analytics)
if (twEventName) { }
sendTwitterPixel(twEventName) break
} }
if (ttEventName) { case 'meta': {
sendTiktokPixel(ttEventName) sendMetaPixel(eventName)
break
}
case 'twitter': {
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>
@@ -199,167 +203,169 @@ onMounted(() => {
: 'w-full md:w-[420px] h-auto md:h-[392px] p-[10px] md:p-4 gap-3', : 'w-full md:w-[420px] h-auto md:h-[392px] p-[10px] md:p-4 gap-3',
]" ]"
> >
<!-- Card Content --> <!-- Card Content -->
<div <div
:class="[ :class="[
'flex flex-col text-left', 'flex flex-col text-left',
locale === 'ko' locale === 'ko'
? 'flex-1 p-[10px] md:p-3 lg:p-4 gap-[8px] md:gap-3' ? 'flex-1 p-[10px] md:p-3 lg:p-4 gap-[8px] md:gap-3'
: 'flex-1 p-[10px] md:p-4 gap-2 md:gap-3', : 'flex-1 p-[10px] md:p-4 gap-2 md:gap-3',
]" ]"
> >
<!-- Badge --> <!-- Badge -->
<div class="inline-flex"> <div class="inline-flex">
<span <span
:class="[
locale === 'ko'
? 'px-1.5 md:px-2 py-0.5 md:py-1 rounded-full text-[12px] md:text-[14px] font-medium leading-5'
: 'px-[6px] md:px-2 py-[2px] md:py-1 rounded-full text-[12px] md:text-[14px] font-medium leading-[18px] md:leading-5',
card.status === 'Y'
? 'bg-[#E2EAFF] text-[#3C75FF]'
: 'bg-[#EBEBEB] text-[#999999]',
]"
>
{{
card.status === 'Y'
? tm('Secure_Enabled')
: tm('Secure_Disabled')
}}
</span>
</div>
<!-- Title -->
<h4
:class="[ :class="[
'text-[#1F1F1F] font-bold',
locale === 'ko' locale === 'ko'
? 'px-1.5 md:px-2 py-0.5 md:py-1 rounded-full text-[12px] md:text-[14px] font-medium leading-5' ? 'text-[18px] md:text-[24px] leading-[26px] md:leading-[34px] tracking-[-0.54px] md:tracking-[-0.72px]'
: 'px-[6px] md:px-2 py-[2px] md:py-1 rounded-full text-[12px] md:text-[14px] font-medium leading-[18px] md:leading-5', : 'text-[18px] md:text-[24px] leading-[26px] md:leading-[34px] tracking-[-0.54px] md:tracking-[-0.72px]',
card.status === 'Y'
? 'bg-[#E2EAFF] text-[#3C75FF]'
: 'bg-[#EBEBEB] text-[#999999]',
]" ]"
> >
{{ {{ card.title }}
card.status === 'Y' </h4>
? tm('Secure_Enabled')
: tm('Secure_Disabled') <!-- Description -->
}} <p
</span> :class="[
'flex-1 text-[#999999] font-[400]',
locale === 'ko'
? 'text-[14px] md:text-base leading-[22px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.48px]'
: 'text-[14px] md:text-base leading-[24px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.48px]',
]"
>
{{ card.description }}
</p>
</div> </div>
<!-- Title --> <!-- Benefit Section -->
<h4
:class="[
'text-[#1F1F1F] font-bold',
locale === 'ko'
? 'text-[18px] md:text-[24px] leading-[26px] md:leading-[34px] tracking-[-0.54px] md:tracking-[-0.72px]'
: 'text-[18px] md:text-[24px] leading-[26px] md:leading-[34px] tracking-[-0.54px] md:tracking-[-0.72px]',
]"
>
{{ card.title }}
</h4>
<!-- Description -->
<p
:class="[
'flex-1 text-[#999999] font-[400]',
locale === 'ko'
? 'text-[14px] md:text-base leading-[22px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.48px]'
: 'text-[14px] md:text-base leading-[24px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.48px]',
]"
>
{{ card.description }}
</p>
</div>
<!-- Benefit Section -->
<div
:class="[
'rounded-2xl flex flex-col',
locale === 'ko'
? 'self-stretch p-[10px] md:p-4 gap-4'
: 'p-[10px] md:p-4 gap-3 md:gap-4',
card.benefitTitle ? 'bg-[#F0F4FF]' : '',
]"
>
<!-- Benefit Info -->
<div <div
v-if="card.benefitTitle"
:class="[ :class="[
'flex items-center', 'rounded-2xl flex flex-col',
locale === 'ko' ? 'gap-[12px]' : 'gap-2 md:gap-[12px]', locale === 'ko'
? 'self-stretch p-[10px] md:p-4 gap-4'
: 'p-[10px] md:p-4 gap-3 md:gap-4',
card.benefitTitle ? 'bg-[#F0F4FF]' : '',
]" ]"
> >
<!-- Benefit Info -->
<div <div
v-if="card.benefitIcon" v-if="card.benefitTitle"
:class="[ :class="[
'bg-[#3C75FF] rounded-[8px] flex items-center justify-center', 'flex items-center',
locale === 'ko' locale === 'ko' ? 'gap-[12px]' : 'gap-2 md:gap-[12px]',
? 'w-[48px] h-[48px]'
: 'w-[40px] h-[40px] md:w-[48px] md:h-[48px]',
]" ]"
> >
<img <div
:src=" v-if="card.benefitIcon"
formatPathHost(card.benefitIcon, { imageType: 'common' })
"
:alt="card.benefitTitle"
:class="[ :class="[
'object-contain rounded-2xl', 'bg-[#3C75FF] rounded-[8px] flex items-center justify-center',
locale === 'ko' locale === 'ko'
? 'w-[48px] h-[48px]' ? 'w-[48px] h-[48px]'
: 'w-[40px] h-[40px] md:w-[48px] md:h-[48px]', : 'w-[40px] h-[40px] md:w-[48px] md:h-[48px]',
]" ]"
loading="lazy"
draggable="false"
/>
</div>
<div class="flex-1 flex flex-col text-left">
<div
:class="[
'text-[#3C75FF] font-bold',
locale === 'ko'
? 'text-[14px] md:text-[18px] leading-[22px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.54px]'
: 'text-[14px] md:text-[18px] leading-[20px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.54px]',
]"
> >
{{ card.benefitTitle }} <img
:src="
formatPathHost(card.benefitIcon, {
imageType: 'common',
})
"
:alt="card.benefitTitle"
:class="[
'object-contain rounded-2xl',
locale === 'ko'
? 'w-[48px] h-[48px]'
: 'w-[40px] h-[40px] md:w-[48px] md:h-[48px]',
]"
loading="lazy"
draggable="false"
/>
</div> </div>
<div <div class="flex-1 flex flex-col text-left">
v-if="card.benefitDesc" <div
:class="[ :class="[
'text-[#3C75FF] font-[400] opacity-90', 'text-[#3C75FF] font-bold',
locale === 'ko' locale === 'ko'
? 'text-[12px] md:text-[13px] leading-[18px] md:leading-[22px] tracking-[-0.325px]' ? 'text-[14px] md:text-[18px] leading-[22px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.54px]'
: 'text-[12px] md:text-[13px] leading-[18px] md:leading-[22px] tracking-[-0.24px] md:tracking-[-0.325px]', : 'text-[14px] md:text-[18px] leading-[20px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.54px]',
]" ]"
> >
{{ card.benefitDesc }} {{ card.benefitTitle }}
</div>
<div
v-if="card.benefitDesc"
:class="[
'text-[#3C75FF] font-[400] opacity-90',
locale === 'ko'
? 'text-[12px] md:text-[13px] leading-[18px] md:leading-[22px] tracking-[-0.325px]'
: 'text-[12px] md:text-[13px] leading-[18px] md:leading-[22px] tracking-[-0.24px] md:tracking-[-0.325px]',
]"
>
{{ card.benefitDesc }}
</div>
</div> </div>
</div> </div>
</div>
<!-- Button --> <!-- Button -->
<AtomsButton <AtomsButton
v-if="card.status === 'N'" v-if="card.status === 'N'"
type="external" type="external"
button-size="size-small md:size-large" button-size="size-small md:size-large"
background-color="#383838" background-color="#383838"
text-color="#FFFFFF" text-color="#FFFFFF"
@click=" @click="
isLogin isLogin
? handleSecureSetting(card.url) ? handleSecureSetting(card.url)
: checkLoginValidation() : checkLoginValidation()
" "
>
<span>{{ tm('Secure_Action_setup') }}</span>
</AtomsButton>
<AtomsButton
v-else
type="action"
button-size="size-small md:size-large"
background-color="#EBEBEB"
text-color="#999999"
disabled
>
<span>{{ tm('Secure_Action_complete') }}</span>
<svg
width="16"
height="18"
viewBox="0 0 16 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
> >
<path <span>{{ tm('Secure_Action_setup') }}</span>
fill-rule="evenodd" </AtomsButton>
clip-rule="evenodd" <AtomsButton
d="M1.4298 2.80644L6.84645 0.240655C7.52385 -0.0802185 8.30948 -0.0802184 8.98688 0.240655L14.4035 2.80645C15.2767 3.22003 15.8333 4.09952 15.8333 5.06564V7.65038C15.8333 13.399 10.6191 16.1288 8.65401 16.9535C8.18024 17.1523 7.6531 17.1523 7.17932 16.9535C5.21423 16.1288 -0.000131724 13.399 2.49573e-09 7.65038L1.11287e-05 5.06566C6.95637e-06 4.09953 0.556675 3.22002 1.4298 2.80644ZM11.4226 7.4063C11.748 7.08086 11.748 6.55323 11.4226 6.22779C11.0972 5.90235 10.5695 5.90235 10.2441 6.22779L7.5 8.97187L6.00592 7.47779C5.68049 7.15235 5.15285 7.15235 4.82741 7.47779C4.50197 7.80323 4.50197 8.33086 4.82741 8.6563L6.91074 10.7396C7.23618 11.0651 7.76382 11.0651 8.08926 10.7396L11.4226 7.4063Z" v-else
fill="#999999" type="action"
/> button-size="size-small md:size-large"
</svg> background-color="#EBEBEB"
</AtomsButton> text-color="#999999"
</div> disabled
>
<span>{{ tm('Secure_Action_complete') }}</span>
<svg
width="16"
height="18"
viewBox="0 0 16 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.4298 2.80644L6.84645 0.240655C7.52385 -0.0802185 8.30948 -0.0802184 8.98688 0.240655L14.4035 2.80645C15.2767 3.22003 15.8333 4.09952 15.8333 5.06564V7.65038C15.8333 13.399 10.6191 16.1288 8.65401 16.9535C8.18024 17.1523 7.6531 17.1523 7.17932 16.9535C5.21423 16.1288 -0.000131724 13.399 2.49573e-09 7.65038L1.11287e-05 5.06566C6.95637e-06 4.09953 0.556675 3.22002 1.4298 2.80644ZM11.4226 7.4063C11.748 7.08086 11.748 6.55323 11.4226 6.22779C11.0972 5.90235 10.5695 5.90235 10.2441 6.22779L7.5 8.97187L6.00592 7.47779C5.68049 7.15235 5.15285 7.15235 4.82741 7.47779C4.50197 7.80323 4.50197 8.33086 4.82741 8.6563L6.91074 10.7396C7.23618 11.0651 7.76382 11.0651 8.08926 10.7396L11.4226 7.4063Z"
fill="#999999"
/>
</svg>
</AtomsButton>
</div>
</div> </div>
</div> </div>
@@ -372,29 +378,29 @@ onMounted(() => {
: 'w-full md:flex-1 p-5 md:p-8 gap-2 md:gap-3', : 'w-full md:flex-1 p-5 md:p-8 gap-2 md:gap-3',
]" ]"
> >
<h5 <h5
:class="[
'text-[#333333] font-bold',
locale === 'ko'
? 'text-[20px] leading-[30px] tracking-[-0.6px]'
: 'text-[16px] md:text-[20px] leading-[24px] md:leading-[30px] tracking-[-0.48px] md:tracking-[-0.6px]',
]"
>
{{ tm('Secure_Notice') }}
</h5>
<ul class="relative flex flex-col items-start justify-start w-full">
<li
v-for="caution in cautionText"
:key="caution"
v-dompurify-html="caution"
:class="[ :class="[
'relative pl-[22px] before:content-[\'\'] before:absolute before:top-[10px] before:left-[9px] before:w-[3px] before:h-[3px] before:rounded-full before:bg-[#999999] text-[#999999] font-[400]', 'text-[#333333] font-bold',
locale === 'ko' locale === 'ko'
? 'text-[14px] leading-[24px] tracking-[-0.42px]' ? 'text-[20px] leading-[30px] tracking-[-0.6px]'
: 'text-[14px] leading-[24px] tracking-[-0.42px]', : 'text-[16px] md:text-[20px] leading-[24px] md:leading-[30px] tracking-[-0.48px] md:tracking-[-0.6px]',
]" ]"
></li> >
</ul> {{ tm('Secure_Notice') }}
</h5>
<ul class="relative flex flex-col items-start justify-start w-full">
<li
v-for="caution in cautionText"
:key="caution"
v-dompurify-html="caution"
:class="[
'relative pl-[22px] before:content-[\'\'] before:absolute before:top-[10px] before:left-[9px] before:w-[3px] before:h-[3px] before:rounded-full before:bg-[#999999] text-[#999999] font-[400]',
locale === 'ko'
? 'text-[14px] leading-[24px] tracking-[-0.42px]'
: 'text-[14px] leading-[24px] tracking-[-0.42px]',
]"
></li>
</ul>
</div> </div>
</div> </div>
</div> </div>

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