feat. 공통 로그 변경, 고정 템플릿 로그 추가

This commit is contained in:
clkim
2025-12-15 15:25:32 +09:00
parent 966c66fe7b
commit f955b76e62
39 changed files with 670 additions and 795 deletions

View File

@@ -1,3 +1,6 @@
@import '@splidejs/vue-splide/css';
@import '@vuepic/vue-datepicker/dist/main.css';
@import './base/_reset.css'; @import './base/_reset.css';
@import './base/_theme.css'; @import './base/_theme.css';
@import './base/_font.css'; @import './base/_font.css';
@@ -8,11 +11,8 @@
@import './components/_layout.css'; @import './components/_layout.css';
@import './components/_modal.css'; @import './components/_modal.css';
@import './components/_splide.css'; @import './components/_splide.css';
/* @import './components/_base-content.css'; */
@import './components/_froala-style.css'; @import './components/_froala-style.css';
@import '@splidejs/vue-splide/css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@@ -1,93 +0,0 @@
/* 에디터 콘텐츠 기본 스타일 유틸리티 */
/* use-base 클래스를 추가하면 하위 자식 요소들에 기본 스타일이 적용됩니다 */
@layer components {
.use-base ul,
.use-base ol {
@apply list-disc pl-6;
}
.use-base ol {
@apply list-decimal;
}
.use-base ul ul,
.use-base ol ol,
.use-base ul ol,
.use-base ol ul {
@apply mb-0;
}
.use-base table {
@apply w-full border-collapse;
}
.use-base .fr-alternate-rows tr:nth-child(even),
.use-base thead,
.use-base tfoot {
@apply bg-[#FAFAFA];
}
.use-base th,
.use-base td {
@apply border border-[#D9D9D9] px-4 py-2 text-left;
}
.use-base th {
@apply font-semibold border-[#D9D9D9];
}
.use-base blockquote {
@apply border-l-4 border-[#D9D9D9] pl-4 italic;
}
.use-base h1 {
@apply text-2xl;
}
.use-base h2 {
@apply text-xl;
}
.use-base h3 {
@apply text-lg;
}
.use-base h4 {
@apply text-base;
}
.use-base h5 {
@apply text-sm;
}
.use-base h6 {
@apply text-xs;
}
.use-base strong,
.use-base b {
@apply font-bold;
}
.use-base em,
.use-base i {
@apply italic;
}
.use-base u {
@apply underline;
}
.use-base s {
@apply line-through;
}
.use-base a {
@apply text-[#3C75FF] underline;
}
.use-base img {
@apply max-w-full h-auto;
}
.use-base iframe {
@apply max-w-full h-auto aspect-video;
}
.use-base pre {
@apply bg-[#FAFAFA] p-4 rounded overflow-x-auto mb-4;
}
.use-base code {
@apply bg-[#FAFAFA] px-1 py-0.5 rounded text-sm;
}
.use-base pre code {
@apply bg-transparent p-0;
}
}

View File

@@ -1,93 +0,0 @@
/* 에디터 콘텐츠 기본 스타일 유틸리티 */
/* use-base 클래스를 추가하면 하위 자식 요소들에 기본 스타일이 적용됩니다 */
@layer components {
.use-base ul,
.use-base ol {
@apply list-disc pl-6;
}
.use-base ol {
@apply list-decimal;
}
.use-base ul ul,
.use-base ol ol,
.use-base ul ol,
.use-base ol ul {
@apply mb-0;
}
.use-base table {
@apply w-full border-collapse;
}
.use-base thead {
@apply bg-gray-100;
}
.use-base th,
.use-base td {
@apply border border-gray-300 px-4 py-2 text-left;
}
.use-base th {
@apply font-semibold bg-gray-50;
}
.use-base tbody tr:nth-child(even) {
@apply bg-gray-50;
}
.use-base blockquote {
@apply border-l-4 border-gray-300 pl-4 italic text-gray-700;
}
.use-base h1 {
@apply text-2xl;
}
.use-base h2 {
@apply text-xl;
}
.use-base h3 {
@apply text-lg;
}
.use-base h4 {
@apply text-base;
}
.use-base h5 {
@apply text-sm;
}
.use-base h6 {
@apply text-xs;
}
.use-base strong,
.use-base b {
@apply font-bold;
}
.use-base em,
.use-base i {
@apply italic;
}
.use-base u {
@apply underline;
}
.use-base s {
@apply line-through;
}
.use-base a {
@apply text-blue-600 underline;
}
.use-base a:hover {
@apply text-blue-800;
}
.use-base img {
@apply max-w-full h-auto my-4;
}
.use-base pre {
@apply bg-gray-100 p-4 rounded overflow-x-auto mb-4;
}
.use-base code {
@apply bg-gray-100 px-1 py-0.5 rounded text-sm;
}
.use-base pre code {
@apply bg-transparent p-0;
}
}

View File

@@ -8,10 +8,10 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const { locale } = useI18n() const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const handlePlayClick = () => { const handlePlayClick = () => {
sendLog(locale.value, useAnalyticsData(props.tracking)) sendLog(locale.value, props.tracking)
} }
</script> </script>

View File

@@ -63,17 +63,17 @@ const onSelectOption = (option: { [key: string | number]: any }): void => {
<div <div
v-if="isActive" v-if="isActive"
:data-placement="props.placement" :data-placement="props.placement"
class="absolute z-[10] top-full left-0 translate-y-[4px] w-full py-[8px] border border-solid border-[1px] border-[rgba(0,0,0,0.3)] rounded-[8px] bg-white shadow-[0_4px_10px_0_rgba(0,0,0,0.10)]" class="absolute z-[10] top-full left-0 translate-y-[4px] w-full py-[8px] border-[1px] border-solid border-[rgba(0,0,0,0.3)] rounded-[8px] bg-white shadow-[0_4px_10px_0_rgba(0,0,0,0.10)]"
> >
<ul class="relative flex flex-col items-center justify-start w-full"> <ul class="relative flex flex-col items-center justify-start w-full">
<li <li
v-for="(option, index) in props.options" v-for="option in props.options"
:key="String(option[props.labelName])" :key="String(option[props.labelName])"
class="relative flex items-center justify-left w-full" class="relative flex items-center justify-left w-full"
> >
<button <button
type="button" type="button"
class="relative flex items-center justify-left w-full py-[8px] pl-[40px] pr-[16px] bg-white text-left text-left text-[14px] font-[400] leading-[24px] tracking-[-0.42px] hover:bg-[rgba(0,0,0,0.04)]" class="relative flex items-center justify-left w-full py-[8px] pl-[40px] pr-[16px] bg-white text-left text-[14px] font-[400] leading-[24px] tracking-[-0.42px] hover:bg-[rgba(0,0,0,0.04)]"
:class=" :class="
selectedColor ? `text-[${selectedColor}]` : 'text-[#333333]' selectedColor ? `text-[${selectedColor}]` : 'text-[#333333]'
" "

View File

@@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ClassType } from '#layers/types/Common'
interface Props { interface Props {
src: string src: string
type?: 'mp4' | 'webm' type?: 'mp4' | 'webm'
@@ -9,7 +7,7 @@ interface Props {
muted?: boolean muted?: boolean
loop?: boolean loop?: boolean
bordered?: boolean bordered?: boolean
class?: ClassType class?: string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {

View File

@@ -2,12 +2,12 @@
import type { TrackingObject } from '#layers/types/api/common' import type { TrackingObject } from '#layers/types/api/common'
const { locale } = useI18n() const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const analytics = { const analytics = {
action_type: 'click', action_type: 'click',
click_item: '홈으로가기', click_item: '홈으로가기',
click_sarea: 'HOME', // TODO: 확인 필요 컴포넌트 id가 뭔가염 뭔가염 click_sarea: 'Home',
} as TrackingObject } as TrackingObject
</script> </script>
@@ -17,7 +17,7 @@ const analytics = {
type="link" type="link"
to="/home" to="/home"
class="btn-home" class="btn-home"
@click="sendLog(locale, useAnalyticsData(analytics))" @click="sendLog(locale, analytics)"
> >
<AtomsIconsHomeFill /> <AtomsIconsHomeFill />
</AtomsButtonCircle> </AtomsButtonCircle>

View File

@@ -22,12 +22,6 @@ const props = withDefaults(defineProps<Props>(), {
disabled: false, disabled: false,
}) })
const PLATFORM_LABEL_KEY: Record<PlatformTransformType, string> = {
pc: 'PC',
google_play: 'Google Play',
app_store: 'App Store',
} as const
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const { tm } = useI18n() const { tm } = useI18n()
const device = useDevice() const device = useDevice()
@@ -37,6 +31,12 @@ const { isProcessing, validateLauncher } = useCheckGameStart()
const { gameData } = storeToRefs(gameDataStore) const { gameData } = storeToRefs(gameDataStore)
const PLATFORM_LABEL_KEY: Record<PlatformTransformType, string> = {
pc: 'PC',
google_play: 'Google Play',
app_store: 'App Store',
} as const
const PLATFORM_ICON_MAP: Record<Platform, string> = { const PLATFORM_ICON_MAP: Record<Platform, string> = {
google_play: 'AtomsIconsLogoGoogle', google_play: 'AtomsIconsLogoGoogle',
app_store: 'AtomsIconsLogoApple', app_store: 'AtomsIconsLogoApple',

View File

@@ -3,19 +3,19 @@ import type { TrackingObject } from '#layers/types/api/common'
const { locale } = useI18n() const { locale } = useI18n()
const { y: windowY } = useWindowScroll({ behavior: 'smooth' }) const { y: windowY } = useWindowScroll({ behavior: 'smooth' })
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const analytics = { const analytics = {
action_type: 'click', action_type: 'click',
click_item: 'TOP버튼', click_item: 'TOP버튼',
click_sarea: 'TOP', // TODO: 확인 필요 컴포넌트 id가 뭔가염 뭔가염 click_sarea: 'TOP',
} as TrackingObject } as TrackingObject
const showBtn = computed(() => windowY.value > 0) const showBtn = computed(() => windowY.value > 0)
const handleScrollToTop = () => { const handleScrollToTop = () => {
windowY.value = 0 windowY.value = 0
sendLog(locale.value, useAnalyticsData(analytics)) sendLog(locale.value, analytics)
} }
</script> </script>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TrackingObject } from '#layers/types/api/common' import type { TrackingObject, ColorObject } from '#layers/types/api/common'
const showSnsList = ref(false) const showSnsList = ref(false)
const { locale, tm } = useI18n() const { locale, tm } = useI18n()
const gameDataStore = useGameDataStore() const gameDataStore = useGameDataStore()
const modalStore = useModalStore() const modalStore = useModalStore()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const { gameData } = storeToRefs(gameDataStore) const { gameData } = storeToRefs(gameDataStore)
const { handleOpenToast } = modalStore const { handleOpenToast } = modalStore
@@ -17,11 +17,9 @@ const analytics = {
} as TrackingObject } as TrackingObject
const snsBackgroundColor = computed(() => { const snsBackgroundColor = computed(() => {
const colorData = gameData.value?.comm_sns_bg_color_json?.display const colorData = gameData.value?.comm_sns_bg_color_json
const colorCode = getColorCode({ ?.display as ColorObject
colorName: colorData?.color_name, const colorCode = getColorCodeFromData(colorData, 'none')
colorCode: colorData?.color_code,
})
return colorCode return colorCode
}) })
const snsList = computed(() => { const snsList = computed(() => {
@@ -43,10 +41,7 @@ const handleCopy = async () => {
const url = window.location.href const url = window.location.href
await navigator.clipboard.writeText(url) await navigator.clipboard.writeText(url)
handleOpenToast({ contentText: tm('Alert_Copy_Complete') }) handleOpenToast({ contentText: tm('Alert_Copy_Complete') })
sendLog( sendLog(locale.value, { ...analytics, click_item: 'URL복사' })
locale.value,
useAnalyticsData({ ...analytics, click_item: 'URL복사' })
)
} catch (error) { } catch (error) {
console.error('[handleCopy] Error:', error) console.error('[handleCopy] Error:', error)
} }
@@ -78,15 +73,11 @@ const handleCopy = async () => {
:href="item.url" :href="item.url"
target="_blank" target="_blank"
class="sns-item" class="sns-item"
rel="noopener noreferrer"
:style="{ :style="{
backgroundImage: `url(${formatPathHost(`/images/common/ic-v2-logo-${key}-fill.png`, { imageType: 'common' })})`, backgroundImage: `url(${formatPathHost(`/images/common/ic-v2-logo-${key}-fill.png`, { imageType: 'common' })})`,
}" }"
@click=" @click="sendLog(locale, { ...analytics, click_item: key })"
sendLog(
locale,
useAnalyticsData({ ...analytics, click_item: key })
)
"
> >
<span class="sr-only">{{ key }}</span> <span class="sr-only">{{ key }}</span>
</a> </a>

View File

@@ -13,7 +13,13 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const { locale } = useI18n() const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const analytics = {
action_type: 'click',
click_item: props.title,
click_sarea: props.analyticsSarea,
}
const isNoImage = computed(() => { const isNoImage = computed(() => {
return !props.imgPath || props.imgPath === null return !props.imgPath || props.imgPath === null
@@ -21,15 +27,6 @@ const isNoImage = computed(() => {
const isShowOverlay = computed(() => { const isShowOverlay = computed(() => {
return props.title || props.description return props.title || props.description
}) })
const handleLinkClick = (title: string) => {
const analytics = {
click_item: title,
action_type: 'click',
click_sarea: props.analyticsSarea,
}
sendLog(locale.value, useAnalyticsData(analytics))
}
</script> </script>
<template> <template>
@@ -64,7 +61,7 @@ const handleLinkClick = (title: string) => {
:to="props.url" :to="props.url"
:target="props.linkTarget || '_self'" :target="props.linkTarget || '_self'"
class="card-link" class="card-link"
@click="handleLinkClick(props.title)" @click="sendLog(locale, analytics)"
/> />
</div> </div>
</template> </template>

View File

@@ -399,7 +399,6 @@ onMounted(() => {
</span> </span>
</template> </template>
<template <template
class="date-picker-calendar-wrap"
#month-year="{ #month-year="{
month, month,
year, year,

View File

@@ -7,7 +7,7 @@ const { locale } = useI18n()
const pageDataStore = usePageDataStore() const pageDataStore = usePageDataStore()
const scrollStore = useScrollStore() const scrollStore = useScrollStore()
const breakpoints = useResponsiveBreakpoints() const breakpoints = useResponsiveBreakpoints()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const { pageData } = storeToRefs(pageDataStore) const { pageData } = storeToRefs(pageDataStore)
@@ -108,10 +108,7 @@ const handleLnbClick = (lnbItem: PageDataLnbMenu) => {
: lnbItem.page_ver_tmpl_name_en : lnbItem.page_ver_tmpl_name_en
scrollStore.scrollToAnchor(targetId) scrollStore.scrollToAnchor(targetId)
sendLog( sendLog(locale.value, lnbItem.tracking_json as TrackingObject)
locale.value,
useAnalyticsData(lnbItem.tracking_json as unknown as TrackingObject)
)
} }
watch(directions, newVal => { watch(directions, newVal => {

View File

@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PageDataResourceGroup } from '#layers/types/api/pageData' import type { PageDataResourceGroup } from '#layers/types/api/pageData'
import { getColorCode } from '#layers/utils/styleUtil'
import { isTypeImage, isTypeText } from '#layers/utils/dataUtil' import { isTypeImage, isTypeText } from '#layers/utils/dataUtil'
interface Props { interface Props {
@@ -15,8 +14,9 @@ const props = withDefaults(defineProps<Props>(), {
const imagePaths = computed(() => getResourceSrc(props.resourcesData)) const imagePaths = computed(() => getResourceSrc(props.resourcesData))
const displayText = computed(() => props.resourcesData?.display?.text) const displayText = computed(() => props.resourcesData?.display?.text)
const colorName = computed(() => props.resourcesData?.display?.color_name) const displayColor = computed(() =>
const colorCode = computed(() => props.resourcesData?.display?.color_code) getColorCodeFromData(props.resourcesData?.display, 'none')
)
// HTML 콘텐츠 정리 (줄바꿈 처리) // HTML 콘텐츠 정리 (줄바꿈 처리)
const sanitizedContent = computed(() => { const sanitizedContent = computed(() => {
@@ -35,7 +35,7 @@ const sanitizedContent = computed(() => {
<span <span
v-else-if="isTypeText(resourcesData?.resource_type)" v-else-if="isTypeText(resourcesData?.resource_type)"
v-dompurify-html="sanitizedContent" v-dompurify-html="sanitizedContent"
:style="{ color: getColorCode({ colorName, colorCode }) }" :style="{ color: displayColor }"
class="block" class="block"
/> />
</template> </template>

View File

@@ -8,15 +8,12 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const { locale } = useI18n() const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const handleArrowClick = (direction: 'prev' | 'next') => { const handleArrowClick = (direction: 'prev' | 'next') => {
if (props.arrowsData) { if (props.arrowsData) {
const arrowIndex = direction === 'prev' ? 0 : 1 const arrowIndex = direction === 'prev' ? 0 : 1
sendLog( sendLog(locale.value, props.arrowsData[arrowIndex]?.tracking)
locale.value,
useAnalyticsData(props.arrowsData[arrowIndex]?.tracking)
)
} }
} }
</script> </script>

View File

@@ -7,11 +7,11 @@ import type {
const { locale } = useI18n() const { locale } = useI18n()
const gameDomain = useGetGameDomain() const gameDomain = useGetGameDomain()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const analytics = { const analytics = {
action_type: 'click', action_type: 'click',
click_sarea: 'eventNavigation', click_sarea: 'EventNavigation',
} }
const isEventNavigationOpen = ref(true) const isEventNavigationOpen = ref(true)
@@ -48,7 +48,7 @@ const toggleEventNavigation = () => {
...analytics, ...analytics,
click_item: isEventNavigationOpen.value ? '열기' : '닫기', click_item: isEventNavigationOpen.value ? '열기' : '닫기',
} as TrackingObject } as TrackingObject
sendLog(locale.value, useAnalyticsData(navigationAnalytics)) sendLog(locale.value, navigationAnalytics)
} }
onMounted(async () => { onMounted(async () => {
@@ -77,15 +77,13 @@ onMounted(async () => {
<AtomsLocaleLink <AtomsLocaleLink
:to="item.page_url" :to="item.page_url"
:target="item.link_type === 2 ? '_blank' : '_self'" :target="item.link_type === 2 ? '_blank' : '_self'"
:rel="item.link_type === 2 ? 'noopener noreferrer' : undefined"
class="item-link" class="item-link"
@click=" @click="
sendLog( sendLog(locale, {
locale, ...analytics,
useAnalyticsData({ click_item: item.banner_title || item.promotion_name,
...analytics, })
click_item: item.banner_title || item.promotion_name,
})
)
" "
> >
<div class="item-thumbnail"> <div class="item-thumbnail">

View File

@@ -1,3 +1,184 @@
<script setup lang="ts">
import type {
FooterMenuItem,
FooterData,
DevCiConfig,
} from '#layers/types/components/footer'
// Configuration
const runtimeConfig = useRuntimeConfig()
const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_footer.json'
// Multilingual
const resultGetMultilingual = await useGetMultilingual({
baseApiUrl: dataResourcesUrl,
fileName: multilingualFileName,
})
const { tm, locale }: any = useI18n({
useScope: 'local',
messages: Object(resultGetMultilingual?.value?.multilingual),
})
// Footer_caution 값이 있고 빈 객체가 아닌지 체크
const hasCautionText = computed(() => {
const value = tm('Footer_caution')
// null, undefined 체크
if (value === null || value === undefined) {
return false
}
// 빈 객체 체크
if (
typeof value === 'object' &&
!Array.isArray(value) &&
Object.keys(value).length === 0
) {
return false
}
// 문자열로 변환하여 빈 문자열 또는 '{}' 문자열 체크
const stringValue = String(value).trim()
if (stringValue === '' || stringValue === '{}') {
return false
}
return true
})
const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore)
// const path = ref<string>(`${staticUrl}/local/template/${gameData.value.s3_folder_name}`)
// 공통다국어 data
const footerLinks = computed((): FooterMenuItem[] => {
const menu = (tm as any)('Footer_Menu')
const menuItems = Array.isArray(menu) ? (menu as FooterMenuItem[]) : []
// ja일 때 fund_display_yn에 따라 id가 footerFund인 항목 처리
if (locale.value === 'ja') {
const fundDisplayYn = footerData.value?.fund_display_yn
const fundDisplayUrl = footerData.value?.fund_display_url
// id가 footerFund인 항목의 인덱스 찾기
const fundIndex = menuItems.findIndex(
(item: any) => item.id === 'footerFund'
)
// fund_display_yn이 'y'가 아니면 id가 footerFund인 항목 제거
if (!fundDisplayYn) {
return menuItems.filter((item: any) => item.id !== 'footerFund')
}
// fund_display_yn이 'y'이면 id가 footerFund인 항목의 url 설정
if (fundDisplayYn && fundIndex !== -1) {
const updatedMenuItems = [...menuItems]
updatedMenuItems[fundIndex] = {
...updatedMenuItems[fundIndex],
url: fundDisplayUrl || updatedMenuItems[fundIndex].url,
}
return updatedMenuItems
}
}
return menuItems
})
const footerData = ref(gameData.value?.footer_json as unknown as FooterData)
const setDevCi = ref<DevCiConfig>({
dev_ci_yn: gameData.value?.footer_dev_ci_img_yn as boolean,
dev_ci_img_path: gameData.value?.footer_dev_ci_img_path as string,
})
const useGameRating = computed<boolean>(() => {
return footerData.value.use_game_rating
})
///local/template/common/grades_age
const getGameRatingImage = computed((): { type: string; image: string } => {
// 안전하게 rating_type 값 확인
if (
!footerData.value?.game_rating_info?.rating_type ||
typeof footerData.value.game_rating_info.rating_type !== 'string'
) {
return { type: '', image: '' }
}
const contentInfo = footerData.value.game_rating_info.rating_type
const ratingType = contentInfo.trim()
// 빈 문자열인 경우 처리
if (!ratingType) {
return { type: '', image: '' }
}
const ageTypeMap: Record<string, string> = {
'6': 'Type6',
'12': 'Type12',
'15': 'Type15',
'18': 'Type18',
'19': 'Type19',
all: 'TypeAll',
e: 'TypeExempt',
}
const type = ageTypeMap[ratingType as keyof typeof ageTypeMap] || 'TypeTest'
return {
type,
image: formatPathHost(
`/images/common/grades_age/${locale.value}/${type}.svg`,
{
imageType: 'common',
}
),
}
})
const getContentInfoImage = computed((): string[] => {
// 안전하게 content_info 값 확인
if (
!footerData.value?.game_rating_info?.content_info ||
typeof footerData.value.game_rating_info.content_info !== 'string'
) {
return []
}
const contentInfo = footerData.value.game_rating_info.content_info.split(',')
contentInfo.pop()
const contentTypeMap: Record<string, string> = {
'1': 'Type-sexual',
'2': 'Type-violence',
'3': 'Type-fear',
'4': 'Type-inapposite',
'5': 'Type-drug',
'6': 'Type-crime',
'7': 'Type-speculation',
}
return contentInfo
.map(item => {
const type = contentTypeMap[item]
return type
? formatPathHost(`/images/common/grades_use/${type}.svg`, {
imageType: 'common',
})
: ''
})
.filter(Boolean)
})
const showAgeRating = ref<boolean>(false)
const toggleAgeRating = (): void => {
showAgeRating.value = !showAgeRating.value
}
const footerAgeRatingInfo = computed((): string[] => {
const info = (tm as any)('Footer_AgeRating_Info')
return Array.isArray(info) ? info : []
})
</script>
<template> <template>
<footer id="footer" ref="footerRef" class="relative bg-blac"> <footer id="footer" ref="footerRef" class="relative bg-blac">
<div <div
@@ -91,13 +272,22 @@
</div> </div>
<!-- 정보 테이블 --> <!-- 정보 테이블 -->
<div v-if="locale === 'zh-tw'" class="px-6 py-6 rounded-b-lg bg-[#292929]"> <div
v-if="locale === 'zh-tw'"
class="px-6 py-6 rounded-b-lg bg-[#292929]"
>
<div class="space-y-2"> <div class="space-y-2">
<div v-dompurify-html="tm('Footer_AgeRating_Caution')"></div> <div
v-dompurify-html="tm('Footer_AgeRating_Caution')"
></div>
</div> </div>
</div> </div>
<div v-else class="px-6 py-6 rounded-b-lg bg-[#A31639]" :class="`bg-${getGameRatingImage.type}`"> <div
v-else
class="px-6 py-6 rounded-b-lg bg-[#A31639]"
:class="`bg-${getGameRatingImage.type}`"
>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex flex-start border-b border-white/10 pb-2"> <div class="flex flex-start border-b border-white/10 pb-2">
<span class="text-white text-sm flex-1"> <span class="text-white text-sm flex-1">
@@ -159,13 +349,12 @@
<div <div
class="language-area static md:absolute bottom-7 right-10 text-white mt-5 md:mt-0 md:bottom-5.5 md:right-4" class="language-area static md:absolute bottom-7 right-10 text-white mt-5 md:mt-0 md:bottom-5.5 md:right-4"
> >
<BlocksLanguageSwitcher :language-order="tm('Footer_Language_Order')" /> <BlocksLanguageSwitcher
:language-order="tm('Footer_Language_Order')"
/>
</div> </div>
<div <div v-if="hasCautionText" class="mt-6 md:mt-6 hidden sm:block">
v-if="hasCautionText"
class="mt-6 md:mt-6 hidden sm:block"
>
<div <div
v-dompurify-html="tm('Footer_caution')" v-dompurify-html="tm('Footer_caution')"
class="text-xs text-white/30" class="text-xs text-white/30"
@@ -178,7 +367,11 @@
<div class="logo-area flex items-center gap-7 mt-6 md:mt-6"> <div class="logo-area flex items-center gap-7 mt-6 md:mt-6">
<span> <span>
<a :href="tm('Footer_Smilegate_Link')" target="_blank" class="smilegate"> <a
:href="tm('Footer_Smilegate_Link')"
target="_blank"
class="smilegate"
>
<img <img
:src=" :src="
formatPathHost(`/images/common/logo_smilegate.png`, { formatPathHost(`/images/common/logo_smilegate.png`, {
@@ -225,183 +418,6 @@
</footer> </footer>
</template> </template>
<script setup lang="ts">
import type {
FooterMenuItem,
FooterData,
DevCiConfig,
} from '#layers/types/Common'
// Configuration
const runtimeConfig = useRuntimeConfig()
const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_footer.json'
// Multilingual
const resultGetMultilingual = await useGetMultilingual({
baseApiUrl: dataResourcesUrl,
fileName: multilingualFileName,
})
const { tm, locale }: any = useI18n({
useScope: 'local',
messages: Object(resultGetMultilingual?.value?.multilingual),
})
// Footer_caution 값이 있고 빈 객체가 아닌지 체크
const hasCautionText = computed(() => {
const value = tm('Footer_caution')
// null, undefined 체크
if (value === null || value === undefined) {
return false
}
// 빈 객체 체크
if (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) {
return false
}
// 문자열로 변환하여 빈 문자열 또는 '{}' 문자열 체크
const stringValue = String(value).trim()
if (stringValue === '' || stringValue === '{}') {
return false
}
return true
})
const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore)
// const path = ref<string>(`${staticUrl}/local/template/${gameData.value.s3_folder_name}`)
// 공통다국어 data
const footerLinks = computed((): FooterMenuItem[] => {
const menu = (tm as any)('Footer_Menu')
const menuItems = Array.isArray(menu) ? (menu as FooterMenuItem[]) : []
// ja일 때 fund_display_yn에 따라 id가 footerFund인 항목 처리
if (locale.value === 'ja') {
const fundDisplayYn = footerData.value?.fund_display_yn
const fundDisplayUrl = footerData.value?.fund_display_url
// id가 footerFund인 항목의 인덱스 찾기
const fundIndex = menuItems.findIndex((item: any) => item.id === 'footerFund')
// fund_display_yn이 'y'가 아니면 id가 footerFund인 항목 제거
if (!fundDisplayYn) {
return menuItems.filter((item: any) => item.id !== 'footerFund')
}
// fund_display_yn이 'y'이면 id가 footerFund인 항목의 url 설정
if (fundDisplayYn && fundIndex !== -1) {
const updatedMenuItems = [...menuItems]
updatedMenuItems[fundIndex] = {
...updatedMenuItems[fundIndex],
url: fundDisplayUrl || updatedMenuItems[fundIndex].url
}
return updatedMenuItems
}
}
return menuItems
})
const footerData = ref(gameData.value?.footer_json as unknown as FooterData)
const setDevCi = ref<DevCiConfig>({
dev_ci_yn: gameData.value?.footer_dev_ci_img_yn as boolean,
dev_ci_img_path: gameData.value?.footer_dev_ci_img_path as string,
})
const useGameRating = computed<boolean>(() => {
return footerData.value.use_game_rating
})
///local/template/common/grades_age
const getGameRatingImage = computed((): { type: string, image: string } => {
// 안전하게 rating_type 값 확인
if (
!footerData.value?.game_rating_info?.rating_type ||
typeof footerData.value.game_rating_info.rating_type !== 'string'
) {
return { type: '', image: '' }
}
const contentInfo = footerData.value.game_rating_info.rating_type
const ratingType = contentInfo.trim()
// 빈 문자열인 경우 처리
if (!ratingType) {
return { type: '', image: '' }
}
const ageTypeMap: Record<string, string> = {
'6': 'Type6',
'12': 'Type12',
'15': 'Type15',
'18': 'Type18',
'19': 'Type19',
all: 'TypeAll',
e: 'TypeExempt',
}
const type = ageTypeMap[ratingType as keyof typeof ageTypeMap] || 'TypeTest'
return {
type,
image: formatPathHost(
`/images/common/grades_age/${locale.value}/${type}.svg`,
{
imageType: 'common',
}
),
}
})
const getContentInfoImage = computed((): string[] => {
// 안전하게 content_info 값 확인
if (
!footerData.value?.game_rating_info?.content_info ||
typeof footerData.value.game_rating_info.content_info !== 'string'
) {
return []
}
const contentInfo = footerData.value.game_rating_info.content_info.split(',')
contentInfo.pop()
const contentTypeMap: Record<string, string> = {
'1': 'Type-sexual',
'2': 'Type-violence',
'3': 'Type-fear',
'4': 'Type-inapposite',
'5': 'Type-drug',
'6': 'Type-crime',
'7': 'Type-speculation',
}
return contentInfo
.map(item => {
const type = contentTypeMap[item]
return type
? formatPathHost(`/images/common/grades_use/${type}.svg`, {
imageType: 'common',
})
: ''
})
.filter(Boolean)
})
const showAgeRating = ref<boolean>(false)
const toggleAgeRating = (): void => {
showAgeRating.value = !showAgeRating.value
}
const footerAgeRatingInfo = computed((): string[] => {
const info = (tm as any)('Footer_AgeRating_Info')
return Array.isArray(info) ? info : []
})
</script>
<style scoped> <style scoped>
em { em {
font-style: normal; font-style: normal;
@@ -412,11 +428,11 @@ em {
} }
.bg-Type12 { .bg-Type12 {
background-color: #4369B1; background-color: #4369b1;
} }
.bg-Type15 { .bg-Type15 {
background-color: #F9B846; background-color: #f9b846;
} }
.bg-Type18 { .bg-Type18 {
@@ -424,11 +440,11 @@ em {
} }
.bg-Type19 { .bg-Type19 {
background-color: #A31639; background-color: #a31639;
} }
.bg-TypeAll { .bg-TypeAll {
background-color: #44A342; background-color: #44a342;
} }
.bg-TypeExempt { .bg-TypeExempt {

View File

@@ -18,7 +18,7 @@ const gameDataStore = useGameDataStore()
const pageDataStore = usePageDataStore() const pageDataStore = usePageDataStore()
const scrollStore = useScrollStore() const scrollStore = useScrollStore()
const breakpoints = useResponsiveBreakpoints() const breakpoints = useResponsiveBreakpoints()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const { gameData } = storeToRefs(gameDataStore) const { gameData } = storeToRefs(gameDataStore)
const { pageLayoutType } = storeToRefs(pageDataStore) const { pageLayoutType } = storeToRefs(pageDataStore)
@@ -145,14 +145,14 @@ const handleSendLog = (item: string) => {
click_item: item, click_item: item,
click_sarea: 'GNB', click_sarea: 'GNB',
} }
sendLog(locale.value, useAnalyticsData(analytics)) sendLog(locale.value, analytics)
} }
const handleGnbItemClick = (gnbItem: GameDataMenu) => { const handleGnbItemClick = (gnbItem: GameDataMenu) => {
if (isNotClickable(gnbItem)) return if (isNotClickable(gnbItem)) return
handleMenuClose() handleMenuClose()
sendLog(locale.value, useAnalyticsData(gnbItem.tracking_json)) sendLog(locale.value, gnbItem.tracking_json)
} }
onMounted(() => { onMounted(() => {
@@ -389,9 +389,7 @@ onMounted(() => {
:text-color=" :text-color="
getColorCodeFromData(start1depthData?.btn_info, 'txt') getColorCodeFromData(start1depthData?.btn_info, 'txt')
" "
@click=" @click="sendLog(locale, start1depthData.tracking)"
sendLog(locale, useAnalyticsData(start1depthData.tracking))
"
> >
{{ start1depthData?.btn_info?.txt_btn_name }} {{ start1depthData?.btn_info?.txt_btn_name }}
</BlocksButtonLauncher> </BlocksButtonLauncher>
@@ -404,9 +402,7 @@ onMounted(() => {
<BlocksButtonLauncher <BlocksButtonLauncher
type="custom" type="custom"
:platform="key" :platform="key"
@click=" @click="sendLog(locale, item.tracking)"
sendLog(locale, useAnalyticsData(item.tracking))
"
> >
{{ item.btn_info?.txt_btn_name }} {{ item.btn_info?.txt_btn_name }}
</BlocksButtonLauncher> </BlocksButtonLauncher>

View File

@@ -75,7 +75,7 @@ watch(isPAssApiLoading, newVal => {
onMounted(() => { onMounted(() => {
const { sendLog } = useAnalytics() const { sendLog } = useAnalytics()
sendLog(locale.value, useAnalyticsData('view')) sendLog(locale.value, 'view')
if (!hasApiCallStarted.value) { if (!hasApiCallStarted.value) {
loadingStore.stopFullLoading() loadingStore.stopFullLoading()

View File

@@ -16,7 +16,7 @@ const device = useDevice()
const modalStore = useModalStore() const modalStore = useModalStore()
const scrollStore = useScrollStore() const scrollStore = useScrollStore()
const breakpoints = useResponsiveBreakpoints() const breakpoints = useResponsiveBreakpoints()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const buttonList = computed<PageDataResourceGroup[]>( const buttonList = computed<PageDataResourceGroup[]>(
() => props.resourcesData ?? [] () => props.resourcesData ?? []
@@ -90,7 +90,7 @@ const downloadFile = async (url: string = '', osType: number = 0) => {
} }
const handleButtonClick = (button: PageDataResourceGroup) => { const handleButtonClick = (button: PageDataResourceGroup) => {
sendLog(locale.value, useAnalyticsData(button.tracking)) sendLog(locale.value, button.tracking)
const btnDetail = button.btn_info?.detail const btnDetail = button.btn_info?.detail

View File

@@ -1,43 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TrackingObject } from '#layers/types/api/common'
interface Props { interface Props {
title: string title: string
description?: string description?: string
link?: string link?: string
target?: '_self' | '_blank' target?: '_self' | '_blank'
linkAnalytics?: TrackingObject
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
target: '_blank', target: '_blank',
}) })
const componentTag = computed((): string => { const { locale } = useI18n()
switch (props.target) { const { sendLog } = useAnalytics()
case '_self':
return 'AtomsLocaleLink'
case '_blank':
return 'a'
default:
return 'a'
}
})
const componentProps = computed(() => {
if (props.target === '_self') {
return {
to: props.link,
}
}
if (props.target === '_blank') {
return {
href: props.link,
target: props.target,
rel: 'noopener noreferrer',
}
}
return {}
})
</script> </script>
<template> <template>
@@ -55,11 +32,12 @@ const componentProps = computed(() => {
> >
{{ props.description }} {{ props.description }}
</p> </p>
<component <AtomsLocaleLink
:is="componentTag"
v-else-if="props.description && props.link" v-else-if="props.description && props.link"
v-bind="componentProps" :to="props.link"
:target="props.target"
class="relative flex items-center justify-center gap-[4px] w-auto h-auto text-[#3C75FF] text-[14px] font-[500] leading-[20px] tracking-[-0.42px] md:text-[16px] md:leading-[24px] md:tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-0 before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20" class="relative flex items-center justify-center gap-[4px] w-auto h-auto text-[#3C75FF] text-[14px] font-[500] leading-[20px] tracking-[-0.42px] md:text-[16px] md:leading-[24px] md:tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-0 before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20"
@click="sendLog(locale, props.linkAnalytics)"
> >
<span>{{ props.description }}</span> <span>{{ props.description }}</span>
<AtomsIconsWebLinkLine <AtomsIconsWebLinkLine
@@ -68,7 +46,7 @@ const componentProps = computed(() => {
color="#3C75FF" color="#3C75FF"
class="icon" class="icon"
/> />
</component> </AtomsLocaleLink>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { globalDateFormat } from '@seed-next/date' import { globalDateFormat } from '@seed-next/date'
import { PREREGIST_ERROR_CODE } from '#layers/composables/usePreregist' import { PREREGIST_ERROR_CODE } from '#layers/composables/usePreregist'
import type { TrackingObject } from '#layers/types/api/common'
interface Props { interface Props {
preregistCode: string preregistCode: string
@@ -8,6 +9,7 @@ interface Props {
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const pageVerTmplNameEn = inject('pageVerTmplNameEn')
// Composables // Composables
const { locale } = useI18n() const { locale } = useI18n()
@@ -25,6 +27,7 @@ const {
} = usePreregist() } = usePreregist()
const { gameData } = storeToRefs(gameDataStore) const { gameData } = storeToRefs(gameDataStore)
const { sendLog } = useAnalytics()
// Constants // Constants
const stoveCs = runtimeConfig.public.stoveCs const stoveCs = runtimeConfig.public.stoveCs
@@ -69,6 +72,16 @@ const toggleExpand = (event: Event) => {
isExpandedMarketing.value = !isExpandedMarketing.value isExpandedMarketing.value = !isExpandedMarketing.value
} }
const handleSendLog = (item: string) => {
const analytics = {
action_type: 'click',
click_item: item,
click_sarea: pageVerTmplNameEn,
} as TrackingObject
sendLog(locale.value, analytics, { useGA: true })
}
/** /**
* 에러 모달 표시 * 에러 모달 표시
*/ */
@@ -204,6 +217,7 @@ const handleSubmit = async (): Promise<void> => {
if (result.code === PREREGIST_ERROR_CODE.SUCCESS) { if (result.code === PREREGIST_ERROR_CODE.SUCCESS) {
currentStep.value = 2 currentStep.value = 2
handleSendLog('사전 등록 하기')
return return
} }
@@ -394,7 +408,9 @@ defineExpose({
v-if="device.isDesktop ? true : device.isAndroid" v-if="device.isDesktop ? true : device.isAndroid"
href="https://play.google.com/store/search?q=stove&c=apps" href="https://play.google.com/store/search?q=stove&c=apps"
target="_blank" target="_blank"
rel="noopener noreferrer"
class="flex h-12 w-full items-center justify-center gap-1.5 rounded-lg bg-white/[0.04] px-8 pl-8 pr-10 text-sm font-medium leading-5 tracking-[-0.42px] text-white no-underline backdrop-blur-[15px] transition-colors duration-200 hover:bg-white/[0.08] md:h-14 md:gap-2 md:text-base md:leading-6 md:tracking-[-0.48px]" class="flex h-12 w-full items-center justify-center gap-1.5 rounded-lg bg-white/[0.04] px-8 pl-8 pr-10 text-sm font-medium leading-5 tracking-[-0.42px] text-white no-underline backdrop-blur-[15px] transition-colors duration-200 hover:bg-white/[0.08] md:h-14 md:gap-2 md:text-base md:leading-6 md:tracking-[-0.48px]"
@click="handleSendLog('Google Play')"
> >
<AtomsIconsLogoGoogle /> <AtomsIconsLogoGoogle />
<span>Google Play</span> <span>Google Play</span>
@@ -403,7 +419,9 @@ defineExpose({
v-if="device.isDesktop ? true : device.isApple" v-if="device.isDesktop ? true : device.isApple"
href="https://apps.apple.com/app/stove-app-stove-app/id1342134971" href="https://apps.apple.com/app/stove-app-stove-app/id1342134971"
target="_blank" target="_blank"
rel="noopener noreferrer"
class="flex h-12 w-full items-center justify-center gap-1.5 rounded-lg bg-white/[0.04] px-8 pl-8 pr-10 text-sm font-medium leading-5 tracking-[-0.42px] text-white no-underline backdrop-blur-[15px] transition-colors duration-200 hover:bg-white/[0.08] md:h-14 md:gap-2 md:text-base md:leading-6 md:tracking-[-0.48px]" class="flex h-12 w-full items-center justify-center gap-1.5 rounded-lg bg-white/[0.04] px-8 pl-8 pr-10 text-sm font-medium leading-5 tracking-[-0.42px] text-white no-underline backdrop-blur-[15px] transition-colors duration-200 hover:bg-white/[0.08] md:h-14 md:gap-2 md:text-base md:leading-6 md:tracking-[-0.48px]"
@click="handleSendLog('App Store')"
> >
<AtomsIconsLogoApple /> <AtomsIconsLogoApple />
<span>App Store</span> <span>App Store</span>

View File

@@ -25,7 +25,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits(['mounted', 'move']) const emit = defineEmits(['mounted', 'move'])
const { locale } = useI18n() const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const splideIndex = defineModel<number>('index', { required: false }) const splideIndex = defineModel<number>('index', { required: false })
@@ -115,7 +115,7 @@ const handleThumbnailClick = (title: string) => {
click_sarea: props.analyticsSarea, click_sarea: props.analyticsSarea,
} }
sendLog(locale.value, useAnalyticsData(paginationAnalytics)) sendLog(locale.value, paginationAnalytics)
} }
onMounted(() => { onMounted(() => {

View File

@@ -69,7 +69,7 @@ export const useAnalyticsData = (
const baseViewArea = pageNameEn.value const baseViewArea = pageNameEn.value
let logData = {} as AnalyticsDetailType let logData = {} as AnalyticsDetailType
// 문자열 'view'만 들어오는 경우 (페이지뷰) // 페이지뷰
if (analytics === 'view') { if (analytics === 'view') {
logData = { logData = {
actionType: analytics, actionType: analytics,
@@ -218,28 +218,6 @@ const sendSA = (
} }
} }
// ============================================================================
// 기본 로그 일괄 전송
// ============================================================================
/**
* 기본 로그 일괄 전송
*
* @param locale 언어 코드
* @param analytics Partial<AnalyticsDetailType>
*/
const sendLog = (locale: string, analytics: Partial<AnalyticsDetailType>) => {
if (isEmptyAnalytics(analytics)) return
// 언어 코드 대문자 변환
analytics.eventLocale = locale.toUpperCase()
// SA 전송 : actionType, logSourceType 유무로 판별 (logSourceType 체크는 주석 처리 되어있던 로직 유지)
if (analytics.actionType && analytics.actionType !== '') {
sendSA(analytics, { mcode: analytics.mcode, options: analytics.options })
}
}
// ============================================================================ // ============================================================================
// Google Analytics // Google Analytics
// ============================================================================ // ============================================================================
@@ -261,19 +239,6 @@ const withGA = (callback: (gtag: any) => void) => {
} }
} }
/**
* Google Analytics 전송 (기본 이벤트만 전송)
*
* @param gaEventName 이벤트명
*/
const sendGAEventOnly = (gaEventName: string) => {
if (!gaEventName) return
withGA(gtag => {
gtag('event', `${gaEventName}`)
})
}
/** /**
* Google Analytics 전송 * Google Analytics 전송
* *
@@ -295,6 +260,49 @@ const sendGA = (analytics: Partial<AnalyticsDetailType>) => {
}) })
} }
/**
* Google Analytics 전송 (기본 이벤트만 전송)
*
* @param gaEventName 이벤트명
*/
const sendGAEventOnly = (gaEventName: string) => {
if (!gaEventName) return
withGA(gtag => {
gtag('event', `${gaEventName}`)
})
}
// ============================================================================
// 기본 로그 일괄 전송
// ============================================================================
/**
* 기본 로그 일괄 전송
*
* @param locale 언어 코드
* @param analytics Partial<AnalyticsDetailType>
*/
const sendLog = (
locale: string,
analytics: string | TrackingObject,
options: { useGA?: boolean } = { useGA: false }
) => {
const analyticsData = useAnalyticsData(analytics)
analyticsData.eventLocale = locale.toUpperCase()
// SA 전송 : actionType, logSourceType 유무로 판별
if (analyticsData.actionType && analyticsData.actionType !== '') {
sendSA(analyticsData, {
mcode: analyticsData?.mcode,
options: analyticsData?.options,
})
if (options.useGA) {
sendGA(analyticsData)
}
}
}
// ============================================================================ // ============================================================================
// Meta / Twitter / TikTok Pixel // Meta / Twitter / TikTok Pixel
// ============================================================================ // ============================================================================
@@ -364,19 +372,13 @@ const sendTiktokPixel = (ttEventName?: string) => {
const sendMarketingLog = ({ const sendMarketingLog = ({
logName, logName,
eventName, eventName,
analytics,
}: { }: {
logName: 'ga' | 'meta' | 'twitter' | 'tiktok' logName: 'ga' | 'meta' | 'twitter' | 'tiktok'
eventName?: string eventName?: string
analytics?: Partial<AnalyticsDetailType>
}) => { }) => {
switch (logName) { switch (logName) {
case 'ga': { case 'ga': {
if (eventName) { sendGAEventOnly(eventName)
sendGAEventOnly(eventName)
} else if (analytics) {
sendGA(analytics)
}
break break
} }
case 'meta': { case 'meta': {

View File

@@ -1,5 +1,4 @@
import VueDatePicker from '@vuepic/vue-datepicker' import VueDatePicker from '@vuepic/vue-datepicker'
import '@vuepic/vue-datepicker/dist/main.css'
export default defineNuxtPlugin(nuxtApp => { export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.component('VueDatePicker', VueDatePicker) nuxtApp.vueApp.component('VueDatePicker', VueDatePicker)

View File

@@ -3,22 +3,30 @@ import type { PageDataValue } from '#layers/types/api/pageData'
export const usePageDataStore = defineStore('pageData', () => { export const usePageDataStore = defineStore('pageData', () => {
const pageData = ref<PageDataValue | null>(null) const pageData = ref<PageDataValue | null>(null)
const pageLayoutType = ref<'default' | 'promotion' | null>(null) const pageLayoutType = ref<'default' | 'promotion' | null>(null)
const pageName = ref<string | null>(null)
const pageNameEn = ref<string | null>(null)
const setPageData = (response: PageDataValue) => { const setPageData = (response: PageDataValue) => {
clearPageData() clearPageData()
pageData.value = response pageData.value = response
pageLayoutType.value = getLayoutType(pageData.value) pageLayoutType.value = getLayoutType(pageData.value)
pageName.value = pageData.value?.page_name
pageNameEn.value = pageData.value?.page_name_en
} }
const clearPageData = () => { const clearPageData = () => {
pageData.value = null pageData.value = null
pageLayoutType.value = null pageLayoutType.value = null
pageName.value = null
pageNameEn.value = null
} }
return { return {
pageData, pageData,
pageLayoutType, pageLayoutType,
pageName,
pageNameEn,
setPageData, setPageData,
clearPageData, clearPageData,
} }

View File

@@ -15,6 +15,7 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
import type { ReqCouponList } from '#layers/types/api/couponData' import type { ReqCouponList } from '#layers/types/api/couponData'
interface Props { interface Props {
id?: string
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string pageVerTmplNameEn: string
@@ -38,6 +39,7 @@ const { t, tm, locale }: any = useI18n({
}) })
// Composables // Composables
const { sendLog } = useAnalytics()
// const { isGameMaintenance, checkGameMaintenance } = useGetGameMaintenance() // const { isGameMaintenance, checkGameMaintenance } = useGetGameMaintenance()
const { isWebInspection, getInspectionDataExternal } = const { isWebInspection, getInspectionDataExternal } =
useGetInspectionDataExternal() useGetInspectionDataExternal()
@@ -72,17 +74,15 @@ const { couponNo, isSelectCharacter, selectCharacter } =
storeToRefs(couponStore) storeToRefs(couponStore)
const { updateCouponNo, updateSelectCharacter, isEmptyCouponNo } = couponStore const { updateCouponNo, updateSelectCharacter, isEmptyCouponNo } = couponStore
// Data
const backgroundData = computed(() =>
getComponentGroup(props.components, 'background')
)
// Refs // Refs
const clientIp = ref('') const clientIp = ref('')
const monthSelectList = ref<Array<number>>([1, 3, 6, 12]) const monthSelectList = ref<Array<number>>([1, 3, 6, 12])
const isSelectCharacterModalOpen = ref(false) const isSelectCharacterModalOpen = ref(false)
// Computed // Computed
const backgroundData = computed(() =>
getComponentGroup(props.components, 'background')
)
const sortedCharacterList = computed(() => { const sortedCharacterList = computed(() => {
return characterList.value return characterList.value
.map(characterInfo => { .map(characterInfo => {
@@ -100,6 +100,15 @@ const sortedCharacterList = computed(() => {
}) })
// Functions // Functions
const handleSendLog = (item: string) => {
const analytics = {
action_type: 'click',
click_item: item,
click_sarea: props.pageVerTmplNameEn,
}
sendLog(locale.value, analytics)
}
/** /**
* @description 기본 Alert 모달 팝업 함수입니다. * @description 기본 Alert 모달 팝업 함수입니다.
* @param text - 모달 내용 * @param text - 모달 내용
@@ -242,6 +251,8 @@ const validationCheckBefore = async () => {
* @description FE 유효성 체크 후 캐릭터 선택 모달 팝업 노출합니다. * @description FE 유효성 체크 후 캐릭터 선택 모달 팝업 노출합니다.
*/ */
const handleCouponUse = async () => { const handleCouponUse = async () => {
handleSendLog(t('Coupon_Registration', {}, { locale: 'ko' }))
const validationCheckBeforeResult = await validationCheckBefore() const validationCheckBeforeResult = await validationCheckBefore()
if (validationCheckBeforeResult !== 0) { if (validationCheckBeforeResult !== 0) {
@@ -272,6 +283,7 @@ const handleCouponUse = async () => {
* @description 캐릭터 선택 후, 확인 버튼 클릭 시 쿠폰 등록 API 호출 함수입니다. * @description 캐릭터 선택 후, 확인 버튼 클릭 시 쿠폰 등록 API 호출 함수입니다.
*/ */
const handleCouponRegister = async () => { const handleCouponRegister = async () => {
handleSendLog('캐릭터 선택 완료')
closeSelectCharacterModal() closeSelectCharacterModal()
const validationCheckBeforeResult = await validationCheckBefore() const validationCheckBeforeResult = await validationCheckBefore()
@@ -328,6 +340,7 @@ const handlePeriodSelect = (month: number) => {
setCouponDate(newStartDate, 'start') setCouponDate(newStartDate, 'start')
setCouponDate(newEndDate, 'end') setCouponDate(newEndDate, 'end')
handleSendLog(t(`Coupon_Month${month}`, {}, { locale: 'ko' }))
} }
/** /**
@@ -338,6 +351,8 @@ const handlePeriodSearch = async () => {
return return
} }
handleSendLog(t('Coupon_Search', {}, { locale: 'ko' }))
const accessToken = csrGetAccessToken() const accessToken = csrGetAccessToken()
const validateTokenResult = await handleTokenValidation(accessToken || '') const validateTokenResult = await handleTokenValidation(accessToken || '')
if (validateTokenResult === false) { if (validateTokenResult === false) {
@@ -431,6 +446,8 @@ const goToCouponBox = () => {
* @description 쿠폰 등록 내역의 사용하기 버튼의 클릭 이벤트 함수입니다. * @description 쿠폰 등록 내역의 사용하기 버튼의 클릭 이벤트 함수입니다.
*/ */
const handleGoToCouponBox = async () => { const handleGoToCouponBox = async () => {
handleSendLog(t('Coupon_Item_Use', {}, { locale: 'ko' }))
const accessToken = csrGetAccessToken() const accessToken = csrGetAccessToken()
const validateTokenResult = await handleTokenValidation(accessToken || '') const validateTokenResult = await handleTokenValidation(accessToken || '')
@@ -485,6 +502,7 @@ onMounted(async () => {
<template> <template>
<WidgetsFixMainTitle <WidgetsFixMainTitle
:id="props.id"
:title="tm('Coupon_Page_Title')" :title="tm('Coupon_Page_Title')"
:resources-data="backgroundData" :resources-data="backgroundData"
/> />
@@ -765,8 +783,8 @@ onMounted(async () => {
class="relative flex flex-col items-center justify-center gap-[24px] w-full" class="relative flex flex-col items-center justify-center gap-[24px] w-full"
> >
<p <p
class="relative flex items-start justify-center w-full text-center text-[#333333] text-[15px] font-[400] leading-[24px] tracking-[-0.45px]"
v-dompurify-html="tm('Coupon_Alert_SelectCharacter')" v-dompurify-html="tm('Coupon_Alert_SelectCharacter')"
class="relative flex items-start justify-center w-full text-center text-[#333333] text-[15px] font-[400] leading-[24px] tracking-[-0.45px]"
></p> ></p>
<AtomsSelect <AtomsSelect

View File

@@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { getComponentGroup, getComponentGroupAry } from '#layers/utils/dataUtil' import { getComponentGroup, getComponentGroupAry } from '#layers/utils/dataUtil'
import type { TrackingObject } from '#layers/types/api/common'
import type { PageDataTemplateComponents } from '#layers/types/api/pageData' import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
import type { Platform } from '#layers/types/components/button' import type { Platform } from '#layers/types/components/button'
// Props // Props
interface Props { interface Props {
id?: string
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string pageVerTmplNameEn: string
@@ -13,6 +15,12 @@ const props = defineProps<Props>()
// Configuration // Configuration
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const device = useDevice()
const gameDataStore = useGameDataStore()
const breakpoints = useResponsiveBreakpoints()
const { gameData } = storeToRefs(gameDataStore)
const { sendLog } = useAnalytics()
const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_download.json' const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_download.json'
@@ -30,27 +38,8 @@ const { tm, locale }: any = useI18n({
}) })
// Composables // Composables
const device = useDevice()
const breakpoints = useResponsiveBreakpoints()
const { checkPCSpec } = useCheckPCSpec(tm) const { checkPCSpec } = useCheckPCSpec(tm)
// Store
const gameDataStore = useGameDataStore()
const { gameData } = storeToRefs(gameDataStore)
// Data
const backgroundData = computed(() =>
getComponentGroup(props.components, 'background')
)
const specCheckData = computed(() =>
getComponentGroupAry(props.components, 'tablePcSpecTool')
)
const schemeFormatData = computed(
() => specCheckData.value[0]?.display?.text || ''
)
const setupUrlData = computed(() => specCheckData.value[1]?.display?.text || '')
// Refs
const specPCRef = ref<HTMLElement | null>(null) const specPCRef = ref<HTMLElement | null>(null)
const specMobileRef = ref<HTMLElement | null>(null) const specMobileRef = ref<HTMLElement | null>(null)
const driverArray = ref<Array<string>>([ const driverArray = ref<Array<string>>([
@@ -71,7 +60,18 @@ const pcSpecArray = ref<Array<string>>([
const mobileSpecArray = ref<Array<string>>(['Android', 'Ios']) const mobileSpecArray = ref<Array<string>>(['Android', 'Ios'])
const mobileOSArray = ref<Array<string>>(['AOS', 'iOS']) const mobileOSArray = ref<Array<string>>(['AOS', 'iOS'])
// Computed // Data
const backgroundData = computed(() =>
getComponentGroup(props.components, 'background')
)
const specCheckData = computed(() =>
getComponentGroupAry(props.components, 'tablePcSpecTool')
)
const schemeFormatData = computed(
() => specCheckData.value[0]?.display?.text || ''
)
const setupUrlData = computed(() => specCheckData.value[1]?.display?.text || '')
const driverList = computed(() => const driverList = computed(() =>
driverArray.value.map(driver => ({ driverArray.value.map(driver => ({
id: `DRIVER_${driver}`, id: `DRIVER_${driver}`,
@@ -108,6 +108,15 @@ const mobileOSList = computed(() =>
) )
// Functions // Functions
const handleSendLog = (item: string) => {
const analytics = {
action_type: 'click',
click_item: item,
click_sarea: props.pageVerTmplNameEn,
}
sendLog(locale.value, analytics)
}
/** /**
* 입력한 pc, mobile 인자값에 따라 해당 최소사양&권장사양 테이블로 포커스 스크롤 이동합니다. * 입력한 pc, mobile 인자값에 따라 해당 최소사양&권장사양 테이블로 포커스 스크롤 이동합니다.
* @param target<string> 'pc' | 'mobile' * @param target<string> 'pc' | 'mobile'
@@ -123,6 +132,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
refs[target]?.getBoundingClientRect().top + window.scrollY - adjustedOffset refs[target]?.getBoundingClientRect().top + window.scrollY - adjustedOffset
window.scrollTo({ top: adjustedOffsetTop, behavior: 'smooth' }) window.scrollTo({ top: adjustedOffsetTop, behavior: 'smooth' })
handleSendLog(`${target === 'pc' ? 'PC' : '모바일'} 버전 사양 확인하기`)
} }
</script> </script>
@@ -196,12 +206,8 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
></p> ></p>
</template> </template>
<AtomsButton <button
type="action" class="relative flex gap-1 text-[14px] font-[500] text-[#1F1F1F] leading-[20px] tracking-[-0.42px] md:text-[16px] md:leading-[24px] md:tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-p before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20"
button-size="size-small"
background-color="transparent"
text-color="#1F1F1F"
class="relative w-auto h-auto px-0 text-[14px] font-[500] leading-[20px] tracking-[-0.42px] md:text-[16px] md:leading-[24px] md:tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-p before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20"
@click="handleMoveFocus('pc')" @click="handleMoveFocus('pc')"
> >
<span>{{ tm('Download_Box_PC_SpecCheck') }}</span> <span>{{ tm('Download_Box_PC_SpecCheck') }}</span>
@@ -210,7 +216,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
color="#1F1F1F" color="#1F1F1F"
class="relative rotate-90" class="relative rotate-90"
/> />
</AtomsButton> </button>
</div> </div>
<div <div
@@ -224,6 +230,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
v-else-if="breakpoints.isMd || breakpoints.isDesktop" v-else-if="breakpoints.isMd || breakpoints.isDesktop"
platform="pc" platform="pc"
class="!w-full !max-w-[300px]" class="!w-full !max-w-[300px]"
@click="handleSendLog('PC 버전 다운로드')"
> >
<span>{{ tm('Download_Button_PC') }}</span> <span>{{ tm('Download_Button_PC') }}</span>
</BlocksButtonLauncher> </BlocksButtonLauncher>
@@ -237,15 +244,16 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
class="relative flex items-center justify-center w-full gap-[8px] text-[#999999] text-[16px] font-[400] leading-[26px] tracking-[-0.48px]" class="relative flex items-center justify-center w-full gap-[8px] text-[#999999] text-[16px] font-[400] leading-[26px] tracking-[-0.48px]"
> >
<span>{{ tm('Download_Text_Not_STOVE_Client') }}</span> <span>{{ tm('Download_Text_Not_STOVE_Client') }}</span>
<NuxtLink <a
:href="stoveClientDownloadUrl" :href="stoveClientDownloadUrl"
target="_self" target="_self"
rel="noopener noreferrer" rel="noopener noreferrer"
class="inline-flex items-center justify-start gap-[4px] text-[#3C75FF] text-[16px] font-[500] reading-[24px] tracking-[-0.48px]" class="inline-flex items-center justify-start gap-[4px] text-[#3C75FF] text-[16px] font-[500] reading-[24px] tracking-[-0.48px]"
@click="handleSendLog('스토브 PC 클라이언트 다운로드')"
> >
<span>{{ tm('Download_Button_STOVE') }}</span> <span>{{ tm('Download_Button_STOVE') }}</span>
<AtomsIconsDownloadLine color="#3C75FF" /> <AtomsIconsDownloadLine color="#3C75FF" />
</NuxtLink> </a>
</p> </p>
</div> </div>
</div> </div>
@@ -269,12 +277,8 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
<span>{{ tm('Download_Box_MOBILE_Title') }}</span> <span>{{ tm('Download_Box_MOBILE_Title') }}</span>
</h4> </h4>
<AtomsButton <button
type="action" class="relative flex gap-1 text-[14px] font-[500] text-[#1F1F1F] leading-[20px] tracking-[-0.42px] md:text-[16px] md:leading-[24px] md:tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-p before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20"
button-size="size-small"
background-color="transparent"
text-color="#1F1F1F"
class="relative w-auto h-auto px-0 text-[14px] font-[500] leading-[20px] tracking-[-0.42px] md:text-[16px] md:leading-[24px] md:tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-p before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20"
@click="handleMoveFocus('mobile')" @click="handleMoveFocus('mobile')"
> >
<span>{{ tm('Download_Box_MOBILE_SpecCheck') }}</span> <span>{{ tm('Download_Box_MOBILE_SpecCheck') }}</span>
@@ -283,7 +287,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
color="#1F1F1F" color="#1F1F1F"
class="relative rotate-90" class="relative rotate-90"
/> />
</AtomsButton> </button>
</div> </div>
<div <div
@@ -300,6 +304,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
" "
:platform="`${os.platformCode as Platform}`" :platform="`${os.platformCode as Platform}`"
class="!w-full" class="!w-full"
@click="handleSendLog(os.platformCode)"
> >
<span>{{ os.platformText }}</span> <span>{{ os.platformText }}</span>
</BlocksButtonLauncher> </BlocksButtonLauncher>
@@ -376,14 +381,15 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
background-color="#383838" background-color="#383838"
text-color="#FFFFFF" text-color="#FFFFFF"
class="shrink-0 w-[206px] px-0 text-[16px]" class="shrink-0 w-[206px] px-0 text-[16px]"
@click=" @click="[
checkPCSpec({ checkPCSpec({
schemeFormat: schemeFormatData, schemeFormat: schemeFormatData,
setupUrl: setupUrlData, setupUrl: setupUrlData,
gameNo: gameData?.game_code?.toString(), gameNo: gameData?.game_code?.toString(),
locale: locale as string, locale: locale as string,
}) }),
" handleSendLog('내 PC 사양 확인하기'),
]"
> >
<em <em
class="inline-flex items-center justify-center gap-[8px] not-italic" class="inline-flex items-center justify-center gap-[8px] not-italic"
@@ -439,6 +445,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
:href="tm(`Download_Driver_${driver.driverCode}_Url`)" :href="tm(`Download_Driver_${driver.driverCode}_Url`)"
@click="handleSendLog(`다운로드_${driver.driverText}`)"
> >
<span>{{ tm('Download_Button_Download') }}</span> <span>{{ tm('Download_Button_Download') }}</span>
</AtomsButton> </AtomsButton>

View File

@@ -12,6 +12,7 @@ interface Props {
} }
const props = defineProps<Props>() const props = defineProps<Props>()
provide('pageVerTmplNameEn', props.pageVerTmplNameEn)
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const device = useDevice() const device = useDevice()
@@ -27,14 +28,15 @@ const resultGetMultilingual = await useGetMultilingual({
baseApiUrl: dataResourcesUrl, baseApiUrl: dataResourcesUrl,
fileName: multilingualFileName, fileName: multilingualFileName,
}) })
const { tm, locale }: any = useI18n({ const { t, tm, locale }: any = useI18n({
useScope: 'local', useScope: 'local',
messages: Object(resultGetMultilingual?.value?.multilingual), messages: Object(resultGetMultilingual?.value?.multilingual),
}) })
const { getOperateResources } = useOperateResources()
const { gameData } = storeToRefs(gameDataStore) const { gameData } = storeToRefs(gameDataStore)
const { pageData } = storeToRefs(pageDataStore) const { pageData } = storeToRefs(pageDataStore)
const { getOperateResources } = useOperateResources()
const { sendLog } = useAnalytics()
// Constants // Constants
const COLOR_INDEX = { BACKGROUND: 0, TEXT: 1 } as const const COLOR_INDEX = { BACKGROUND: 0, TEXT: 1 } as const
@@ -65,6 +67,10 @@ const preImgSnsData = computed(() =>
const preDescriptionData = computed(() => const preDescriptionData = computed(() =>
getComponentGroup(props.components, 'description') getComponentGroup(props.components, 'description')
) )
const preregistSNS = computed(
() => getSupportedPlatforms('2', gameData?.value?.os_type) as Platform[]
)
// SNS Buttons // SNS Buttons
const snsButtonsData = computed(() => { const snsButtonsData = computed(() => {
const buttons = getComponentGroupAry(props.components, 'imgSnsButton') const buttons = getComponentGroupAry(props.components, 'imgSnsButton')
@@ -73,9 +79,9 @@ const snsButtonsData = computed(() => {
if (!buttons?.length) return [] if (!buttons?.length) return []
return buttons.map((button, index) => ({ return buttons.map((button, index) => ({
image: button,
link: links?.[index]?.display?.text ?? '',
id: button.id ?? `sns-${index}`, id: button.id ?? `sns-${index}`,
link: links?.[index]?.display?.text ?? '',
image: button,
})) }))
}) })
@@ -86,18 +92,15 @@ const buttonColors = computed(() => {
'preregistButtonColor' 'preregistButtonColor'
) )
if (!colorData?.length)
return { backgroundColor: undefined, textColor: undefined }
return { return {
backgroundColor: getColorCode({ backgroundColor: getColorCodeFromData(
colorName: colorData[COLOR_INDEX.BACKGROUND]?.display?.color_name, colorData[COLOR_INDEX.BACKGROUND]?.display,
colorCode: colorData[COLOR_INDEX.BACKGROUND]?.display?.color_code, 'none'
}), ),
textColor: getColorCode({ textColor: getColorCodeFromData(
colorName: colorData[COLOR_INDEX.TEXT]?.display?.color_name, colorData[COLOR_INDEX.TEXT]?.display,
colorCode: colorData[COLOR_INDEX.TEXT]?.display?.color_code, 'none'
}), ),
} }
}) })
@@ -214,9 +217,23 @@ const isRunButtonVisible = (marketType?: Platform): boolean => {
} }
} }
// Handler const handleSendLog = (item: string) => {
const analytics = {
action_type: 'click',
click_item: item,
click_sarea: props.pageVerTmplNameEn,
}
sendLog(locale.value, analytics)
}
const handlePreregistClick = () => { const handlePreregistClick = () => {
preregistModalRef.value?.handleOpenPreregist() preregistModalRef.value?.handleOpenPreregist()
handleSendLog(`STOVE_${t('Preregist_Btn_Preegist', {}, { locale: 'ko' })}`)
}
const handleSnsButtonClick = (btn: { id: string; link: string }) => {
csrGoExternalLink(btn.link)
handleSendLog(btn.id)
} }
</script> </script>
@@ -272,7 +289,12 @@ const handlePreregistClick = () => {
:key="btn.id" :key="btn.id"
class="w-[48px] h-[40px] md:w-[72px] md:h-[56px]" class="w-[48px] h-[40px] md:w-[72px] md:h-[56px]"
> >
<a :href="btn.link" target="_blank" rel="noopener noreferrer"> <a
:href="btn.link"
target="_blank"
rel="noopener noreferrer"
@click="handleSendLog(btn.id)"
>
<AtomsImg <AtomsImg
:src="getResourceSrc(btn.image)" :src="getResourceSrc(btn.image)"
:alt="btn.image?.display?.text" :alt="btn.image?.display?.text"
@@ -293,16 +315,18 @@ const handlePreregistClick = () => {
{{ tm('Preregist_Btn_Preegist') }} {{ tm('Preregist_Btn_Preegist') }}
</BlocksButtonLauncher> </BlocksButtonLauncher>
<template v-if="gameData?.platform_type !== '1'"> <template v-if="gameData?.platform_type !== '1'">
<template <template v-for="sns in preregistSNS" :key="`preregist-${sns}`">
v-for="platform in getSupportedPlatforms('2', gameData?.os_type)"
:key="`preregist-${platform}`"
>
<BlocksButtonLauncher <BlocksButtonLauncher
v-if="isRunButtonVisible(platform as Platform)" v-if="isRunButtonVisible(sns)"
type="duplication" type="duplication"
:platform="platform as Platform" :platform="sns"
:background-color="buttonColors.backgroundColor" :background-color="buttonColors.backgroundColor"
:text-color="buttonColors.textColor" :text-color="buttonColors.textColor"
@click="
handleSendLog(
`${sns}_${t('Preregist_Btn_Preegist', {}, { locale: 'ko' })}`
)
"
> >
{{ tm('Preregist_Btn_Preegist') }} {{ tm('Preregist_Btn_Preegist') }}
</BlocksButtonLauncher> </BlocksButtonLauncher>

View File

@@ -4,6 +4,7 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
// Props // Props
interface Props { interface Props {
id?: string
components: PageDataTemplateComponents components: PageDataTemplateComponents
pageVerTmplSeq: number pageVerTmplSeq: number
pageVerTmplNameEn: string pageVerTmplNameEn: string
@@ -11,6 +12,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const { handleTokenValidation } = useTokenValidation() const { handleTokenValidation } = useTokenValidation()
const { sendLog } = useAnalytics()
// Configuration // Configuration
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
@@ -23,43 +25,89 @@ const resultGetMultilingual = await useGetMultilingual({
baseApiUrl: dataResourcesUrl, baseApiUrl: dataResourcesUrl,
fileName: multilingualFileName, fileName: multilingualFileName,
}) })
const { tm, locale }: any = useI18n({ const { t, tm, locale }: any = useI18n({
useScope: 'local', useScope: 'local',
messages: Object(resultGetMultilingual?.value?.multilingual), messages: Object(resultGetMultilingual?.value?.multilingual),
}) })
const isLogin = ref(false)
const secureSetting = ref({ const secureSetting = ref({
otpLoginYn: 'N', otpLoginYn: 'N',
abroadLoginBlockYn: 'N', abroadLoginBlockYn: 'N',
pcRegisterYn: 'N', pcRegisterYn: 'N',
}) })
// 회원 보안 설정 설정 // Computed
const handleSecureSetting = (url: string) => { const backgroundData = computed(() =>
window.open(url, '_blank') getComponentGroup(props.components, 'background')
} )
const secureCards = computed(() => {
const allCards = [
{
id: 'SECURE_CARD_0',
title: tm('Secure_Stove_otp') || '스토브 인증기 (OTP)',
description:
tm('Secure_Stove_otp_desc') ||
'스토브 앱으로 인증 후 안전하게 로그인하세요.',
status: secureSetting.value.otpLoginYn,
benefitTitle: tm('Secure_Stove_otp_benefits') || '스토브 OTP 혜택',
benefitDesc: tm('Secure_Defense_bonus_10') || '방어력 +10',
benefitIcon: '/images/common/img_OTP.png',
buttonDisabled: false,
url: tm('Secure_OtpLogin_Url'),
analyticsItem: '스토브 인증기',
},
{
id: 'SECURE_CARD_1',
title: tm('Secure_Block_foreign_login') || '해외 로그인 차단',
description:
tm('Secure_Block_foreign_login_desc') ||
'접속 국가를 제한하여 의심 로그인을 차단해요.',
status: secureSetting.value.abroadLoginBlockYn,
benefitTitle: '',
benefitDesc: '',
benefitIcon: '',
buttonDisabled: false,
url: tm('Secure_AbroadLogin_Url'),
analyticsItem: '해외 로그인 차단',
},
{
id: 'SECURE_CARD_2',
title: tm('Secure_Trusted_pc_management') || '지정 PC 관리',
description:
tm('Secure_Trusted_pc_desc') ||
'지정 PC에서만 로그인할 수 있게 설정해 보세요.',
status: secureSetting.value.pcRegisterYn,
benefitTitle: '',
benefitDesc: '',
benefitIcon: '',
buttonDisabled: false,
url: tm('Secure_PcRegister_Url'),
analyticsItem: '지정 PC 관리',
},
]
// 로그인 유효성 체크 // 한국어일 때는 모든 카드 노출, 그 외 언어일 때는 SECURE_CARD_0만 노출
const checkLoginValidation = async () => { if (locale.value === 'ko') {
const accessToken = useCookie('SUAT') return allCards
const validateTokenResult = await handleTokenValidation( }
accessToken.value || '' return allCards.filter(card => card.id === 'SECURE_CARD_0')
) })
isLogin.value = validateTokenResult // 유의사항 내용 다국어 조회
} const cautionText = computed(() => {
return tm('Secure_Notice_Content') || []
})
// 회원 보안 설정 조회 // 회원 보안 설정 조회
const fnGetSecuritySetting = async () => { const fnGetSecuritySetting = async () => {
const accessToken = useCookie('SUAT') const accessToken = useCookie('SUAT')
checkLoginValidation()
const apiBase = `${stoveApiBaseUrl}/auth-secure/v1.0` const apiBase = `${stoveApiBaseUrl}/auth-secure/v1.0`
const headers = { const headers = {
Authorization: `Bearer ${accessToken.value}`, Authorization: `Bearer ${accessToken.value}`,
'Content-Type': 'application/json;charset=UTF-8', 'Content-Type': 'application/json;charset=UTF-8',
} }
if (accessToken) return
try { try {
const result = await commonFetch('GET', `${apiBase}/security/setting`, { const result = await commonFetch('GET', `${apiBase}/security/setting`, {
headers, headers,
@@ -81,65 +129,23 @@ const fnGetSecuritySetting = async () => {
console.error(e) console.error(e)
} }
} }
// Data
const backgroundData = computed(() =>
getComponentGroup(props.components, 'background')
)
// Computed const handleClickSetup = async card => {
const secureCards = computed(() => { const analytics = {
const allCards = [ action_type: 'click',
{ click_item: `${t('Secure_Action_setup', {}, { locale: 'ko' })}_${card.analyticsItem}`,
id: 'SECURE_CARD_0', click_sarea: props.pageVerTmplNameEn,
title: tm('Secure_Stove_otp') || '스토브 인증기 (OTP)',
description:
tm('Secure_Stove_otp_desc') ||
'스토브 앱으로 인증 후 안전하게 로그인하세요.',
status: secureSetting.value.otpLoginYn,
benefitTitle: tm('Secure_Stove_otp_benefits') || '스토브 OTP 혜택',
benefitDesc: tm('Secure_Defense_bonus_10') || '방어력 +10',
benefitIcon: '/images/common/img_OTP.png',
buttonDisabled: false,
url: tm('Secure_OtpLogin_Url'),
},
{
id: 'SECURE_CARD_1',
title: tm('Secure_Block_foreign_login') || '해외 로그인 차단',
description:
tm('Secure_Block_foreign_login_desc') ||
'접속 국가를 제한하여 의심 로그인을 차단해요.',
status: secureSetting.value.abroadLoginBlockYn,
benefitTitle: '',
benefitDesc: '',
benefitIcon: '',
buttonDisabled: false,
url: tm('Secure_AbroadLogin_Url'),
},
{
id: 'SECURE_CARD_2',
title: tm('Secure_Trusted_pc_management') || '지정 PC 관리',
description:
tm('Secure_Trusted_pc_desc') ||
'지정 PC에서만 로그인할 수 있게 설정해 보세요.',
status: secureSetting.value.pcRegisterYn,
benefitTitle: '',
benefitDesc: '',
benefitIcon: '',
buttonDisabled: false,
url: tm('Secure_PcRegister_Url'),
},
]
// 한국어일 때는 모든 카드 노출, 그 외 언어일 때는 SECURE_CARD_0만 노출
if (locale.value === 'ko') {
return allCards
} }
return allCards.filter(card => card.id === 'SECURE_CARD_0') sendLog(locale.value, analytics)
})
// 유의사항 내용 다국어 조회 const accessToken = useCookie('SUAT')
const cautionText = computed(() => { const validateTokenResult = await handleTokenValidation(
return tm('Secure_Notice_Content') || [] accessToken.value || ''
}) )
if (validateTokenResult === false) return
window.open(card.url, '_blank')
}
onMounted(() => { onMounted(() => {
fnGetSecuritySetting() fnGetSecuritySetting()
@@ -148,6 +154,7 @@ onMounted(() => {
<template> <template>
<WidgetsFixMainTitle <WidgetsFixMainTitle
:id="props.id"
:title="tm('Secure_Page_Title') || '보안 강화 캠페인'" :title="tm('Secure_Page_Title') || '보안 강화 캠페인'"
:resources-data="backgroundData" :resources-data="backgroundData"
class="mx-auto" class="mx-auto"
@@ -333,11 +340,7 @@ onMounted(() => {
button-size="size-small md:size-large" button-size="size-small md:size-large"
background-color="#383838" background-color="#383838"
text-color="#FFFFFF" text-color="#FFFFFF"
@click=" @click="handleClickSetup(card)"
isLogin
? handleSecureSetting(card.url)
: checkLoginValidation()
"
> >
<span>{{ tm('Secure_Action_setup') }}</span> <span>{{ tm('Secure_Action_setup') }}</span>
</AtomsButton> </AtomsButton>

View File

@@ -6,6 +6,7 @@ import {
getComponentContainer, getComponentContainer,
} from '#layers/utils/dataUtil' } from '#layers/utils/dataUtil'
import { getYouTubeThumbnail } from '#layers/utils/youtubeUtil' import { getYouTubeThumbnail } from '#layers/utils/youtubeUtil'
import type { TrackingObject } from '#layers/types/api/common'
import type { import type {
PageDataTemplateComponents, PageDataTemplateComponents,
PageDataResourceGroups, PageDataResourceGroups,
@@ -25,6 +26,7 @@ const pageDataStore = usePageDataStore()
const modalStore = useModalStore() const modalStore = useModalStore()
const breakpoints = useResponsiveBreakpoints() const breakpoints = useResponsiveBreakpoints()
const { getOperateResources } = useOperateResources() const { getOperateResources } = useOperateResources()
const { sendLog } = useAnalytics()
const { pageData } = storeToRefs(pageDataStore) const { pageData } = storeToRefs(pageDataStore)
@@ -39,11 +41,30 @@ const resultGetMultilingual = await useGetMultilingual({
fileName: multilingualFileName, fileName: multilingualFileName,
}) })
const { tm, locale } = useI18n({ const { tm, t, locale } = useI18n({
useScope: 'local', useScope: 'local',
messages: Object(resultGetMultilingual?.value?.multilingual), messages: Object(resultGetMultilingual?.value?.multilingual),
}) })
const analytics = {
action_type: 'click',
click_sarea: props.pageVerTmplNameEn,
} as TrackingObject
const arrowsData: PageDataResourceGroups = [
{
tracking: {
...analytics,
click_item: '1. 컨텐츠 리스트 - 네비게이션(좌)',
},
},
{
tracking: {
...analytics,
click_item: '1. 컨텐츠 리스트 - 네비게이션(우)',
},
},
]
const recommendHover = ref(false) const recommendHover = ref(false)
const backgroundData = computed(() => const backgroundData = computed(() =>
@@ -53,23 +74,6 @@ const officialUrlData = computed(
() => getComponentGroup(props.components, 'officialUrl')?.display?.text ?? '' () => getComponentGroup(props.components, 'officialUrl')?.display?.text ?? ''
) )
const arrowsData: PageDataResourceGroups = [
{
tracking: {
action_type: 'click',
click_item: '1. 컨텐츠 리스트 - 네비게이션(좌)',
click_sarea: 'Home_GameInfo__arrow',
},
},
{
tracking: {
action_type: 'click',
click_item: '1. 컨텐츠 리스트 - 네비게이션(우)',
click_sarea: 'Home_GameInfo__arrow',
},
},
]
const { data: slideData } = await useAsyncData( const { data: slideData } = await useAsyncData(
`fx-video-01-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`, `fx-video-01-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`,
async () => { async () => {
@@ -131,13 +135,19 @@ const hasMore = computed(
() => visibleVideos.value.length < recentVideos.value.length () => visibleVideos.value.length < recentVideos.value.length
) )
const handleVideoClick = (url: string) => { const handleVideoClick = (item: OperateGroupItem) => {
modalStore.handleOpenYoutube({ youtubeUrl: url }) modalStore.handleOpenYoutube({ youtubeUrl: item.url })
sendLog(locale.value, { ...analytics, click_item: item.title })
} }
const handleLoadMoreRecent = () => { const handleLoadMoreRecent = () => {
if (hasMore.value) { if (hasMore.value) {
currentRecentPage.value++ currentRecentPage.value++
sendLog(locale.value, {
...analytics,
click_item: t('Text_More', {}, { locale: 'ko' }),
})
} }
} }
</script> </script>
@@ -154,6 +164,10 @@ const handleLoadMoreRecent = () => {
:title="tm('Video_Section_Recommendation_Title')" :title="tm('Video_Section_Recommendation_Title')"
:description="tm('Video_Page_Link')" :description="tm('Video_Page_Link')"
:link="officialUrlData" :link="officialUrlData"
:link-analytics="{
...analytics,
click_item: t('Video_Page_Link', {}, { locale: 'ko' }),
}"
/> />
<div <div
class="relative content-static bg-[#fff] rounded-[12px] md:rounded-[16px]" class="relative content-static bg-[#fff] rounded-[12px] md:rounded-[16px]"
@@ -173,7 +187,7 @@ const handleLoadMoreRecent = () => {
> >
<div <div
class="overflow-hidden relative aspect-[16/9] flex-shrink-0 w-full rounded-[4px] cursor-pointer group sm:w-[60.3%] sm:rounded-[8px] md:w-[56%] lg:w-[710px] lg:rounded-[12px]" class="overflow-hidden relative aspect-[16/9] flex-shrink-0 w-full rounded-[4px] cursor-pointer group sm:w-[60.3%] sm:rounded-[8px] md:w-[56%] lg:w-[710px] lg:rounded-[12px]"
@click="handleVideoClick(item.url)" @click="handleVideoClick(item)"
> >
<img <img
:src="getYouTubeThumbnail(item.url, 'maxres')" :src="getYouTubeThumbnail(item.url, 'maxres')"
@@ -191,7 +205,7 @@ const handleLoadMoreRecent = () => {
</div> </div>
<div <div
class="w-full mx-[8px] pb-[20px] border-b border-[rgba(0,0,0,0.08)] cursor-pointer sm:mx-[0] sm:pt-[12px] sm:pr-[16px] sm:pb-[0] sm:border-none md:pt-[20px] md:pr-[28px] lg:pt-[40px] lg:pr-[48px]" class="w-full mx-[8px] pb-[20px] border-b border-[rgba(0,0,0,0.08)] cursor-pointer sm:mx-[0] sm:pt-[12px] sm:pr-[16px] sm:pb-[0] sm:border-none md:pt-[20px] md:pr-[28px] lg:pt-[40px] lg:pr-[48px]"
@click="handleVideoClick(item.url)" @click="handleVideoClick(item)"
@mouseenter="recommendHover = true" @mouseenter="recommendHover = true"
@mouseleave="recommendHover = false" @mouseleave="recommendHover = false"
> >
@@ -225,7 +239,7 @@ const handleLoadMoreRecent = () => {
v-for="(item, index) in visibleVideos" v-for="(item, index) in visibleVideos"
:key="`recent-${item.url}-${index}`" :key="`recent-${item.url}-${index}`"
class="p-3 rounded-[12px] bg-white md:p-4 md:rounded-[16px] lg:p-5 group cursor-pointer" class="p-3 rounded-[12px] bg-white md:p-4 md:rounded-[16px] lg:p-5 group cursor-pointer"
@click="handleVideoClick(item.url)" @click="handleVideoClick(item)"
> >
<div <div
class="overflow-hidden relative aspect-[16/9] w-full rounded-[4px] sm:rounded-[8px] lg:rounded-[12px]" class="overflow-hidden relative aspect-[16/9] w-full rounded-[4px] sm:rounded-[8px] lg:rounded-[12px]"

View File

@@ -141,6 +141,7 @@ const getArticleUrl = (articleId: string) => {
:url="getArticleUrl(item.article_id)" :url="getArticleUrl(item.article_id)"
:analytics-sarea="pageVerTmplNameEn" :analytics-sarea="pageVerTmplNameEn"
link-target="_blank" link-target="_blank"
rel="noopener noreferrer"
/> />
</div> </div>
</SplideSlide> </SplideSlide>

View File

@@ -15,7 +15,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const { locale } = useI18n() const { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics() const { sendLog } = useAnalytics()
const splideRef = ref<SplideSlide | null>(null) const splideRef = ref<SplideSlide | null>(null)
const currentSlideIndex = ref<number | null>(null) const currentSlideIndex = ref<number | null>(null)
@@ -29,7 +29,7 @@ const paginationData = computed(() => {
const goToSlide = (index: number, title: string) => { const goToSlide = (index: number, title: string) => {
const splide = splideRef.value?.splide const splide = splideRef.value?.splide
const paginationAnalytics = { const analytics = {
action_type: 'click', action_type: 'click',
click_item: title, click_item: title,
click_sarea: props.pageVerTmplNameEn, click_sarea: props.pageVerTmplNameEn,
@@ -37,7 +37,7 @@ const goToSlide = (index: number, title: string) => {
if (splide) { if (splide) {
splide.go(index) splide.go(index)
sendLog(locale.value, useAnalyticsData(paginationAnalytics)) sendLog(locale.value, analytics)
} }
} }
</script> </script>

View File

@@ -1,8 +1,5 @@
import type { HTMLAttributes } from 'vue'
import type { StoveJsService } from '#layers/types/Stove' import type { StoveJsService } from '#layers/types/Stove'
export type ClassType = HTMLAttributes['class']
declare global { declare global {
interface Window { interface Window {
stoveJsService?: StoveJsService stoveJsService?: StoveJsService
@@ -27,39 +24,9 @@ interface ParsedCustomLinkOptions {
query?: Record<string, any> query?: Record<string, any>
} }
export type {
// 타입 정의 CommonRequestType,
interface FooterMenuItem { CommonResponseType,
title: string CommonPeriodType,
url: string ParsedCustomLinkOptions,
target?: string
active?: string
} }
interface GameRatingInfo {
title: string
company_name: string
rating_grade: string
reg_no: string
prod_date: string
rating_class_no: string
rating_type: string
content_info: string
}
interface FooterData {
use_game_rating: boolean
game_rating_info: GameRatingInfo
use_dev_ci_url?: boolean
dev_ci_url?: string
dev_ci_img_path?: string
fund_display_yn?: string
fund_display_url?: string
}
interface DevCiConfig {
dev_ci_yn: boolean
dev_ci_img_path: string
}
export type { CommonRequestType, CommonResponseType, CommonPeriodType, ParsedCustomLinkOptions, FooterMenuItem, GameRatingInfo, FooterData, DevCiConfig }

View File

@@ -49,3 +49,12 @@ export interface TrackingObject {
click_item: string click_item: string
click_sarea: string click_sarea: string
} }
export interface ColorObject {
color_code?: string
color_name?: string
color_code_btn?: string
color_name_btn?: string
color_code_txt?: string
color_name_txt?: string
}

View File

@@ -1,4 +1,4 @@
import type { TrackingObject } from '#layers/types/api/common' import type { TrackingObject, ColorObject } from '#layers/types/api/common'
// API 요청 파라미터 타입 // API 요청 파라미터 타입
export interface GameDataRequest { export interface GameDataRequest {
@@ -31,10 +31,7 @@ export interface GameDataValue {
key_color_json: GameDataKeyColors key_color_json: GameDataKeyColors
use_game_font: boolean use_game_font: boolean
comm_sns_bg_color_json: { comm_sns_bg_color_json: {
display: { display: ColorObject
color_code: string
color_name: string
}
} }
comm_multilang_filename: string comm_multilang_filename: string
footer_dev_ci_img_yn: boolean footer_dev_ci_img_yn: boolean
@@ -134,11 +131,7 @@ export interface GameDataGlobal {
lang_json: string // JSON 문자열로 변경 lang_json: string // JSON 문자열로 변경
} }
export interface GameDataResourceGroupBtnInfo { export interface GameDataResourceGroupBtnInfo extends ColorObject {
color_code_btn: string
color_name_btn: string
color_code_txt: string
color_name_txt: string
disabled: boolean disabled: boolean
txt_btn_name: string txt_btn_name: string
detail: Record<string, any> detail: Record<string, any>
@@ -176,7 +169,7 @@ export interface GameDataMenu {
url_path: string url_path: string
link_target: string link_target: string
children: GameDataMenuChildren children: GameDataMenuChildren
tracking_json: string | TrackingObject // JSON 문자열 또는 객체로 변경 tracking_json: TrackingObject
} }
// GNB 설정 타입 // GNB 설정 타입

View File

@@ -1,4 +1,4 @@
import type { TrackingObject } from '#layers/types/api/common' import type { TrackingObject, ColorObject } from '#layers/types/api/common'
// API 요청 파라미터 타입 // API 요청 파라미터 타입
export interface PageDataRequest { export interface PageDataRequest {
@@ -53,7 +53,7 @@ export interface PageDataLnbMenu {
menu_name: string menu_name: string
target_type: number target_type: number
page_ver_tmpl_name_en: string page_ver_tmpl_name_en: string
tracking_json: Record<string, TrackingObject> tracking_json: TrackingObject
children?: Record<string, PageDataLnbMenu> children?: Record<string, PageDataLnbMenu>
} }
@@ -83,27 +83,23 @@ export interface PageDataResourceGroupResPath {
path_pc?: string path_pc?: string
} }
export interface PageDataResourceGroupBtnInfo { export interface PageDataResourceGroupBtnInfo extends ColorObject {
color_code_btn: string
color_name_btn: string
color_code_txt: string
color_name_txt: string
disabled: boolean disabled: boolean
txt_btn_name: string txt_btn_name: string
detail: Record<string, any> detail: Record<string, any>
} }
// 리소스 그룹 타입 // 리소스 그룹 타입
export interface PageDataResourceGroupDisplay extends ColorObject {
text: string
}
export interface PageDataResourceGroup { export interface PageDataResourceGroup {
group_label?: string group_label?: string
resource_type?: PageDataResourceGroupType resource_type?: PageDataResourceGroupType
res_path?: PageDataResourceGroupResPath res_path?: PageDataResourceGroupResPath
btn_info?: PageDataResourceGroupBtnInfo btn_info?: PageDataResourceGroupBtnInfo
display?: { display?: PageDataResourceGroupDisplay
text: string
color_code?: string
color_name?: string
}
tracking?: TrackingObject tracking?: TrackingObject
} }

View File

@@ -0,0 +1,34 @@
interface FooterMenuItem {
title: string
url: string
target?: string
active?: string
}
interface GameRatingInfo {
title: string
company_name: string
rating_grade: string
reg_no: string
prod_date: string
rating_class_no: string
rating_type: string
content_info: string
}
interface FooterData {
use_game_rating: boolean
game_rating_info: GameRatingInfo
use_dev_ci_url?: boolean
dev_ci_url?: string
dev_ci_img_path?: string
fund_display_yn?: string
fund_display_url?: string
}
interface DevCiConfig {
dev_ci_yn: boolean
dev_ci_img_path: string
}
export type { FooterMenuItem, GameRatingInfo, FooterData, DevCiConfig }

View File

@@ -3,11 +3,8 @@
* @description ui 처리에 필요한 유틸리티 함수를 제공합니다. * @description ui 처리에 필요한 유틸리티 함수를 제공합니다.
*/ */
import type { GameDataResourceGroupBtnInfo } from '#layers/types/api/gameData' import type { ColorObject } from '#layers/types/api/common'
import type { import type { PageDataResourceGroups } from '#layers/types/api/pageData'
PageDataResourceGroups,
PageDataResourceGroupBtnInfo,
} from '#layers/types/api/pageData'
/** /**
* 색상값을 반환합니다. * 색상값을 반환합니다.
@@ -15,7 +12,7 @@ import type {
* @param colorCode 색상 코드 * @param colorCode 색상 코드
* @returns 색상 값 * @returns 색상 값
*/ */
export const getColorCode = ({ const getColorCode = ({
colorName, colorName,
colorCode, colorCode,
}: { }: {
@@ -24,6 +21,7 @@ export const getColorCode = ({
}) => { }) => {
if (colorName) return `var(--${colorName})` if (colorName) return `var(--${colorName})`
else if (colorCode) return colorCode else if (colorCode) return colorCode
return 'var(--primary)'
} }
/** /**
@@ -33,11 +31,15 @@ export const getColorCode = ({
* @returns 색상 값 * @returns 색상 값
*/ */
export const getColorCodeFromData = ( export const getColorCodeFromData = (
data: GameDataResourceGroupBtnInfo | PageDataResourceGroupBtnInfo, data: ColorObject,
type: 'btn' | 'txt' = 'txt' type: 'btn' | 'txt' | 'none' = 'txt'
) => { ) => {
const suffix = type === 'btn' ? '_btn' : '_txt' const suffixMap: Record<'btn' | 'txt' | 'none', string> = {
btn: '_btn',
txt: '_txt',
none: '',
}
const suffix = suffixMap[type]
const colorName = data?.[`color_name${suffix}` as keyof typeof data] const colorName = data?.[`color_name${suffix}` as keyof typeof data]
const colorCode = data?.[`color_code${suffix}` as keyof typeof data] const colorCode = data?.[`color_code${suffix}` as keyof typeof data]
@@ -61,15 +63,14 @@ export const getPaginationClass = (
} }
// 색상 추출 또는 기본값 사용 // 색상 추출 또는 기본값 사용
const paginationActive = getColorCode({ const paginationActive = getColorCodeFromData(
colorName: paginationGroups[0]?.display?.color_name, paginationGroups[0]?.display,
colorCode: paginationGroups[0]?.display?.color_code, 'none'
}) )
const paginationDisabled = getColorCodeFromData(
const paginationDisabled = getColorCode({ paginationGroups[1]?.display,
colorName: paginationGroups[1]?.display?.color_name, 'none'
colorCode: paginationGroups[1]?.display?.color_code, )
})
return { return {
'--pagination-active': paginationActive, '--pagination-active': paginationActive,
'--pagination-disabled': paginationDisabled, '--pagination-disabled': paginationDisabled,