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);
}
.splide-arrows {
.splide__arrows {
@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]
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;
md:w-[48px] md:h-[48px];
}
.arrow-prev {
.splide__arrow--prev {
@apply bg-[image:var(--arrow-prev)];
}
.arrow-next {
.splide__arrow--next {
@apply bg-[image:var(--arrow-next)];
}
.type-full .arrow-prev {
.type-full .splide__arrow--prev {
@apply left-10;
}
.type-full .arrow-next {
.type-full .splide__arrow--next {
@apply right-10;
}

View File

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

View File

@@ -3,6 +3,7 @@ interface Props {
title: string
description: string | number
imgPath: string | null
analyticsSarea: string
linkTarget?: '_blank' | '_self'
url?: string
alt?: string
@@ -10,8 +11,9 @@ interface Props {
}
const props = defineProps<Props>()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const { sendLog, useAnalyticsData } = useAnalytics()
const isNoImage = computed(() => {
return !props.imgPath || props.imgPath === null
@@ -21,14 +23,12 @@ const isShowOverlay = computed(() => {
})
const handleLinkClick = (title: string) => {
const trackingData = {
tracking: {
click_item: title,
action_type: 'click',
click_sarea: '',
},
const analytics = {
click_item: title,
action_type: 'click',
click_sarea: props.analyticsSarea,
}
sendLog(locale.value, useAnalyticsLogDataDirect(trackingData, 1))
sendLog(locale.value, useAnalyticsData(analytics))
}
</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'
const MORE_WIDTH = 72
const START_WIDTH_MARGIN = 40
const START_MARGIN = 40
const route = useRoute()
const { tm } = useI18n()
const { locale, tm } = useI18n()
const { width } = useWindowSize()
const gameDataStore = useGameDataStore()
const pageDataStore = usePageDataStore()
const scrollStore = useScrollStore()
const breakpoints = useResponsiveBreakpoints()
const { sendLog, useAnalyticsData } = useAnalytics()
const { gameData } = storeToRefs(gameDataStore)
const { pageLayoutType } = storeToRefs(pageDataStore)
@@ -95,7 +96,7 @@ const calculateOverflow = () => {
}
const screenWidth = width.value
const totalNavWidth = navWidth.value + startWidth.value + START_WIDTH_MARGIN
const totalNavWidth = navWidth.value + startWidth.value + START_MARGIN
// 해상도가 navWidth + startWidth보다 작은 경우
if (screenWidth < totalNavWidth) {
@@ -120,18 +121,6 @@ const calculateOverflow = () => {
// 100ms마다 최대 1회 실행
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) => {
return gnbItem.click_action_type === 0
}
@@ -140,6 +129,32 @@ const has2depthButton = (gnbItem: GameDataMenu) => {
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(() => {
overflowCount.value = 0
isMounted.value = true
@@ -170,7 +185,11 @@ onMounted(() => {
v-if="gnbData"
: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
:src="formatPathHost(gnbData?.bi_path)"
:alt="gameData?.game_name"
@@ -183,11 +202,14 @@ onMounted(() => {
</button>
<div
:class="['nav-wrap', { 'is-open': isMenuOpen }]"
@click="handleMenuClose()"
@click="handleMenuClose"
>
<div ref="navAreaRef" class="nav-area" @click.stop>
<div class="nav-logo">
<AtomsLocaleLink to="/home" @click="handleMenuClose">
<AtomsLocaleLink
to="/home"
@click="[handleMenuClose(), handleSendLog('BI')]"
>
<img
:src="formatPathHost(gnbData?.bi_path)"
:alt="gameData?.game_name"
@@ -219,7 +241,7 @@ onMounted(() => {
'router-link-active': hasActiveChild(gnbItem.children),
},
]"
@click="handleMenuClose(isNotClickable(gnbItem))"
@click="handleGnbItemClick(gnbItem)"
>
<span>{{ gnbItem.menu_name }}</span>
<AtomsIconsWebLinkLine
@@ -248,7 +270,7 @@ onMounted(() => {
:to="child.url_path"
:target="child.link_target"
class="item-link"
@click="handleMenuClose(isNotClickable(child))"
@click="handleGnbItemClick(child)"
>
<span>{{ child.menu_name }}</span>
<AtomsIconsWebLinkLine
@@ -293,7 +315,7 @@ onMounted(() => {
),
},
]"
@click="handleMenuClose(isNotClickable(gnbItem))"
@click="handleGnbItemClick(gnbItem)"
>
<span>{{ gnbItem.menu_name }}</span>
</component>
@@ -340,7 +362,7 @@ onMounted(() => {
'nav-1depth',
{ 'router-link-active': pageLayoutType === 'promotion' },
]"
@click="handleMenuClose"
@click="[handleMenuClose(), handleSendLog('이벤트')]"
>
<span
class="flex items-center gap-1 flex-1 text-gradient-pink"
@@ -367,6 +389,9 @@ onMounted(() => {
:text-color="
getColorCodeFromData(start1depthData?.btn_info, 'txt')
"
@click="
sendLog(locale, useAnalyticsData(start1depthData.tracking))
"
>
{{ start1depthData?.btn_info?.txt_btn_name }}
</BlocksButtonLauncher>
@@ -376,7 +401,13 @@ onMounted(() => {
>
<ul>
<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 }}
</BlocksButtonLauncher>
</li>
@@ -385,7 +416,7 @@ onMounted(() => {
</template>
</div>
</ClientOnly>
<button class="btn-close" @click="handleMenuClose()">
<button class="btn-close" @click="handleMenuClose">
<AtomsIconsCloseLine
size="24"
color="var(--foreground-reversal)"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { Splide, SplideSlide } from '@splidejs/vue-splide'
import { useSplideArrow } from '#layers/composables/useSplideArrow'
import { Splide, SplideTrack, SplideSlide } from '@splidejs/vue-splide'
import type { Splide as SplideType, Options } from '@splidejs/splide'
import type {
PageDataResourceGroups,
@@ -8,10 +7,12 @@ import type {
} from '#layers/types/api/pageData'
interface Props {
drag?: boolean
thumbnailData: PageDataResourceGroup[]
paginationData?: PageDataResourceGroups
paginationData: PageDataResourceGroups
analyticsSarea: string
drag?: boolean
arrows?: boolean
arrowsData?: PageDataResourceGroups
variant?: 'default' | 'media'
}
@@ -21,13 +22,13 @@ const props = withDefaults(defineProps<Props>(), {
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 })
// Splide composable
const { addArrowClickListeners } = useSplideArrow()
let mainInst: SplideType | null = null
let thumbsInst: SplideType | null = null
let removeArrowListeners: (() => void) | null = null
@@ -62,12 +63,6 @@ const thumbOptions = computed<Options>(() => ({
isNavigation: true,
updateOnMove: true,
flickPower: 300,
classes: {
arrows: 'splide-arrows',
arrow: 'splide-arrow',
prev: 'arrow-prev',
next: 'arrow-next',
},
breakpoints: {
[BREAKPOINTS.md - 1]: {
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(() => {
mainInst = mainRef.value?.splide ?? null
thumbsInst = thumbsRef.value?.splide ?? null
if (mainInst && 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"
ref="thumbsRef"
:options="thumbOptions"
:arrows-data="props.arrowsData"
:has-track="false"
class="thumbnail-splide"
:style="getPaginationClass(paginationData)"
>
<SplideSlide
v-for="(item, index) in props.thumbnailData"
:key="index"
class="thumbnail-slide"
>
<AtomsImg
:src="getThumbnailSrc(item)"
alt="thumbnail image"
class="slide-image"
/>
</SplideSlide>
<SplideTrack>
<SplideSlide
v-for="(item, index) in props.thumbnailData"
:key="index"
class="thumbnail-slide"
@click="handleThumbnailClick(item?.group_label ?? `${index + 1}`)"
>
<AtomsImg
:src="getThumbnailSrc(item)"
alt="thumbnail image"
class="slide-image"
/>
</SplideSlide>
</SplideTrack>
<BlocksSlideArrows v-if="props.arrows" :arrows-data="props.arrowsData" />
</Splide>
</div>
</template>
@@ -226,10 +228,10 @@ onBeforeUnmount(() => {
.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];
}
.thumbnail-carousel.thumbnail-default:deep(.arrow-prev) {
.thumbnail-carousel.thumbnail-default:deep(.splide__arrow--prev) {
@apply left-0;
}
.thumbnail-carousel.thumbnail-default:deep(.arrow-next) {
.thumbnail-carousel.thumbnail-default:deep(.splide__arrow--next) {
@apply right-0;
}
.thumbnail-carousel.thumbnail-default .thumbnail-slide {
@@ -253,10 +255,10 @@ onBeforeUnmount(() => {
sm:max-w-[calc(100%+80px)] sm:mx-[-40px]
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];
}
.thumbnail-carousel.thumbnail-media:deep(.arrow-next) {
.thumbnail-carousel.thumbnail-media:deep(.splide__arrow--next) {
@apply right-[0];
}
.thumbnail-carousel.thumbnail-media .thumbnail-slide {

View File

@@ -1,5 +1,5 @@
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 {
IdentityInfo,
@@ -11,63 +11,9 @@ declare const svcLog: any
declare const twq: 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으로부터 값 추출하여 세팅
const findValueFromOption = (target: string, { options = {} }: any) => {
@@ -92,69 +38,112 @@ const findValueFromOption = (target: string, { options = {} }: any) => {
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 전송
*
* @param {AnalyticsDetailType} analytics
* @param {object} options
* 로그 데이터를 생성하는 composable (직접 객체 반환)
* @param analytics 트래킹 데이터
* @param type 'page' | 'template'
* @returns 분석용 로그 데이터 객체 (ref 없이)
*/
const sendGA = (analytics: AnalyticsDetailType, { options = {} }: any) => {
if (!import.meta.client) return
export const useAnalyticsData = (
analytics: string | TrackingObject,
type: 'page' | 'template' = 'template'
): Partial<AnalyticsDetailType> => {
const pageDataStore = usePageDataStore()
const { pageName, pageNameEn } = storeToRefs(pageDataStore)
try {
const { gtag } = useGtag()
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)
if (!analytics || !pageName.value || !pageNameEn.value) {
return {}
}
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) 전송
*
* @param {AnalyticsDetailType} analytics
* @param {string} mcode
* @param {object} options
* @param analytics Partial<AnalyticsDetailType>
* @param param1 mcode, options
*/
const sendSA = (
analytics: AnalyticsDetailType,
{ mcode = '', options = {} }: any
analytics: Partial<AnalyticsDetailType>,
{ mcode = '', options = {} }: { mcode?: string; options?: any } = {}
) => {
if (!import.meta.client) return
if (!isClient()) return
if (isEmptyAnalytics(analytics)) return
const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore)
try {
const gameNo = gameData.value.game_code
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 memberNo = `${csrGetStoveMemberNo()}`
const actionType = analytics.actionType || ''
const logSourceType = analytics.logSourceType || ''
const viewArea = analytics.viewArea || ''
const viewType = analytics.viewType || ''
const clickArea = analytics.clickArea || ''
const clickSarea = findValueFromOption(analytics.clickSarea || '', {
options,
})
const eventLocale = analytics.eventLocale || ''
const {
actionType = '',
logSourceType = '',
viewArea = '',
viewType = '',
clickArea = '',
clickSarea = '',
eventLocale = '',
clickItem,
} = analytics
const identityInfo: IdentityInfo = {
app_id: 'stove',
@@ -173,7 +162,7 @@ const sendSA = (
media_page: '',
}
let actionParam = {}
let actionParam: ActionInfo['action_param'] = {}
if (actionType === 'view') {
actionParam = {
@@ -190,7 +179,7 @@ const sendSA = (
click_area: clickArea,
click_sarea: clickSarea,
click_item: {
pwt_click_item: analytics.clickItem,
pwt_click_item: clickItem,
game_no: gameNo,
lang_cd: eventLocale,
...options?.clickItem,
@@ -206,8 +195,8 @@ const sendSA = (
const amplitudeActionInfo = {
...actionInfo,
url: import.meta.client ? `${location?.href || ''}` : '',
agent: import.meta.client ? `${navigator?.userAgent || ''}` : '',
url: isClient() ? `${location?.href || ''}` : '',
agent: isClient() ? `${navigator?.userAgent || ''}` : '',
}
const amplitudeActionParams: {
@@ -218,66 +207,107 @@ const sendSA = (
event_properties: amplitudeActionInfo,
}
// 81plug
svcLog.identity(identityInfo)
svcLog.action(actionInfo, {}, {}) // 81plug warning log 제거를 위해 2번째 인자부터 빈 객체 세팅
// 81plug warning log 제거를 위해 2번째 인자부터 빈 객체 세팅
svcLog.action(actionInfo, {}, {})
// Amplitude
amplitude.track(amplitudeActionParams)
} catch (e) {
console.error('[Exception] useAnalytics.sendSA: ', e)
}
}
// ============================================================================
// 기본 로그 일괄 전송
// ============================================================================
/**
* 기본 로그 일괄 전송
*
* @param {string} locale
* @param {AnalyticsDetailType} analytics
* @param locale 언어 코드
* @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()
if (analytics) {
// GA 전송 : eventCategory 유무로 판별
sendGA(analytics, { options: analytics.options })
// SA 전송 : actionType, logSourceType 유무로 판별
if (
analytics.actionType &&
analytics.actionType !== ''
// analytics.logSourceType &&
// analytics.logSourceType !== ''
) {
sendSA(analytics, { mcode: analytics.mcode, options: analytics.options })
}
// SA 전송 : actionType, logSourceType 유무로 판별 (logSourceType 체크는 주석 처리 되어있던 로직 유지)
if (analytics.actionType && analytics.actionType !== '') {
sendSA(analytics, { mcode: analytics.mcode, options: analytics.options })
}
}
// ============================================================================
// Google Analytics
// ============================================================================
/** GA 공통 설정 래퍼 */
const withGA = (callback: (gtag: any) => void) => {
if (!isClient()) return
try {
const { gtag } = useGtag()
const hostname = window?.location?.hostname || ''
gtag('set', 'cookie_domain', hostname) // env 값으로 설정 시 쿠키 생성 안 돼서 window.location.hostname으로 설정
gtag('set', 'cookie_expires', '0') // 0으로 설정 시 쿠키가 Session 기반 쿠키로 전환
callback(gtag)
} catch (e) {
console.error('[Exception] useAnalytics.withGA: ', e)
}
}
/**
* Google Analytics 전송 (기본 이벤트만 전송)
*
* @param {string} gaEventName
* @param gaEventName 이벤트명
*/
const sendGAEventOnly = (gaEventName: string) => {
if (!import.meta.client) return
if (!gaEventName) return
try {
const { gtag } = useGtag()
gtag('set', 'cookie_domain', `${window?.location?.hostname || ''}`) // env 값으로 설정 시 쿠키 생성 안 돼서 window.location.hostname으로 설정
gtag('set', 'cookie_expires', '0') // 0으로 설정 시 쿠키가 Session 기반 쿠키로 전환
withGA(gtag => {
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()] }) 선언
*/
const sendMetaPixel = (fbEventName: string) => {
if (!import.meta.client) return
const sendMetaPixel = (fbEventName?: string) => {
if (!isClient() || !fbEventName) return
try {
const { $fbq } = useNuxtApp()
@@ -292,11 +322,11 @@ const sendMetaPixel = (fbEventName: string) => {
/**
* X(트위터) 픽셀 전송
*
* @param {string} twEventName
* @param twEventName 이벤트명
* @description 수집 대상 페이지에 useHead({ script: [loadTwitterPixelScript()] }) 선언
*/
const sendTwitterPixel = (twEventName: string) => {
if (!import.meta.client) return
const sendTwitterPixel = (twEventName?: string) => {
if (!isClient() || !twEventName) return
try {
twq('event', twEventName, {})
@@ -308,11 +338,11 @@ const sendTwitterPixel = (twEventName: string) => {
/**
* 틱톡 픽셀 전송
*
* @param {string} ttEventName
* @param ttEventName 이벤트명
* @description 수집 대상 페이지에 onMounted(() => { loadTikTokPixelScript() }) 선언
*/
const sendTiktokPixel = (ttEventName: string) => {
if (!import.meta.client) return
const sendTiktokPixel = (ttEventName?: string) => {
if (!isClient() || !ttEventName) return
try {
ttq.track(ttEventName)
@@ -321,45 +351,61 @@ const sendTiktokPixel = (ttEventName: string) => {
}
}
// ============================================================================
// 마케팅 스크립트 일괄 전송
// ============================================================================
/**
* 마케팅 인텔리전스 팀 요청 마케팅 스크립트 일괄 전송
*
* @param {string} gaEventName
* @param {string} fbEventName
* @param {string} twEventName
* @param {string} ttEventName
* @param logName 'ga' | 'meta' | 'twitter' | 'tiktok'
* @param eventName 이벤트명 (ga: eventOnly, meta/twitter/tiktok: 필수)
* @param analytics GA용 analytics 데이터 (eventName 없을 때만 사용)
*/
const sendMarketingScript = ({
gaEventName,
fbEventName,
twEventName,
ttEventName,
const sendMarketingLog = ({
logName,
eventName,
analytics,
}: {
gaEventName?: string
fbEventName?: string
twEventName?: string
ttEventName?: string
logName: 'ga' | 'meta' | 'twitter' | 'tiktok'
eventName?: string
analytics?: Partial<AnalyticsDetailType>
}) => {
if (gaEventName) {
sendGAEventOnly(gaEventName)
}
if (fbEventName) {
sendMetaPixel(fbEventName)
}
if (twEventName) {
sendTwitterPixel(twEventName)
}
if (ttEventName) {
sendTiktokPixel(ttEventName)
switch (logName) {
case 'ga': {
if (eventName) {
sendGAEventOnly(eventName)
} else if (analytics) {
sendGA(analytics)
}
break
}
case 'meta': {
sendMetaPixel(eventName)
break
}
case 'twitter': {
sendTwitterPixel(eventName)
break
}
case 'tiktok': {
sendTiktokPixel(eventName)
break
}
default:
break
}
}
// ============================================================================
// export
// ============================================================================
export default () => {
return {
sendGA,
sendSA,
sendLog,
sendMarketingScript,
useAnalyticsLogDataDirect,
sendMarketingLog,
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 { ReqCouponList } from '#layers/types/api/couponData'
// Props
interface Props {
id?: string
components: PageDataTemplateComponents
pageVerTmplSeq: number
pageVerTmplNameEn: string
}
const props = defineProps<Props>()
// Configuration
provide('pageVerTmplNameEn', props.pageVerTmplNameEn)
const runtimeConfig = useRuntimeConfig()
const stoveApiUrl = runtimeConfig.public.stoveApiUrl as string
const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_coupon.json'
@@ -486,7 +487,6 @@ onMounted(async () => {
<template>
<WidgetsFixMainTitle
:id="props.id"
:title="tm('Coupon_Page_Title')"
:resources-data="backgroundData"
/>

View File

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

View File

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

View File

@@ -4,9 +4,9 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
// Props
interface Props {
id?: string
components: PageDataTemplateComponents
pageVerTmplSeq: number
pageVerTmplNameEn: string
}
const props = defineProps<Props>()
@@ -148,7 +148,6 @@ onMounted(() => {
<template>
<WidgetsFixMainTitle
:id="props.id"
:title="tm('Secure_Page_Title') || '보안 강화 캠페인'"
:resources-data="backgroundData"
class="mx-auto"
@@ -156,7 +155,9 @@ onMounted(() => {
<div class="section-container static">
<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) -->
<div
v-if="locale !== 'ko'"
@@ -170,7 +171,10 @@ onMounted(() => {
<p
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>
</div>
@@ -199,167 +203,169 @@ onMounted(() => {
: 'w-full md:w-[420px] h-auto md:h-[392px] p-[10px] md:p-4 gap-3',
]"
>
<!-- Card Content -->
<div
:class="[
'flex flex-col text-left',
locale === 'ko'
? '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',
]"
>
<!-- Badge -->
<div class="inline-flex">
<span
<!-- Card Content -->
<div
:class="[
'flex flex-col text-left',
locale === 'ko'
? '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',
]"
>
<!-- Badge -->
<div class="inline-flex">
<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="[
'text-[#1F1F1F] font-bold',
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]',
? '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.status === 'Y'
? tm('Secure_Enabled')
: tm('Secure_Disabled')
}}
</span>
{{ 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>
<!-- Title -->
<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 -->
<!-- Benefit Section -->
<div
v-if="card.benefitTitle"
:class="[
'flex items-center',
locale === 'ko' ? 'gap-[12px]' : 'gap-2 md:gap-[12px]',
'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
v-if="card.benefitIcon"
v-if="card.benefitTitle"
:class="[
'bg-[#3C75FF] rounded-[8px] flex items-center justify-center',
locale === 'ko'
? 'w-[48px] h-[48px]'
: 'w-[40px] h-[40px] md:w-[48px] md:h-[48px]',
'flex items-center',
locale === 'ko' ? 'gap-[12px]' : 'gap-2 md:gap-[12px]',
]"
>
<img
:src="
formatPathHost(card.benefitIcon, { imageType: 'common' })
"
:alt="card.benefitTitle"
<div
v-if="card.benefitIcon"
:class="[
'object-contain rounded-2xl',
'bg-[#3C75FF] rounded-[8px] flex items-center justify-center',
locale === 'ko'
? 'w-[48px] 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
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 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 }}
</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>
<!-- Button -->
<AtomsButton
v-if="card.status === 'N'"
type="external"
button-size="size-small md:size-large"
background-color="#383838"
text-color="#FFFFFF"
@click="
isLogin
? handleSecureSetting(card.url)
: 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"
<!-- Button -->
<AtomsButton
v-if="card.status === 'N'"
type="external"
button-size="size-small md:size-large"
background-color="#383838"
text-color="#FFFFFF"
@click="
isLogin
? handleSecureSetting(card.url)
: checkLoginValidation()
"
>
<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>
<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
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>
@@ -372,29 +378,29 @@ onMounted(() => {
: 'w-full md:flex-1 p-5 md:p-8 gap-2 md:gap-3',
]"
>
<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"
<h5
: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'
? 'text-[14px] leading-[24px] tracking-[-0.42px]'
: 'text-[14px] leading-[24px] tracking-[-0.42px]',
? 'text-[20px] leading-[30px] tracking-[-0.6px]'
: '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>

View File

@@ -6,13 +6,17 @@ import {
getComponentContainer,
} from '#layers/utils/dataUtil'
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'
interface Props {
id?: string
components: PageDataTemplateComponents
pageVerTmplSeq: number
pageVerTmplNameEn: string
}
const props = defineProps<Props>()
@@ -49,6 +53,23 @@ const officialUrlData = computed(
() => 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(
`fx-video-01-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`,
async () => {
@@ -137,11 +158,12 @@ const handleLoadMoreRecent = () => {
<div
class="relative content-static bg-[#fff] rounded-[12px] md:rounded-[16px]"
>
<BlocksSlideFade
<WidgetsSlideFade
v-model:index="currentRecommendedIndex"
:autoplay="recommendedVideos.length > 1"
:interval="3000"
:arrows="recommendedVideos.length > 1"
:arrows-data="arrowsData"
:pagination="false"
:drag="false"
>
@@ -185,7 +207,7 @@ const handleLoadMoreRecent = () => {
</p>
</div>
</SplideSlide>
</BlocksSlideFade>
</WidgetsSlideFade>
<div v-if="recommendedVideos.length > 1" class="splide-pagination">
<span class="font-[700] text-[#1F1F1F]">
{{ currentRecommendedIndex + 1 }}
@@ -253,20 +275,20 @@ const handleLoadMoreRecent = () => {
.splide {
@apply pb-[68px] sm:pb-[0];
}
.splide:deep(.splide-arrows) {
.splide:deep(.splide__arrows) {
@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
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')]
sm:left-[calc(60.3%+21px)]
md:left-[calc(56%+39px)]
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')]
sm:right-[28px]
md:right-[unset] md:left-[calc(56%+99px)]

View File

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

View File

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

View File

@@ -9,10 +9,14 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props {
components: PageDataTemplateComponents
pageVerTmplSeq: number
pageVerTmplNameEn: string
}
const props = defineProps<Props>()
const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics()
const splideRef = ref<SplideSlide | null>(null)
const currentSlideIndex = ref<number | null>(null)
@@ -23,17 +27,24 @@ const paginationData = computed(() => {
return getComponentGroupAry(props.components, 'pagination')
})
const goToSlide = (index: number) => {
const goToSlide = (index: number, title: string) => {
const splide = splideRef.value?.splide
const paginationAnalytics = {
action_type: 'click',
click_item: title,
click_sarea: props.pageVerTmplNameEn,
}
if (splide) {
splide.go(index)
sendLog(locale.value, useAnalyticsData(paginationAnalytics))
}
}
</script>
<template>
<section class="section-standard">
<BlocksSlideFade
<WidgetsSlideFade
v-if="slideData"
ref="splideRef"
v-model:index="currentSlideIndex"
@@ -66,7 +77,7 @@ const goToSlide = (index: number) => {
/>
</div>
</SplideSlide>
</BlocksSlideFade>
</WidgetsSlideFade>
<div
v-if="slideData && slideData.length > 1"
class="splide-pagination"
@@ -88,7 +99,12 @@ const goToSlide = (index: number) => {
'btn-pagination',
{ 'is-active': currentSlideIndex === index },
]"
@click="goToSlide(index)"
@click="
goToSlide(
index,
getComponentGroup(item, 'pagenaviTitle')?.display?.text || ''
)
"
>
<span class="item-bullet"></span>
<span class="item-title">
@@ -105,7 +121,7 @@ const goToSlide = (index: number) => {
<style scoped>
.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 {
@apply relative;
@@ -122,7 +138,7 @@ const goToSlide = (index: number) => {
color: var(--pagination-disabled);
}
.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);
}
.progress-fill {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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