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/_theme.css';
@import './base/_font.css';
@@ -8,11 +11,8 @@
@import './components/_layout.css';
@import './components/_modal.css';
@import './components/_splide.css';
/* @import './components/_base-content.css'; */
@import './components/_froala-style.css';
@import '@splidejs/vue-splide/css';
@tailwind base;
@tailwind components;
@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 { locale } = useI18n()
const { sendLog, useAnalyticsData } = useAnalytics()
const { sendLog } = useAnalytics()
const handlePlayClick = () => {
sendLog(locale.value, useAnalyticsData(props.tracking))
sendLog(locale.value, props.tracking)
}
</script>

View File

@@ -63,17 +63,17 @@ const onSelectOption = (option: { [key: string | number]: any }): void => {
<div
v-if="isActive"
: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">
<li
v-for="(option, index) in props.options"
v-for="option in props.options"
:key="String(option[props.labelName])"
class="relative flex items-center justify-left w-full"
>
<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="
selectedColor ? `text-[${selectedColor}]` : 'text-[#333333]'
"

View File

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

View File

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

View File

@@ -22,12 +22,6 @@ const props = withDefaults(defineProps<Props>(), {
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 { tm } = useI18n()
const device = useDevice()
@@ -37,6 +31,12 @@ const { isProcessing, validateLauncher } = useCheckGameStart()
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> = {
google_play: 'AtomsIconsLogoGoogle',
app_store: 'AtomsIconsLogoApple',

View File

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

View File

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

View File

@@ -13,7 +13,13 @@ interface Props {
const props = defineProps<Props>()
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(() => {
return !props.imgPath || props.imgPath === null
@@ -21,15 +27,6 @@ const isNoImage = computed(() => {
const isShowOverlay = computed(() => {
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>
<template>
@@ -64,7 +61,7 @@ const handleLinkClick = (title: string) => {
:to="props.url"
:target="props.linkTarget || '_self'"
class="card-link"
@click="handleLinkClick(props.title)"
@click="sendLog(locale, analytics)"
/>
</div>
</template>

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,11 +7,11 @@ import type {
const { locale } = useI18n()
const gameDomain = useGetGameDomain()
const { sendLog, useAnalyticsData } = useAnalytics()
const { sendLog } = useAnalytics()
const analytics = {
action_type: 'click',
click_sarea: 'eventNavigation',
click_sarea: 'EventNavigation',
}
const isEventNavigationOpen = ref(true)
@@ -48,7 +48,7 @@ const toggleEventNavigation = () => {
...analytics,
click_item: isEventNavigationOpen.value ? '열기' : '닫기',
} as TrackingObject
sendLog(locale.value, useAnalyticsData(navigationAnalytics))
sendLog(locale.value, navigationAnalytics)
}
onMounted(async () => {
@@ -77,15 +77,13 @@ onMounted(async () => {
<AtomsLocaleLink
:to="item.page_url"
:target="item.link_type === 2 ? '_blank' : '_self'"
:rel="item.link_type === 2 ? 'noopener noreferrer' : undefined"
class="item-link"
@click="
sendLog(
locale,
useAnalyticsData({
...analytics,
click_item: item.banner_title || item.promotion_name,
})
)
sendLog(locale, {
...analytics,
click_item: item.banner_title || item.promotion_name,
})
"
>
<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>
<footer id="footer" ref="footerRef" class="relative bg-blac">
<div
@@ -91,13 +272,22 @@
</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 v-dompurify-html="tm('Footer_AgeRating_Caution')"></div>
<div
v-dompurify-html="tm('Footer_AgeRating_Caution')"
></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="flex flex-start border-b border-white/10 pb-2">
<span class="text-white text-sm flex-1">
@@ -159,13 +349,12 @@
<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"
>
<BlocksLanguageSwitcher :language-order="tm('Footer_Language_Order')" />
<BlocksLanguageSwitcher
:language-order="tm('Footer_Language_Order')"
/>
</div>
<div
v-if="hasCautionText"
class="mt-6 md:mt-6 hidden sm:block"
>
<div v-if="hasCautionText" class="mt-6 md:mt-6 hidden sm:block">
<div
v-dompurify-html="tm('Footer_caution')"
class="text-xs text-white/30"
@@ -178,7 +367,11 @@
<div class="logo-area flex items-center gap-7 mt-6 md:mt-6">
<span>
<a :href="tm('Footer_Smilegate_Link')" target="_blank" class="smilegate">
<a
:href="tm('Footer_Smilegate_Link')"
target="_blank"
class="smilegate"
>
<img
:src="
formatPathHost(`/images/common/logo_smilegate.png`, {
@@ -225,183 +418,6 @@
</footer>
</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>
em {
font-style: normal;
@@ -412,11 +428,11 @@ em {
}
.bg-Type12 {
background-color: #4369B1;
background-color: #4369b1;
}
.bg-Type15 {
background-color: #F9B846;
background-color: #f9b846;
}
.bg-Type18 {
@@ -424,11 +440,11 @@ em {
}
.bg-Type19 {
background-color: #A31639;
background-color: #a31639;
}
.bg-TypeAll {
background-color: #44A342;
background-color: #44a342;
}
.bg-TypeExempt {

View File

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

View File

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

View File

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

View File

@@ -1,43 +1,20 @@
<script setup lang="ts">
import type { TrackingObject } from '#layers/types/api/common'
interface Props {
title: string
description?: string
link?: string
target?: '_self' | '_blank'
linkAnalytics?: TrackingObject
}
const props = withDefaults(defineProps<Props>(), {
target: '_blank',
})
const componentTag = computed((): string => {
switch (props.target) {
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 {}
})
const { locale } = useI18n()
const { sendLog } = useAnalytics()
</script>
<template>
@@ -55,11 +32,12 @@ const componentProps = computed(() => {
>
{{ props.description }}
</p>
<component
:is="componentTag"
<AtomsLocaleLink
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"
@click="sendLog(locale, props.linkAnalytics)"
>
<span>{{ props.description }}</span>
<AtomsIconsWebLinkLine
@@ -68,7 +46,7 @@ const componentProps = computed(() => {
color="#3C75FF"
class="icon"
/>
</component>
</AtomsLocaleLink>
</div>
</div>
</template>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { globalDateFormat } from '@seed-next/date'
import { PREREGIST_ERROR_CODE } from '#layers/composables/usePreregist'
import type { TrackingObject } from '#layers/types/api/common'
interface Props {
preregistCode: string
@@ -8,6 +9,7 @@ interface Props {
}
const props = defineProps<Props>()
const pageVerTmplNameEn = inject('pageVerTmplNameEn')
// Composables
const { locale } = useI18n()
@@ -25,6 +27,7 @@ const {
} = usePreregist()
const { gameData } = storeToRefs(gameDataStore)
const { sendLog } = useAnalytics()
// Constants
const stoveCs = runtimeConfig.public.stoveCs
@@ -69,6 +72,16 @@ const toggleExpand = (event: Event) => {
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) {
currentStep.value = 2
handleSendLog('사전 등록 하기')
return
}
@@ -394,7 +408,9 @@ defineExpose({
v-if="device.isDesktop ? true : device.isAndroid"
href="https://play.google.com/store/search?q=stove&c=apps"
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]"
@click="handleSendLog('Google Play')"
>
<AtomsIconsLogoGoogle />
<span>Google Play</span>
@@ -403,7 +419,9 @@ defineExpose({
v-if="device.isDesktop ? true : device.isApple"
href="https://apps.apple.com/app/stove-app-stove-app/id1342134971"
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]"
@click="handleSendLog('App Store')"
>
<AtomsIconsLogoApple />
<span>App Store</span>

View File

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

View File

@@ -69,7 +69,7 @@ export const useAnalyticsData = (
const baseViewArea = pageNameEn.value
let logData = {} as AnalyticsDetailType
// 문자열 'view'만 들어오는 경우 (페이지뷰)
// 페이지뷰
if (analytics === 'view') {
logData = {
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
// ============================================================================
@@ -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 전송
*
@@ -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
// ============================================================================
@@ -364,19 +372,13 @@ const sendTiktokPixel = (ttEventName?: string) => {
const sendMarketingLog = ({
logName,
eventName,
analytics,
}: {
logName: 'ga' | 'meta' | 'twitter' | 'tiktok'
eventName?: string
analytics?: Partial<AnalyticsDetailType>
}) => {
switch (logName) {
case 'ga': {
if (eventName) {
sendGAEventOnly(eventName)
} else if (analytics) {
sendGA(analytics)
}
sendGAEventOnly(eventName)
break
}
case 'meta': {

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
<script setup lang="ts">
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 { Platform } from '#layers/types/components/button'
// Props
interface Props {
id?: string
components: PageDataTemplateComponents
pageVerTmplSeq: number
pageVerTmplNameEn: string
@@ -13,6 +15,12 @@ const props = defineProps<Props>()
// Configuration
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 multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_download.json'
@@ -30,27 +38,8 @@ const { tm, locale }: any = useI18n({
})
// Composables
const device = useDevice()
const breakpoints = useResponsiveBreakpoints()
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 specMobileRef = ref<HTMLElement | null>(null)
const driverArray = ref<Array<string>>([
@@ -71,7 +60,18 @@ const pcSpecArray = ref<Array<string>>([
const mobileSpecArray = ref<Array<string>>(['Android', '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(() =>
driverArray.value.map(driver => ({
id: `DRIVER_${driver}`,
@@ -108,6 +108,15 @@ const mobileOSList = computed(() =>
)
// Functions
const handleSendLog = (item: string) => {
const analytics = {
action_type: 'click',
click_item: item,
click_sarea: props.pageVerTmplNameEn,
}
sendLog(locale.value, analytics)
}
/**
* 입력한 pc, mobile 인자값에 따라 해당 최소사양&권장사양 테이블로 포커스 스크롤 이동합니다.
* @param target<string> 'pc' | 'mobile'
@@ -123,6 +132,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
refs[target]?.getBoundingClientRect().top + window.scrollY - adjustedOffset
window.scrollTo({ top: adjustedOffsetTop, behavior: 'smooth' })
handleSendLog(`${target === 'pc' ? 'PC' : '모바일'} 버전 사양 확인하기`)
}
</script>
@@ -196,12 +206,8 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
></p>
</template>
<AtomsButton
type="action"
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"
<button
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"
@click="handleMoveFocus('pc')"
>
<span>{{ tm('Download_Box_PC_SpecCheck') }}</span>
@@ -210,7 +216,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
color="#1F1F1F"
class="relative rotate-90"
/>
</AtomsButton>
</button>
</div>
<div
@@ -224,6 +230,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
v-else-if="breakpoints.isMd || breakpoints.isDesktop"
platform="pc"
class="!w-full !max-w-[300px]"
@click="handleSendLog('PC 버전 다운로드')"
>
<span>{{ tm('Download_Button_PC') }}</span>
</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]"
>
<span>{{ tm('Download_Text_Not_STOVE_Client') }}</span>
<NuxtLink
<a
:href="stoveClientDownloadUrl"
target="_self"
rel="noopener noreferrer"
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>
<AtomsIconsDownloadLine color="#3C75FF" />
</NuxtLink>
</a>
</p>
</div>
</div>
@@ -269,12 +277,8 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
<span>{{ tm('Download_Box_MOBILE_Title') }}</span>
</h4>
<AtomsButton
type="action"
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"
<button
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"
@click="handleMoveFocus('mobile')"
>
<span>{{ tm('Download_Box_MOBILE_SpecCheck') }}</span>
@@ -283,7 +287,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
color="#1F1F1F"
class="relative rotate-90"
/>
</AtomsButton>
</button>
</div>
<div
@@ -300,6 +304,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
"
:platform="`${os.platformCode as Platform}`"
class="!w-full"
@click="handleSendLog(os.platformCode)"
>
<span>{{ os.platformText }}</span>
</BlocksButtonLauncher>
@@ -376,14 +381,15 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
background-color="#383838"
text-color="#FFFFFF"
class="shrink-0 w-[206px] px-0 text-[16px]"
@click="
@click="[
checkPCSpec({
schemeFormat: schemeFormatData,
setupUrl: setupUrlData,
gameNo: gameData?.game_code?.toString(),
locale: locale as string,
})
"
}),
handleSendLog('내 PC 사양 확인하기'),
]"
>
<em
class="inline-flex items-center justify-center gap-[8px] not-italic"
@@ -439,6 +445,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
target="_blank"
rel="noopener noreferrer"
:href="tm(`Download_Driver_${driver.driverCode}_Url`)"
@click="handleSendLog(`다운로드_${driver.driverText}`)"
>
<span>{{ tm('Download_Button_Download') }}</span>
</AtomsButton>

View File

@@ -12,6 +12,7 @@ interface Props {
}
const props = defineProps<Props>()
provide('pageVerTmplNameEn', props.pageVerTmplNameEn)
const runtimeConfig = useRuntimeConfig()
const device = useDevice()
@@ -27,14 +28,15 @@ const resultGetMultilingual = await useGetMultilingual({
baseApiUrl: dataResourcesUrl,
fileName: multilingualFileName,
})
const { tm, locale }: any = useI18n({
const { t, tm, locale }: any = useI18n({
useScope: 'local',
messages: Object(resultGetMultilingual?.value?.multilingual),
})
const { getOperateResources } = useOperateResources()
const { gameData } = storeToRefs(gameDataStore)
const { pageData } = storeToRefs(pageDataStore)
const { getOperateResources } = useOperateResources()
const { sendLog } = useAnalytics()
// Constants
const COLOR_INDEX = { BACKGROUND: 0, TEXT: 1 } as const
@@ -65,6 +67,10 @@ const preImgSnsData = computed(() =>
const preDescriptionData = computed(() =>
getComponentGroup(props.components, 'description')
)
const preregistSNS = computed(
() => getSupportedPlatforms('2', gameData?.value?.os_type) as Platform[]
)
// SNS Buttons
const snsButtonsData = computed(() => {
const buttons = getComponentGroupAry(props.components, 'imgSnsButton')
@@ -73,9 +79,9 @@ const snsButtonsData = computed(() => {
if (!buttons?.length) return []
return buttons.map((button, index) => ({
image: button,
link: links?.[index]?.display?.text ?? '',
id: button.id ?? `sns-${index}`,
link: links?.[index]?.display?.text ?? '',
image: button,
}))
})
@@ -86,18 +92,15 @@ const buttonColors = computed(() => {
'preregistButtonColor'
)
if (!colorData?.length)
return { backgroundColor: undefined, textColor: undefined }
return {
backgroundColor: getColorCode({
colorName: colorData[COLOR_INDEX.BACKGROUND]?.display?.color_name,
colorCode: colorData[COLOR_INDEX.BACKGROUND]?.display?.color_code,
}),
textColor: getColorCode({
colorName: colorData[COLOR_INDEX.TEXT]?.display?.color_name,
colorCode: colorData[COLOR_INDEX.TEXT]?.display?.color_code,
}),
backgroundColor: getColorCodeFromData(
colorData[COLOR_INDEX.BACKGROUND]?.display,
'none'
),
textColor: getColorCodeFromData(
colorData[COLOR_INDEX.TEXT]?.display,
'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 = () => {
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>
@@ -272,7 +289,12 @@ const handlePreregistClick = () => {
:key="btn.id"
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
:src="getResourceSrc(btn.image)"
:alt="btn.image?.display?.text"
@@ -293,16 +315,18 @@ const handlePreregistClick = () => {
{{ tm('Preregist_Btn_Preegist') }}
</BlocksButtonLauncher>
<template v-if="gameData?.platform_type !== '1'">
<template
v-for="platform in getSupportedPlatforms('2', gameData?.os_type)"
:key="`preregist-${platform}`"
>
<template v-for="sns in preregistSNS" :key="`preregist-${sns}`">
<BlocksButtonLauncher
v-if="isRunButtonVisible(platform as Platform)"
v-if="isRunButtonVisible(sns)"
type="duplication"
:platform="platform as Platform"
:platform="sns"
:background-color="buttonColors.backgroundColor"
:text-color="buttonColors.textColor"
@click="
handleSendLog(
`${sns}_${t('Preregist_Btn_Preegist', {}, { locale: 'ko' })}`
)
"
>
{{ tm('Preregist_Btn_Preegist') }}
</BlocksButtonLauncher>

View File

@@ -4,6 +4,7 @@ import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
// Props
interface Props {
id?: string
components: PageDataTemplateComponents
pageVerTmplSeq: number
pageVerTmplNameEn: string
@@ -11,6 +12,7 @@ interface Props {
const props = defineProps<Props>()
const { handleTokenValidation } = useTokenValidation()
const { sendLog } = useAnalytics()
// Configuration
const runtimeConfig = useRuntimeConfig()
@@ -23,43 +25,89 @@ const resultGetMultilingual = await useGetMultilingual({
baseApiUrl: dataResourcesUrl,
fileName: multilingualFileName,
})
const { tm, locale }: any = useI18n({
const { t, tm, locale }: any = useI18n({
useScope: 'local',
messages: Object(resultGetMultilingual?.value?.multilingual),
})
const isLogin = ref(false)
const secureSetting = ref({
otpLoginYn: 'N',
abroadLoginBlockYn: 'N',
pcRegisterYn: 'N',
})
// 회원 보안 설정 설정
const handleSecureSetting = (url: string) => {
window.open(url, '_blank')
}
// Computed
const backgroundData = computed(() =>
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 관리',
},
]
// 로그인 유효성 체크
const checkLoginValidation = async () => {
const accessToken = useCookie('SUAT')
const validateTokenResult = await handleTokenValidation(
accessToken.value || ''
)
isLogin.value = validateTokenResult
}
// 한국어일 때는 모든 카드 노출, 그 외 언어일 때는 SECURE_CARD_0만 노출
if (locale.value === 'ko') {
return allCards
}
return allCards.filter(card => card.id === 'SECURE_CARD_0')
})
// 유의사항 내용 다국어 조회
const cautionText = computed(() => {
return tm('Secure_Notice_Content') || []
})
// 회원 보안 설정 조회
const fnGetSecuritySetting = async () => {
const accessToken = useCookie('SUAT')
checkLoginValidation()
const apiBase = `${stoveApiBaseUrl}/auth-secure/v1.0`
const headers = {
Authorization: `Bearer ${accessToken.value}`,
'Content-Type': 'application/json;charset=UTF-8',
}
if (accessToken) return
try {
const result = await commonFetch('GET', `${apiBase}/security/setting`, {
headers,
@@ -81,65 +129,23 @@ const fnGetSecuritySetting = async () => {
console.error(e)
}
}
// Data
const backgroundData = computed(() =>
getComponentGroup(props.components, 'background')
)
// Computed
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'),
},
{
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
const handleClickSetup = async card => {
const analytics = {
action_type: 'click',
click_item: `${t('Secure_Action_setup', {}, { locale: 'ko' })}_${card.analyticsItem}`,
click_sarea: props.pageVerTmplNameEn,
}
return allCards.filter(card => card.id === 'SECURE_CARD_0')
})
// 유의사항 내용 다국어 조회
const cautionText = computed(() => {
return tm('Secure_Notice_Content') || []
})
sendLog(locale.value, analytics)
const accessToken = useCookie('SUAT')
const validateTokenResult = await handleTokenValidation(
accessToken.value || ''
)
if (validateTokenResult === false) return
window.open(card.url, '_blank')
}
onMounted(() => {
fnGetSecuritySetting()
@@ -148,6 +154,7 @@ onMounted(() => {
<template>
<WidgetsFixMainTitle
:id="props.id"
:title="tm('Secure_Page_Title') || '보안 강화 캠페인'"
:resources-data="backgroundData"
class="mx-auto"
@@ -333,11 +340,7 @@ onMounted(() => {
button-size="size-small md:size-large"
background-color="#383838"
text-color="#FFFFFF"
@click="
isLogin
? handleSecureSetting(card.url)
: checkLoginValidation()
"
@click="handleClickSetup(card)"
>
<span>{{ tm('Secure_Action_setup') }}</span>
</AtomsButton>

View File

@@ -6,6 +6,7 @@ import {
getComponentContainer,
} from '#layers/utils/dataUtil'
import { getYouTubeThumbnail } from '#layers/utils/youtubeUtil'
import type { TrackingObject } from '#layers/types/api/common'
import type {
PageDataTemplateComponents,
PageDataResourceGroups,
@@ -25,6 +26,7 @@ const pageDataStore = usePageDataStore()
const modalStore = useModalStore()
const breakpoints = useResponsiveBreakpoints()
const { getOperateResources } = useOperateResources()
const { sendLog } = useAnalytics()
const { pageData } = storeToRefs(pageDataStore)
@@ -39,11 +41,30 @@ const resultGetMultilingual = await useGetMultilingual({
fileName: multilingualFileName,
})
const { tm, locale } = useI18n({
const { tm, t, locale } = useI18n({
useScope: 'local',
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 backgroundData = computed(() =>
@@ -53,23 +74,6 @@ const officialUrlData = computed(
() => getComponentGroup(props.components, 'officialUrl')?.display?.text ?? ''
)
const arrowsData: PageDataResourceGroups = [
{
tracking: {
action_type: 'click',
click_item: '1. 컨텐츠 리스트 - 네비게이션(좌)',
click_sarea: 'Home_GameInfo__arrow',
},
},
{
tracking: {
action_type: 'click',
click_item: '1. 컨텐츠 리스트 - 네비게이션(우)',
click_sarea: 'Home_GameInfo__arrow',
},
},
]
const { data: slideData } = await useAsyncData(
`fx-video-01-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`,
async () => {
@@ -131,13 +135,19 @@ const hasMore = computed(
() => visibleVideos.value.length < recentVideos.value.length
)
const handleVideoClick = (url: string) => {
modalStore.handleOpenYoutube({ youtubeUrl: url })
const handleVideoClick = (item: OperateGroupItem) => {
modalStore.handleOpenYoutube({ youtubeUrl: item.url })
sendLog(locale.value, { ...analytics, click_item: item.title })
}
const handleLoadMoreRecent = () => {
if (hasMore.value) {
currentRecentPage.value++
sendLog(locale.value, {
...analytics,
click_item: t('Text_More', {}, { locale: 'ko' }),
})
}
}
</script>
@@ -154,6 +164,10 @@ const handleLoadMoreRecent = () => {
:title="tm('Video_Section_Recommendation_Title')"
:description="tm('Video_Page_Link')"
:link="officialUrlData"
:link-analytics="{
...analytics,
click_item: t('Video_Page_Link', {}, { locale: 'ko' }),
}"
/>
<div
class="relative content-static bg-[#fff] rounded-[12px] md:rounded-[16px]"
@@ -173,7 +187,7 @@ const handleLoadMoreRecent = () => {
>
<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]"
@click="handleVideoClick(item.url)"
@click="handleVideoClick(item)"
>
<img
:src="getYouTubeThumbnail(item.url, 'maxres')"
@@ -191,7 +205,7 @@ const handleLoadMoreRecent = () => {
</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]"
@click="handleVideoClick(item.url)"
@click="handleVideoClick(item)"
@mouseenter="recommendHover = true"
@mouseleave="recommendHover = false"
>
@@ -225,7 +239,7 @@ const handleLoadMoreRecent = () => {
v-for="(item, index) in visibleVideos"
:key="`recent-${item.url}-${index}`"
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
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)"
:analytics-sarea="pageVerTmplNameEn"
link-target="_blank"
rel="noopener noreferrer"
/>
</div>
</SplideSlide>

View File

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

View File

@@ -1,8 +1,5 @@
import type { HTMLAttributes } from 'vue'
import type { StoveJsService } from '#layers/types/Stove'
export type ClassType = HTMLAttributes['class']
declare global {
interface Window {
stoveJsService?: StoveJsService
@@ -27,39 +24,9 @@ interface ParsedCustomLinkOptions {
query?: Record<string, any>
}
// 타입 정의
interface FooterMenuItem {
title: string
url: string
target?: string
active?: string
export type {
CommonRequestType,
CommonResponseType,
CommonPeriodType,
ParsedCustomLinkOptions,
}
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_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 요청 파라미터 타입
export interface GameDataRequest {
@@ -31,10 +31,7 @@ export interface GameDataValue {
key_color_json: GameDataKeyColors
use_game_font: boolean
comm_sns_bg_color_json: {
display: {
color_code: string
color_name: string
}
display: ColorObject
}
comm_multilang_filename: string
footer_dev_ci_img_yn: boolean
@@ -134,11 +131,7 @@ export interface GameDataGlobal {
lang_json: string // JSON 문자열로 변경
}
export interface GameDataResourceGroupBtnInfo {
color_code_btn: string
color_name_btn: string
color_code_txt: string
color_name_txt: string
export interface GameDataResourceGroupBtnInfo extends ColorObject {
disabled: boolean
txt_btn_name: string
detail: Record<string, any>
@@ -176,7 +169,7 @@ export interface GameDataMenu {
url_path: string
link_target: string
children: GameDataMenuChildren
tracking_json: string | TrackingObject // JSON 문자열 또는 객체로 변경
tracking_json: TrackingObject
}
// 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 요청 파라미터 타입
export interface PageDataRequest {
@@ -53,7 +53,7 @@ export interface PageDataLnbMenu {
menu_name: string
target_type: number
page_ver_tmpl_name_en: string
tracking_json: Record<string, TrackingObject>
tracking_json: TrackingObject
children?: Record<string, PageDataLnbMenu>
}
@@ -83,27 +83,23 @@ export interface PageDataResourceGroupResPath {
path_pc?: string
}
export interface PageDataResourceGroupBtnInfo {
color_code_btn: string
color_name_btn: string
color_code_txt: string
color_name_txt: string
export interface PageDataResourceGroupBtnInfo extends ColorObject {
disabled: boolean
txt_btn_name: string
detail: Record<string, any>
}
// 리소스 그룹 타입
export interface PageDataResourceGroupDisplay extends ColorObject {
text: string
}
export interface PageDataResourceGroup {
group_label?: string
resource_type?: PageDataResourceGroupType
res_path?: PageDataResourceGroupResPath
btn_info?: PageDataResourceGroupBtnInfo
display?: {
text: string
color_code?: string
color_name?: string
}
display?: PageDataResourceGroupDisplay
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 처리에 필요한 유틸리티 함수를 제공합니다.
*/
import type { GameDataResourceGroupBtnInfo } from '#layers/types/api/gameData'
import type {
PageDataResourceGroups,
PageDataResourceGroupBtnInfo,
} from '#layers/types/api/pageData'
import type { ColorObject } from '#layers/types/api/common'
import type { PageDataResourceGroups } from '#layers/types/api/pageData'
/**
* 색상값을 반환합니다.
@@ -15,7 +12,7 @@ import type {
* @param colorCode 색상 코드
* @returns 색상 값
*/
export const getColorCode = ({
const getColorCode = ({
colorName,
colorCode,
}: {
@@ -24,6 +21,7 @@ export const getColorCode = ({
}) => {
if (colorName) return `var(--${colorName})`
else if (colorCode) return colorCode
return 'var(--primary)'
}
/**
@@ -33,11 +31,15 @@ export const getColorCode = ({
* @returns 색상 값
*/
export const getColorCodeFromData = (
data: GameDataResourceGroupBtnInfo | PageDataResourceGroupBtnInfo,
type: 'btn' | 'txt' = 'txt'
data: ColorObject,
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 colorCode = data?.[`color_code${suffix}` as keyof typeof data]
@@ -61,15 +63,14 @@ export const getPaginationClass = (
}
// 색상 추출 또는 기본값 사용
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,
})
const paginationActive = getColorCodeFromData(
paginationGroups[0]?.display,
'none'
)
const paginationDisabled = getColorCodeFromData(
paginationGroups[1]?.display,
'none'
)
return {
'--pagination-active': paginationActive,
'--pagination-disabled': paginationDisabled,