619 lines
20 KiB
Vue
619 lines
20 KiB
Vue
<script setup lang="ts">
|
|
import { useThrottleFn } from '@vueuse/core'
|
|
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
|
import type {
|
|
GameDataMenu,
|
|
GameDataMenuChildren,
|
|
GameDataResourceGroup,
|
|
GameDataResourceGroupSet,
|
|
} from '#layers/types/api/gameData'
|
|
import type { TrackingObject } from '#layers/types/api/common'
|
|
|
|
const MORE_WIDTH = 72
|
|
const START_MARGIN = 40
|
|
|
|
const route = useRoute()
|
|
const { locale, tm } = useI18n()
|
|
const { width } = useWindowSize()
|
|
const gameDataStore = useGameDataStore()
|
|
const pageDataStore = usePageDataStore()
|
|
const scrollStore = useScrollStore()
|
|
const breakpoints = useResponsiveBreakpoints()
|
|
const { sendLog } = useAnalytics()
|
|
|
|
const { gameId, gameName, imgJson, gnb, eventBanner } =
|
|
storeToRefs(gameDataStore)
|
|
const { pageLayoutType } = storeToRefs(pageDataStore)
|
|
const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
|
|
|
const navAreaRef = ref<HTMLElement | null>(null)
|
|
const startRef = ref<HTMLElement | null>(null)
|
|
|
|
const { width: startWidth } = useElementSize(startRef)
|
|
|
|
const isMounted = ref(false)
|
|
const isMenuOpen = ref(false)
|
|
const navWidth = ref(0)
|
|
const officialItemWidths = ref<number[]>([])
|
|
const overflowCount = ref<number>(0)
|
|
|
|
const biSmallPath = computed(() => imgJson.value?.bi_small)
|
|
const gnbMenusCount = computed(() => {
|
|
const menus = gnb.value?.menus
|
|
if (!menus || typeof menus !== 'object') return 0
|
|
return Object.keys(menus).length
|
|
})
|
|
const currentPath = computed(() => formatPathWithoutLocale(route.path))
|
|
const start1depthData = computed(
|
|
() => gnb.value?.buttons[0]?.button_json as GameDataResourceGroup
|
|
)
|
|
const start2depthData = computed(
|
|
() => gnb.value?.buttons[1]?.button_json as GameDataResourceGroupSet
|
|
)
|
|
|
|
// 자식 중 활성 링크 존재 여부 확인
|
|
const hasActiveChild = (children?: GameDataMenuChildren) => {
|
|
return formatToArray(children).some(child => {
|
|
if (!child?.url_path || child.click_action_type === 2) return false
|
|
return formatPathWithoutLocale(child.url_path) === currentPath.value
|
|
})
|
|
}
|
|
|
|
// navAreaRef의 넓이를 구하는 함수
|
|
const calculateNavWidth = () => {
|
|
if (!import.meta.client) return
|
|
if (!navAreaRef.value || !gnb.value) return 0
|
|
|
|
const navAreaWidth = navAreaRef.value.offsetWidth
|
|
navWidth.value = navAreaWidth + MORE_WIDTH
|
|
}
|
|
|
|
// official 자식들의 넓이를 구하는 함수
|
|
const calculateOfficialItemWidths = () => {
|
|
if (!import.meta.client) return
|
|
if (!navAreaRef.value) return
|
|
|
|
const officialItems = navAreaRef.value.querySelectorAll('.official .nav-item')
|
|
const widths: number[] = []
|
|
|
|
// 뒤에서부터 순서대로 넓이 계산
|
|
for (let i = officialItems.length - 1; i >= 0; i--) {
|
|
const item = officialItems[i] as HTMLElement
|
|
if (item) {
|
|
widths.push(item.offsetWidth)
|
|
}
|
|
}
|
|
|
|
officialItemWidths.value = widths
|
|
}
|
|
|
|
// 오버플로우 계산 함수
|
|
const calculateOverflow = () => {
|
|
if (!import.meta.client) return
|
|
if (!navAreaRef.value || !startRef.value) return
|
|
|
|
if (breakpoints.value.isMobile) {
|
|
overflowCount.value = 0
|
|
return
|
|
}
|
|
|
|
const screenWidth = width.value
|
|
const totalNavWidth = navWidth.value + startWidth.value + START_MARGIN
|
|
|
|
// 해상도가 navWidth + startWidth보다 작은 경우
|
|
if (screenWidth < totalNavWidth) {
|
|
let removedCount = 0
|
|
let currentTotal = totalNavWidth
|
|
|
|
for (let i = 0; i < officialItemWidths.value.length; i++) {
|
|
currentTotal -= officialItemWidths.value[i]
|
|
removedCount++
|
|
|
|
if (currentTotal <= screenWidth) {
|
|
break
|
|
}
|
|
}
|
|
|
|
overflowCount.value = removedCount
|
|
} else {
|
|
overflowCount.value = 0
|
|
}
|
|
}
|
|
|
|
// 100ms마다 최대 1회 실행
|
|
const throttledCalculateOverflow = useThrottleFn(calculateOverflow, 100)
|
|
|
|
const isNotClickable = (gnbItem: GameDataMenu) => {
|
|
return gnbItem.click_action_type === 0
|
|
}
|
|
|
|
const has2depthButton = (gnbItem: GameDataMenu) => {
|
|
return gnbItem.children && Object.keys(gnbItem.children).length > 0
|
|
}
|
|
|
|
const handleMenuOpen = () => {
|
|
isMenuOpen.value = true
|
|
scrollStore.controlScrollLock(true)
|
|
}
|
|
|
|
const handleMenuClose = () => {
|
|
isMenuOpen.value = false
|
|
scrollStore.controlScrollLock(false)
|
|
}
|
|
|
|
const handleSendLog = (item: string | TrackingObject) => {
|
|
const analytics: TrackingObject =
|
|
typeof item === 'string'
|
|
? {
|
|
action_type: 'click',
|
|
click_item: item,
|
|
click_sarea: 'GNB',
|
|
click_area: gameId.value,
|
|
}
|
|
: { ...item, click_area: gameId.value }
|
|
sendLog(locale.value, analytics)
|
|
}
|
|
|
|
const handleGnbItemClick = (gnbItem: GameDataMenu) => {
|
|
if (isNotClickable(gnbItem)) return
|
|
|
|
handleMenuClose()
|
|
handleSendLog(gnbItem.tracking_json)
|
|
}
|
|
|
|
onMounted(() => {
|
|
overflowCount.value = 0
|
|
isMounted.value = true
|
|
|
|
// 초기 계산
|
|
nextTick(() => {
|
|
calculateNavWidth()
|
|
calculateOfficialItemWidths()
|
|
calculateOverflow()
|
|
})
|
|
|
|
watchEffect(() => {
|
|
if (!startWidth.value) return
|
|
throttledCalculateOverflow()
|
|
})
|
|
|
|
// 화면 크기 변경 시 오버플로우 재계산
|
|
watch(width, () => {
|
|
throttledCalculateOverflow()
|
|
if (isMenuOpen.value && breakpoints.value.isDesktop) {
|
|
handleMenuClose()
|
|
}
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<header :class="['header', { 'empty-game': !gnb }]">
|
|
<BlocksStoveGnb class="h-[48px]" />
|
|
<div v-if="gnb" :class="['game-wrap', { 'is-fixed': isPassedStoveGnb }]">
|
|
<AtomsLocaleLink
|
|
to="/home"
|
|
class="mx-auto md:hidden"
|
|
@click="handleSendLog('BI')"
|
|
>
|
|
<img
|
|
:src="formatPathHost(biSmallPath)"
|
|
:alt="gameName"
|
|
class="h-[30px]"
|
|
/>
|
|
</AtomsLocaleLink>
|
|
<button class="btn-open" @click="handleMenuOpen">
|
|
<AtomsIconsMenuBoldLine class="mx-auto" />
|
|
<span class="sr-only">menu open</span>
|
|
</button>
|
|
<div
|
|
:class="['nav-wrap', { 'is-open': isMenuOpen }]"
|
|
@click="handleMenuClose"
|
|
>
|
|
<div ref="navAreaRef" class="nav-area" @click.stop>
|
|
<div class="nav-logo">
|
|
<AtomsLocaleLink
|
|
to="/home"
|
|
@click="[handleMenuClose(), handleSendLog('BI')]"
|
|
>
|
|
<img
|
|
:src="formatPathHost(biSmallPath)"
|
|
:alt="gameName"
|
|
class="h-[30px]"
|
|
/>
|
|
</AtomsLocaleLink>
|
|
</div>
|
|
<nav :class="['nav-list', { 'is-mounted': isMounted }]">
|
|
<template v-if="gnbMenusCount > 0">
|
|
<div class="official custom-theme-scrollbar">
|
|
<div
|
|
v-for="(gnbItem, key) in gnb?.menus"
|
|
:key="key"
|
|
class="nav-item group"
|
|
:class="{
|
|
'is-hidden':
|
|
breakpoints.isDesktop &&
|
|
overflowCount > 0 &&
|
|
Number(key) > gnbMenusCount - overflowCount,
|
|
}"
|
|
>
|
|
<component
|
|
:is="isNotClickable(gnbItem) ? 'span' : 'AtomsLocaleLink'"
|
|
:to="gnbItem?.url_path"
|
|
:target="gnbItem?.link_target"
|
|
:class="[
|
|
'nav-1depth',
|
|
{
|
|
'router-link-active': hasActiveChild(gnbItem.children),
|
|
},
|
|
]"
|
|
@click="handleGnbItemClick(gnbItem)"
|
|
>
|
|
<span>{{ gnbItem.menu_name }}</span>
|
|
<AtomsIconsWebLinkLine
|
|
v-if="gnbItem?.link_target === '_blank'"
|
|
/>
|
|
<AtomsIconsArrowDownFill
|
|
v-if="has2depthButton(gnbItem)"
|
|
class="hidden md:block"
|
|
/>
|
|
<AtomsIconsArrowRightLine
|
|
v-if="!has2depthButton(gnbItem)"
|
|
class="ml-auto md:hidden"
|
|
/>
|
|
</component>
|
|
<Transition name="fade">
|
|
<div v-if="has2depthButton(gnbItem)" class="nav-2depth">
|
|
<ul>
|
|
<li
|
|
v-for="child in gnbItem.children"
|
|
:key="child.menu_name"
|
|
>
|
|
<component
|
|
:is="
|
|
isNotClickable(child) ? 'span' : 'AtomsLocaleLink'
|
|
"
|
|
:to="child.url_path"
|
|
:target="child.link_target"
|
|
class="item-link"
|
|
@click="handleGnbItemClick(child)"
|
|
>
|
|
<span>{{ child.menu_name }}</span>
|
|
<AtomsIconsWebLinkLine
|
|
v-if="child?.link_target === '_blank'"
|
|
/>
|
|
</component>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</div>
|
|
<div v-if="overflowCount > 0" class="more group">
|
|
<button class="btn-more">
|
|
<AtomsIconsOptionHorizontalFill class="mx-auto" />
|
|
<span class="sr-only">more</span>
|
|
</button>
|
|
<div class="more-list">
|
|
<div class="list-inner">
|
|
<div
|
|
v-for="(gnbItem, key) in gnb?.menus"
|
|
:key="key"
|
|
:class="{
|
|
'is-hidden':
|
|
breakpoints.isDesktop &&
|
|
overflowCount > 0 &&
|
|
Number(key) <= gnbMenusCount - overflowCount,
|
|
}"
|
|
>
|
|
<component
|
|
:is="
|
|
isNotClickable(gnbItem) ? 'span' : 'AtomsLocaleLink'
|
|
"
|
|
:to="gnbItem?.url_path"
|
|
:target="gnbItem?.link_target"
|
|
:class="[
|
|
'nav-1depth',
|
|
'item-link',
|
|
{
|
|
'router-link-active': hasActiveChild(
|
|
gnbItem.children
|
|
),
|
|
},
|
|
]"
|
|
@click="handleGnbItemClick(gnbItem)"
|
|
>
|
|
<span>{{ gnbItem.menu_name }}</span>
|
|
</component>
|
|
<div v-if="gnbItem?.children">
|
|
<ul>
|
|
<li
|
|
v-for="child in gnbItem.children"
|
|
:key="child.menu_name"
|
|
>
|
|
<component
|
|
:is="
|
|
isNotClickable(child)
|
|
? 'span'
|
|
: 'AtomsLocaleLink'
|
|
"
|
|
:to="child?.url_path"
|
|
:target="child?.link_target"
|
|
class="item-link"
|
|
>
|
|
<span>{{ child.menu_name }}</span>
|
|
<AtomsIconsWebLinkLine
|
|
v-if="child?.link_target === '_blank'"
|
|
/>
|
|
</component>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<ClientOnly>
|
|
<div v-if="eventBanner" class="event">
|
|
<div class="nav-item">
|
|
<AtomsLocaleLink
|
|
:to="eventBanner?.page_url"
|
|
:target="eventBanner?.link_type === 1 ? '_self' : '_blank'"
|
|
:class="[
|
|
'nav-1depth',
|
|
{ 'router-link-active': pageLayoutType === 'promotion' },
|
|
]"
|
|
@click="[handleMenuClose(), handleSendLog('이벤트')]"
|
|
>
|
|
<span
|
|
class="flex items-center gap-1 flex-1 text-gradient-pink"
|
|
>
|
|
<AtomsIconsStarFill />
|
|
<span>{{ tm('Gnb_Event') }}</span>
|
|
<AtomsIconsStarFill />
|
|
<AtomsIconsArrowRightLine class="ml-auto md:hidden" />
|
|
</span>
|
|
</AtomsLocaleLink>
|
|
</div>
|
|
</div>
|
|
</ClientOnly>
|
|
</nav>
|
|
<ClientOnly>
|
|
<div ref="startRef" class="btn-start">
|
|
<template v-if="start1depthData">
|
|
<BlocksButtonLauncher
|
|
platform="pc"
|
|
variant="custom"
|
|
:background-color="
|
|
getColorCodeFromData(start1depthData?.btn_info, 'btn')
|
|
"
|
|
:text-color="
|
|
getColorCodeFromData(start1depthData?.btn_info, 'txt')
|
|
"
|
|
:use-game-font="
|
|
start1depthData?.btn_info?.use_game_font === 1
|
|
"
|
|
@click="handleSendLog(start1depthData.tracking)"
|
|
>
|
|
{{ start1depthData?.btn_info?.txt_btn_name }}
|
|
</BlocksButtonLauncher>
|
|
<div
|
|
v-if="breakpoints.isDesktop && start2depthData"
|
|
class="nav-2depth"
|
|
>
|
|
<ul>
|
|
<li v-for="(item, key) in start2depthData" :key="key">
|
|
<BlocksButtonLauncher
|
|
type="single"
|
|
variant="custom"
|
|
:platform="key"
|
|
@click="handleSendLog(item.tracking)"
|
|
>
|
|
{{ item.btn_info?.txt_btn_name }}
|
|
</BlocksButtonLauncher>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</ClientOnly>
|
|
<button class="btn-close" @click="handleMenuClose">
|
|
<AtomsIconsCloseLine
|
|
size="24"
|
|
color="var(--foreground-reversal)"
|
|
class="mx-auto"
|
|
/>
|
|
<span class="sr-only">menu close</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.header {
|
|
@apply bg-theme-foreground text-theme-foreground-reversal relative font-[500] tracking-[-0.48px] z-[200];
|
|
}
|
|
.game-wrap {
|
|
@apply absolute flex w-full h-[48px] items-center whitespace-nowrap px-[52px] bg-theme-foreground sm:px-[72px] md:h-16 md:pl-0 md:pr-[40px]
|
|
before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-px before:bg-theme-foreground-reversal-6;
|
|
}
|
|
.game-wrap.is-fixed {
|
|
@apply fixed top-0;
|
|
}
|
|
|
|
.game-logo {
|
|
@apply mx-auto shrink-0 md:mx-0;
|
|
}
|
|
|
|
.btn-open,
|
|
.btn-close {
|
|
@apply absolute w-[40px] h-[40px] md:hidden;
|
|
}
|
|
.btn-open {
|
|
@apply top-[4px] left-[12px] sm:left-[32px];
|
|
}
|
|
.btn-close {
|
|
@apply top-[11px] left-[12px];
|
|
}
|
|
|
|
.nav-wrap {
|
|
@apply fixed top-0 left-0 bottom-0 w-0 mt-[var(--scroll-stove-position,48px)] md:relative md:w-full md:h-full md:mt-0;
|
|
}
|
|
.nav-wrap.is-open {
|
|
@apply w-full;
|
|
}
|
|
.nav-wrap.is-open::before {
|
|
@apply content-[''] absolute inset-0 w-[100vw] h-full bg-[rgba(0,0,0,0.6)] md:hidden;
|
|
}
|
|
.nav-wrap.is-open .nav-area {
|
|
@apply h-full translate-x-0 transition-transform duration-300 md:transform-none;
|
|
}
|
|
|
|
.nav-area {
|
|
@apply flex flex-col w-[100vw] max-w-[360px] min-w-[320px] bg-theme-foreground-10 translate-x-[-100%]
|
|
md:inline-flex md:flex-row md:w-auto md:max-w-[100%] md:h-full md:pl-[40px] md:items-center md:bg-transparent md:transform-none;
|
|
}
|
|
|
|
.nav-logo {
|
|
@apply flex items-center justify-center shrink-0 h-[62px];
|
|
}
|
|
|
|
.nav-list {
|
|
@apply overflow-hidden flex flex-col order-1 h-full mt-2 mb-6 pl-2 pr-1
|
|
md:flex-row md:order-none md:h-full md:my-0 md:ml-10 md:mr-6 md:px-0;
|
|
}
|
|
.nav-list.is-mounted {
|
|
@apply md:overflow-visible;
|
|
}
|
|
|
|
.nav-list a {
|
|
@apply transition-[background] hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10;
|
|
}
|
|
.nav-list a.nav-1depth:not(.item-link) {
|
|
@apply md:hover:bg-transparent md:active:bg-transparent;
|
|
}
|
|
|
|
.nav-1depth.router-link-active {
|
|
@apply bg-theme-foreground-reversal-8 md:bg-transparent;
|
|
}
|
|
.more-list .nav-1depth.router-link-active {
|
|
@apply bg-theme-foreground-reversal-8;
|
|
}
|
|
.nav-item:hover .nav-1depth::after,
|
|
.nav-1depth.router-link-active::after {
|
|
@apply opacity-100 md:transition-opacity;
|
|
}
|
|
|
|
.nav-item {
|
|
@apply relative flex flex-col text-[16px] md:items-center md:h-full;
|
|
}
|
|
.nav-item:last-of-type::after {
|
|
@apply hidden;
|
|
}
|
|
.nav-item::after {
|
|
@apply content-[''] h-px my-2 mx-3 bg-theme-foreground-reversal-8 md:hidden;
|
|
}
|
|
|
|
.nav-1depth {
|
|
@apply flex items-center py-[9px] px-3 gap-1 rounded-[12px] md:h-full md:py-0 md:px-0;
|
|
}
|
|
.nav-1depth::after {
|
|
@apply md:content-[''] md:absolute md:bottom-0 md:left-0 md:w-full md:h-[2px] md:bg-theme-foreground-reversal md:opacity-0;
|
|
}
|
|
|
|
.nav-2depth {
|
|
@apply text-[15px] group-hover:block md:hidden md:absolute md:top-[64px] md:left-[-28px] md:pt-1;
|
|
}
|
|
.nav-2depth ul {
|
|
@apply bg-theme-foreground-10 rounded-[20px] md:min-w-[190px] md:p-3 md:shadow-lg;
|
|
}
|
|
.nav-2depth .item-link {
|
|
@apply flex items-center gap-1 py-[9px] px-5 rounded-[12px]
|
|
md:py-[11px] md:px-4;
|
|
}
|
|
|
|
.official {
|
|
@apply overflow-x-hidden overflow-y-scroll pb-2 md:flex md:items-center md:space-x-8 md:pb-0 md:overflow-visible;
|
|
}
|
|
.custom-theme-scrollbar::-webkit-scrollbar {
|
|
@apply w-1;
|
|
}
|
|
.custom-theme-scrollbar::-webkit-scrollbar-thumb {
|
|
@apply border-0;
|
|
}
|
|
|
|
.more {
|
|
@apply relative hidden ml-[32px] pt-[11px] md:block;
|
|
}
|
|
.btn-more {
|
|
@apply w-[40px] h-[40px] rounded-[12px] bg-theme-foreground-reversal-6 hover:bg-theme-foreground-reversal-10 active:bg-theme-foreground-reversal-4;
|
|
}
|
|
.more-list {
|
|
@apply hidden absolute top-[64px] left-[-20px] pt-1 group-hover:block;
|
|
}
|
|
.list-inner {
|
|
@apply min-w-[190px] p-3 rounded-[20px] bg-theme-foreground-10 shadow-lg;
|
|
}
|
|
.more-list .item-link {
|
|
@apply flex items-center gap-1 py-[10px] px-4 rounded-[12px];
|
|
}
|
|
.more-list li .item-link {
|
|
@apply px-6;
|
|
}
|
|
.more-list .nav-1depth {
|
|
@apply after:hidden;
|
|
}
|
|
|
|
.official ~ .event {
|
|
@apply md:ml-[64px]
|
|
before:content-[''] before:block before:h-px before:mb-2 before:mx-3 before:bg-theme-foreground-reversal-8 md:before:hidden
|
|
after:content-[''] after:absolute md:after:top-[50%] md:after:left-[-32px] md:after:w-[1px] md:after:h-[16px] md:after:bg-theme-foreground-gray-750 md:after:translate-y-[-50%];
|
|
}
|
|
.event {
|
|
@apply relative pr-1 md:pr-0;
|
|
}
|
|
|
|
.is-hidden {
|
|
@apply hidden;
|
|
}
|
|
|
|
.btn-start {
|
|
@apply relative py-2 px-5 md:absolute md:top-[0] md:right-0 md:flex md:items-center md:h-full md:py-0 md:px-0;
|
|
}
|
|
.btn-start:hover .nav-2depth {
|
|
@apply md:block;
|
|
}
|
|
.btn-start:deep(.btn-base.default[data-variant='custom']) {
|
|
@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 {
|
|
@apply left-[unset] right-[-40px];
|
|
}
|
|
.btn-start .nav-2depth:deep(.btn-base) {
|
|
@apply w-full h-[48px] px-4
|
|
hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10;
|
|
}
|
|
.btn-start .nav-2depth:deep(.btn-base) .btn-content {
|
|
@apply justify-start;
|
|
}
|
|
.btn-start .nav-2depth:deep(.btn-base) .icon-platform {
|
|
@apply mr-1.5;
|
|
}
|
|
.btn-start .nav-2depth:deep(.btn-base) .text {
|
|
@apply text-[15px] text-theme-foreground-reversal;
|
|
}
|
|
|
|
[data-theme='light'] {
|
|
.btn-start .nav-2depth:deep(.btn-base) .icon-platform svg {
|
|
@apply fill-[#1F1F1F];
|
|
}
|
|
}
|
|
</style>
|