Merge branch 'feature/20250910-all' into feature/20251001-gil
This commit is contained in:
31
app/app.vue
31
app/app.vue
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useNuxtApp } from 'nuxt/app'
|
||||
import type { GameDataMetaTag, GameDataValue } from '#layers/types/api/gameData'
|
||||
import { getResolvedHost } from '#layers/utils/styleUtil'
|
||||
import type {
|
||||
GameDataFavicon,
|
||||
GameDataMetaTag,
|
||||
GameDataValue,
|
||||
} from '#layers/types/api/gameData'
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
@@ -24,9 +28,9 @@ const getGameDataFromServer = (): GameDataValue | null => {
|
||||
|
||||
// 통합 메타데이터 설정
|
||||
const setupAllMetaData = (data: GameDataValue) => {
|
||||
const meta = data.meta_tag
|
||||
const faviconPath = data.favicon_path
|
||||
const theme = data.gnb.theme_type || 'dark'
|
||||
const meta = data.meta_tag_json ?? ({} as GameDataMetaTag)
|
||||
const faviconPath = data.favicon_json ?? ({} as GameDataFavicon)
|
||||
const theme = data.gnb?.theme_type ?? 'dark'
|
||||
|
||||
// 파비콘 링크 생성
|
||||
const faviconLinks = [
|
||||
@@ -62,7 +66,7 @@ const setupAllMetaData = (data: GameDataValue) => {
|
||||
]
|
||||
|
||||
// 색상 CSS 변수 생성
|
||||
const cssVariables = Object.entries(data.key_code_codes)
|
||||
const cssVariables = Object.entries(data.key_color_json ?? {})
|
||||
.map(([key, value]) => `--${key}: ${value};`)
|
||||
.join('\n ')
|
||||
|
||||
@@ -73,7 +77,7 @@ const setupAllMetaData = (data: GameDataValue) => {
|
||||
`
|
||||
|
||||
useHead({
|
||||
title: meta.page_title,
|
||||
title: meta?.page_title ?? '',
|
||||
meta: [
|
||||
{ name: 'description', content: meta.page_desc },
|
||||
{ property: 'og:title', content: meta.og_title },
|
||||
@@ -100,7 +104,7 @@ const setupAllMetaData = (data: GameDataValue) => {
|
||||
|
||||
// 메타 데이터 설정
|
||||
const setupMetaData = (data: GameDataValue) => {
|
||||
metaData.value = data.meta_tag
|
||||
metaData.value = data.meta_tag_json
|
||||
setupAllMetaData(data)
|
||||
}
|
||||
|
||||
@@ -121,12 +125,13 @@ gtag('event', 'screen_view', {
|
||||
|
||||
onMounted(() => {
|
||||
useEventListener('scroll', scrollStore.updateScrollValue)
|
||||
if (gameData.value?.comm_img) {
|
||||
gameData.value?.comm_img.groups.forEach(group => {
|
||||
const cssVarName = `--${group.img_name}`
|
||||
const imageUrl = `url(${getResolvedHost(group.img_path.comm)})`
|
||||
|
||||
document.documentElement.style.setProperty(cssVarName, imageUrl)
|
||||
if (gameData.value?.comm_img_json?.groups) {
|
||||
const groups = gameData.value.comm_img_json.groups
|
||||
groups.forEach(({ img_name, img_path }) => {
|
||||
document.documentElement.style.setProperty(
|
||||
`--${img_name}`,
|
||||
`url(${getResolvedHost(img_path?.comm ?? '')})`
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,20 +15,19 @@
|
||||
}
|
||||
|
||||
/* Title Utility Classes */
|
||||
.title-lg {
|
||||
.title-xlg {
|
||||
@apply line-clamp-4 text-[24px] font-[700] leading-[34px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-3 md:text-[50px] md:leading-[70px];
|
||||
}
|
||||
.title-md {
|
||||
.title-lg {
|
||||
@apply line-clamp-4 text-[20px] font-[700] leading-[30px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-3 md:text-[42px] md:leading-[56px];
|
||||
}
|
||||
.title-sm {
|
||||
.title-md {
|
||||
@apply line-clamp-2 text-[16px] font-[500] leading-[24px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-1 md:text-[24px] md:leading-[34px];
|
||||
}
|
||||
.title-xs {
|
||||
.title-sm {
|
||||
@apply text-[15px] font-[500] leading-[24px] tracking-[-0.45px] md:text-[20px] md:leading-[30px] md:tracking-[-0.6px];
|
||||
}
|
||||
|
||||
/* Description Utility Classes */
|
||||
.description-lg {
|
||||
@apply line-clamp-4 text-[15px] font-[400] leading-[24px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-3 md:text-[20px] md:leading-[30px];
|
||||
.title-xs {
|
||||
@apply text-[14px] font-[500] leading-[20px] tracking-[-0.42px] md:text-[18px] md:leading-[26px] md:tracking-[-0.54px];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
}
|
||||
|
||||
.splide-pagination-bullet {
|
||||
@apply relative w-2 h-2 rounded-full bg-[var(--primary)] opacity-100 md:w-3 md:h-3
|
||||
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:rounded-full after:bg-[rgba(0,0,0,0.5)] after:transition-opacity after:duration-300 after:ease-in-out after:opacity-100;
|
||||
@apply relative w-2 h-2 rounded-full opacity-100 md:w-3 md:h-3 transition-all duration-300 ease-in-out;
|
||||
background-color: var(--pagination-disabled);
|
||||
}
|
||||
|
||||
.splide-pagination-bullet:hover,
|
||||
.splide-pagination-bullet.is-active {
|
||||
@apply after:opacity-0;
|
||||
background-color: var(--pagination-active);
|
||||
}
|
||||
|
||||
.splide-arrows {
|
||||
|
||||
62
layers/components/atoms/Img.vue
Normal file
62
layers/components/atoms/Img.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { getResolvedHost } from '#layers/utils/styleUtil'
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
resourcesData?: PageDataResourceGroup
|
||||
objectFit?: 'contain' | 'cover'
|
||||
alt?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
objectFit: 'contain',
|
||||
})
|
||||
|
||||
const imagePaths = computed(() => {
|
||||
if (!props.resourcesData?.res_path) return null
|
||||
|
||||
const pcPath =
|
||||
props.resourcesData.res_path.path_pc ?? props.resourcesData.res_path.path_mo
|
||||
const moPath =
|
||||
props.resourcesData.res_path.path_mo ?? props.resourcesData.res_path.path_pc
|
||||
|
||||
return {
|
||||
pc: pcPath ? getResolvedHost(pcPath) : '',
|
||||
mo: moPath ? getResolvedHost(moPath) : '',
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<picture v-if="imagePaths">
|
||||
<source media="(min-width: 1024px)" :srcset="imagePaths.pc" />
|
||||
<source media="(max-width: 1023px)" :srcset="imagePaths.mo" />
|
||||
<img
|
||||
:src="imagePaths.pc"
|
||||
:alt="alt"
|
||||
:class="`w-full h-full object-${objectFit}`"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 이미지 깨짐 시 보더 및 아이콘 제거 */
|
||||
img {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 깨진 이미지 아이콘과 alt 텍스트 숨김 */
|
||||
img::before,
|
||||
img::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* alt 텍스트 영역 숨김 */
|
||||
img[alt] {
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
86
layers/components/atoms/Video.vue
Normal file
86
layers/components/atoms/Video.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
src: string
|
||||
type?: 'mp4' | 'webm'
|
||||
play?: boolean
|
||||
autoplay?: boolean
|
||||
muted?: boolean
|
||||
loop?: boolean
|
||||
playsinline?: boolean
|
||||
class?: ClassType
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'mp4',
|
||||
play: false,
|
||||
muted: true,
|
||||
loop: true,
|
||||
playsinline: true,
|
||||
})
|
||||
|
||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||
|
||||
// autoplay prop 변경 시 재생/정지 제어
|
||||
watch(
|
||||
() => props.play,
|
||||
shouldPlay => {
|
||||
if (!videoRef.value) return
|
||||
|
||||
if (shouldPlay) {
|
||||
videoRef.value.play().catch(err => {
|
||||
console.warn('Video play failed:', err)
|
||||
})
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
videoRef.value.pause()
|
||||
videoRef.value.currentTime = 0
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// src 변경 시 비디오 다시 로드
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
if (!videoRef.value) return
|
||||
|
||||
// 비디오 시간 초기화 및 새 소스 로드
|
||||
videoRef.value.currentTime = 0
|
||||
videoRef.value.load()
|
||||
|
||||
// 재생 중이었다면 다시 재생
|
||||
if (props.play) {
|
||||
nextTick(() => {
|
||||
videoRef.value?.play().catch(err => {
|
||||
console.warn('Video play failed:', err)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.src" :class="['video-box', props.class]">
|
||||
<video
|
||||
ref="videoRef"
|
||||
:autoplay="props.autoplay"
|
||||
:muted="props.muted"
|
||||
:loop="props.loop"
|
||||
:playsinline="props.playsinline"
|
||||
>
|
||||
<source :src="props.src" :type="`video/${props.type}`" />
|
||||
</video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.video-box {
|
||||
@apply overflow-hidden relative rounded-[4px] md:rounded-[8px]
|
||||
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:border after:border-white after:opacity-10 after:rounded-[4px] after:md:rounded-[8px];
|
||||
}
|
||||
.video-box video {
|
||||
@apply absolute top-0 left-0 w-full h-full object-cover;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { ClassType } from '#layers/types/Common'
|
||||
|
||||
interface Props {
|
||||
to: string
|
||||
target?: string
|
||||
class?: ClassType
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
target: '',
|
||||
class: '',
|
||||
})
|
||||
|
||||
const componentTag = computed(() => {
|
||||
return props.target === '_blank' ? 'a' : 'AtomsLocaleLink'
|
||||
})
|
||||
|
||||
const componentProps = computed(() => {
|
||||
if (props.target === '_blank') {
|
||||
return {
|
||||
href: props.to,
|
||||
target: props.target,
|
||||
class: props.class,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
to: props.to,
|
||||
class: props.class,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="componentTag" v-bind="{ ...componentProps }">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
@@ -9,7 +9,7 @@ const { gameData } = useGameDataStore()
|
||||
|
||||
const stoveInflowPath = runtimeConfig.public.stoveInflowPath
|
||||
const stoveGameNo = runtimeConfig.public.stoveGameNo
|
||||
const gnbData = gameData?.stove_gnb
|
||||
const gnbData = gameData?.stove_gnb_json
|
||||
|
||||
const languageCodes = computed(() => {
|
||||
if (Array.isArray(availableLocales)) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
import { getResolvedHost, getColorCode } from '#layers/utils/styleUtil'
|
||||
import { isTypeImage, isTypeText } from '#layers/utils/dataUtil'
|
||||
|
||||
interface Props {
|
||||
resourcesData?: PageDataResourceGroup
|
||||
@@ -11,27 +13,28 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
objectFit: 'contain',
|
||||
})
|
||||
|
||||
const breakpoints = useResponsiveBreakpointsReliable()
|
||||
const imagePaths = computed(() => {
|
||||
if (!props.resourcesData?.res_path) return null
|
||||
|
||||
const pcPath =
|
||||
props.resourcesData.res_path.path_pc ?? props.resourcesData.res_path.path_mo
|
||||
const moPath =
|
||||
props.resourcesData.res_path.path_mo ?? props.resourcesData.res_path.path_pc
|
||||
|
||||
return {
|
||||
pc: pcPath ? getResolvedHost(pcPath) : '',
|
||||
mo: moPath ? getResolvedHost(moPath) : '',
|
||||
}
|
||||
})
|
||||
const displayText = computed(() => {
|
||||
return props.resourcesData?.display?.text || 'image'
|
||||
})
|
||||
const imageSrc = computed(() => {
|
||||
return getResponsiveSrc(props.resourcesData?.res_path)
|
||||
})
|
||||
const colorName = computed(() => {
|
||||
return props.resourcesData?.display?.color_name
|
||||
})
|
||||
const colorCode = computed(() => {
|
||||
return props.resourcesData?.display?.color_code
|
||||
})
|
||||
const currentImageSrc = computed(() => {
|
||||
if (!imageSrc.value) return ''
|
||||
|
||||
return breakpoints.value.isMobile
|
||||
? imageSrc.value.mobileSrc || ''
|
||||
: imageSrc.value.pcSrc || ''
|
||||
})
|
||||
|
||||
// HTML 콘텐츠 정리 (줄바꿈 처리)
|
||||
const sanitizedContent = computed(() => {
|
||||
@@ -41,13 +44,16 @@ const sanitizedContent = computed(() => {
|
||||
|
||||
<template>
|
||||
<!-- 이미지 -->
|
||||
<img
|
||||
v-if="isTypeImage(resourcesData?.resource_type) && currentImageSrc"
|
||||
:src="currentImageSrc"
|
||||
:alt="alt || displayText"
|
||||
:class="`w-full h-full object-${objectFit}`"
|
||||
loading="lazy"
|
||||
/>
|
||||
<picture v-if="isTypeImage(resourcesData?.resource_type) && imagePaths">
|
||||
<source :srcset="imagePaths.pc" media="(min-width: 1024px)" />
|
||||
<source :srcset="imagePaths.mo" media="(max-width: 1023px)" />
|
||||
<img
|
||||
:src="imagePaths.pc"
|
||||
:alt="alt || displayText"
|
||||
:class="`w-full h-full object-${objectFit}`"
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
<!-- 텍스트 -->
|
||||
<span
|
||||
v-else-if="isTypeText(resourcesData?.resource_type)"
|
||||
|
||||
@@ -6,40 +6,27 @@ const { fullLoading } = storeToRefs(loadingStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-300 ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-opacity duration-300 ease-in-out"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="fullLoading"
|
||||
class="fixed inset-0 bg-black/80 flex items-center justify-center z-[9999]"
|
||||
>
|
||||
<!-- 메인 스피너 -->
|
||||
<div class="relative w-16 h-16">
|
||||
<!-- 외부 링 -->
|
||||
<div
|
||||
class="absolute inset-0 border-4 border-transparent border-t-blue-500 rounded-full animate-spin"
|
||||
/>
|
||||
<!-- 중간 링 -->
|
||||
<div
|
||||
class="absolute inset-1 border-4 border-transparent border-t-purple-500 rounded-full animate-spin"
|
||||
style="animation-delay: -0.3s"
|
||||
/>
|
||||
<!-- 내부 링 -->
|
||||
<div
|
||||
class="absolute inset-2 border-4 border-transparent border-t-cyan-500 rounded-full animate-spin"
|
||||
style="animation-delay: -0.6s"
|
||||
/>
|
||||
<!-- 중심 링 -->
|
||||
<div
|
||||
class="absolute inset-3 border-4 border-transparent border-t-emerald-500 rounded-full animate-spin"
|
||||
style="animation-delay: -0.9s"
|
||||
/>
|
||||
</div>
|
||||
<Transition name="fade">
|
||||
<div v-if="fullLoading" class="spinner-container">
|
||||
<div class="spinner-line"></div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.spinner-container {
|
||||
@apply fixed inset-0 bg-black/90 flex items-center justify-center z-[900];
|
||||
}
|
||||
.spinner {
|
||||
@apply w-[80px] h-[80px] bg-cover bg-center bg-no-repeat bg-[url('/images/common/publisning_template_loader_black.png')];
|
||||
}
|
||||
|
||||
[data-theme='light'] {
|
||||
.spinner-container {
|
||||
@apply bg-white/90;
|
||||
}
|
||||
.spinner {
|
||||
@apply bg-[url('/images/common/publisning_template_loader_white.png')];
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,42 +14,29 @@ const canTeleport = (localId: string) => {
|
||||
<template>
|
||||
<template v-for="[localId, loadingInfo] in localLoadings" :key="localId">
|
||||
<Teleport v-if="canTeleport(localId)" :to="`#${localId}`">
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-300 ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-opacity duration-300 ease-in-out"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="loadingInfo.active"
|
||||
class="fixed inset-0 bg-black/80 flex items-center justify-center z-[9999]"
|
||||
>
|
||||
<!-- 메인 스피너 -->
|
||||
<div class="relative w-16 h-16">
|
||||
<!-- 외부 링 -->
|
||||
<div
|
||||
class="absolute inset-0 border-4 border-transparent border-t-blue-500 rounded-full animate-spin"
|
||||
/>
|
||||
<!-- 중간 링 -->
|
||||
<div
|
||||
class="absolute inset-1 border-4 border-transparent border-t-purple-500 rounded-full animate-spin"
|
||||
style="animation-delay: -0.3s"
|
||||
/>
|
||||
<!-- 내부 링 -->
|
||||
<div
|
||||
class="absolute inset-2 border-4 border-transparent border-t-cyan-500 rounded-full animate-spin"
|
||||
style="animation-delay: -0.6s"
|
||||
/>
|
||||
<!-- 중심 링 -->
|
||||
<div
|
||||
class="absolute inset-3 border-4 border-transparent border-t-emerald-500 rounded-full animate-spin"
|
||||
style="animation-delay: -0.9s"
|
||||
/>
|
||||
</div>
|
||||
<Transition name="fade">
|
||||
<div v-if="loadingInfo.active" class="spinner-container">
|
||||
<div class="spinner-line"></div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.spinner-container {
|
||||
@apply fixed inset-0 bg-black/90 flex items-center justify-center z-[900];
|
||||
}
|
||||
.spinner {
|
||||
@apply w-[80px] h-[80px] bg-cover bg-center bg-no-repeat bg-[url('/images/common/publisning_template_loader_black.png')];
|
||||
}
|
||||
|
||||
[data-theme='light'] {
|
||||
.spinner-container {
|
||||
@apply bg-white/90;
|
||||
}
|
||||
.spinner {
|
||||
@apply bg-[url('/images/common/publisning_template_loader_white.png')];
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { getYouTubeEmbedUrl } from '#layers/utils/youtube'
|
||||
import { getYouTubeEmbedUrl } from '@/layers/utils/youtubeUtil'
|
||||
|
||||
interface Props {
|
||||
youtubeUrl: string
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useSplideArrow } from '#layers/composables/useSplideArrow'
|
||||
interface Props {
|
||||
slideItemSize: SlideItemSize
|
||||
slideItemLength?: number
|
||||
autoplay?: boolean | string
|
||||
autoplay?: boolean
|
||||
arrows?: boolean
|
||||
pagination?: boolean
|
||||
class?: string
|
||||
@@ -19,14 +19,12 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
pagination: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['mounted', 'move', 'moved', 'arrowClick'])
|
||||
const emit = defineEmits(['mounted', 'move', 'arrowClick'])
|
||||
|
||||
// Splide 화살표 로직을 위한 composable 사용
|
||||
const { addArrowClickListeners } = useSplideArrow()
|
||||
|
||||
const isMultipleItems = computed(() => {
|
||||
return props.slideItemLength > 1
|
||||
})
|
||||
const isMultipleItems = computed(() => (props.slideItemLength ?? 0) > 1)
|
||||
|
||||
const options = computed((): ResponsiveOptions => {
|
||||
return {
|
||||
@@ -34,7 +32,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 +84,7 @@ const style = computed(() => {
|
||||
const handleSplideMounted = (splide: SplideType) => {
|
||||
emit('mounted', splide)
|
||||
splide.refresh()
|
||||
|
||||
|
||||
// 화살표 버튼 클릭 이벤트 리스너 추가
|
||||
nextTick(() => {
|
||||
addArrowClickListeners(splide, (direction, targetIndex) => {
|
||||
@@ -103,27 +101,14 @@ 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>
|
||||
<div :class="`center-highlight ${props.class || ''}`" :style="style">
|
||||
<div :class="`center-focus ${props.class || ''}`" :style="style">
|
||||
<Splide
|
||||
:options="options"
|
||||
@splide:mounted="handleSplideMounted"
|
||||
@splide:move="handleMove"
|
||||
@splide:moved="handleMoved"
|
||||
>
|
||||
<slot />
|
||||
</Splide>
|
||||
@@ -131,58 +116,58 @@ const handleMoved = (
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center-highlight {
|
||||
.center-focus {
|
||||
@apply w-full;
|
||||
}
|
||||
.center-highlight:deep(.splide__slide) {
|
||||
.center-focus:deep(.splide__slide) {
|
||||
@apply flex items-center justify-center;
|
||||
width: var(--banner-width-mo);
|
||||
height: var(--banner-height-mo-active);
|
||||
margin-right: var(--banner-gap-mo);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.center-highlight:deep(.splide__slide) .slide-inner {
|
||||
.center-focus:deep(.splide__slide) .slide-inner {
|
||||
width: var(--banner-width-mo);
|
||||
height: var(--banner-height-mo);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.center-highlight:deep(.splide__slide.is-active) {
|
||||
.center-focus:deep(.splide__slide.is-active) {
|
||||
width: var(--banner-width-mo-container);
|
||||
opacity: 1;
|
||||
}
|
||||
.center-highlight:deep(.splide__slide.is-active) .slide-inner {
|
||||
.center-focus:deep(.splide__slide.is-active) .slide-inner {
|
||||
width: var(--banner-width-mo-active);
|
||||
height: var(--banner-height-mo-active);
|
||||
opacity: 1;
|
||||
transition: all 0.45s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.center-highlight:deep(.splide__slide.is-next),
|
||||
.center-highlight:deep(.splide__slide.is-prev) {
|
||||
.center-focus:deep(.splide__slide.is-next),
|
||||
.center-focus:deep(.splide__slide.is-prev) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* PC 스타일 */
|
||||
@media (min-width: 1024px) {
|
||||
.center-highlight:deep(.splide__slide) {
|
||||
.center-focus:deep(.splide__slide) {
|
||||
width: var(--banner-width-pc);
|
||||
height: var(--banner-height-pc-active);
|
||||
margin-right: var(--banner-gap-pc);
|
||||
}
|
||||
.center-highlight:deep(.splide__slide) .slide-inner {
|
||||
.center-focus:deep(.splide__slide) .slide-inner {
|
||||
width: var(--banner-width-pc);
|
||||
height: var(--banner-height-pc);
|
||||
}
|
||||
.center-highlight:deep(.splide__slide.is-active) {
|
||||
.center-focus:deep(.splide__slide.is-active) {
|
||||
width: var(--banner-width-pc-container);
|
||||
}
|
||||
.center-highlight:deep(.splide__slide.is-active) .slide-inner {
|
||||
.center-focus:deep(.splide__slide.is-active) .slide-inner {
|
||||
width: var(--banner-width-pc-active);
|
||||
height: var(--banner-height-pc-active);
|
||||
}
|
||||
.center-highlight:deep(.arrow-prev) {
|
||||
.center-focus:deep(.arrow-prev) {
|
||||
left: 28px;
|
||||
}
|
||||
.center-highlight:deep(.arrow-next) {
|
||||
.center-focus:deep(.arrow-next) {
|
||||
right: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { SlideItemSize } from '#layers/types/components/slide'
|
||||
interface Props {
|
||||
slideItemSize: SlideItemSize
|
||||
slideItemLength?: number
|
||||
autoplay?: boolean | string
|
||||
autoplay?: boolean
|
||||
arrows?: boolean
|
||||
pagination?: boolean
|
||||
class?: string
|
||||
@@ -18,14 +18,12 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
pagination: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['mounted', 'move', 'moved', 'arrowClick'])
|
||||
const emit = defineEmits(['mounted', 'move', 'arrowClick'])
|
||||
|
||||
// Splide 화살표 로직을 위한 composable 사용
|
||||
const { addArrowClickListeners } = useSplideArrow()
|
||||
|
||||
const isMultipleItems = computed(() => {
|
||||
return props.slideItemLength > 1
|
||||
})
|
||||
const isMultipleItems = computed(() => (props.slideItemLength ?? 0) > 1)
|
||||
|
||||
const options = computed((): ResponsiveOptions => {
|
||||
return {
|
||||
@@ -33,7 +31,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 +86,7 @@ const style = computed(() => {
|
||||
const handleSplideMounted = (splide: SplideType) => {
|
||||
emit('mounted', splide)
|
||||
splide.refresh()
|
||||
|
||||
|
||||
// 화살표 버튼 클릭 이벤트 리스너 추가
|
||||
nextTick(() => {
|
||||
addArrowClickListeners(splide, (direction, targetIndex) => {
|
||||
@@ -105,15 +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>
|
||||
@@ -122,7 +111,6 @@ const handleMoved = (
|
||||
:options="options"
|
||||
@splide:mounted="handleSplideMounted"
|
||||
@splide:move="handleMove"
|
||||
@splide:moved="handleMoved"
|
||||
>
|
||||
<slot />
|
||||
</Splide>
|
||||
|
||||
86
layers/components/blocks/slide/Default.vue
Normal file
86
layers/components/blocks/slide/Default.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { Splide } from '@splidejs/vue-splide'
|
||||
import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide'
|
||||
import type { PageDataResourceGroups } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
type?: ResponsiveOptions['type']
|
||||
slideItemLength?: number
|
||||
autoplay?: boolean
|
||||
arrows?: boolean
|
||||
pagination?: boolean
|
||||
paginationData?: PageDataResourceGroups
|
||||
breakpoints?: ResponsiveOptions['breakpoints']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
autoplay: false,
|
||||
arrows: true,
|
||||
pagination: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['mounted', 'move', 'arrowClick'])
|
||||
|
||||
// Splide 화살표 로직을 위한 composable 사용
|
||||
const { addArrowClickListeners } = useSplideArrow()
|
||||
|
||||
const isMultipleItems = computed(() => (props.slideItemLength ?? 0) > 1)
|
||||
const resolvedType = computed<ResponsiveOptions['type']>(() => {
|
||||
if (props.type) return props.type
|
||||
return isMultipleItems.value ? 'loop' : 'slide'
|
||||
})
|
||||
|
||||
const options = computed((): ResponsiveOptions => {
|
||||
return {
|
||||
type: resolvedType.value,
|
||||
autoWidth: true,
|
||||
autoHeight: true,
|
||||
speed: 500,
|
||||
updateOnMove: true,
|
||||
autoplay: props.autoplay,
|
||||
arrows: props.arrows && isMultipleItems.value,
|
||||
pagination: props.pagination && isMultipleItems.value,
|
||||
classes: {
|
||||
arrows: 'splide-arrows',
|
||||
arrow: 'splide-arrow',
|
||||
prev: 'arrow-prev',
|
||||
next: 'arrow-next',
|
||||
pagination: 'splide-pagination-bullets',
|
||||
page: 'splide-pagination-bullet',
|
||||
},
|
||||
...(props.breakpoints ? { breakpoints: props.breakpoints } : {}),
|
||||
}
|
||||
})
|
||||
|
||||
const handleSplideMounted = (splide: SplideType) => {
|
||||
emit('mounted', splide)
|
||||
|
||||
// 화살표 버튼 클릭 이벤트 리스너 추가
|
||||
nextTick(() => {
|
||||
addArrowClickListeners(splide, (direction, targetIndex) => {
|
||||
emit('arrowClick', direction, targetIndex)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleMove = (
|
||||
splide: SplideType,
|
||||
newIndex: number,
|
||||
oldIndex: number,
|
||||
destIndex: number
|
||||
) => {
|
||||
emit('move', splide, newIndex, oldIndex, destIndex)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Splide
|
||||
:options="options"
|
||||
class="w-full"
|
||||
:style="getPaginationClass(props.paginationData)"
|
||||
@splide:mounted="handleSplideMounted"
|
||||
@splide:move="handleMove"
|
||||
>
|
||||
<slot />
|
||||
</Splide>
|
||||
</template>
|
||||
@@ -1,13 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { Splide } from '@splidejs/vue-splide'
|
||||
import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide'
|
||||
import { useSplideArrow } from '#layers/composables/useSplideArrow'
|
||||
import { getPaginationClass } from '#layers/utils/styleUtil'
|
||||
import type { Splide as SplideType, ResponsiveOptions } from '@splidejs/splide'
|
||||
import type { PageDataResourceGroups } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
autoplay?: boolean | string
|
||||
autoplay?: boolean
|
||||
arrows?: boolean
|
||||
pagination?: boolean
|
||||
class?: string
|
||||
paginationData?: PageDataResourceGroups
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -16,7 +18,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
pagination: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['mounted', 'move', 'moved', 'arrowClick'])
|
||||
const emit = defineEmits(['mounted', 'move', 'arrowClick'])
|
||||
|
||||
const splideRef = ref()
|
||||
// Splide 화살표 로직을 위한 composable 사용
|
||||
@@ -28,7 +30,7 @@ const options = computed((): ResponsiveOptions => {
|
||||
rewind: true,
|
||||
perPage: 1,
|
||||
perMove: 1,
|
||||
speed: 600,
|
||||
speed: 500,
|
||||
updateOnMove: true,
|
||||
autoplay: props.autoplay,
|
||||
pauseOnHover: false,
|
||||
@@ -52,7 +54,6 @@ defineExpose({
|
||||
|
||||
const handleSplideMounted = (splide: SplideType) => {
|
||||
emit('mounted', splide)
|
||||
splide.refresh()
|
||||
|
||||
// 화살표 버튼 클릭 이벤트 리스너 추가
|
||||
nextTick(() => {
|
||||
@@ -70,16 +71,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>
|
||||
@@ -87,9 +78,9 @@ const handleMoved = (
|
||||
ref="splideRef"
|
||||
:options="options"
|
||||
class="h-full"
|
||||
:style="getPaginationClass(props.paginationData)"
|
||||
@splide:mounted="handleSplideMounted"
|
||||
@splide:move="handleMove"
|
||||
@splide:moved="handleMoved"
|
||||
>
|
||||
<slot />
|
||||
</Splide>
|
||||
|
||||
@@ -19,7 +19,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
drag: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['arrowClick'])
|
||||
const emit = defineEmits(['mounted', 'move', 'arrowClick'])
|
||||
|
||||
// Splide 화살표 로직을 위한 composable 사용
|
||||
const { addArrowClickListeners } = useSplideArrow()
|
||||
@@ -32,8 +32,6 @@ defineExpose({
|
||||
thumbsInst: computed(() => thumbsInst),
|
||||
})
|
||||
|
||||
const breakpoints = useResponsiveBreakpointsReliable()
|
||||
|
||||
const mainRef = ref<InstanceType<typeof Splide> | null>(null)
|
||||
const thumbsRef = ref<InstanceType<typeof Splide> | null>(null)
|
||||
|
||||
@@ -42,7 +40,7 @@ const mainOptions = computed<Options>(() => ({
|
||||
rewind: true,
|
||||
perPage: 1,
|
||||
perMove: 1,
|
||||
speed: 600,
|
||||
speed: 500,
|
||||
arrows: false,
|
||||
pagination: false,
|
||||
drag: props.drag,
|
||||
@@ -72,17 +70,23 @@ const getThumbnailSrc = (item: PageDataTemplateComponentSet) => {
|
||||
return mediaComponent ? getMediaImgSrc(mediaComponent) : ''
|
||||
}
|
||||
|
||||
const pagenaviThumbnailComponent = getComponentGroup(
|
||||
item,
|
||||
'pagenaviThumbnail'
|
||||
)
|
||||
const pagenaviThumbnailSrc = getResponsiveSrc(
|
||||
pagenaviThumbnailComponent?.res_path
|
||||
)
|
||||
const thumbnailComponent = getComponentGroup(item, 'pagenaviThumbnail')
|
||||
const thumbnailPath = getDeviceSrc(thumbnailComponent?.res_path)
|
||||
|
||||
return breakpoints.value.isMobile
|
||||
? pagenaviThumbnailSrc?.mobileSrc
|
||||
: pagenaviThumbnailSrc?.pcSrc || ''
|
||||
return thumbnailPath?.pcSrc
|
||||
}
|
||||
|
||||
const handleMove = (
|
||||
splide: SplideType,
|
||||
newIndex: number,
|
||||
oldIndex: number,
|
||||
destIndex: number
|
||||
) => {
|
||||
emit('move', splide, newIndex, oldIndex, destIndex)
|
||||
}
|
||||
|
||||
const handleSplideMounted = (splide: SplideType) => {
|
||||
emit('mounted', splide)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -109,7 +113,13 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<div :class="['thumbnail-carousel', $attrs?.class, `thumbnail-${variant}`]">
|
||||
<!-- 메인 슬라이드 -->
|
||||
<Splide ref="mainRef" :options="mainOptions" class="main-splide">
|
||||
<Splide
|
||||
ref="mainRef"
|
||||
:options="mainOptions"
|
||||
class="main-splide"
|
||||
@splide:move="handleMove"
|
||||
@splide:mounted="handleSplideMounted"
|
||||
>
|
||||
<slot />
|
||||
</Splide>
|
||||
<!-- 썸네일 슬라이드 -->
|
||||
@@ -118,7 +128,7 @@ onBeforeUnmount(() => {
|
||||
ref="thumbsRef"
|
||||
:options="thumbOptions"
|
||||
class="thumbnail-splide"
|
||||
:style="getPaginationClass(paginationData, { type: 'thumbnail' })"
|
||||
:style="getPaginationClass(paginationData)"
|
||||
>
|
||||
<SplideSlide
|
||||
v-for="(item, index) in props.slideData"
|
||||
|
||||
@@ -201,7 +201,7 @@ onBeforeUnmount(() => {
|
||||
Object.keys(gnbData?.menus).length - overflowNam,
|
||||
}"
|
||||
>
|
||||
<BlocksHybridLink
|
||||
<AtomsLocaleLink
|
||||
:to="gnbItem.url_path"
|
||||
:target="gnbItem.link_target"
|
||||
:class="['nav-1depth', { active: isNavItemActive(gnbItem) }]"
|
||||
@@ -211,7 +211,7 @@ onBeforeUnmount(() => {
|
||||
v-if="gnbItem.children"
|
||||
class="hidden md:block"
|
||||
/>
|
||||
</BlocksHybridLink>
|
||||
</AtomsLocaleLink>
|
||||
<Transition name="fade">
|
||||
<div v-if="gnbItem.children" class="nav-2depth">
|
||||
<ul>
|
||||
@@ -219,7 +219,7 @@ onBeforeUnmount(() => {
|
||||
v-for="child in gnbItem.children"
|
||||
:key="child.menu_name"
|
||||
>
|
||||
<BlocksHybridLink
|
||||
<AtomsLocaleLink
|
||||
:to="child.url_path"
|
||||
:target="child.link_target"
|
||||
>
|
||||
@@ -227,7 +227,7 @@ onBeforeUnmount(() => {
|
||||
<AtomsIconsWebLinkLine
|
||||
v-if="child.link_target === '_blank'"
|
||||
/>
|
||||
</BlocksHybridLink>
|
||||
</AtomsLocaleLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -250,20 +250,20 @@ onBeforeUnmount(() => {
|
||||
Object.keys(gnbData?.menus).length - overflowNam,
|
||||
}"
|
||||
>
|
||||
<BlocksHybridLink
|
||||
<AtomsLocaleLink
|
||||
:to="gnbItem.url_path"
|
||||
:target="gnbItem.link_target"
|
||||
:class="`${isNavItemActive(gnbItem) ? 'active' : ''}`"
|
||||
>
|
||||
<span>{{ gnbItem.menu_name }}</span>
|
||||
</BlocksHybridLink>
|
||||
</AtomsLocaleLink>
|
||||
<div v-if="gnbItem.children">
|
||||
<ul>
|
||||
<li
|
||||
v-for="child in gnbItem.children"
|
||||
:key="child.menu_name"
|
||||
>
|
||||
<BlocksHybridLink
|
||||
<AtomsLocaleLink
|
||||
:to="child.url_path"
|
||||
:target="child.link_target"
|
||||
>
|
||||
@@ -271,7 +271,7 @@ onBeforeUnmount(() => {
|
||||
<AtomsIconsWebLinkLine
|
||||
v-if="child.link_target === '_blank'"
|
||||
/>
|
||||
</BlocksHybridLink>
|
||||
</AtomsLocaleLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -279,17 +279,19 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="event">
|
||||
<div v-if="gameData?.event_banner" class="event">
|
||||
<div class="nav-item">
|
||||
<BlocksHybridLink
|
||||
:to="'/event'"
|
||||
:target="'_self'"
|
||||
<AtomsLocaleLink
|
||||
:to="gameData.event_banner?.page_url"
|
||||
:target="
|
||||
gameData.event_banner?.link_type === 1 ? '_self' : '_blank'
|
||||
"
|
||||
class="nav-1depth text-gradient-pink"
|
||||
>
|
||||
<AtomsIconsStarFill />
|
||||
<span>이벤트</span>
|
||||
<AtomsIconsStarFill />
|
||||
</BlocksHybridLink>
|
||||
</AtomsLocaleLink>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -22,7 +22,8 @@ const shouldShowMetaTag = computed(() => props.pageData.meta_tag_type === 2)
|
||||
// 템플릿 표시 여부 확인
|
||||
const isTemplateVisible = (template: PageDataTemplate): boolean => {
|
||||
return Boolean(
|
||||
template?.components && Object.keys(template.components).length > 0
|
||||
template?.page_ver_tmpl_json &&
|
||||
Object.keys(template?.page_ver_tmpl_json).length > 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,14 +35,14 @@ const visibleTemplates = computed(() =>
|
||||
// SEO 메타 태그 설정
|
||||
const setupSeoMeta = (metaTag: PageDataMetaTag) => {
|
||||
useSeoMeta({
|
||||
title: metaTag.page_title ?? '',
|
||||
description: metaTag.page_desc ?? '',
|
||||
ogTitle: metaTag.og_title ?? '',
|
||||
ogDescription: metaTag.og_desc ?? '',
|
||||
ogImage: metaTag.og_image ?? '',
|
||||
twitterTitle: metaTag.x_title ?? '',
|
||||
twitterImage: metaTag.x_image ?? '',
|
||||
twitterDescription: metaTag.x_desc ?? '',
|
||||
title: metaTag?.page_title ?? '',
|
||||
description: metaTag?.page_desc ?? '',
|
||||
ogTitle: metaTag?.og_title ?? '',
|
||||
ogDescription: metaTag?.og_desc ?? '',
|
||||
ogImage: metaTag?.og_image ?? '',
|
||||
twitterTitle: metaTag?.x_title ?? '',
|
||||
twitterImage: metaTag?.x_image ?? '',
|
||||
twitterDescription: metaTag?.x_desc ?? '',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,8 +53,8 @@ onMounted(() => {
|
||||
|
||||
// 메타 태그 설정 감시
|
||||
watchEffect(() => {
|
||||
if (shouldShowMetaTag.value && props.pageData.meta_tag) {
|
||||
setupSeoMeta(props.pageData.meta_tag)
|
||||
if (shouldShowMetaTag.value && props.pageData?.meta_tag_json) {
|
||||
setupSeoMeta(props.pageData?.meta_tag_json)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -67,8 +68,8 @@ watchEffect(() => {
|
||||
>
|
||||
<component
|
||||
:is="getTemplateComponent(template.template_code)"
|
||||
:components="template.components"
|
||||
:page-ver-tmpl-seq="template.page_ver_tmpl_seq.toString()"
|
||||
:components="template.page_ver_tmpl_json"
|
||||
:page-ver-tmpl-seq="template.page_ver_tmpl_seq"
|
||||
/>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
@@ -4,72 +4,70 @@ import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
interface Props {
|
||||
resourcesData: PageDataResourceGroup
|
||||
gradient?: string
|
||||
size?: 'contain' | 'cover'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
gradient: '',
|
||||
size: 'cover',
|
||||
})
|
||||
|
||||
const breakpoints = useResponsiveBreakpointsReliable()
|
||||
const { getCurrentSrc } = useResponsiveSrc()
|
||||
|
||||
const resPath = computed(() => {
|
||||
return props.resourcesData?.res_path
|
||||
})
|
||||
const bgStyles = computed(() => {
|
||||
return getResponsiveSrc(resPath.value, {
|
||||
resourcesType: 'bg',
|
||||
})
|
||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||
|
||||
const resPath = computed(() => props.resourcesData?.res_path)
|
||||
const imageSrc = computed(() => {
|
||||
return getCurrentSrc(resPath.value)
|
||||
})
|
||||
const videoSrc = computed(() => {
|
||||
return getResponsiveSrc(resPath.value, {
|
||||
resourcesType: 'video',
|
||||
})
|
||||
return getCurrentSrc(resPath.value, { resourcesType: 'video' })
|
||||
})
|
||||
const posterSrc = computed(() => {
|
||||
return getResponsiveSrc(resPath.value)
|
||||
return getCurrentSrc(resPath.value)
|
||||
})
|
||||
const currentVideoSrc = computed(() => {
|
||||
if (!videoSrc.value) return ''
|
||||
return breakpoints.value.isMobile
|
||||
? videoSrc.value.mobileSrc
|
||||
: videoSrc.value.pcSrc
|
||||
})
|
||||
const currentPosterSrc = computed(() => {
|
||||
if (!posterSrc.value) return ''
|
||||
return breakpoints.value.isMobile
|
||||
? posterSrc.value.mobileSrc
|
||||
: posterSrc.value.pcSrc
|
||||
const imageClasses = computed(() => [
|
||||
`w-full h-full bg-center bg-no-repeat`,
|
||||
props.size === 'contain' ? 'bg-contain' : 'bg-cover',
|
||||
])
|
||||
const gradientClasses = computed(() => [
|
||||
'absolute bottom-0 left-0 right-0',
|
||||
props.gradient,
|
||||
])
|
||||
|
||||
// src 변경 시 비디오 다시 로드
|
||||
watch(videoSrc, () => {
|
||||
if (!videoRef.value) return
|
||||
|
||||
videoRef.value.currentTime = 0
|
||||
videoRef.value.load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0 w-full h-full">
|
||||
<!-- 이미지 타입-->
|
||||
<!-- 이미지 타입 -->
|
||||
<div
|
||||
v-if="isTypeImage(resourcesData?.resource_type)"
|
||||
class="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
:class="getResponsiveClass()"
|
||||
:style="bgStyles"
|
||||
v-if="isTypeImage(resourcesData?.resource_type) && imageSrc"
|
||||
:class="imageClasses"
|
||||
:style="{ backgroundImage: `url(${imageSrc})` }"
|
||||
/>
|
||||
|
||||
<!-- 비디오 타입 -->
|
||||
<video
|
||||
v-else-if="isTypeVideo(resourcesData?.resource_type) && currentVideoSrc"
|
||||
v-else-if="isTypeVideo(resourcesData?.resource_type) && videoSrc"
|
||||
ref="videoRef"
|
||||
class="w-full h-full object-cover"
|
||||
:poster="currentPosterSrc"
|
||||
:poster="posterSrc"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
>
|
||||
<source :src="currentVideoSrc" type="video/mp4" />
|
||||
<source :src="currentVideoSrc" type="video/webm" />
|
||||
<source :src="videoSrc" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
<!-- 그라디언트 오버레이 (gradient가 true일 때만) -->
|
||||
<div
|
||||
v-if="props.gradient"
|
||||
:class="`absolute bottom-0 left-0 right-0 ${props.gradient}`"
|
||||
/>
|
||||
<!-- 그라디언트 오버레이 -->
|
||||
<div v-if="gradient" :class="gradientClasses" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
|
||||
import type { SlideItemSize } from '#layers/types/components/slide'
|
||||
|
||||
interface BannerListProps {
|
||||
resourcesData: OperateGroupItem[]
|
||||
slideItemSize: SlideItemSize
|
||||
arrows?: boolean
|
||||
pagination?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<BannerListProps>(), {
|
||||
arrows: true,
|
||||
pagination: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BlocksSlideCenterHighlight
|
||||
:slide-item-size="props.slideItemSize"
|
||||
:slide-item-length="props.resourcesData.length"
|
||||
:pagination="false"
|
||||
class="mt-[36px] md:mt-[60px]"
|
||||
>
|
||||
<SplideSlide v-for="(item, index) in props.resourcesData" :key="index">
|
||||
<BlocksCardNews
|
||||
:title="item.title"
|
||||
:description="item.option01"
|
||||
:img-path="getResolvedHost(item.img_path)"
|
||||
:url="item.url"
|
||||
:link-target="item.link_target"
|
||||
class="slide-inner"
|
||||
/>
|
||||
</SplideSlide>
|
||||
</BlocksSlideCenterHighlight>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center-highlight:deep(.splide__slide.is-active .card-link) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.center-highlight:deep(.splide__slide .card-link) {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -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,15 +57,18 @@ 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) {
|
||||
const url = gameData?.market[marketType]?.url
|
||||
const url = gameData?.market_json[marketType]?.url
|
||||
window.open(url, '_blank')
|
||||
return
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -7,7 +7,13 @@ const props = defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>
|
||||
<p class="description">
|
||||
<BlocksVisualContent :resources-data="props.resourcesData" />
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.description {
|
||||
@apply line-clamp-4 text-[15px] font-[400] leading-[24px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-3 md:text-[20px] md:leading-[30px];
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
resourcesData: PageDataResourceGroup
|
||||
}>()
|
||||
tag?: 'h3' | 'h4' | 'p'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tag: 'h3',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3>
|
||||
<component :is="props.tag">
|
||||
<BlocksVisualContent :resources-data="props.resourcesData" />
|
||||
</h3>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -63,7 +65,6 @@ export const useAnalyticsLogDataDirect = (
|
||||
return logData
|
||||
}
|
||||
|
||||
|
||||
// target에 {XX1, XX2}와 같은 형태가 포함되어 있을 경우 options.clickItem으로부터 값 추출하여 세팅
|
||||
const findValueFromOption = (target: string, { options = {} }: any) => {
|
||||
if (target.includes('{') && target.includes('}')) {
|
||||
@@ -340,5 +341,11 @@ const sendMarketingScript = ({
|
||||
}
|
||||
|
||||
export default () => {
|
||||
return { sendGA, sendSA, sendLog, sendMarketingScript, useAnalyticsLogDataDirect }
|
||||
return {
|
||||
sendGA,
|
||||
sendSA,
|
||||
sendLog,
|
||||
sendMarketingScript,
|
||||
useAnalyticsLogDataDirect,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { getDeviceSrc } from '#layers/utils/styleUtil'
|
||||
import type { PageDataResourceGroupResPath } from '#layers/types/api/pageData'
|
||||
|
||||
const BREAKPOINTS = {
|
||||
xs: 360,
|
||||
sm: 768,
|
||||
@@ -6,18 +10,49 @@ const BREAKPOINTS = {
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 확실한 반응형 브레이크포인트 헬퍼 (useWindowSize 기반)
|
||||
* useMediaQuery 기반 반응형 브레이크포인트
|
||||
*/
|
||||
export const useResponsiveBreakpointsReliable = () => {
|
||||
const { width } = useWindowSize()
|
||||
export const useResponsiveBreakpoints = () => {
|
||||
const ssrWidth = BREAKPOINTS.xs
|
||||
const isXs = useMediaQuery(`(min-width: ${BREAKPOINTS.xs}px)`, { ssrWidth })
|
||||
const isSm = useMediaQuery(`(min-width: ${BREAKPOINTS.sm}px)`, { ssrWidth })
|
||||
const isMd = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`, { ssrWidth })
|
||||
const isLg = useMediaQuery(`(min-width: ${BREAKPOINTS.lg}px)`, { ssrWidth })
|
||||
const isMobile = useMediaQuery(`(max-width: ${BREAKPOINTS.md - 1}px)`, {
|
||||
ssrWidth,
|
||||
})
|
||||
const isTablet = useMediaQuery(
|
||||
`(min-width: ${BREAKPOINTS.sm}px) and (max-width: ${BREAKPOINTS.md - 1}px)`,
|
||||
{ ssrWidth }
|
||||
)
|
||||
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`, {
|
||||
ssrWidth,
|
||||
})
|
||||
|
||||
return computed(() => ({
|
||||
xs: width.value >= BREAKPOINTS.xs,
|
||||
sm: width.value >= BREAKPOINTS.sm,
|
||||
md: width.value >= BREAKPOINTS.md,
|
||||
lg: width.value >= BREAKPOINTS.lg,
|
||||
isMobile: width.value < BREAKPOINTS.md,
|
||||
isTablet: width.value >= BREAKPOINTS.sm && width.value < BREAKPOINTS.md,
|
||||
isDesktop: width.value >= BREAKPOINTS.md,
|
||||
isXs: isXs.value,
|
||||
isSm: isSm.value,
|
||||
isMd: isMd.value,
|
||||
isLg: isLg.value,
|
||||
isMobile: isMobile.value,
|
||||
isTablet: isTablet.value,
|
||||
isDesktop: isDesktop.value,
|
||||
}))
|
||||
}
|
||||
|
||||
export const useResponsiveSrc = () => {
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
|
||||
const getCurrentSrc = (
|
||||
path: PageDataResourceGroupResPath,
|
||||
options?: {
|
||||
resourcesType?: 'image' | 'video'
|
||||
}
|
||||
) => {
|
||||
const result = getDeviceSrc(path, options)
|
||||
if (!result) return ''
|
||||
return breakpoints.value.isMobile ? result.mobileSrc : result.pcSrc
|
||||
}
|
||||
|
||||
return { getCurrentSrc }
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const useResourcesData = () => {
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl
|
||||
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/operateResources`
|
||||
|
||||
const queryParams: Record<string, string> = {
|
||||
const queryParams: Record<string, string | number> = {
|
||||
page_seq: pageSeq,
|
||||
page_ver: pageVer,
|
||||
page_ver_tmpl_seq: pageVerTmplSeq,
|
||||
|
||||
@@ -7,6 +7,8 @@ import GrGallery03 from '#layers/templates/GrGallery03/index.vue'
|
||||
import GrDetail01 from '#layers/templates/GrDetail01/index.vue'
|
||||
import GrDetail02 from '#layers/templates/GrDetail02/index.vue'
|
||||
import GrDetail03 from '#layers/templates/GrDetail03/index.vue'
|
||||
// import GrBoard01 from '#layers/templates/GrBoard01/index.vue'
|
||||
import GrContents01 from '#layers/templates/GrContents01/index.vue'
|
||||
|
||||
const templateRegistry = {
|
||||
GR_VISUAL_01: { component: GrVisual01 },
|
||||
@@ -19,7 +21,7 @@ const templateRegistry = {
|
||||
GR_DETAIL_01: { component: GrDetail01 },
|
||||
GR_DETAIL_02: { component: GrDetail02 },
|
||||
GR_DETAIL_03: { component: GrDetail03 },
|
||||
// GR_CONTENTS_01: { component: GrContents01 },
|
||||
GR_CONTENTS_01: { component: GrContents01 },
|
||||
} as const
|
||||
|
||||
type TemplateKey = keyof typeof templateRegistry
|
||||
|
||||
146
layers/templates/GrContents01/index.vue
Normal file
146
layers/templates/GrContents01/index.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import {
|
||||
getComponentContainer,
|
||||
getComponentGroup,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { locale } = useI18n()
|
||||
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
|
||||
|
||||
const backgroundData = computed(() =>
|
||||
getComponentGroup(props.components, 'background')
|
||||
)
|
||||
const mainTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'mainTitle')
|
||||
)
|
||||
const subTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'subTitle')
|
||||
)
|
||||
const descriptionData = computed(() =>
|
||||
getComponentGroup(props.components, 'description')
|
||||
)
|
||||
const slideData = computed(() => {
|
||||
return getComponentContainer(props.components, 'group_sets')
|
||||
})
|
||||
const buttonListData = computed(() => {
|
||||
return getComponentGroupAry(props.components, 'buttonList')
|
||||
})
|
||||
const paginationData = computed(() => {
|
||||
return getComponentGroupAry(props.components, 'pagination')
|
||||
})
|
||||
|
||||
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>
|
||||
<section class="relative py-[80px] md:py-[120px]">
|
||||
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
|
||||
<div class="section-content px-0">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
class="title-xlg mx-[20px] sm:mx-[40px]"
|
||||
/>
|
||||
<WidgetsSubTitle
|
||||
v-if="subTitleData"
|
||||
:resources-data="subTitleData"
|
||||
class="title-sm mt-2 mx-[20px] sm:mx-[40px]"
|
||||
/>
|
||||
<template v-if="slideData">
|
||||
<div v-if="slideData.length <= 2" class="img-container">
|
||||
<div
|
||||
v-for="(item, index) in slideData"
|
||||
:key="index"
|
||||
:class="[{ 'slide-2': slideData.length === 2 }]"
|
||||
>
|
||||
<AtomsImg
|
||||
:resources-data="getComponentGroup(item, 'imgList')"
|
||||
:page-ver-tmpl-seq="props.pageVerTmplSeq"
|
||||
:alt="getComponentGroup(item, 'subTitle')?.display?.text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BlocksSlideDefault
|
||||
v-else
|
||||
:type="slideData.length === 3 ? 'slide' : 'loop'"
|
||||
:slide-item-length="slideData?.length"
|
||||
:arrows="slideData.length === 3 ? false : true"
|
||||
:pagination="slideData.length === 3 ? false : true"
|
||||
:pagination-data="paginationData"
|
||||
:breakpoints="{
|
||||
1023: {
|
||||
type: 'loop',
|
||||
pagination: false,
|
||||
},
|
||||
}"
|
||||
class="mt-[32px]"
|
||||
@arrow-click="onArrowClick"
|
||||
>
|
||||
<SplideSlide
|
||||
v-for="(item, index) in slideData"
|
||||
:key="index"
|
||||
class="mr-4"
|
||||
>
|
||||
<div class="slide-inner w-[295px] sm:w-[304px]">
|
||||
<AtomsImg
|
||||
:resources-data="getComponentGroup(item, 'imgList')"
|
||||
:page-ver-tmpl-seq="props.pageVerTmplSeq"
|
||||
:alt="getComponentGroup(item, 'subTitle')?.display?.text"
|
||||
/>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
</BlocksSlideDefault>
|
||||
</template>
|
||||
<WidgetsButtonList
|
||||
v-if="buttonListData"
|
||||
:resources-data="buttonListData"
|
||||
:page-ver-tmpl-seq="props.pageVerTmplSeq"
|
||||
class="mt-[56px] mx-[20px] sm:mx-[40px]"
|
||||
/>
|
||||
<WidgetsDescription
|
||||
v-if="descriptionData"
|
||||
:resources-data="descriptionData"
|
||||
class="mt-8 mx-[20px] sm:mx-[40px]"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.img-container {
|
||||
@apply flex flex-wrap justify-center gap-4 box-content mx-auto mt-[32px]
|
||||
max-w-[688px] px-[20px]
|
||||
md:max-w-[944px] md:px-[40px];
|
||||
}
|
||||
.splide {
|
||||
@apply md:max-w-[1088px];
|
||||
}
|
||||
.splide:deep(.splide__track) {
|
||||
@apply md:w-[944px] md:mx-auto;
|
||||
}
|
||||
.splide:deep(.splide__track) {
|
||||
@apply !px-[20px] sm:!px-[40px] md:!px-[0];
|
||||
}
|
||||
.splide:deep(.arrow-next) {
|
||||
@apply md:-right-[0];
|
||||
}
|
||||
.splide:deep(.arrow-prev) {
|
||||
@apply md:-left-[0];
|
||||
}
|
||||
.slide-2 {
|
||||
@apply max-w-[335px] md:max-w-[464px];
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import {
|
||||
getComponentContainer,
|
||||
getComponentGroupAry,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import type { Splide as SplideType } from '@splidejs/splide'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: string
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const splideRef = ref<SplideSlide | null>(null)
|
||||
const currentSlide = ref<number | null>(null)
|
||||
const currentSlideIndex = ref<number | null>(null)
|
||||
|
||||
const slideData = computed(() => {
|
||||
return getComponentContainer(props.components, 'group_sets', { maxLength: 5 })
|
||||
@@ -26,18 +31,13 @@ const goToSlide = (index: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const splide = splideRef.value?.splide
|
||||
if (splide) {
|
||||
currentSlide.value = splide.index
|
||||
const handleSplideMounted = (splide: SplideType) => {
|
||||
currentSlideIndex.value = splide.index
|
||||
}
|
||||
|
||||
splide.on('move', (newIndex: number) => {
|
||||
currentSlide.value = newIndex
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
currentSlideIndex.value = newIndex
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,6 +50,8 @@ onMounted(() => {
|
||||
:arrows="false"
|
||||
:pagination="false"
|
||||
class="h-full"
|
||||
@move="handleSplideMove"
|
||||
@mounted="handleSplideMounted"
|
||||
>
|
||||
<SplideSlide v-for="(item, index) in slideData" :key="index">
|
||||
<WidgetsBackground
|
||||
@@ -60,17 +62,17 @@ onMounted(() => {
|
||||
<WidgetsMainTitle
|
||||
v-if="hasComponentGroup(item, 'mainTitle')"
|
||||
:resources-data="getComponentGroup(item, 'mainTitle')"
|
||||
class="title-md"
|
||||
class="title-lg"
|
||||
/>
|
||||
<WidgetsSubTitle
|
||||
v-if="hasComponentGroup(item, 'subTitle')"
|
||||
:resources-data="getComponentGroup(item, 'subTitle')"
|
||||
class="title-sm mt-0.5 line-clamp-3 md:mt-1 md:line-clamp-2"
|
||||
class="title-md mt-0.5 line-clamp-3 md:mt-1 md:line-clamp-2"
|
||||
/>
|
||||
<WidgetsDescription
|
||||
v-if="hasComponentGroup(item, 'description')"
|
||||
:resources-data="getComponentGroup(item, 'description')"
|
||||
class="description-lg mt-4 md:mt-6"
|
||||
class="mt-4 md:mt-6"
|
||||
/>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
@@ -86,13 +88,16 @@ onMounted(() => {
|
||||
:class="[
|
||||
'pagination-item',
|
||||
{
|
||||
'is-active': currentSlide === index,
|
||||
'is-completed': index < currentSlide,
|
||||
'is-active': currentSlideIndex === index,
|
||||
'is-completed': index < currentSlideIndex,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<button
|
||||
:class="['btn-pagination', { 'is-active': currentSlide === index }]"
|
||||
:class="[
|
||||
'btn-pagination',
|
||||
{ 'is-active': currentSlideIndex === index },
|
||||
]"
|
||||
@click="goToSlide(index)"
|
||||
>
|
||||
<span class="item-bullet"></span>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<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 {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: string
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -32,17 +36,17 @@ const paginationData = computed(() => {
|
||||
<WidgetsMainTitle
|
||||
v-if="hasComponentGroup(item, 'mainTitle')"
|
||||
:resources-data="getComponentGroup(item, 'mainTitle')"
|
||||
class="title-md"
|
||||
class="title-lg"
|
||||
/>
|
||||
<WidgetsSubTitle
|
||||
v-if="hasComponentGroup(item, 'subTitle')"
|
||||
:resources-data="getComponentGroup(item, 'subTitle')"
|
||||
class="title-sm mt-0.5 line-clamp-3 md:mt-1 md:line-clamp-2"
|
||||
class="title-md mt-0.5 line-clamp-3 md:mt-1 md:line-clamp-2"
|
||||
/>
|
||||
<WidgetsDescription
|
||||
v-if="hasComponentGroup(item, 'description')"
|
||||
:resources-data="getComponentGroup(item, 'description')"
|
||||
class="description-lg mt-4 md:mt-6"
|
||||
class="mt-4 md:mt-6"
|
||||
/>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import {
|
||||
getComponentContainer,
|
||||
getComponentGroupAry,
|
||||
getComponentGroup,
|
||||
hasComponentGroup,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import type { Splide as SplideType } from '@splidejs/splide'
|
||||
import type {
|
||||
PageDataTemplateComponents,
|
||||
PageDataTemplateComponent,
|
||||
} from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: string
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { getCurrentSrc } = useResponsiveSrc()
|
||||
|
||||
const currentSlideIndex = ref<number>(0)
|
||||
|
||||
const slideData = computed(() => {
|
||||
return getComponentContainer(props.components, 'group_sets', {
|
||||
maxLength: 10,
|
||||
@@ -17,6 +31,15 @@ const slideData = computed(() => {
|
||||
const paginationData = computed(() => {
|
||||
return getComponentGroupAry(props.components, 'pagination')
|
||||
})
|
||||
|
||||
const videoSrc = (item: PageDataTemplateComponent) => {
|
||||
const src = getComponentGroup(item, 'video')?.res_path
|
||||
return getCurrentSrc(src, { resourcesType: 'video' })
|
||||
}
|
||||
|
||||
const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
currentSlideIndex.value = newIndex
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,32 +47,51 @@ const paginationData = computed(() => {
|
||||
<BlocksSlideThumbnail
|
||||
:slide-data="slideData"
|
||||
:pagination-data="paginationData"
|
||||
@move="handleSplideMove"
|
||||
>
|
||||
<SplideSlide v-for="(item, index) in slideData" :key="index">
|
||||
<WidgetsBackground
|
||||
v-if="hasComponentGroup(item, 'background')"
|
||||
:resources-data="getComponentGroup(item, 'background')"
|
||||
/>
|
||||
<div class="section-content max-w-[1024px] mx-auto items-start">
|
||||
<WidgetsBackground
|
||||
v-if="hasComponentGroup(item, 'foreground')"
|
||||
size="contain"
|
||||
:resources-data="getComponentGroup(item, 'foreground')"
|
||||
/>
|
||||
<div
|
||||
class="section-content max-w-[1024px] mx-auto items-start pt-[48px] md:pt-0"
|
||||
>
|
||||
<WidgetsSubTitle
|
||||
v-if="hasComponentGroup(item, 'category')"
|
||||
:resources-data="getComponentGroup(item, 'category')"
|
||||
class="title-sm mb-2 line-clamp-1 text-left md:mb-5"
|
||||
class="title-xs mb-2 line-clamp-1 text-left md:mb-5"
|
||||
/>
|
||||
<WidgetsMainTitle
|
||||
v-if="hasComponentGroup(item, 'mainTitle')"
|
||||
:resources-data="getComponentGroup(item, 'mainTitle')"
|
||||
class="title-md line-clamp-1 text-left"
|
||||
class="title-lg line-clamp-1 text-left"
|
||||
/>
|
||||
<WidgetsSubTitle
|
||||
v-if="hasComponentGroup(item, 'subTitle')"
|
||||
:resources-data="getComponentGroup(item, 'subTitle')"
|
||||
class="title-sm mt-1 line-clamp-1 text-left"
|
||||
tag="p"
|
||||
class="title-md mt-1 line-clamp-1 text-left"
|
||||
/>
|
||||
<WidgetsDescription
|
||||
v-if="hasComponentGroup(item, 'description')"
|
||||
:resources-data="getComponentGroup(item, 'description')"
|
||||
class="description-lg mt-2 text-left md:mt-5"
|
||||
class="mt-2 text-left md:mt-5"
|
||||
/>
|
||||
<AtomsVideo
|
||||
v-if="hasComponentGroup(item, 'video')"
|
||||
:src="videoSrc(item)"
|
||||
:play="currentSlideIndex === index"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
class="aspect-[16/10] w-[258px] mt-8 md:w-[496px] md:mt-10"
|
||||
/>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import {
|
||||
getComponentContainer,
|
||||
getComponentGroupAry,
|
||||
getComponentGroup,
|
||||
isTypeVideo,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import { getMediaImgSrc, getMediaSrc } from '#layers/utils/youtubeUtil'
|
||||
import type {
|
||||
PageDataTemplateComponents,
|
||||
PageDataTemplateComponentSet,
|
||||
@@ -7,7 +14,7 @@ import type {
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: string
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -27,9 +34,9 @@ const mainTitleData = computed(() =>
|
||||
const slideData = computed(() =>
|
||||
getComponentContainer(props.components, 'group_sets')
|
||||
)
|
||||
const _videoPlayData = computed(() =>
|
||||
getComponentGroup(props.components, 'videoPlay')
|
||||
)
|
||||
const paginationData = computed(() => {
|
||||
return getComponentGroupAry(props.components, 'pagination')
|
||||
})
|
||||
|
||||
const getMediaComponent = (item: PageDataTemplateComponentSet) => {
|
||||
return getComponentGroup(item, 'media')
|
||||
@@ -75,13 +82,16 @@ const handleVideoClick = (index: number) => {
|
||||
locale.value,
|
||||
useAnalyticsLogDataDirect(
|
||||
(sendingGroup as any) || getComponentGroup(props.components, 'videoPlay'),
|
||||
1
|
||||
props.pageVerTmplSeq
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const stopVideo = () => {
|
||||
playingSlideIndex.value = null
|
||||
// 전환 시간 후 완전히 제거
|
||||
setTimeout(() => {
|
||||
playingSlideIndex.value = null
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const onArrowClick = (direction, targetIndex) => {
|
||||
@@ -89,15 +99,6 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
const logTracking = arrowGroupAry?.[direction === 'prev' ? 0 : 1]
|
||||
sendLog(locale.value, useAnalyticsLogDataDirect(logTracking, 1))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const mainInst = slideThumbnailRef.value?.mainInst
|
||||
if (mainInst) {
|
||||
mainInst.on('moved', stopVideo)
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -107,7 +108,7 @@ onMounted(() => {
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
class="title-sm"
|
||||
class="title-md"
|
||||
/>
|
||||
<BlocksSlideThumbnail
|
||||
ref="slideThumbnailRef"
|
||||
@@ -115,6 +116,8 @@ onMounted(() => {
|
||||
variant="media"
|
||||
:drag="false"
|
||||
class="mt-[24px] md:mt-[32px]"
|
||||
:pagination-data="paginationData"
|
||||
@move="stopVideo"
|
||||
@arrow-click="onArrowClick"
|
||||
>
|
||||
<SplideSlide
|
||||
@@ -126,7 +129,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)"
|
||||
@@ -157,6 +162,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;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import {
|
||||
getComponentContainer,
|
||||
getComponentGroup,
|
||||
ensureMinimumSlideData,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import type { Splide as SplideType } from '@splidejs/splide'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: string
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -21,7 +21,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 +43,7 @@ const slideItemSize = {
|
||||
},
|
||||
}
|
||||
|
||||
const handleChange = (
|
||||
_splide: SplideType,
|
||||
newIndex: number,
|
||||
_oldIndex: number,
|
||||
_destIndex: number
|
||||
) => {
|
||||
const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
buttonListData.value = getComponentGroupAry(
|
||||
slideData.value[newIndex],
|
||||
'buttonList'
|
||||
@@ -70,7 +65,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
class="title-sm"
|
||||
class="title-md mx-[20px] sm:mx-[40px]"
|
||||
/>
|
||||
<BlocksSlideCenterFocus
|
||||
v-if="slideData"
|
||||
@@ -78,14 +73,14 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
:slide-item-length="slideData?.length"
|
||||
:pagination="false"
|
||||
class="mt-[24px] md:mt-[48px]"
|
||||
@move="handleChange"
|
||||
@move="handleSplideMove"
|
||||
@arrow-click="onArrowClick"
|
||||
>
|
||||
<SplideSlide v-for="(item, index) in slideData" :key="index">
|
||||
<div class="slide-inner border-line mt-auto">
|
||||
<BlocksVisualContent
|
||||
:resources-data="getComponentGroup(item, 'imgList')"
|
||||
:page-ver-tmpl-seq="Number(props.pageVerTmplSeq)"
|
||||
:page-ver-tmpl-seq="props.pageVerTmplSeq"
|
||||
object-fit="cover"
|
||||
:alt="getComponentGroup(item, 'subTitle')?.display?.text"
|
||||
/>
|
||||
@@ -95,7 +90,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
<WidgetsButtonList
|
||||
v-if="buttonListData"
|
||||
:resources-data="buttonListData"
|
||||
:page-ver-tmpl-seq="Number(props.pageVerTmplSeq)"
|
||||
:page-ver-tmpl-seq="props.pageVerTmplSeq"
|
||||
class="mt-[40px] md:mt-[56px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import {
|
||||
getComponentContainer,
|
||||
getComponentGroup,
|
||||
ensureMinimumSlideData,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import type { Splide as SplideType } from '@splidejs/splide'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: string
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -20,9 +20,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')
|
||||
@@ -44,7 +44,7 @@ const slideItemSize = {
|
||||
},
|
||||
}
|
||||
|
||||
const handleChange = (
|
||||
const handleSplideMove = (
|
||||
_splide: SplideType,
|
||||
newIndex: number,
|
||||
_oldIndex: number,
|
||||
@@ -76,7 +76,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
class="title-sm"
|
||||
class="title-md mx-[20px] sm:mx-[40px]"
|
||||
/>
|
||||
<BlocksSlideCenterHighlight
|
||||
v-if="slideData"
|
||||
@@ -84,7 +84,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
:slide-item-length="slideData?.length"
|
||||
:pagination="false"
|
||||
class="mt-[24px] md:mt-[48px]"
|
||||
@move="handleChange"
|
||||
@move="handleSplideMove"
|
||||
@arrow-click="onArrowClick"
|
||||
>
|
||||
<SplideSlide v-for="(item, index) in slideData" :key="index">
|
||||
@@ -100,18 +100,18 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
<WidgetsSubTitle
|
||||
v-if="imgTitleData"
|
||||
:resources-data="imgTitleData"
|
||||
class="title-md mt-[32px] line-clamp-2 md:line-clamp-1"
|
||||
class="title-lg mt-[32px] mx-[20px] line-clamp-2 sm:mx-[40px] md:line-clamp-1"
|
||||
/>
|
||||
<WidgetsDescription
|
||||
v-if="descriptionData"
|
||||
:resources-data="descriptionData"
|
||||
class="mt-[8px] md:mt-[16px]"
|
||||
class="mt-[8px] mx-[20px] sm:mx-[40px] md:mt-[16px]"
|
||||
/>
|
||||
<WidgetsButtonList
|
||||
v-if="buttonListData"
|
||||
:resources-data="buttonListData"
|
||||
:page-ver-tmpl-seq="Number(props.pageVerTmplSeq)"
|
||||
class="mt-[32px]"
|
||||
:page-ver-tmpl-seq="props.pageVerTmplSeq"
|
||||
class="mt-[32px] mx-[20px] sm:mx-[40px]"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: string
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -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="props.pageVerTmplSeq"
|
||||
/>
|
||||
<WidgetsButtonList
|
||||
v-if="buttonListData.length > 0"
|
||||
button-type="market"
|
||||
:resources-data="buttonListData"
|
||||
:page-ver-tmpl-seq="props.pageVerTmplSeq"
|
||||
class="mt-[22px] md:mt-[52px]"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -2,20 +2,24 @@
|
||||
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
|
||||
pageVerTmplSeq: string
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const pageDataStore = usePageDataStore()
|
||||
const { getResourcesData } = useResourcesData()
|
||||
const { locale } = useI18n()
|
||||
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
|
||||
|
||||
const { pageData } = storeToRefs(pageDataStore)
|
||||
|
||||
@@ -31,34 +35,34 @@ 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',
|
||||
langCode: locale.value,
|
||||
})
|
||||
|
||||
const bannerList = getComponentContainer(operateGroupList, 'bannerList', {
|
||||
hasGroup: true,
|
||||
minLength: 4,
|
||||
}) as OperateGroupItem[]
|
||||
|
||||
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 +75,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>
|
||||
@@ -93,29 +94,22 @@ console.log('resourcesData.value===', resourcesData.value)
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
class="w-full max-w-[355px] md:max-w-[944px]"
|
||||
class="w-full max-w-[355px] mx-[20px] sm:mx-[40px] md:max-w-[944px]"
|
||||
/>
|
||||
<WidgetsDescription
|
||||
v-if="descriptionData"
|
||||
:resources-data="descriptionData"
|
||||
class="w-full max-w-[355px] md:max-w-[944px]"
|
||||
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="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]"
|
||||
:page-ver-tmpl-seq="props.pageVerTmplSeq"
|
||||
/>
|
||||
<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"
|
||||
|
||||
@@ -6,22 +6,25 @@ import {
|
||||
getComponentGroup,
|
||||
hasComponentGroup,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import { getPaginationClass } from '#layers/utils/styleUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: string
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const { locale } = useI18n()
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { locale } = useI18n()
|
||||
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
|
||||
|
||||
const slideData = computed(() => {
|
||||
return getComponentContainer(props.components, 'group_sets')
|
||||
})
|
||||
const paginationData = computed(() => {
|
||||
return getComponentGroupAry(props.components, 'pagination')
|
||||
})
|
||||
|
||||
const onArrowClick = direction => {
|
||||
const arrowGroupAry = getComponentGroupAry(props.components, 'arrow')
|
||||
@@ -37,6 +40,7 @@ const onArrowClick = direction => {
|
||||
:arrows="true"
|
||||
:pagination="true"
|
||||
class="h-full"
|
||||
:pagination-data="paginationData"
|
||||
@arrow-click="onArrowClick"
|
||||
>
|
||||
<SplideSlide v-for="(item, index) in slideData" :key="index">
|
||||
@@ -48,22 +52,21 @@ const onArrowClick = direction => {
|
||||
<WidgetsSubTitle
|
||||
v-if="hasComponentGroup(item, 'subTitle')"
|
||||
:resources-data="getComponentGroup(item, 'subTitle')"
|
||||
class="title-sm"
|
||||
class="title-md"
|
||||
/>
|
||||
<WidgetsMainTitle
|
||||
v-if="hasComponentGroup(item, 'mainTitle')"
|
||||
:resources-data="getComponentGroup(item, 'mainTitle')"
|
||||
class="title-lg"
|
||||
class="title-xlg"
|
||||
/>
|
||||
<WidgetsDescription
|
||||
v-if="hasComponentGroup(item, 'description')"
|
||||
:resources-data="getComponentGroup(item, 'description')"
|
||||
class="description-lg"
|
||||
/>
|
||||
<WidgetsButtonList
|
||||
v-if="hasComponentGroup(item, 'buttonList')"
|
||||
:resources-data="getComponentGroupAry(item, 'buttonList')"
|
||||
:page-ver-tmpl-seq="Number(props.pageVerTmplSeq)"
|
||||
:page-ver-tmpl-seq="props.pageVerTmplSeq"
|
||||
class="mt-[28px] md:mt-[52px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,9 @@ export interface GameDataValue {
|
||||
ga_code: string
|
||||
design_theme: number
|
||||
lang_codes: string[]
|
||||
key_code_codes: GameDataKeyCodeCodes
|
||||
key_color_json: GameDataKeyColors
|
||||
use_game_font: boolean
|
||||
comm_sns_bg_color_code: string
|
||||
comm_sns_bg_color_json: string
|
||||
comm_multilang_filename: string
|
||||
footer_dev_ci_img_yn: boolean
|
||||
footer_dev_ci_img_path: string
|
||||
@@ -34,19 +34,20 @@ export interface GameDataValue {
|
||||
gnb: GameDataGnb
|
||||
intro: GameDataIntro
|
||||
inspection: Record<string, any>
|
||||
stove_gnb: GameDataStoveGnb
|
||||
favicon_path: GameDataFaviconPath
|
||||
meta_tag: GameDataMetaTag
|
||||
sns: GameDataSns
|
||||
footer: string // JSON 문자열로 변경
|
||||
comm_img: GameDataCommImg
|
||||
market: Record<string, { url: string }>
|
||||
stove_gnb_json: GameDataStoveGnb
|
||||
favicon_json: GameDataFavicon
|
||||
meta_tag_json: GameDataMetaTag
|
||||
sns_json: GameDataSns
|
||||
footer_json: string // JSON 문자열로 변경
|
||||
comm_img_json: GameDataCommImg
|
||||
market_json: Record<string, { url: string }>
|
||||
event_banner: GameDataEventBanner
|
||||
}
|
||||
|
||||
// ===== 세부 데이터 타입들 =====
|
||||
|
||||
// 키 코드 코드 타입
|
||||
export interface GameDataKeyCodeCodes {
|
||||
export interface GameDataKeyColors {
|
||||
primary: string
|
||||
'text-primary': string
|
||||
'text-secondary': string
|
||||
@@ -62,7 +63,7 @@ export interface GameDataGameFont {
|
||||
}
|
||||
|
||||
// 파비콘 경로 타입
|
||||
export interface GameDataFaviconPath {
|
||||
export interface GameDataFavicon {
|
||||
'16_16': string
|
||||
'32_32': string
|
||||
'72_72': string
|
||||
@@ -116,7 +117,7 @@ export interface GameDataCommImg {
|
||||
// Global 설정 타입
|
||||
export interface GameDataGlobal {
|
||||
system_font: string // JSON 문자열로 변경
|
||||
lang: string // JSON 문자열로 변경
|
||||
lang_json: string // JSON 문자열로 변경
|
||||
}
|
||||
|
||||
// 트래킹 타입
|
||||
@@ -129,7 +130,7 @@ export interface GameDataTracking {
|
||||
// 버튼 타입
|
||||
export interface GameDataButton {
|
||||
depth_type: number
|
||||
button: string // JSON 문자열로 변경
|
||||
button_json: string // JSON 문자열로 변경
|
||||
}
|
||||
|
||||
export type GameDataMenuChildren = Record<string, GameDataMenu>
|
||||
@@ -144,7 +145,7 @@ export interface GameDataMenu {
|
||||
url_path: string
|
||||
link_target: string
|
||||
children: GameDataMenuChildren
|
||||
tracking: string | GameDataTracking // JSON 문자열 또는 객체로 변경
|
||||
tracking_json: string | GameDataTracking // JSON 문자열 또는 객체로 변경
|
||||
}
|
||||
|
||||
// GNB 설정 타입
|
||||
@@ -180,6 +181,14 @@ export interface GameDataStoveGnb {
|
||||
stove_install_button_visible: string
|
||||
}
|
||||
|
||||
// 이벤트 배너 타입
|
||||
export interface GameDataEventBanner {
|
||||
link_type: 1 | 2
|
||||
page_url: string
|
||||
page_url_type: 1 | 2
|
||||
promotion_name: string
|
||||
}
|
||||
|
||||
// ===== API 관련 타입들 =====
|
||||
|
||||
// API 에러 응답 타입
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface PageDataValue {
|
||||
lnb_text_color_code_active: string
|
||||
lnb_text_color_code_deactive: string
|
||||
lnb_menus: PageDataLnbMenu[]
|
||||
meta_tag: PageDataMetaTag
|
||||
meta_tag_json: PageDataMetaTag
|
||||
templates: Record<string, PageDataTemplate>
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface PageDataLnbMenu {
|
||||
menu_name: string
|
||||
target_type: number
|
||||
page_ver_tmpl_name_en: string
|
||||
// tracking: Record<string, PageDataTracking>
|
||||
tracking_json: Record<string, PageDataTracking>
|
||||
}
|
||||
|
||||
// 메타 태그 타입
|
||||
@@ -121,16 +121,19 @@ export type PageDataTemplateComponentSet = PageDataTemplateComponent & {
|
||||
// 템플릿 컴포넌트 타입 - 세 가지 패턴
|
||||
export type PageDataTemplateComponents =
|
||||
| PageDataTemplateComponent // 단일 컴포넌트 패턴
|
||||
| { group_sets: PageDataTemplateComponentSet[], arrow: PageDataArrowComponent } // 그룹 세트 패턴
|
||||
|
||||
| {
|
||||
group_sets: PageDataTemplateComponentSet[]
|
||||
arrow: PageDataArrowComponent
|
||||
} // 그룹 세트 패턴
|
||||
|
||||
// 템플릿 타입
|
||||
export interface PageDataTemplate {
|
||||
page_ver_tmpl_seq: number
|
||||
tmpl_sort_order: number
|
||||
page_ver_tmpl_name: string
|
||||
page_ver_tmpl_name_en: string
|
||||
page_ver_tmpl_json: PageDataTemplateComponents
|
||||
tmpl_sort_order: number
|
||||
template_code: string
|
||||
components: PageDataTemplateComponents
|
||||
}
|
||||
|
||||
// ===== API 관련 타입들 =====
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface ResourcesDataResponse {
|
||||
export interface GetResourcesDataParams {
|
||||
pageSeq: string
|
||||
pageVer: string
|
||||
pageVerTmplSeq: string
|
||||
pageVerTmplSeq: number
|
||||
langCode: string
|
||||
q?: string
|
||||
qc?: string
|
||||
|
||||
@@ -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') - 밀리초 또는 초
|
||||
|
||||
@@ -15,6 +15,9 @@ import type {
|
||||
* @returns 완전한 이미지 URL
|
||||
*/
|
||||
export const getResolvedHost = (path: string): string => {
|
||||
// path가 없으면 빈 문자열 반환
|
||||
if (!path || typeof path !== 'string') return ''
|
||||
|
||||
if (
|
||||
path.startsWith('http://') ||
|
||||
path.startsWith('https://') ||
|
||||
@@ -33,35 +36,33 @@ export const getResolvedHost = (path: string): string => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 반응형 리소스(이미지/비디오)를 처리하여 PC/모바일 버전을 반환합니다.
|
||||
* 디바이스 리소스(이미지/비디오)를 처리하여 PC/모바일 버전을 반환합니다.
|
||||
* @param pathArray 리소스 경로 배열
|
||||
* @param options 리소스 타입 옵션
|
||||
* @returns 반응형 리소스 객체 또는 null
|
||||
* @returns 디바이스 리소스 객체 또는 null
|
||||
*/
|
||||
export const getResponsiveSrc = (
|
||||
export const getDeviceSrc = (
|
||||
pathArray: PageDataResourceGroupResPath,
|
||||
options: {
|
||||
resourcesType?: 'image' | 'bg' | 'video'
|
||||
} = {}
|
||||
options?: {
|
||||
resourcesType?: 'image' | 'video'
|
||||
}
|
||||
) => {
|
||||
const { resourcesType = 'image' } = options
|
||||
// pathArray가 없으면 null 반환
|
||||
if (!pathArray) return null
|
||||
|
||||
const { resourcesType = 'image' } = options ?? {}
|
||||
const pcField = resourcesType === 'video' ? 'path_vid_pc' : 'path_pc'
|
||||
const mobileField = resourcesType === 'video' ? 'path_vid_mo' : 'path_mo'
|
||||
|
||||
if (!pathArray?.[mobileField]) {
|
||||
return null
|
||||
}
|
||||
const pcPath = pathArray[pcField] || pathArray[mobileField]
|
||||
const mobilePath = pathArray[mobileField] || pathArray[pcField]
|
||||
|
||||
// 경로가 없으면 null 반환
|
||||
if (!pcPath && !mobilePath) return null
|
||||
|
||||
const resolvedImages = {
|
||||
pc: getResolvedHost(pathArray[pcField] || pathArray[mobileField]),
|
||||
mobile: getResolvedHost(pathArray[mobileField]),
|
||||
}
|
||||
|
||||
if (resourcesType === 'bg') {
|
||||
return {
|
||||
'--pc-bg': `url(${resolvedImages.pc})`,
|
||||
'--mobile-bg': `url(${resolvedImages.mobile})`,
|
||||
}
|
||||
pc: pcPath ? getResolvedHost(pcPath) : '',
|
||||
mobile: mobilePath ? getResolvedHost(mobilePath) : '',
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -70,14 +71,6 @@ export const getResponsiveSrc = (
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 반응형 배경 이미지를 위한 CSS 클래스를 반환합니다.
|
||||
* @returns 반응형 배경 클래스 배열
|
||||
*/
|
||||
export const getResponsiveClass = () => {
|
||||
return ['bg-[image:var(--mobile-bg)]', 'md:bg-[image:var(--pc-bg)]']
|
||||
}
|
||||
|
||||
/**
|
||||
* 색상값을 반환합니다.
|
||||
* @param colorName 색상 이름
|
||||
@@ -105,35 +98,22 @@ export const getColorCode = ({
|
||||
* @returns Record<string, string> CSS 변수 객체
|
||||
*/
|
||||
export const getPaginationClass = (
|
||||
paginationGroups?: PageDataResourceGroups,
|
||||
{ type }: { type: 'thumbnail' | 'bullet' } = { type: 'bullet' }
|
||||
paginationGroups?: PageDataResourceGroups
|
||||
): Record<string, string> => {
|
||||
// 기본 색상 상수
|
||||
const DEFAULT_ACTIVE = 'var(--primary)'
|
||||
const DEFAULT_DISABLED =
|
||||
type === 'bullet' ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.1)'
|
||||
|
||||
// Early return: 데이터가 없거나 충분하지 않은 경우
|
||||
if (!paginationGroups || paginationGroups.length < 2) {
|
||||
return {
|
||||
'--pagination-active': DEFAULT_ACTIVE,
|
||||
'--pagination-disabled': DEFAULT_DISABLED,
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// 색상 추출 또는 기본값 사용
|
||||
const paginationActive =
|
||||
getColorCode({
|
||||
colorName: paginationGroups[0]?.display?.color_name,
|
||||
colorCode: paginationGroups[0]?.display?.color_code,
|
||||
}) || DEFAULT_ACTIVE
|
||||
|
||||
const paginationDisabled =
|
||||
getColorCode({
|
||||
colorName: paginationGroups[1]?.display?.color_name,
|
||||
colorCode: paginationGroups[1]?.display?.color_code,
|
||||
}) || DEFAULT_DISABLED
|
||||
const paginationActive = getColorCode({
|
||||
colorName: paginationGroups[0]?.display?.color_name,
|
||||
colorCode: paginationGroups[0]?.display?.color_code,
|
||||
})
|
||||
|
||||
const paginationDisabled = getColorCode({
|
||||
colorName: paginationGroups[1]?.display?.color_name,
|
||||
colorCode: paginationGroups[1]?.display?.color_code,
|
||||
})
|
||||
return {
|
||||
'--pagination-active': paginationActive,
|
||||
'--pagination-disabled': paginationDisabled,
|
||||
|
||||
BIN
public/images/common/publisning_template_loader_black.png
Normal file
BIN
public/images/common/publisning_template_loader_black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
BIN
public/images/common/publisning_template_loader_white.png
Normal file
BIN
public/images/common/publisning_template_loader_white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 257 KiB |
Reference in New Issue
Block a user