fix. 컴포넌트 dataUtil 통일

This commit is contained in:
clkim
2025-10-21 12:18:06 +09:00
parent e86920b496
commit fdf1d9175c
18 changed files with 159 additions and 184 deletions

View File

@@ -10,28 +10,25 @@ interface Props {
}
const props = defineProps<Props>()
const {locale} = useI18n()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const handleLinkClick = (title) => {
const handleLinkClick = (title: string) => {
const trackingData = {
tracking: {
click_item: title,
action_type: 'click',
click_sarea: ''
}
click_sarea: '',
},
}
sendLog(locale.value, useAnalyticsLogDataDirect(trackingData, 1))
}
</script>
<template>
<div
v-if="props.title || props.description"
:class="`card-news ${props.class || ''}`"
@click="handleLinkClick(props.title)"
>
<img
:src="props.imgPath"
@@ -52,6 +49,7 @@ const handleLinkClick = (title) => {
:href="props.url"
:target="props.linkTarget"
class="card-link"
@click="handleLinkClick(props.title)"
/>
</div>
</template>

View File

@@ -34,7 +34,7 @@ const options = computed((): ResponsiveOptions => {
focus: 'center',
autoWidth: true,
autoHeight: true,
speed: 400,
speed: 500,
updateOnMove: true,
arrows: props.arrows && isMultipleItems.value,
pagination: props.pagination && isMultipleItems.value,
@@ -86,7 +86,7 @@ const style = computed(() => {
const handleSplideMounted = (splide: SplideType) => {
emit('mounted', splide)
splide.refresh()
// 화살표 버튼 클릭 이벤트 리스너 추가
nextTick(() => {
addArrowClickListeners(splide, (direction, targetIndex) => {
@@ -103,18 +103,6 @@ const handleMove = (
) => {
emit('move', splide, newIndex, oldIndex, destIndex)
}
const handleMoved = (
splide: SplideType,
newIndex: number,
oldIndex: number,
destIndex: number
) => {
emit('moved', splide, newIndex, oldIndex, destIndex)
}
</script>
<template>
@@ -123,7 +111,6 @@ const handleMoved = (
:options="options"
@splide:mounted="handleSplideMounted"
@splide:move="handleMove"
@splide:moved="handleMoved"
>
<slot />
</Splide>

View File

@@ -33,7 +33,7 @@ const options = computed((): ResponsiveOptions => {
focus: 'center',
autoWidth: true,
autoHeight: true,
speed: 400,
speed: 500,
updateOnMove: true,
arrows: props.arrows && isMultipleItems.value,
pagination: props.pagination && isMultipleItems.value,
@@ -88,7 +88,7 @@ const style = computed(() => {
const handleSplideMounted = (splide: SplideType) => {
emit('mounted', splide)
splide.refresh()
// 화살표 버튼 클릭 이벤트 리스너 추가
nextTick(() => {
addArrowClickListeners(splide, (direction, targetIndex) => {

View File

@@ -28,7 +28,7 @@ const options = computed((): ResponsiveOptions => {
rewind: true,
perPage: 1,
perMove: 1,
speed: 600,
speed: 500,
updateOnMove: true,
autoplay: props.autoplay,
pauseOnHover: false,

View File

@@ -42,7 +42,7 @@ const mainOptions = computed<Options>(() => ({
rewind: true,
perPage: 1,
perMove: 1,
speed: 600,
speed: 500,
arrows: false,
pagination: false,
drag: props.drag,

View File

@@ -13,6 +13,9 @@ interface ButtonListProps {
const props = defineProps<ButtonListProps>()
const { gameData } = useGameDataStore()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const BUTTON_TYPE_MAP = {
URL: {
_self: 'internal' as const,
@@ -54,11 +57,14 @@ const getButtonBackgroundImage = (
return ''
}
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const handleButtonClick = (btnInfo: PageDataResourceGroupBtnInfo, index: any) => {
sendLog(locale.value, useAnalyticsLogDataDirect(props.resourcesData[index], props.pageVerTmplSeq))
const handleButtonClick = (
btnInfo: PageDataResourceGroupBtnInfo,
index: any
) => {
sendLog(
locale.value,
useAnalyticsLogDataDirect(props.resourcesData[index], props.pageVerTmplSeq)
)
const marketType = btnInfo?.detail?.market_type
if (marketType) {
@@ -100,7 +106,7 @@ const handleButtonClick = (btnInfo: PageDataResourceGroupBtnInfo, index: any) =>
}"
@click="handleButtonClick(button.btn_info, index)"
>
{{ button.btn_info?.txt_btn_name }}
{{ button.btn_info?.txt_btn_name }}
</AtomsButton>
</div>
</template>

View File

@@ -6,19 +6,19 @@ const props = defineProps<{
pageVerTmplSeq: number
}>()
// YouTube 모달 스토어 사용
const modalStore = useModalStore()
const {locale} = useI18n()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
// 비디오 플레이 버튼 클릭 핸들러
const handleVideoPlayClick = () => {
const youtubeUrl = props.resourcesData?.display?.text ?? ''
modalStore.handleOpenYoutube({ youtubeUrl })
sendLog(locale.value, useAnalyticsLogDataDirect(props.resourcesData, props.pageVerTmplSeq))
sendLog(
locale.value,
useAnalyticsLogDataDirect(props.resourcesData, props.pageVerTmplSeq)
)
}
</script>

View File

@@ -1,9 +1,11 @@
import * as amplitude from '@amplitude/analytics-browser'
import type {
AnalyticsDetailType,
} from '../types/AnalyticsType'
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
import type { IdentityInfo, ActionInfo, MarketingInfo } from '../types/Stove'
import type { AnalyticsDetailType } from '#layers/types/AnalyticsType'
import type {
IdentityInfo,
ActionInfo,
MarketingInfo,
} from '#layers/types/Stove'
declare const svcLog: any
declare const twq: any
@@ -72,20 +74,20 @@ export const useAnalyticsLogDataDirect = (
// logSourceType:pageDataTrack.logSourceType,
// viewArea:pageDataTrack.viewArea,
// viewType:pageDataTrack.viewType,
clickArea:pageData.page_name_en,
clickArea: pageData.page_name_en,
clickSarea: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name_en,
clickItem: `${pageData.templates[pageVerTmplSeq].page_ver_tmpl_name}_${pageDataTrack?.click_item}`,
event: pageData.page_name,
eventCategory: `${pageData.page_name}_${pageDataTrack?.click_item}`,
template_code: pageData.templates[pageVerTmplSeq].template_code,
page_ver_tmpl_name: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name,
page_ver_tmpl_name_en: pageData.templates[pageVerTmplSeq].page_ver_tmpl_name_en,
page_ver_tmpl_name_en:
pageData.templates[pageVerTmplSeq].page_ver_tmpl_name_en,
} as unknown as AnalyticsDetailType
return logData
}
// target에 {XX1, XX2}와 같은 형태가 포함되어 있을 경우 options.clickItem으로부터 값 추출하여 세팅
const findValueFromOption = (target: string, { options = {} }: any) => {
if (target.includes('{') && target.includes('}')) {
@@ -362,5 +364,12 @@ const sendMarketingScript = ({
}
export default () => {
return { sendGA, sendSA, sendLog, sendMarketingScript, useAnalyticsLogData, useAnalyticsLogDataDirect }
return {
sendGA,
sendSA,
sendLog,
sendMarketingScript,
useAnalyticsLogData,
useAnalyticsLogDataDirect,
}
}

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import { SplideSlide } from '@splidejs/vue-splide'
import {
getComponentContainer,
getComponentGroupAry,
} from '#layers/utils/dataUtil'
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props {

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import { SplideSlide } from '@splidejs/vue-splide'
import {
getComponentContainer,
getComponentGroupAry,
} from '#layers/utils/dataUtil'
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props {

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import { SplideSlide } from '@splidejs/vue-splide'
import {
getComponentContainer,
getComponentGroupAry,
} from '#layers/utils/dataUtil'
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
interface Props {

View File

@@ -1,5 +1,10 @@
<script setup lang="ts">
import { SplideSlide } from '@splidejs/vue-splide'
import {
getComponentContainer,
getComponentGroupAry,
getComponentGroup,
} from '#layers/utils/dataUtil'
import type {
PageDataTemplateComponents,
PageDataTemplateComponentSet,
@@ -61,7 +66,10 @@ const handleVideoClick = (index: number) => {
}
const stopVideo = () => {
playingSlideIndex.value = null
// 전환 시간 후 완전히 제거
setTimeout(() => {
playingSlideIndex.value = null
}, 400)
}
const onArrowClick = (direction, targetIndex) => {
@@ -74,7 +82,7 @@ onMounted(() => {
nextTick(() => {
const mainInst = slideThumbnailRef.value?.mainInst
if (mainInst) {
mainInst.on('moved', stopVideo)
mainInst.on('move', stopVideo)
}
})
})
@@ -106,7 +114,9 @@ onMounted(() => {
:src="getMediaImgSrcFromItem(item)"
alt="main image"
class="slide-image"
:class="{ 'opacity-0': playingSlideIndex === index }"
:class="{
'opacity-0': playingSlideIndex === index,
}"
/>
<AtomsButtonPlay
v-if="isPassVideo(item, index)"
@@ -137,6 +147,9 @@ onMounted(() => {
.main-slide {
@apply relative aspect-[16/9];
}
.slide-image {
@apply transition-opacity duration-500 ease-in-out;
}
.btn-play {
@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
}

View File

@@ -1,9 +1,6 @@
<script setup lang="ts">
import { SplideSlide } from '@splidejs/vue-splide'
import {
getComponentGroup,
ensureMinimumSlideData,
} from '#layers/utils/dataUtil'
import { getComponentGroup } from '#layers/utils/dataUtil'
import type { Splide as SplideType } from '@splidejs/splide'
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
@@ -21,7 +18,7 @@ const mainTitleData = computed(() =>
getComponentGroup(props.components, 'mainTitle')
)
const slideData = computed(() => {
return ensureMinimumSlideData(props.components)
return getComponentContainer(props.components, 'group_sets', { minLength: 4 })
})
const buttonListData = ref(
@@ -43,12 +40,7 @@ const slideItemSize = {
},
}
const handleChange = (
_splide: SplideType,
newIndex: number,
_oldIndex: number,
_destIndex: number
) => {
const handleChange = (_splide: SplideType, newIndex: number) => {
buttonListData.value = getComponentGroupAry(
slideData.value[newIndex],
'buttonList'

View File

@@ -1,9 +1,6 @@
<script setup lang="ts">
import { SplideSlide } from '@splidejs/vue-splide'
import {
getComponentGroup,
ensureMinimumSlideData,
} from '#layers/utils/dataUtil'
import { getComponentGroup } from '#layers/utils/dataUtil'
import type { Splide as SplideType } from '@splidejs/splide'
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
@@ -20,9 +17,9 @@ const backgroundData = computed(() =>
const mainTitleData = computed(() =>
getComponentGroup(props.components, 'mainTitle')
)
const slideData = computed(() => {
return ensureMinimumSlideData(props.components)
})
const slideData = computed(() =>
getComponentContainer(props.components, 'group_sets', { minLength: 4 })
)
const imgTitleData = ref(getComponentGroup(slideData?.value[0], 'imgTitle'))
const descriptionData = ref(
getComponentGroup(slideData?.value[0], 'description')

View File

@@ -44,22 +44,18 @@ const buttonListData = computed(() =>
:resources-data="descriptionData"
class="w-full max-w-[355px] md:max-w-[944px]"
/>
<client-only>
<WidgetsVideoPlay
v-if="videoPlayData"
:resources-data="videoPlayData"
:page-ver-tmpl-seq="Number(props.pageVerTmplSeq)"
/>
</client-only>
<client-only>
<WidgetsButtonList
v-if="buttonListData.length > 0"
:resources-data="buttonListData"
:page-ver-tmpl-seq="Number(props.pageVerTmplSeq)"
button-type="market"
class="mt-[22px] md:mt-[52px]"
/>
</client-only>
<WidgetsVideoPlay
v-if="videoPlayData"
:resources-data="videoPlayData"
:page-ver-tmpl-seq="Number(props.pageVerTmplSeq)"
/>
<WidgetsButtonList
v-if="buttonListData.length > 0"
button-type="market"
:resources-data="buttonListData"
:page-ver-tmpl-seq="Number(props.pageVerTmplSeq)"
class="mt-[22px] md:mt-[52px]"
/>
</div>
</section>
</template>

View File

@@ -2,10 +2,12 @@
import { SplideSlide } from '@splidejs/vue-splide'
import {
getComponentGroup,
getComponentGroupAry,
ensureMinimumSlideOperateData,
getComponentContainer,
} from '#layers/utils/dataUtil'
import { formatTimestamp } from '#layers/utils/formatUtil'
import { getResolvedHost } from '#layers/utils/styleUtil'
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
interface Props {
components: PageDataTemplateComponents
@@ -16,6 +18,8 @@ const props = defineProps<Props>()
const pageDataStore = usePageDataStore()
const { getResourcesData } = useResourcesData()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const { pageData } = storeToRefs(pageDataStore)
@@ -31,34 +35,35 @@ const descriptionData = computed(() =>
const videoPlayData = computed(() =>
getComponentGroup(props.components, 'videoPlay')
)
const buttonListData = computed(() =>
getComponentGroupAry(props.components, 'buttonList')
)
// 비동기 데이터 로딩
const { data: resourcesData } = await useLazyAsyncData(
'gr-visual-02-resources',
const { data: slideData } = await useAsyncData(
`gr-visual-02-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`,
async () => {
if (!pageData.value?.page_seq || !pageData.value?.page_ver) {
return null
return []
}
return await getResourcesData({
const operateGroupList = await getResourcesData({
pageSeq: pageData.value.page_seq,
pageVer: pageData.value.page_ver,
pageVerTmplSeq: props.pageVerTmplSeq,
langCode: 'ko',
})
const bannerList = getComponentContainer(operateGroupList, 'bannerList', {
hasGroup: true,
minLength: 4,
}) as OperateGroupItem[]
console.log('bannerList', bannerList)
return bannerList
},
{
default: () => [],
server: false,
}
)
const slideData = computed(() => {
if (!resourcesData.value) return []
const data = getComponentGroupAry(resourcesData.value, 'bannerList')
return ensureMinimumSlideOperateData(data)
})
const slideItemSize = {
mo: {
width: 276,
@@ -71,15 +76,12 @@ const slideItemSize = {
gap: 32,
},
}
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const onArrowClick = (direction, targetIndex) => {
const onArrowClick = direction => {
const arrowGroupAry = getComponentGroupAry(props.components, 'arrow')
const logTracking = arrowGroupAry?.[direction === 'prev' ? 0 : 1]
sendLog(locale.value, useAnalyticsLogDataDirect(logTracking, 1))
}
console.log('resourcesData.value===', resourcesData.value)
</script>
<template>
@@ -105,17 +107,10 @@ console.log('resourcesData.value===', resourcesData.value)
:resources-data="videoPlayData"
:page-ver-tmpl-seq="Number(props.pageVerTmplSeq)"
/>
<WidgetsButtonList
v-if="buttonListData.length > 0"
:resources-data="buttonListData"
:page-ver-tmpl-seq="Number(props.pageVerTmplSeq)"
class="mt-[48px] md:mt-[72px]"
/>
<BlocksSlideCenterHighlight
v-if="slideData"
v-if="slideData && slideData.length > 0"
:slide-item-size="slideItemSize"
:slide-item-length="slideData?.length"
:slide-item-length="slideData.length"
:pagination="false"
class="mt-[36px] md:mt-[60px]"
@arrow-click="onArrowClick"

View File

@@ -13,10 +13,9 @@ interface Props {
pageVerTmplSeq: string
}
const { locale } = useI18n()
const props = defineProps<Props>()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const slideData = computed(() => {

View File

@@ -7,13 +7,9 @@ import type {
PageDataValue,
PageDataResourceContainer,
PageDataTemplateComponents,
PageDataTemplateComponentSet,
PageDataResourceGroupType,
} from '#layers/types/api/pageData'
import type {
OperateComponents,
OperateGroupItem,
} from '#layers/types/api/resourcesData'
import type { OperateComponents } from '#layers/types/api/resourcesData'
/**
* 페이지 데이터를 기반으로 레이아웃 타입을 결정합니다.
@@ -64,24 +60,6 @@ export const isTypeButton = (type: PageDataResourceGroupType): boolean => {
return type === 'BTN'
}
/**
* 컴포넌트 컨테이너를 반환합니다.
* @param components props.components
* @param componentName 컴포넌트 이름
* @param options 옵션 (maxLength: 최대 길이)
* @returns 컴포넌트 컨테이너
*/
export const getComponentContainer = (
components: PageDataTemplateComponents | OperateComponents,
componentName: string,
{ maxLength }: { maxLength?: number } = {}
) => {
if (!components) return []
const container = components[componentName] || []
return maxLength ? container.slice(0, maxLength) : container
}
/**
* 컴포넌트 그룹에 데이터가 존재하는지 확인합니다.
* @param components props.components 또는 group 객체
@@ -98,6 +76,45 @@ export const hasComponentGroup = (
return component?.groups && component.groups.length > 0
}
/**
* 컴포넌트 컨테이너를 반환합니다.
* @param components props.components
* @param componentName 컴포넌트 이름
* @param options 옵션
* - hasGroup: groups 속성에서 데이터 가져오기 (기본값: false)
* - maxLength: 최대 길이 제한
* - minLength: 최소 길이 보장 (데이터 반복)
* @returns 컴포넌트 컨테이너 배열
*/
export const getComponentContainer = (
components: PageDataTemplateComponents | OperateComponents,
componentName: string,
options: { hasGroup?: boolean; maxLength?: number; minLength?: number } = {}
) => {
if (!components) return []
const { hasGroup = false, maxLength, minLength } = options
// 1. 초기 컨테이너 가져오기
const component = components[componentName]
if (!component) return []
let result = hasGroup && 'groups' in component ? component.groups : component
// 2. 최소 길이 보장 (데이터 복제)
if (minLength && result.length > 1 && result.length < minLength) {
const repeatTimes = Math.ceil(minLength / result.length)
result = Array(repeatTimes).fill(result).flat()
}
// 3. 최대 길이 제한
if (maxLength && result.length > maxLength) {
return result.slice(0, maxLength)
}
return result
}
/**
* 컴포넌트 그룹의 첫 번째 데이터를 반환합니다.
* @param components props.components 또는 group 객체
@@ -128,52 +145,6 @@ export const getComponentGroupAry = (
return components[componentName]?.groups || []
}
/**
* 슬라이드 데이터를 최소 개수로 보장합니다. (페이지 데이터용)
* @param components 원본 데이터 배열 또는 객체
* @param minCount 최소 보장할 개수 (기본값: 3)
* @returns 최소 개수가 보장된 데이터 배열
*/
export const ensureMinimumSlideData = (
components: PageDataTemplateComponents,
minCount: number = 4
): PageDataTemplateComponentSet[] => {
if (!components) return []
const arrayData = Array.isArray(components.group_sets)
? components.group_sets
: []
// 빈 배열이거나 이미 최소 개수를 만족하면 그대로 반환
if (arrayData.length <= 1 || arrayData.length >= minCount) {
return arrayData
}
// 최소 개수를 보장하기 위해 데이터 반복
const repeatTimes = Math.ceil(minCount / arrayData.length)
return Array(repeatTimes).fill(arrayData).flat()
}
/**
* 슬라이드 데이터를 최소 개수로 보장합니다. (운영 그룹용)
* @param data 원본 데이터 배열
* @param minCount 최소 보장할 개수 (기본값: 3)
* @returns 최소 개수가 보장된 데이터 배열
*/
export const ensureMinimumSlideOperateData = (
data: OperateGroupItem[],
minCount: number = 4
): OperateGroupItem[] => {
// 빈 배열이거나 이미 최소 개수를 만족하면 그대로 반환
if (data.length <= 1 || data.length >= minCount) {
return data
}
// 최소 개수를 보장하기 위해 데이터 반복
const repeatTimes = Math.ceil(minCount / data.length)
return Array(repeatTimes).fill(data).flat()
}
/**
* 현재 시간의 타임스탬프를 반환합니다.
* @param unit 단위 ('ms' | 's') - 밀리초 또는 초