Merge branch 'feature/202501107-all' into feature/20251001-gil

This commit is contained in:
“hyeonggkim”
2025-10-30 18:32:19 +09:00
41 changed files with 402 additions and 202 deletions

View File

@@ -145,15 +145,16 @@ onBeforeUnmount(() => {
<h1 class="sr-only">{{ gameData?.game_name }}</h1>
<NuxtPage />
<WidgetsModalClient />
<!-- 공통 모달 컴포넌트 -->
<AtomsModalYouTube
<BlocksModalYouTube
v-model:is-open="youtube.storeIsOpen"
:youtube-url="youtube.storeYoutubeUrl"
:is-outside-close="youtube.storeIsOutsideClose"
:modal-name="youtube.storeModalName"
@close-button-event="handleResetYoutube"
/>
<AtomsModalConfirm
<BlocksModalConfirm
v-model:is-open="confirm.storeIsOpen"
:is-show-dimmed="confirm.storeIsShowDimmed"
:content-text="confirm.storeContentText"
@@ -164,7 +165,7 @@ onBeforeUnmount(() => {
@confirm-button-event="confirm.storeConfirmButtonEvent"
@cancel-button-event="confirm.storeCancelButtonEvent"
/>
<AtomsModalAlert
<BlocksModalAlert
v-model:is-open="alert.storeIsOpen"
:is-show-dimmed="alert.storeIsShowDimmed"
:content-text="alert.storeContentText"

View File

@@ -16,11 +16,4 @@
border: none;
outline: none;
}
/* 라이트 테마 색상 */
[data-theme='light'] {
.main {
@apply bg-theme-foreground;
}
}
}

View File

@@ -48,7 +48,6 @@ const componentTag = computed(() => {
})
const isDuplication = computed(() => props.type === 'duplication')
const isSingle = computed(() => props.type === 'single')
const isCustom = computed(() => props.type === 'custom')
const platformIcon = computed(() => PLATFORM_ICON_MAP[props.platform])
const duplicationImage = computed(() =>
isDuplication.value ? DUP_IMAGE_MAP[props.platform] : ''
@@ -80,8 +79,8 @@ const handleClick = () => {
return
}
const url = gameData?.market_json[props.platform]?.url || ''
window.open(url, '_blank')
const url = gameData?.market_json[props.platform]?.url
if (url) window.open(url, '_blank')
}
</script>
@@ -103,7 +102,7 @@ const handleClick = () => {
<span class="btn-content">
<component
:is="platformIcon"
v-if="!isDuplication && !isCustom"
v-if="!isDuplication"
class="icon-platform"
/>
<span class="text">
@@ -114,25 +113,19 @@ const handleClick = () => {
</span>
</span>
</component>
<ClientOnly>
<Teleport to="#teleports">
<BlocksModalClient />
</Teleport>
</ClientOnly>
</template>
<style scoped>
.btn-base {
@apply overflow-hidden inline-block relative font-medium cursor-pointer
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-white/10
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;
@apply overflow-hidden inline-flex relative font-medium rounded-lg cursor-pointer
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-white/10 before:rounded-lg
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 after:rounded-lg;
}
.btn-base:hover {
@apply after:opacity-20;
}
.btn-content {
@apply relative flex items-center z-[1] text-left;
@apply relative flex items-center w-full text-left z-[1];
}
.icon-platform {
@apply w-5 h-5 flex-shrink-0;
@@ -154,9 +147,8 @@ const handleClick = () => {
/* default */
.btn-base.default {
@apply w-[296px] py-3.5 px-5 rounded-[4px]
md:w-[356px] md:py-4 md:px-6
before:rounded-[4px] after:rounded-[4px];
@apply w-[296px] py-3.5 px-5
md:w-[356px] md:py-4 md:px-6;
}
.btn-base.default .text {
@apply pl-2 pr-4 line-clamp-2 text-[14px]
@@ -177,8 +169,7 @@ const handleClick = () => {
/* duplication */
.btn-base.duplication {
@apply items-start bg-[16px_50%] bg-[length:auto_28px] bg-no-repeat rounded-[4px]
before:rounded-[4px] after:rounded-[4px]
@apply bg-[16px_50%] bg-[length:auto_28px] bg-no-repeat
pt-[22px] pl-[47px] pr-[22px] pb-[7px] text-[11px]
md:h-[64px] md:pt-[30px] md:pl-[64px] md:pr-[28px] md:pb-[11px] md:text-[12px] md:bg-[20px_50%] md:bg-[length:auto_40px];
}
@@ -201,9 +192,8 @@ const handleClick = () => {
/* single */
.btn-base.single {
@apply justify-center items-center text-[14px]
h-[40px] px-3.5 rounded-[4px]
md:h-[48px] md:rounded-[8px]
before:rounded-[4px] md:before:rounded-[8px];
h-[40px] px-3.5
md:h-[48px];
}
.btn-base.single.no-text {
@apply min-w-[40px] px-0 md:min-w-[48px];

View File

@@ -18,7 +18,7 @@ const handleScrollToTop = () => {
<style scoped>
.btn-top {
@apply relative rounded-full bg-[image:var(--button-top)] bg-center bg-cover bg-no-repeat z-[100]
@apply relative rounded-full bg-[image:var(--button-top)] bg-center bg-cover bg-no-repeat
w-[40px] h-[40px] md:w-[48px] md:h-[48px]
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:rounded-full after:opacity-0 after:transition-all after:duration-300 after:ease-in-out;
}

View File

@@ -0,0 +1,139 @@
<script setup lang="ts">
const showSnsList = ref(false)
const isForceClosed = ref(false)
const { gameData } = useGameDataStore()
const snsBackgroundColor = computed(() => {
const colorData = gameData?.comm_sns_bg_color_json?.display
const colorCode = getColorCode({
colorName: colorData?.color_name,
colorCode: colorData?.color_code,
})
return colorCode
})
const snsList = computed(() => {
return gameData?.sns_json
})
const handleMouseEnter = () => {
if (isForceClosed.value) return
showSnsList.value = true
}
const handleMouseLeave = () => {
if (isForceClosed.value) return
showSnsList.value = false
}
const handleForceClose = () => {
isForceClosed.value = true
showSnsList.value = false
// 일정 시간 뒤 다시 hover 가능하도록 초기화
setTimeout(() => {
isForceClosed.value = false
}, 500)
}
const handleCopy = async () => {
if (!import.meta.client) return
try {
const url = window.location.href
await navigator.clipboard.writeText(url)
console.log('✅ 복사 성공:', url)
} catch (err) {
console.error('❌ 복사 실패:', err)
}
}
</script>
<template>
<div
v-if="Object.keys(snsList).length > 0"
class="sns-container"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@click="handleMouseEnter"
>
<button class="btn-sns" :style="{ backgroundColor: snsBackgroundColor }">
<AtomsIconsShareLine class="icon-share" />
<span class="sr-only">sns</span>
</button>
<transition name="fade">
<div
v-if="showSnsList"
class="sns-list"
:style="{ backgroundColor: snsBackgroundColor }"
>
<template v-for="(item, key) in snsList" :key="key">
<a
v-if="item?.url"
:href="item?.url"
target="_blank"
class="sns-item"
:style="{
backgroundImage: `url(/images/common/ic-v2-logo-${key}-fill.svg)`,
}"
>
<span class="sr-only">{{ key }}</span>
</a>
</template>
<button
type="button"
class="sns-item"
:style="{
backgroundImage: `url(/images/common/ic-v2-community-link-line.svg)`,
}"
@click="handleCopy"
>
<span class="sr-only">copy</span>
</button>
<div class="close-container">
<button
type="button"
class="opacity-50 z-[1] hover:opacity-100"
@click="handleForceClose"
>
<span class="sr-only">close</span>
<AtomsIconsCloseLine size="24" />
</button>
</div>
</div>
</transition>
</div>
</template>
<style scoped>
.btn-sns {
@apply relative rounded-full flex items-center justify-center
w-[40px] h-[40px] md:w-[48px] md:h-[48px]
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.06)] before:rounded-full
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:rounded-full after:opacity-0 after:transition-all after:duration-300 after:ease-in-out;
}
.btn-sns:hover {
@apply after:opacity-10;
}
.btn-sns:hover .icon-share {
@apply fill-white;
}
.sns-list {
@apply absolute bottom-0 right-0 flex items-center justify-center gap-4 rounded-full
h-[40px] md:h-[48px] pl-4 pr-3
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.06)] before:rounded-full;
}
.sns-item {
@apply w-[24px] h-[24px] bg-center bg-cover bg-no-repeat opacity-50 z-[1]
hover:opacity-100;
}
.sns-item:hover {
@apply opacity-100;
}
.close-container {
@apply relative flex pl-4
before:content-[''] before:absolute before:top-1/2 before:left-0 before:w-[1px] before:h-[20px] before:bg-[rgba(255,255,255,0.1)] before:translate-y-[-50%];
}
</style>

View File

@@ -6,7 +6,7 @@ interface Props {
withDefaults(defineProps<Props>(), {
size: 20,
color: '#ffffff',
color: 'white',
})
</script>
@@ -15,14 +15,14 @@ withDefaults(defineProps<Props>(), {
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
viewBox="0 0 36 36"
viewBox="0 0 20 20"
:fill="color"
>
<path
d="M22.0265 6.77515C22.9737 5.57867 23.6003 3.90666 23.4254 2.25C22.0702 2.31136 20.4381 3.20105 19.4618 4.39753C18.5874 5.45595 17.8297 7.15864 18.0337 8.78462C19.5346 8.90734 21.0793 7.97163 22.0265 6.77515Z"
d="M12.237 3.76397C12.7632 3.09926 13.1113 2.17037 13.0142 1.25C12.2613 1.28409 11.3545 1.77836 10.8121 2.44307C10.3264 3.03109 9.90542 3.97702 10.0188 4.88035C10.8526 4.94852 11.7108 4.42868 12.237 3.76397Z"
/>
<path
d="M24.1784 9.38534L23.8464 9.35745C22.5604 9.25719 21.3837 9.62957 20.3897 10.0069L19.3814 10.3981C18.917 10.5722 18.5149 10.6972 18.1869 10.6972C17.8232 10.6972 17.3973 10.5679 16.9256 10.3916L15.7142 9.92036C14.9662 9.64193 14.1546 9.40031 13.3246 9.42125C10.8217 9.45315 8.526 10.8727 7.23469 13.1216C4.63612 17.6354 6.56512 24.3024 9.09991 27.9708L9.53965 28.5946L9.84388 29.0098L10.1578 29.4174C11.1678 30.689 12.3337 31.7497 13.755 31.703C14.523 31.6702 15.0938 31.4589 15.6575 31.2216L16.1434 31.0155C16.8008 30.7416 17.5202 30.4909 18.5855 30.4909C19.511 30.4909 20.1636 30.6964 20.7491 30.9348L21.4811 31.246C22.0469 31.481 22.6338 31.6749 23.4478 31.6552C25.1351 31.6284 26.305 30.3637 27.3637 28.8914L27.6635 28.466L28.1083 27.8128C28.254 27.5914 28.3903 27.3714 28.5175 27.1552L28.76 26.7285C28.7984 26.6584 28.8359 26.5889 28.8723 26.5201L29.0796 26.1159C29.1123 26.0501 29.144 25.9851 29.1747 25.921L29.3482 25.5483L29.4998 25.2015L29.6302 24.8848L29.7873 24.4749L29.9 24.155L30 23.8398L29.8843 23.7918L29.5934 23.6516L29.2859 23.4809L29.0483 23.3338L28.7907 23.1589C27.6392 22.3393 26.0767 20.7067 26.0464 17.8746C26.0241 15.2734 27.512 13.6173 28.4644 12.8256L28.6993 12.6386C28.736 12.6106 28.7714 12.5843 28.8052 12.5597L29.0641 12.3809L29.2348 12.2763C28.0657 10.5643 26.4786 9.86607 25.2717 9.57188L24.9218 9.49514L24.5996 9.43898L24.3097 9.39975C24.2644 9.39442 24.2206 9.38964 24.1784 9.38534Z"
d="M13.4325 5.21408L13.248 5.19858C12.5336 5.14288 11.8799 5.34976 11.3277 5.55942L10.7675 5.77674C10.5095 5.87346 10.2861 5.9429 10.1039 5.9429C9.90183 5.9429 9.66519 5.87108 9.40315 5.77314L8.73016 5.51131C8.31461 5.35663 7.86368 5.2224 7.40258 5.23403C6.01207 5.25175 4.73671 6.04037 4.01931 7.28977C2.57567 9.79743 3.64733 13.5013 5.05555 15.5393L5.29985 15.8859L5.46886 16.1166L5.64325 16.343C6.20438 17.0495 6.8521 17.6387 7.64171 17.6128C8.06839 17.5946 8.38546 17.4772 8.69864 17.3454L8.96859 17.2308C9.33383 17.0787 9.73346 16.9394 10.3253 16.9394C10.8395 16.9394 11.2021 17.0535 11.5273 17.186L11.934 17.3589C12.2483 17.4894 12.5744 17.5972 13.0266 17.5862C13.964 17.5713 14.6139 16.8687 15.2021 16.0508L15.3687 15.8144L15.6157 15.4515C15.6967 15.3285 15.7724 15.2063 15.8431 15.0862L15.9778 14.8492C15.9992 14.8102 16.02 14.7716 16.0402 14.7334L16.1554 14.5088C16.1735 14.4723 16.1911 14.4361 16.2082 14.4006L16.3046 14.1935L16.3888 14.0008L16.4613 13.8249L16.5485 13.5971L16.6111 13.4194L16.6667 13.2443L16.6024 13.2177L16.4408 13.1398L16.27 13.0449L16.138 12.9632L15.9949 12.8661C15.3552 12.4107 14.4871 11.5037 14.4702 9.93034C14.4579 8.4852 15.2845 7.56519 15.8136 7.12531L15.9441 7.02142C15.9645 7.00591 15.9841 6.9913 16.0029 6.97759L16.1467 6.87828L16.2416 6.82014C15.5921 5.86906 14.7104 5.48115 14.0399 5.31771L13.8455 5.27508L13.6665 5.24388L13.5054 5.22208C13.4803 5.21912 13.4559 5.21647 13.4325 5.21408Z"
/>
</svg>
</template>

View File

@@ -6,7 +6,7 @@ interface Props {
withDefaults(defineProps<Props>(), {
size: 24,
color: 'white',
color: 'rgba(255,255,255,0.5)',
})
</script>
@@ -16,13 +16,12 @@ withDefaults(defineProps<Props>(), {
:width="size"
:height="size"
viewBox="0 0 24 24"
fill="none"
:fill="color"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.75 2C14.6789 2 13 3.67893 13 5.75C13 6.04651 13.0344 6.33499 13.0995 6.61165C13.06 6.6295 13.0213 6.65017 12.9836 6.67375L8.98359 9.17375C8.93046 9.20696 8.88163 9.24445 8.83728 9.2855C8.16435 8.64391 7.25318 8.25 6.25 8.25C4.17893 8.25 2.5 9.92893 2.5 12C2.5 14.0711 4.17893 15.75 6.25 15.75C7.25317 15.75 8.16434 15.3561 8.83728 14.7145C8.88163 14.7556 8.93046 14.793 8.98359 14.8263L12.9836 17.3263C12.996 17.334 13.0086 17.3415 13.0212 17.3486C13.0072 17.4805 13 17.6144 13 17.75C13 19.8211 14.6789 21.5 16.75 21.5C18.8211 21.5 20.5 19.8211 20.5 17.75C20.5 15.6789 18.8211 14 16.75 14C15.521 14 14.43 14.5912 13.7461 15.5048L10.0164 13.1737C9.95959 13.1382 9.90054 13.1093 9.84012 13.0867C9.9441 12.7428 10 12.3779 10 12C10 11.6221 9.9441 11.2572 9.84012 10.9133C9.90054 10.8907 9.95959 10.8618 10.0164 10.8263L14.0164 8.32626L14.0218 8.32286C14.7056 9.04763 15.675 9.5 16.75 9.5C18.8211 9.5 20.5 7.82107 20.5 5.75C20.5 3.67893 18.8211 2 16.75 2ZM15 5.75C15 4.7835 15.7835 4 16.75 4C17.7165 4 18.5 4.7835 18.5 5.75C18.5 6.7165 17.7165 7.5 16.75 7.5C15.7835 7.5 15 6.7165 15 5.75ZM6.25 10.25C5.2835 10.25 4.5 11.0335 4.5 12C4.5 12.9665 5.2835 13.75 6.25 13.75C7.2165 13.75 8 12.9665 8 12C8 11.0335 7.2165 10.25 6.25 10.25ZM16.75 16C15.7835 16 15 16.7835 15 17.75C15 18.7165 15.7835 19.5 16.75 19.5C17.7165 19.5 18.5 18.7165 18.5 17.75C18.5 16.7835 17.7165 16 16.75 16Z"
:fill="color"
/>
</svg>
</template>

View File

@@ -72,7 +72,7 @@ const handleLinkClick = (title: string) => {
@apply absolute bottom-0 left-0 w-full pt-[14px] px-[18px] pb-[16px] flex flex-col justify-end border-t border-white/10 bg-black/40 shadow-[0_-10px_10px_0_rgba(0,0,0,0.25)] backdrop-blur-[25px] md:pt-[20px] text-left md:px-[26px] md:pb-[26px];
}
.card-title {
@apply text-[14px] leading-[20px] font-medium text-white md:text-[18px] md:leading-[26px];
@apply text-[14px] leading-[20px] font-medium text-white line-clamp-1 md:text-[18px] md:leading-[26px];
}
.card-description {
@apply mt-[6px] text-[12px] leading-[18px] text-white/50 md:mt-1 md:text-[14px] md:leading-[24px];

View File

@@ -22,15 +22,17 @@ const pinToParent = computed(() => {
</script>
<template>
<div :class="['utile-container', { 'is-fixed': pinToParent }]">
<AtomsButtonScrollTop v-if="props.isShowTopBtn" />
<AtomsButtonSns v-if="props.isShowSnsBtn" />
</div>
<ClientOnly>
<div :class="['utile-container', { 'is-fixed': pinToParent }]">
<AtomsButtonScrollTop v-if="props.isShowTopBtn" />
<AtomsButtonSns v-if="props.isShowSnsBtn" />
</div>
</ClientOnly>
</template>
<style scoped>
.utile-container {
@apply fixed flex flex-col
@apply fixed flex flex-col z-[100]
bottom-[12px] right-[12px] gap-2 md:bottom-[40px] md:right-[40px] md:gap-3;
}
.utile-container.is-fixed {

View File

@@ -6,6 +6,7 @@ import type { PageDataResourceGroups } from '#layers/types/api/pageData'
interface Props {
type?: ResponsiveOptions['type']
slideItemLength?: number
gap?: number
autoplay?: boolean
perPage?: number
drag?: boolean
@@ -16,6 +17,7 @@ interface Props {
}
const props = withDefaults(defineProps<Props>(), {
gap: 0,
autoplay: false,
perPage: 1,
drag: true,
@@ -39,6 +41,7 @@ const options = computed((): ResponsiveOptions => {
type: slideType.value,
autoWidth: true,
autoHeight: true,
gap: props.gap,
perPage: props.perPage,
speed: 500,
updateOnMove: true,

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { onClickOutside, useWindowSize } from '@vueuse/core'
import { useGameDataStore } from '#layers/stores/useGameDataStore'
import { useScrollStore } from '#layers/stores/useScrollStore'
import { onClickOutside, useWindowSize } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import type {
GameDataValue,
GameDataMenu,
GameDataMenuChildren,
GameDataResourceGroup,
GameDataResourceGroupSet,
} from '#layers/types/api/gameData'
const route = useRoute()
@@ -27,6 +28,12 @@ const startWidth = ref(0)
const officialItemWidths = ref<number[]>([])
const overflowNam = ref<number>(0)
const gnb1depthButtonData = computed(
() => gnbData?.buttons[0]?.button_json as GameDataResourceGroup
)
const gnb2depthButtonData = computed(
() => gnbData?.buttons[1]?.button_json as GameDataResourceGroupSet
)
const currentPath = computed(() => formatPathWithoutLocale(route.path))
const pathMatches = (base: string, current: string) => {
@@ -134,6 +141,10 @@ const stopClickOutside = onClickOutside(
() => (isMenuOpen.value = false)
)
const has2depthButton = (gnbItem: GameDataMenu) => {
return gnbItem.children && Object.keys(gnbItem.children).length > 0
}
// 화면 크기 변경 시 오버플로우 재계산
watch(width, () => {
calculateOverflow()
@@ -165,7 +176,7 @@ onBeforeUnmount(() => {
<div class="game-wrapper" :class="{ 'is-fixed': isPassedStoveGnb }">
<AtomsLocaleLink to="/brand" class="mx-auto md:hidden">
<img
:src="gnbData?.bi_path"
:src="getResolvedHost(gnbData?.bi_path)"
:alt="gameData?.game_name"
class="h-[30px]"
/>
@@ -182,7 +193,7 @@ onBeforeUnmount(() => {
<div class="nav-logo">
<AtomsLocaleLink to="/brand" @click="handleMenuClose">
<img
:src="gnbData?.bi_path"
:src="getResolvedHost(gnbData?.bi_path)"
:alt="gameData?.game_name"
class="h-[30px]"
/>
@@ -208,12 +219,12 @@ onBeforeUnmount(() => {
>
<span>{{ gnbItem.menu_name }}</span>
<AtomsIconsArrowDownFill
v-if="gnbItem.children"
v-if="has2depthButton(gnbItem)"
class="hidden md:block"
/>
</AtomsLocaleLink>
<Transition name="fade">
<div v-if="gnbItem.children" class="nav-2depth">
<div v-if="has2depthButton(gnbItem)" class="nav-2depth">
<ul>
<li
v-for="child in gnbItem.children"
@@ -296,14 +307,34 @@ onBeforeUnmount(() => {
</div>
</nav>
<div ref="startRef" class="btn-start">
<BlocksButtonDownload
<AtomsButtonLauncher
type="custom"
platform="pc"
class="w-full md:w-auto"
style="font-size: 16px"
:background-color="
getColorCode({
colorName: gnb1depthButtonData?.btn_info?.color_name_btn,
colorCode: gnb1depthButtonData?.btn_info?.color_code_btn,
})
"
:text-color="
getColorCode({
colorName: gnb1depthButtonData?.btn_info?.color_name_txt,
colorCode: gnb1depthButtonData?.btn_info?.color_code_txt,
})
"
class="nav-1depth"
>
게임 시작
</BlocksButtonDownload>
{{ gnb1depthButtonData?.btn_info?.txt_btn_name }}
</AtomsButtonLauncher>
<div v-if="gnb2depthButtonData" class="nav-2depth">
<ul>
<li v-for="(item, key) in gnb2depthButtonData" :key="key">
<AtomsButtonLauncher type="custom" :platform="key">
{{ item.btn_info?.txt_btn_name }}
</AtomsButtonLauncher>
</li>
</ul>
</div>
</div>
<button class="btn-close" @click="handleMenuClose">
<AtomsIconsMenuCloseLine class="mx-auto" />
@@ -340,9 +371,6 @@ onBeforeUnmount(() => {
.btn-close {
@apply top-[11px] left-[12px];
}
.btn-start {
@apply relative mt-2 px-5 md:absolute md:right-0 md:mt-0 md:px-0;
}
.gnb-game {
@apply absolute top-0 left-0 w-0 md:relative md:w-full md:!h-full;
@@ -445,4 +473,36 @@ onBeforeUnmount(() => {
.is-hidden {
@apply hidden;
}
.btn-start {
@apply relative mt-2 px-5 md:absolute md:top-[0] md:right-0 md:flex md:items-center md:h-full md:mt-0 md:px-0;
}
.btn-start:hover .nav-2depth {
@apply md:block;
}
.btn-start:deep(.nav-1depth) {
@apply w-full h-[48px] px-10 md:w-auto;
}
.btn-start:deep(.nav-1depth) .icon-platform {
@apply hidden;
}
.btn-start:deep(.nav-1depth) .btn-content {
@apply justify-center text-center;
}
.btn-start .nav-2depth {
@apply left-[unset] right-[-40px];
}
.btn-start .nav-2depth:deep(.btn-base) {
@apply w-full h-[48px] px-4 bg-transparent before:hidden after:hidden
hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10;
}
.btn-start .nav-2depth:deep(.btn-base) .text {
@apply ml-1.5 text-[15px] text-theme-foreground-reversal;
}
[data-theme='light'] {
.btn-start .nav-2depth:deep(.btn-base) .icon-platform {
@apply fill-[#1F1F1F];
}
}
</style>

View File

@@ -59,7 +59,6 @@ watchEffect(() => {
setupSeoMeta(props.pageData?.meta_tag_json)
}
})
</script>
<template>
@@ -84,6 +83,12 @@ watchEffect(() => {
<style scoped>
.main {
@apply relative pt-[48px] md:pt-[64px];
@apply relative min-h-[200px] pt-[48px] md:min-h-[300px] md:pt-[64px];
}
[data-theme='light'] {
.main {
@apply bg-theme-foreground;
}
}
</style>

View File

@@ -5,73 +5,74 @@ import type {
} from '#layers/types/api/pageData'
import type { ButtonType } from '#layers/types/components/button'
interface ButtonListProps {
interface Props {
resourcesData: PageDataResourceGroup[]
pageVerTmplSeq: number
}
const props = defineProps<ButtonListProps>()
const props = defineProps<Props>()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const BUTTON_TYPE_MAP = {
URL: {
_self: 'internal' as const,
_blank: 'external' as const,
},
DOWNLOAD: 'download' as const,
} as const
const DEFAULT_BUTTON_TYPE: ButtonType = 'action'
const getButtonType = (btnInfo: PageDataResourceGroupBtnInfo): ButtonType => {
const btnType = btnInfo?.detail?.btn_type
const btnTarget = btnInfo?.detail?.action?.link_target
if (btnType === 'URL' && btnTarget) {
return BUTTON_TYPE_MAP.URL[btnTarget] || DEFAULT_BUTTON_TYPE
}
if (btnType === 'DOWNLOAD') {
return BUTTON_TYPE_MAP.DOWNLOAD
}
return DEFAULT_BUTTON_TYPE
const getBtnType = (item?: PageDataResourceGroupBtnInfo): ButtonType => {
const t = item?.detail?.btn_type
const target = item?.detail?.action?.link_target
if (t === 'URL' && target)
return target === '_blank' ? 'external' : 'internal'
if (t === 'DOWNLOAD') return 'download'
return 'action'
}
const handleSendLog = (index: number) => {
const getBgColor = (item?: PageDataResourceGroupBtnInfo): string =>
getColorCode({
colorName: item?.color_name_btn,
colorCode: item?.color_code_btn,
})
const getTextColor = (item?: PageDataResourceGroupBtnInfo): string =>
getColorCode({
colorName: item?.color_name_txt,
colorCode: item?.color_code_txt,
})
const handleLogClick = (index: number) => {
sendLog(
locale.value,
useAnalyticsLogDataDirect(props.resourcesData[index], props.pageVerTmplSeq)
)
}
// 편의상
const buttonList = computed(() => props.resourcesData || [])
</script>
<template>
<div
v-if="props.resourcesData?.length"
v-if="buttonList.length"
class="flex flex-wrap justify-center gap-3 md:gap-4"
>
<template v-for="(button, index) in props.resourcesData" :key="index">
<template v-for="(button, index) in buttonList" :key="index">
<AtomsButtonLauncher
v-if="button.btn_info?.detail?.btn_type === 'RUN'"
type="duplication"
:platform="button.btn_info?.detail?.market_type"
:background-color="getBgColor(button.btn_info)"
:text-color="getTextColor(button.btn_info)"
:disabled="button?.btn_info?.disabled"
@click="handleLogClick(index)"
>
{{ button.btn_info?.txt_btn_name }}
</AtomsButtonLauncher>
<AtomsButton
:type="getButtonType(button.btn_info)"
:target="button.btn_info?.detail?.action?.link_target"
v-else
:type="getBtnType(button.btn_info)"
:href="button.btn_info?.detail?.action?.url"
:target="button.btn_info?.detail?.action?.link_target"
:rel="button.btn_info?.detail?.action?.rel"
:background-color="
getColorCode({
colorName: button.btn_info?.color_name_btn,
colorCode: button.btn_info?.color_code_btn,
})
"
:text-color="
getColorCode({
colorName: button.btn_info?.color_name_txt,
colorCode: button.btn_info?.color_code_txt,
})
"
:disabled="button.btn_info?.disabled"
@click="handleSendLog(index)"
:background-color="getBgColor(button.btn_info)"
:text-color="getTextColor(button.btn_info)"
:disabled="button?.btn_info?.disabled"
@click="handleLogClick(index)"
>
{{ button.btn_info?.txt_btn_name }}
</AtomsButton>

View File

@@ -4,7 +4,7 @@ const { isShowCheckLauncher, isShowDownloadLauncher, downloadLauncher } =
</script>
<template>
<AtomsModalLayer
<BlocksModalLayer
v-model:is-open="isShowCheckLauncher"
:is-show-dimmed="true"
:is-outside-close="false"
@@ -45,7 +45,7 @@ const { isShowCheckLauncher, isShowDownloadLauncher, downloadLauncher } =
></p>
</div>
</Transition>
</AtomsModalLayer>
</BlocksModalLayer>
</template>
<style scoped>

View File

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 348 B

View File

Before

Width:  |  Height:  |  Size: 500 B

After

Width:  |  Height:  |  Size: 500 B

View File

Before

Width:  |  Height:  |  Size: 373 B

After

Width:  |  Height:  |  Size: 373 B

View File

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -14,19 +14,12 @@ const props = defineProps<Props>()
const gameDataStore = useGameDataStore()
const pageDataStore = usePageDataStore()
const { getCwmsArticleData } = useResourcesData()
const { locale } = useI18n()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const { locale } = useI18n()
const { gameData } = storeToRefs(gameDataStore)
const { pageData } = storeToRefs(pageDataStore)
const defaultSlideItem = {
article_id: '',
media_thumbnail_url: '',
title: '',
create_datetime: 0,
}
const boardId = computed(
() => getComponentGroup(props.components, 'boardId')?.display?.text
)
@@ -65,28 +58,33 @@ const { data: slideData } = await useAsyncData(
server: false,
}
)
const getResponsiveMultiple = (): number => {
const breakpoints = useResponsiveBreakpoints()
if (breakpoints.value.isSm) return 1 // sm: 1개 단위
if (breakpoints.value.isMd) return 2 // md: 2개 단위
return 4 // 그 외: 4개 단위
}
const fillToMultipleOfFour = (arr: CwmsArticleItem[]): CwmsArticleItem[] => {
const remainderSize = getResponsiveMultiple()
const remainder = arr.length % remainderSize
const fillCount = remainder === 0 ? 0 : remainderSize - remainder
return [
...arr,
...Array(fillCount).fill({ ...defaultSlideItem } as CwmsArticleItem),
]
}
const filledSlideData = computed(() => {
return fillToMultipleOfFour(slideData.value)
})
const slideLength = computed(() => slideData.value.length)
const slideClass = computed(() => ({
'is-one-item': slideLength.value === 1,
'is-two-items': slideLength.value === 2,
'is-three-items': slideLength.value === 3,
'is-four-items': slideLength.value === 4,
}))
const splideOptions = computed(() => ({
gap: 20,
perPage: 4,
drag: false,
arrows: slideLength.value > 4,
pagination: slideLength.value > 4,
breakpoints: {
[BREAKPOINTS.lg - 1]: {
perPage: 2,
arrows: slideLength.value > 2,
pagination: slideLength.value > 2,
},
[BREAKPOINTS.md - 1]: {
drag: true,
perPage: 1,
arrows: false,
pagination: false,
},
},
}))
const getArticleUrl = (articleId: string) => {
const communityUrl = gameData.value?.url_json?.community
@@ -105,7 +103,7 @@ const onArrowClick = (direction, targetIndex) => {
<template>
<section
class="relative py-[80px] md:py-[120px]"
class="relative min-h-[483px] py-[80px] md:min-h-[684px] md:py-[120px]"
:style="getPaginationClass(paginationData)"
>
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
@@ -117,37 +115,24 @@ const onArrowClick = (direction, targetIndex) => {
/>
<ClientOnly>
<BlocksSlideDefault
v-if="slideData.length > 0"
:slide-item-length="slideData.length"
:per-page="4"
:drag="false"
:arrows="slideData.length > 4"
:pagination="slideData.length > 4"
:breakpoints="{
[BREAKPOINTS.lg - 1]: {
perPage: 2,
arrows: slideData.length > 2,
pagination: slideData.length > 2,
},
[BREAKPOINTS.md - 1]: {
drag: true,
perPage: 1,
arrows: false,
pagination: false,
},
}"
:class="{ 'is-single': slideData.length === 1 }"
v-if="slideLength > 0"
:slide-item-length="slideLength"
v-bind="splideOptions"
:class="slideClass"
@arrow-click="onArrowClick"
>
<SplideSlide v-for="(item, index) in filledSlideData" :key="index">
<SplideSlide
v-for="(item, index) in slideData"
:key="item.article_id || `real-${index}`"
>
<div class="slide-inner">
<BlocksCardNews
:title="item.title"
:description="
formatTimestamp(item?.create_datetime, 'YYYY.MM.DD')
formatTimestamp(item.create_datetime, 'YYYY.MM.DD')
"
:img-path="getResolvedHost(item?.media_thumbnail_url)"
:url="getArticleUrl(item?.article_id)"
:img-path="getResolvedHost(item.media_thumbnail_url)"
:url="getArticleUrl(item.article_id)"
link-target="_blank"
/>
</div>
@@ -182,17 +167,24 @@ const onArrowClick = (direction, targetIndex) => {
.splide:deep(.arrow-next) {
@apply top-[calc(50%-28px)] right-[0];
}
.splide__slide {
@apply mr-[20px];
}
.slide-inner {
@apply w-[275px] aspect-[1/1] md:w-[306px] md:box-border;
}
.splide.is-single {
@apply flex justify-center w-auto;
.splide.is-one-item:deep(.splide__track) {
@apply max-w-[315px] sm:max-w-[355px] mx-auto md:max-w-[306px];
}
.splide.is-single:deep(.splide__track) {
@apply w-[calc(100%-20px)] md:w-auto;
.splide.is-two-items:deep(.splide__track) {
@apply lg:max-w-[632px];
}
.splide.is-two-items:deep(.splide__list) {
@apply md:!translate-x-0;
}
.splide.is-three-items:deep(.splide__track) {
@apply lg:max-w-[958px];
}
.splide.is-three-items:deep(.splide__list),
.splide.is-four-items:deep(.splide__list) {
@apply lg:!translate-x-0;
}
</style>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { getComponentGroup } from '#layers/utils/dataUtil'
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
import AtomsImg from '#layers/components/atoms/Img.vue'
interface Props {
components: PageDataTemplateComponents
@@ -71,7 +70,7 @@ const buttonListData = computed(() => {
<style scoped>
.img-container {
@apply flex flex-col items-center justify-center gap-4 box-content mx-auto mt-[32px]
w-[334px]
w-[440px]
md:w-[944px];
}
.img-item {

View File

@@ -158,6 +158,14 @@ const onArrowClick = (direction, targetIndex) => {
.thumbnail-carousel:deep(.main-splide) {
@apply overflow-hidden rounded-lg border border-white/10 shadow-[0_4px_20px_0_rgba(0,0,0,0.5)];
}
.thumbnail-carousel:deep(.thumbnail-slide) {
@apply opacity-50;
}
.thumbnail-carousel:deep(.thumbnail-slide:hover),
.thumbnail-carousel:deep(.thumbnail-slide.is-active) {
@apply opacity-100;
}
.main-slide {
@apply relative aspect-[16/9];
}

View File

@@ -59,33 +59,12 @@ const handleSendLog = (index: number) => {
:resources-data="videoPlayData"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
/>
<div
v-if="buttonListData.length > 0"
class="flex flex-wrap justify-center gap-3 mt-[22px] md:gap-4 md:mt-[52px]"
>
<BlocksButtonDownload
v-for="(button, index) in buttonListData"
:key="index"
type="duplication"
:platform="button.btn_info?.detail?.market_type"
:background-color="
getColorCode({
colorName: button.btn_info?.color_name_btn,
colorCode: button.btn_info?.color_code_btn,
})
"
:text-color="
getColorCode({
colorName: button.btn_info?.color_name_txt,
colorCode: button.btn_info?.color_code_txt,
})
"
:disabled="button.btn_info?.disabled"
@click="handleSendLog(index)"
>
{{ button.btn_info?.txt_btn_name }}
</BlocksButtonDownload>
</div>
<WidgetsButtonList
v-if="buttonListData"
:resources-data="buttonListData"
:page-ver-tmpl-seq="props.pageVerTmplSeq"
class="mt-[22px] md:mt-[52px]"
/>
</div>
</section>
</template>

View File

@@ -24,7 +24,12 @@ export interface GameDataValue {
lang_codes: string[]
key_color_json: GameDataKeyColors
use_game_font: boolean
comm_sns_bg_color_json: string
comm_sns_bg_color_json: {
display: {
color_code: string
color_name: string
}
}
comm_multilang_filename: string
footer_dev_ci_img_yn: boolean
footer_dev_ci_img_path: string
@@ -121,6 +126,16 @@ 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
disabled: boolean
txt_btn_name: string
detail: Record<string, any>
}
// 트래킹 타입
export interface GameDataTracking {
viewType: string
@@ -128,10 +143,24 @@ export interface GameDataTracking {
clickSarea: string
}
export interface GameDataResourceGroup {
btn_info?: GameDataResourceGroupBtnInfo
tracking: GameDataTracking
}
type MarketPlatform = 'pc' | 'app_store' | 'google_play'
export type GameDataResourceGroupSet = Partial<
Record<MarketPlatform, GameDataResourceGroup>
>
export type GameDataButtonJson =
| GameDataResourceGroup
| GameDataResourceGroupSet
// 버튼 타입
export interface GameDataButton {
depth_type: number
button_json: string // JSON 문자열로 변경
depth_type: 1 | 2
button_json: GameDataButtonJson
}
export type GameDataMenuChildren = Record<string, GameDataMenu>

View File

Before

Width:  |  Height:  |  Size: 702 KiB

After

Width:  |  Height:  |  Size: 702 KiB

View File

Before

Width:  |  Height:  |  Size: 702 KiB

After

Width:  |  Height:  |  Size: 702 KiB

View File

Before

Width:  |  Height:  |  Size: 702 KiB

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

Before

Width:  |  Height:  |  Size: 794 KiB

After

Width:  |  Height:  |  Size: 794 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

Before

Width:  |  Height:  |  Size: 794 KiB

After

Width:  |  Height:  |  Size: 794 KiB