fix. [SWV-866] 액션버튼 기능 개선 (이미지 타입 추가)

Made-with: Cursor
This commit is contained in:
clkim
2026-02-27 15:15:59 +09:00
parent 9208aae87f
commit fd83d3ae94
25 changed files with 308 additions and 143 deletions

View File

@@ -69,10 +69,9 @@
type="single" type="single"
platform="pc" platform="pc"
class="inspection-launcher" class="inspection-launcher"
:icon-component="AtomsIconsPlayRoundFill"
:icon-props="{ size: 16, color: '#332C2A' }"
> >
{{ tm('Txt_Game_Start') }} {{ tm('Txt_Game_Start') }}
<AtomsIconsPlayRoundFill :size="16" color="#332C2A" />
</BlocksButtonLauncher> </BlocksButtonLauncher>
</div> </div>
</div> </div>
@@ -358,6 +357,9 @@ definePageMeta({
.button-group:deep(.inspection-launcher.btn-base) { .button-group:deep(.inspection-launcher.btn-base) {
@apply flex-1 bg-[var(--primary)] px-0 rounded md:rounded-lg; @apply flex-1 bg-[var(--primary)] px-0 rounded md:rounded-lg;
} }
.button-group:deep(.inspection-launcher) .text {
@apply flex items-center justify-center gap-[2px] mr-0 md:gap-[4px];
}
.button-group:deep(.inspection-launcher.btn-base .icon-platform) { .button-group:deep(.inspection-launcher.btn-base .icon-platform) {
@apply hidden; @apply hidden;
} }

13
app/pages/teaser.vue Normal file
View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
definePageMeta({
layout: false, // 동적 레이아웃을 위해 기본 레이아웃 비활성화
middleware: ['inspection'],
})
// 진입 시 /home으로 리다이렉트
await navigateTo('/home', { replace: true })
</script>
<template>
<div />
</template>

View File

@@ -1,32 +1,45 @@
<script setup lang="ts"> <script setup lang="ts">
interface props { interface props {
<<<<<<< HEAD
type?: 'button' | 'link' type?: 'button' | 'link'
to?: string to?: string
target?: '_self' | '_blank' target?: '_self' | '_blank'
=======
type?: 'internal' | 'external' | 'action'
href?: string
>>>>>>> feature/20250228_SWV-866
backgroundColor?: string backgroundColor?: string
srOnly?: string srOnly?: string
} }
const props = withDefaults(defineProps<props>(), { const props = withDefaults(defineProps<props>(), {
<<<<<<< HEAD
type: 'button', type: 'button',
to: '', to: '',
target: '_self', target: '_self',
=======
type: 'action',
>>>>>>> feature/20250228_SWV-866
backgroundColor: '', backgroundColor: '',
srOnly: '', srOnly: '',
}) })
const componentTag = computed((): string => { const componentTag = computed((): string => {
switch (props.type) { switch (props.type) {
case 'link': case 'internal':
return 'AtomsLocaleLink' return 'AtomsLocaleLink'
case 'external':
return 'a'
default: default:
return 'button' return 'button'
} }
}) })
const componentProps = computed(() => { const componentProps = computed(() => {
switch (props.type) { switch (props.type) {
case 'link': case 'internal':
return { to: props.to, target: props.target } return { to: props.href, target: '_self' }
case 'external':
return { href: props.href, target: '_blank' }
default: default:
return {} return {}
} }

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import type { ImageButtonType } from '#layers/types/components/button'
interface props {
type?: ImageButtonType
href?: string
backgroundImage: string
alt: string
disabled?: boolean
}
const props = withDefaults(defineProps<props>(), {
type: 'action',
disabled: false,
})
const componentTag = computed((): string => {
switch (props.type) {
case 'external':
return 'a'
case 'internal':
return 'AtomsLocaleLink'
default:
return 'button'
}
})
const componentProps = computed(() => {
if (props.type === 'external') {
return {
href: props.href,
target: '_blank',
}
}
if (props.type === 'internal') {
if (props.href) {
return {
to: props.href,
target: '_self',
}
}
return {}
}
return {}
})
const buttonStyle = computed(() => {
return {
backgroundImage: props.backgroundImage,
}
})
</script>
<template>
<component
:is="componentTag"
v-bind="{ ...componentProps }"
:class="['btn-base', { disabled: props.disabled }]"
:style="buttonStyle"
:disabled="props.disabled"
>
<img
v-if="props.backgroundImage"
:src="props.backgroundImage"
:alt="props.alt"
class="btn-bg"
/>
</component>
</template>
<style scoped>
.btn-base {
@apply overflow-hidden relative h-[48px] md:h-[64px] rounded-[8px] md:rounded-[10px] cursor-pointer
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:transition-opacity after:duration-300 after:ease-in-out after:opacity-0
hover:after:opacity-20;
}
.btn-base.disabled {
@apply cursor-default pointer-events-none
after:opacity-20 after:z-[2];
}
.btn-bg {
@apply w-full h-full object-contain;
}
</style>

View File

@@ -2,23 +2,23 @@
import type { TrackingObject } from '#layers/types/api/common' import type { TrackingObject } from '#layers/types/api/common'
interface Props { interface Props {
category?: 'system' | 'image' variant?: 'videoPlay' | 'videoPlayImg'
backgroundColor?: string backgroundColor?: string
tracking: TrackingObject tracking: TrackingObject
} }
const props = withDefaults(defineProps<Props>(), { category: 'system' }) const props = withDefaults(defineProps<Props>(), { variant: 'videoPlay' })
const { locale } = useI18n() const { locale } = useI18n()
const { sendLog } = useAnalytics() const { sendLog } = useAnalytics()
const buttonClasses = computed(() => [ const buttonClasses = computed(() => [
'btn-play', 'btn-play',
props.category === 'system' ? 'play-icon' : 'play-image', props.variant === 'videoPlay' ? 'play-icon' : 'play-image',
]) ])
const buttonStyle = computed(() => const buttonStyle = computed(() =>
props.category === 'system' && props.backgroundColor props.variant === 'videoPlay' && props.backgroundColor
? { backgroundColor: props.backgroundColor } ? { backgroundColor: props.backgroundColor }
: {} : {}
) )
@@ -28,7 +28,7 @@ const onClick = () => sendLog(locale.value, props.tracking)
<template> <template>
<button :class="buttonClasses" :style="buttonStyle" @click="onClick"> <button :class="buttonClasses" :style="buttonStyle" @click="onClick">
<span v-if="props.category === 'system'" class="icon"> <span v-if="props.variant === 'videoPlay'" class="icon">
<AtomsIconsArrowRightFill /> <AtomsIconsArrowRightFill />
</span> </span>
<span class="sr-only">Play</span> <span class="sr-only">Play</span>
@@ -37,12 +37,11 @@ const onClick = () => sendLog(locale.value, props.tracking)
<style scoped> <style scoped>
.btn-play { .btn-play {
@apply relative flex items-center justify-center; @apply relative flex items-center justify-center rounded-full w-[60px] h-[60px] md:w-[80px] md:h-[80px];
} }
.play-icon { .play-icon {
@apply w-[60px] h-[60px] md:w-[80px] md:h-[80px] rounded-full @apply before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-[rgba(255,255,255,0.5)] before:rounded-full
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-[rgba(255,255,255,0.5)] before:rounded-full
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:rounded-[50%] after:opacity-0 after:transition-opacity after:duration-300 after:ease-in-out after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:rounded-[50%] after:opacity-0 after:transition-opacity after:duration-300 after:ease-in-out
hover:after:opacity-10; hover:after:opacity-10;
} }
@@ -54,16 +53,13 @@ const onClick = () => sendLog(locale.value, props.tracking)
} }
.play-image { .play-image {
@apply w-[69px] h-[69px] md:w-[110px] md:h-[110px]; @apply overflow-hidden;
} }
.play-image::before { .play-image::before {
@apply content-[''] absolute inset-0 z-0 bg-no-repeat bg-center bg-cover bg-[url(/images/common/btn_play/btn_default.png)] transition-opacity duration-300 ease-out; @apply content-[''] absolute inset-0 z-0 rounded-full bg-no-repeat bg-center bg-cover bg-[url(/images/common/btn_play/btn_default_m.png)] md:bg-[url(/images/common/btn_play/btn_default.png)];
} }
.play-image::after { .play-image::after {
@apply content-[''] absolute inset-0 z-0 bg-no-repeat bg-center bg-cover bg-[url(/images/common/btn_play/btn_hover.png)] opacity-0 transition-opacity duration-300 ease-out; @apply content-[''] absolute inset-0 z-0 rounded-full bg-no-repeat bg-center bg-cover bg-[url(/images/common/btn_play/btn_hover_m.png)] md:bg-[url(/images/common/btn_play/btn_hover.png)] opacity-0 transition-opacity duration-300 ease-out;
}
.play-image:hover::before {
@apply opacity-0;
} }
.play-image:hover::after { .play-image:hover::after {
@apply opacity-100; @apply opacity-100;

View File

@@ -5,12 +5,11 @@ interface props {
type?: ButtonType type?: ButtonType
size?: string size?: string
variant?: ButtonVariant variant?: ButtonVariant
target?: '_self' | '_blank'
href?: string href?: string
backgroundColor?: string backgroundColor?: string
textColor?: string textColor?: string
disabled?: boolean disabled?: boolean
gradient?: boolean useGradient?: boolean
useGameFont?: boolean useGameFont?: boolean
} }
@@ -21,7 +20,7 @@ const props = withDefaults(defineProps<props>(), {
target: '_self', target: '_self',
textColor: 'var(--alternative-02)', textColor: 'var(--alternative-02)',
disabled: false, disabled: false,
gradient: false, useGradient: false,
useGameFont: false, useGameFont: false,
}) })
@@ -31,21 +30,20 @@ const { fontFamily } = storeToRefs(gameDataStore)
const componentTag = computed((): string => { const componentTag = computed((): string => {
switch (props.type) { switch (props.type) {
case 'external': case 'external':
case 'link':
return 'a' return 'a'
case 'download': case 'download':
return props.href ? 'a' : 'button' return props.href ? 'a' : 'button'
case 'internal': case 'internal':
return props.href ? 'AtomsLocaleLink' : 'button' return 'AtomsLocaleLink'
default: default:
return 'button' return 'button'
} }
}) })
const componentProps = computed(() => { const componentProps = computed(() => {
if (props.type === 'external' || props.type === 'link') { if (props.type === 'external') {
return { return {
href: props.href, href: props.href,
target: props.target, target: '_blank',
} }
} }
@@ -53,6 +51,7 @@ const componentProps = computed(() => {
if (props.href) { if (props.href) {
return { return {
to: props.href, to: props.href,
target: '_self',
} }
} }
return {} return {}
@@ -106,7 +105,8 @@ const textStyle = computed(() => {
:style="buttonStyle" :style="buttonStyle"
:disabled="props.disabled" :disabled="props.disabled"
> >
<i v-if="props.gradient" class="btn-gradient"></i> <!-- 그라데이션 -->
<i v-if="props.useGradient" class="btn-gradient"></i>
<span class="btn-content" :style="textStyle"> <span class="btn-content" :style="textStyle">
<slot /> <slot />
<AtomsIconsLongArrowRightLine <AtomsIconsLongArrowRightLine

View File

@@ -14,8 +14,8 @@ const analytics = {
<template> <template>
<AtomsButtonCircle <AtomsButtonCircle
sr-only="home" sr-only="home"
type="link" type="internal"
to="/home" href="/home"
class="btn-home" class="btn-home"
background-color="rgb(0 0 0 / 0.2)" background-color="rgb(0 0 0 / 0.2)"
@click="sendLog(locale, analytics)" @click="sendLog(locale, analytics)"

View File

@@ -1,29 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CSSProperties, Component } from 'vue' import type { CSSProperties } from 'vue'
import type { PlatformTransformType } from '#layers/types/api/gameData' import type { PlatformTransformType } from '#layers/types/api/gameData'
import type { import type {
DownloadButtonType, LauncherButtonType,
ButtonVariant, ButtonVariant,
Platform, Platform,
} from '#layers/types/components/button' } from '#layers/types/components/button'
interface Props { interface Props {
type?: DownloadButtonType type?: LauncherButtonType
platform: Platform platform: Platform
variant?: ButtonVariant variant?: ButtonVariant
backgroundColor?: string backgroundColor?: string
textColor?: string textColor?: string
iconComponent?: Component
iconProps?: Record<string, any>
disabled?: boolean disabled?: boolean
useGameFont?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
type: 'default', type: 'default',
variant: 'filled', variant: 'filled',
disabled: false, disabled: false,
useGameFont: false,
}) })
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
@@ -33,7 +29,7 @@ const gameDataStore = useGameDataStore()
const modalStore = useModalStore() const modalStore = useModalStore()
const { isProcessing, validateLauncher } = useCheckGameStart() const { isProcessing, validateLauncher } = useCheckGameStart()
const { gameName, platformType, osType, marketJson, fontFamily } = const { gameName, platformType, osType, marketJson } =
storeToRefs(gameDataStore) storeToRefs(gameDataStore)
const PLATFORM_ICON_MAP: Record<Platform, string> = { const PLATFORM_ICON_MAP: Record<Platform, string> = {
@@ -55,11 +51,6 @@ const componentTag = computed(() => {
} }
return 'button' return 'button'
}) })
const shouldShowPlatformIcon = computed(
() =>
(props.type === 'default' && props.variant !== 'custom') ||
props.type === 'single'
)
const shouldShowDownloadIcon = computed( const shouldShowDownloadIcon = computed(
() => () =>
props.platform === 'pc' && props.platform === 'pc' &&
@@ -91,9 +82,6 @@ const textStyle = computed<CSSProperties>(() => {
if (props.textColor) { if (props.textColor) {
style.color = props.textColor style.color = props.textColor
} }
if (props.useGameFont && fontFamily.value) {
style.fontFamily = fontFamily.value
}
return style return style
}) })
@@ -158,24 +146,19 @@ const handleClick = () => {
:is="componentTag" :is="componentTag"
v-bind="$attrs" v-bind="$attrs"
:class="['btn-base', props.type, { 'no-text': !$slots.default }]" :class="['btn-base', props.type, { 'no-text': !$slots.default }]"
:data-variant="props.variant"
:data-platform="props.platform" :data-platform="props.platform"
:data-variant="props.variant"
:style="buttonStyle" :style="buttonStyle"
:disabled="disabled || isProcessing" :disabled="disabled || isProcessing"
@click="handleClick" @click="handleClick"
> >
<span class="btn-content"> <span class="btn-content">
<span v-if="shouldShowPlatformIcon" class="icon-platform"> <span v-if="props.type !== 'duplication'" class="icon-platform">
<component :is="platformIcon" /> <component :is="platformIcon" />
</span> </span>
<span class="text" :style="textStyle"> <span class="text" :style="textStyle">
<slot /> <slot />
</span> </span>
<component
:is="props.iconComponent"
v-if="props.iconComponent"
v-bind="props.iconProps"
/>
<span v-if="shouldShowDownloadIcon" class="icon-download"> <span v-if="shouldShowDownloadIcon" class="icon-download">
<AtomsIconsDownloadLine /> <AtomsIconsDownloadLine />
</span> </span>
@@ -193,9 +176,6 @@ const handleClick = () => {
.icon-platform { .icon-platform {
@apply w-5 h-5 mr-2 flex-shrink-0; @apply w-5 h-5 mr-2 flex-shrink-0;
} }
.icon-download {
@apply ml-auto pl-4;
}
.btn-base[data-variant='filled'] { .btn-base[data-variant='filled'] {
@apply bg-[#383838] text-[#ffffff] @apply bg-[#383838] text-[#ffffff]
@@ -208,6 +188,9 @@ const handleClick = () => {
@apply bg-white text-[#1F1F1F] @apply bg-white text-[#1F1F1F]
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-black/10 before:rounded-lg; before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-black/10 before:rounded-lg;
} }
.btn-base[data-variant='outlined']:hover {
@apply before:border-[#999];
}
.btn-base[data-variant='outlined'][data-platform='app_store'] svg, .btn-base[data-variant='outlined'][data-platform='app_store'] svg,
.btn-base[data-variant='outlined'][data-platform='pc'] svg, .btn-base[data-variant='outlined'][data-platform='pc'] svg,
.btn-base[data-variant='outlined'][data-platform='stove'] svg { .btn-base[data-variant='outlined'][data-platform='stove'] svg {
@@ -226,13 +209,15 @@ const handleClick = () => {
@apply line-clamp-2 text-[14px] @apply line-clamp-2 text-[14px]
md:text-[16px]; md:text-[16px];
} }
.btn-base.default[data-variant='outlined'] .icon-download { .btn-base.default[data-variant='outlined'] .icon-download {
@apply border-black/10; @apply border-black/10;
} }
.btn-base.default[data-variant='outlined'] .icon-download svg { .btn-base.default[data-variant='outlined'] .icon-download svg {
@apply fill-[#1F1F1F]; @apply fill-[#1F1F1F];
} }
.icon-download {
@apply ml-auto pl-4;
}
/* duplication */ /* duplication */
.btn-base.duplication { .btn-base.duplication {

View File

@@ -2,7 +2,7 @@
import type { ColorObject, TrackingObject } from '#layers/types/api/common' import type { ColorObject, TrackingObject } from '#layers/types/api/common'
interface Props { interface Props {
color: ColorObject backgroundColor: ColorObject
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@@ -19,7 +19,7 @@ const analytics = {
const showBtn = computed(() => windowY.value > 0) const showBtn = computed(() => windowY.value > 0)
const backgroundColor = computed( const backgroundColor = computed(
() => getColorCodeFromData(props.color, 'none') ?? 'var(--primary)' () => getColorCodeFromData(props.backgroundColor, 'none') ?? 'var(--primary)'
) )
const handleScrollToTop = () => { const handleScrollToTop = () => {

View File

@@ -10,7 +10,7 @@ const props = defineProps<Props>()
const { locale } = useI18n() const { locale } = useI18n()
const { sendLog } = useAnalytics() const { sendLog } = useAnalytics()
const getArrowBgColor = (direction: 'prev' | 'next') => { const getArrowBackgroundColor = (direction: 'prev' | 'next') => {
return ( return (
getColorCodeFromData( getColorCodeFromData(
props.arrowsData?.[direction === 'prev' ? 0 : 1]?.display, props.arrowsData?.[direction === 'prev' ? 0 : 1]?.display,
@@ -32,7 +32,7 @@ const handleArrowClick = (direction: 'prev' | 'next') => {
<AtomsButtonCircle <AtomsButtonCircle
sr-only="Previous" sr-only="Previous"
class="splide-arrow splide__arrow--prev" class="splide-arrow splide__arrow--prev"
:background-color="getArrowBgColor('prev')" :background-color="getArrowBackgroundColor('prev')"
@click="handleArrowClick('prev')" @click="handleArrowClick('prev')"
> >
<AtomsIconsArrowRightLine color="#ffffff" /> <AtomsIconsArrowRightLine color="#ffffff" />
@@ -40,7 +40,7 @@ const handleArrowClick = (direction: 'prev' | 'next') => {
<AtomsButtonCircle <AtomsButtonCircle
sr-only="Next" sr-only="Next"
class="splide-arrow splide__arrow--next" class="splide-arrow splide__arrow--next"
:background-color="getArrowBgColor('next')" :background-color="getArrowBackgroundColor('next')"
@click="handleArrowClick('next')" @click="handleArrowClick('next')"
> >
<AtomsIconsArrowRightLine color="#ffffff" /> <AtomsIconsArrowRightLine color="#ffffff" />

View File

@@ -59,9 +59,8 @@ const handleCopy = async () => {
<template v-for="(item, key) in snsJson" :key="key"> <template v-for="(item, key) in snsJson" :key="key">
<AtomsButtonCircle <AtomsButtonCircle
v-if="item.use_yn === 1 && item.url" v-if="item.use_yn === 1 && item.url"
type="link" type="external"
:to="item.url" :href="item.url"
target="_blank"
:class="['btn-sns', key]" :class="['btn-sns', key]"
:sr-only="key" :sr-only="key"
@click="sendLog(locale, { ...analytics, click_item: key })" @click="sendLog(locale, { ...analytics, click_item: key })"
@@ -79,6 +78,7 @@ const handleCopy = async () => {
</AtomsButtonCircle> </AtomsButtonCircle>
</template> </template>
<AtomsButtonCircle <AtomsButtonCircle
type="action"
class="btn-sns link" class="btn-sns link"
sr-only="copy" sr-only="copy"
@click="handleCopy" @click="handleCopy"

View File

@@ -586,6 +586,9 @@ onMounted(() => {
.btn-start:deep(.btn-base.default[data-variant='custom']) { .btn-start:deep(.btn-base.default[data-variant='custom']) {
@apply w-full h-[48px] px-10 font-[700] text-[16px]; @apply w-full h-[48px] px-10 font-[700] text-[16px];
} }
.btn-start:deep(.btn-base.default[data-variant='custom']) .icon-platform {
@apply hidden;
}
.btn-start .nav-2depth { .btn-start .nav-2depth {
@apply left-[unset] right-[-40px]; @apply left-[unset] right-[-40px];

View File

@@ -106,7 +106,7 @@ onMounted(() => {
<div v-if="isShowTopBtn" class="utile-wrap"> <div v-if="isShowTopBtn" class="utile-wrap">
<BlocksButtonScrollTop <BlocksButtonScrollTop
v-if="isShowTopBtn" v-if="isShowTopBtn"
:color="pageData?.top_btn_color_json" :background-color="pageData?.top_btn_color_json"
/> />
</div> </div>
</ClientOnly> </ClientOnly>

View File

@@ -5,6 +5,37 @@ import type {
} from '#layers/types/api/pageData' } from '#layers/types/api/pageData'
import type { ButtonType } from '#layers/types/components/button' import type { ButtonType } from '#layers/types/components/button'
/** 어드민 버튼 유형 (시스템 버튼 / 이미지 버튼) */
const BUTTON_CATEGORY = {
SYSTEM: 'SYSTEM', // 시스템 버튼
IMAGE: 'IMAGE', // 이미지 버튼
} as const
/** 어드민 버튼 타입 */
const BUTTON_ACTION_TYPE = {
URL: 'URL',
RUN: 'RUN',
POP: 'POP',
DOWNLOAD: 'DOWNLOAD',
ANCHOR: 'ANCHOR',
MOV: 'MOV',
DEACTIVE: 'DEACTIVE',
} as const
const OS_TYPE = {
PC: 1,
} as const
const MARKET_TYPE = {
PC: 'pc',
GOOGLE_PLAY: 'google_play',
APP_STORE: 'app_store',
} as const
const LINK_TARGET = {
BLANK: '_blank',
} as const
interface Props { interface Props {
resourcesData: PageDataResourceGroup[] resourcesData: PageDataResourceGroup[]
buttonSize?: string buttonSize?: string
@@ -25,15 +56,35 @@ const buttonList = computed<PageDataResourceGroup[]>(
() => props.resourcesData ?? [] () => props.resourcesData ?? []
) )
/** 버튼 유형이 '시스템 버튼'인지 확인 */
const isSystemButton = (button: PageDataResourceGroup): boolean => {
// [TODO] 어디민 개발 후 수정 필요
return button.btn_info?.btn_category === BUTTON_CATEGORY.SYSTEM
}
/** 버튼 타입이 '게임 실행'인지 확인 */
const isLauncherButton = (button: PageDataResourceGroup): boolean => {
return button.btn_info?.detail?.btn_type === BUTTON_ACTION_TYPE.RUN
}
/** 버튼 타입이 '비활성화'인지 확인 */
const isDisabled = (button: PageDataResourceGroup): boolean => {
return button.btn_info?.detail?.btn_type === BUTTON_ACTION_TYPE.DEACTIVE
}
const usesGameFont = (btnInfo?: PageDataResourceGroupBtnInfo): boolean => {
return btnInfo?.use_game_font === 1
}
const getButtonType = (btnInfo?: PageDataResourceGroupBtnInfo): ButtonType => { const getButtonType = (btnInfo?: PageDataResourceGroupBtnInfo): ButtonType => {
const btnType = btnInfo?.detail?.btn_type const btnType = btnInfo?.detail?.btn_type
const target = btnInfo?.detail?.action?.link_target const target = btnInfo?.detail?.action?.link_target
if (btnType === 'URL' && target) { if (btnType === BUTTON_ACTION_TYPE.URL && target) {
return target === '_blank' ? 'external' : 'internal' return target === LINK_TARGET.BLANK ? 'external' : 'internal'
} }
if (btnType === 'DOWNLOAD') return 'download' if (btnType === BUTTON_ACTION_TYPE.DOWNLOAD) return 'download'
return 'action' return 'action'
} }
@@ -44,19 +95,37 @@ const isRunButtonVisible = (btnInfo: PageDataResourceGroupBtnInfo): boolean => {
const marketType = btnInfo?.detail?.market_type const marketType = btnInfo?.detail?.market_type
switch (marketType) { switch (marketType) {
case 'pc': case MARKET_TYPE.PC:
return false return false
case 'google_play': case MARKET_TYPE.GOOGLE_PLAY:
return device.isAndroid return device.isAndroid
case 'app_store': case MARKET_TYPE.APP_STORE:
return device.isApple return device.isApple
default: default:
return true return true
} }
} }
const downloadFile = async (url: string = '', osType: number = 0) => { const openPopupModal = (detail: Record<string, any>) => {
if (osType === 1 && breakpoints.value?.isMobile) { modalStore.handleOpenContent({
contentTitle: detail?.title,
tabInfo: detail?.tab_info,
})
}
const scrollToAnchor = (detail: Record<string, any>) => {
scrollStore.scrollToAnchor(detail?.page_ver_tmpl_name_en ?? '')
}
const openYoutubeModal = (detail: Record<string, any>) => {
modalStore.handleOpenYoutube({ youtubeUrl: detail?.url ?? '' })
}
const downloadFile = async (detail: Record<string, any>) => {
const url = detail?.file_path ?? ''
const osType = detail?.os_type ?? 0
if (osType === OS_TYPE.PC && breakpoints.value?.isMobile) {
modalStore.handleOpenAlert({ contentText: tm('Alert_Download_PC') }) modalStore.handleOpenAlert({ contentText: tm('Alert_Download_PC') })
return return
} }
@@ -64,25 +133,22 @@ const downloadFile = async (url: string = '', osType: number = 0) => {
const fileUrl = formatPathHost(url) const fileUrl = formatPathHost(url)
try { try {
const res = await $fetch<Blob>(fileUrl, { const blob = await $fetch<Blob>(fileUrl, {
method: 'GET', method: 'GET',
responseType: 'blob', responseType: 'blob',
timeout: 10000, timeout: 10000,
}) })
const blob = res
const blobUrl = URL.createObjectURL(blob) const blobUrl = URL.createObjectURL(blob)
const fileName = fileUrl.split('/').pop() ?? 'download'
const pathPart = fileUrl.split('/').pop() ?? 'download' const anchor = document.createElement('a')
const a = document.createElement('a') anchor.href = blobUrl
anchor.download = fileName
document.body.appendChild(anchor)
anchor.click()
anchor.remove()
a.href = blobUrl
a.download = pathPart
document.body.appendChild(a)
a.click()
a.remove()
// 메모리 정리
URL.revokeObjectURL(blobUrl) URL.revokeObjectURL(blobUrl)
modalStore.handleOpenAlert({ contentText: tm('Alert_Download_Success') }) modalStore.handleOpenAlert({ contentText: tm('Alert_Download_Success') })
@@ -92,30 +158,24 @@ const downloadFile = async (url: string = '', osType: number = 0) => {
} }
} }
const buttonClickHandlers: Record<
string,
(detail: Record<string, any>) => void
> = {
[BUTTON_ACTION_TYPE.POP]: openPopupModal,
[BUTTON_ACTION_TYPE.ANCHOR]: scrollToAnchor,
[BUTTON_ACTION_TYPE.MOV]: openYoutubeModal,
[BUTTON_ACTION_TYPE.DOWNLOAD]: downloadFile,
}
const handleButtonClick = (button: PageDataResourceGroup) => { const handleButtonClick = (button: PageDataResourceGroup) => {
sendLog(locale.value, button.tracking) sendLog(locale.value, button.tracking)
const btnDetail = button.btn_info?.detail const btnDetail = button.btn_info?.detail
const btnType = btnDetail?.btn_type
switch (btnDetail?.btn_type) { const handler = buttonClickHandlers[btnType]
case 'POP': handler?.(btnDetail)
modalStore.handleOpenContent({
contentTitle: btnDetail?.title,
tabInfo: btnDetail?.tab_info,
})
return
case 'ANCHOR':
scrollStore.scrollToAnchor(btnDetail?.page_ver_tmpl_name_en ?? '')
return
case 'MOV':
modalStore.handleOpenYoutube({ youtubeUrl: btnDetail.url ?? '' })
return
case 'DOWNLOAD':
downloadFile(btnDetail?.file_path, btnDetail?.os_type)
return
default:
return
}
} }
</script> </script>
@@ -126,35 +186,46 @@ const handleButtonClick = (button: PageDataResourceGroup) => {
class="flex flex-wrap justify-center items-center gap-3 md:gap-4" class="flex flex-wrap justify-center items-center gap-3 md:gap-4"
> >
<template v-for="(button, index) in buttonList" :key="index"> <template v-for="(button, index) in buttonList" :key="index">
<template v-if="button.btn_info?.detail?.btn_type === 'RUN'"> <!-- 버튼 유형: 시스템 버튼 -->
<BlocksButtonLauncher
v-if="isRunButtonVisible(button.btn_info)"
type="duplication"
:platform="button.btn_info?.detail?.market_type"
:background-color="getColorCodeFromData(button.btn_info, 'btn')"
:text-color="getColorCodeFromData(button.btn_info, 'txt')"
:use-game-font="button.btn_info?.use_game_font === 1"
@click="handleButtonClick(button)"
>
{{ button.btn_info?.txt_btn_name }}
</BlocksButtonLauncher>
</template>
<AtomsButton <AtomsButton
v-else v-if="isSystemButton(button)"
:type="getButtonType(button.btn_info)" :type="getButtonType(button.btn_info)"
:size="buttonSize" :size="buttonSize"
:href="button.btn_info?.detail?.action?.url" :href="button.btn_info?.detail?.action?.url"
:target="button.btn_info?.detail?.action?.link_target"
:background-color="getColorCodeFromData(button.btn_info, 'btn')" :background-color="getColorCodeFromData(button.btn_info, 'btn')"
:text-color="getColorCodeFromData(button.btn_info, 'txt')" :text-color="getColorCodeFromData(button.btn_info, 'txt')"
:disabled="button.btn_info?.detail?.btn_type === 'DEACTIVE'" :disabled="isDisabled(button)"
:gradient="true" :use-gradient="true"
:use-game-font="button.btn_info?.use_game_font === 1" :use-game-font="usesGameFont(button.btn_info)"
@click="handleButtonClick(button)" @click="handleButtonClick(button)"
> >
{{ button.btn_info?.txt_btn_name }} {{ button.btn_info?.txt_btn_name }}
</AtomsButton> </AtomsButton>
<!-- 버튼 유형: 이미지 버튼 + 타입: 게임 실행 -->
<BlocksButtonLauncher
v-else-if="
isLauncherButton(button) && isRunButtonVisible(button.btn_info!)
"
type="duplication"
:platform="button.btn_info?.detail?.market_type"
:background-color="getColorCodeFromData(button.btn_info, 'btn')"
:text-color="getColorCodeFromData(button.btn_info, 'txt')"
@click="handleButtonClick(button)"
>
{{ button.btn_info?.txt_btn_name }}
</BlocksButtonLauncher>
<!-- 버튼 유형: 이미지 버튼 + 타입: 기타 -->
<!-- [TODO] api 개발 후 수정 필요 (background-image, alt 에 연결 필요) -->
<AtomsButtonImage
v-else-if="!isSystemButton(button) && !isLauncherButton(button)"
:href="button.btn_info?.detail?.action?.url"
:background-image="'/images/test.png'"
alt="test"
:disabled="isDisabled(button)"
@click="handleButtonClick(button)"
/>
</template> </template>
</div> </div>
</template> </template>

View File

@@ -2,7 +2,7 @@
import type { PageDataResourceGroup } from '#layers/types/api/pageData' import type { PageDataResourceGroup } from '#layers/types/api/pageData'
interface Props { interface Props {
category?: 'system' | 'image' variant?: 'videoPlay' | 'videoPlayImg'
resourcesData: PageDataResourceGroup resourcesData: PageDataResourceGroup
} }
@@ -26,7 +26,7 @@ const handleVideoPlayClick = () => {
<template> <template>
<AtomsButtonPlay <AtomsButtonPlay
v-motion-stagger v-motion-stagger
:category="props.category" :variant="props.variant"
:background-color="backgroundColor" :background-color="backgroundColor"
:tracking="props.resourcesData.tracking" :tracking="props.resourcesData.tracking"
@click="handleVideoPlayClick" @click="handleVideoPlayClick"

View File

@@ -444,12 +444,11 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
</p> </p>
<AtomsButton <AtomsButton
type="link" type="external"
size="size-small" size="size-small"
background-color="#383838" background-color="#383838"
text-color="#FFFFFF" text-color="#FFFFFF"
class="w-full px-0" class="btn-download"
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}`)" @click="handleSendLog(`다운로드_${driver.driverText}`)"
@@ -527,12 +526,10 @@ table td {
md:h-[80px] md:py-[14px] md:px-[20px] md:text-[16px] md:leading-[26px] md:tracking-[-0.48px]; md:h-[80px] md:py-[14px] md:px-[20px] md:text-[16px] md:leading-[26px] md:tracking-[-0.48px];
} }
/* 플랫폼별 다운로드 Mobile Overflow Visible 처리 */ .btn-download {
.splide :deep(.splide__track) { @apply w-full px-0;
overflow: visible !important;
} }
.btn-download:deep(.icon-external) {
::v-deep([data-platform='stove']) .icon-platform { @apply hidden;
display: none !important;
} }
</style> </style>

View File

@@ -20,7 +20,7 @@ const descriptionData = computed(() =>
getComponentGroup(props.components, 'description') getComponentGroup(props.components, 'description')
) )
const videoPlayData = computed(() => const videoPlayData = computed(() =>
getComponentGroup(props.components, 'videoPlay') getComponentGroup(props.components, 'videoPlayImg')
) )
// [TODO] api 수정 후 사용 // [TODO] api 수정 후 사용
// const videoPlayData = computed(() => // const videoPlayData = computed(() =>
@@ -51,7 +51,7 @@ const buttonListData = computed(() =>
/> />
<WidgetsVideoPlay <WidgetsVideoPlay
v-if="videoPlayData" v-if="videoPlayData"
category="image" variant="videoPlayImg"
:resources-data="videoPlayData" :resources-data="videoPlayData"
/> />
<WidgetsButtonList <WidgetsButtonList

View File

@@ -105,7 +105,7 @@ const slideItemSize = {
/> />
<WidgetsVideoPlay <WidgetsVideoPlay
v-if="videoPlayData" v-if="videoPlayData"
category="image" variant="videoPlayImg"
:resources-data="videoPlayData" :resources-data="videoPlayData"
/> />
<WidgetsSlideCenterHighlight <WidgetsSlideCenterHighlight

View File

@@ -86,6 +86,7 @@ export interface PageDataResourceGroupResPath {
} }
export interface PageDataResourceGroupBtnInfo extends ColorObject { export interface PageDataResourceGroupBtnInfo extends ColorObject {
btn_category: string
txt_btn_name: string txt_btn_name: string
detail: Record<string, any> detail: Record<string, any>
disabled?: boolean disabled?: boolean

View File

@@ -1,11 +1,8 @@
export type ButtonType = export type ButtonType = 'internal' | 'external' | 'download' | 'action'
| 'internal'
| 'external'
| 'download'
| 'action'
| 'link'
export type DownloadButtonType = 'default' | 'single' | 'duplication' export type ImageButtonType = 'internal' | 'external' | 'action'
export type LauncherButtonType = 'default' | 'duplication' | 'single'
export type ButtonSize = 'large' | 'medium' | 'small' | 'extra-small' export type ButtonSize = 'large' | 'medium' | 'small' | 'extra-small'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
public/images/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB