433 lines
13 KiB
Vue
433 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
|
import { useScrollStore } from '#layers/stores/useScrollStore'
|
|
import { useWindowScroll, onClickOutside, useWindowSize } from '@vueuse/core'
|
|
import { storeToRefs } from 'pinia'
|
|
import type {
|
|
GameDataValue,
|
|
GameDataMenu,
|
|
GameDataMenuChildren,
|
|
} from '#layers/types/api/gameData'
|
|
|
|
const route = useRoute()
|
|
const { y: windowY } = useWindowScroll()
|
|
const gameDataStore = useGameDataStore()
|
|
const scrollStore = useScrollStore()
|
|
const _breakpoints = useResponsiveBreakpointsReliable()
|
|
|
|
const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
|
|
|
const gameData = gameDataStore.gameData as GameDataValue
|
|
const gnbList = (gameData?.gnb?.menus ?? {}) as GameDataMenuChildren
|
|
const isMenuOpen = ref(false)
|
|
const navAreaRef = ref<HTMLElement>()
|
|
const startRef = ref<HTMLElement>()
|
|
const navWidth = ref(0)
|
|
const startWidth = ref(0)
|
|
const officialItemWidths = ref<number[]>([])
|
|
const overflowNam = ref<number>(0)
|
|
|
|
const currentPath = computed(() => formatPathWithoutLocale(route.path))
|
|
|
|
const pathMatches = (base: string, current: string) => {
|
|
if (!base || base === '/') return current === '/'
|
|
return current === base || current.startsWith(base + '/')
|
|
}
|
|
|
|
/** header overlay 높이 계산 (re-compute 최소화) */
|
|
const scrollPositionHeight = computed(() => {
|
|
const gnbHeight = scrollStore.stoveGnbHeight
|
|
const y = windowY.value
|
|
if (y === 0) return `${gnbHeight}px`
|
|
if (y >= gnbHeight) return '0px'
|
|
return `${gnbHeight - y}`
|
|
})
|
|
|
|
/** 자식 중 활성 링크 존재 여부 */
|
|
const hasActiveChild = (children?: GameDataMenuChildren) => {
|
|
const cur = currentPath.value
|
|
return formatToArray(children).some(child => {
|
|
if (!child?.url_path || !isInternalUrl(child.url_path)) return false
|
|
return pathMatches(formatPathWithoutLocale(child.url_path), cur)
|
|
})
|
|
}
|
|
|
|
/** 1Depth 활성화 여부 */
|
|
const isNavItemActive = (gnbItem: GameDataMenu): boolean => {
|
|
const cur = currentPath.value
|
|
const base = gnbItem?.url_path
|
|
const selfActive =
|
|
!!base &&
|
|
isInternalUrl(base) &&
|
|
pathMatches(formatPathWithoutLocale(base), cur)
|
|
return selfActive || hasActiveChild(gnbItem.children)
|
|
}
|
|
|
|
const handleMenuOpen = () => (isMenuOpen.value = true)
|
|
const handleMenuClose = () => (isMenuOpen.value = false)
|
|
|
|
// navAreaRef의 넓이를 구하는 함수
|
|
const calculateNavWidth = () => {
|
|
if (!navAreaRef.value) return 0
|
|
|
|
const navAreaWidth = navAreaRef.value.offsetWidth
|
|
return navAreaWidth
|
|
}
|
|
|
|
// startRef의 넓이를 구하는 함수
|
|
const calculateStartWidth = () => {
|
|
if (!startRef.value) return 0
|
|
|
|
const startWidth = startRef.value.offsetWidth
|
|
return startWidth + 40
|
|
}
|
|
|
|
// official 자식들의 넓이를 구하는 함수 (뒤에서부터 순서대로)
|
|
const calculateOfficialItemWidths = () => {
|
|
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
|
|
|
|
// 해상도 체크 및 오버플로우 계산
|
|
calculateOverflow()
|
|
}
|
|
|
|
console.log(0, Object.keys(gnbList).length)
|
|
|
|
// 오버플로우 계산 함수
|
|
const calculateOverflow = () => {
|
|
if (!navAreaRef.value) return
|
|
|
|
const totalNavWidth = navWidth.value + startWidth.value
|
|
const screenWidth = width.value
|
|
|
|
console.log('calculateOverflow called:', {
|
|
screenWidth,
|
|
totalNavWidth,
|
|
navWidth: navWidth.value,
|
|
startWidth: startWidth.value,
|
|
officialItemWidths: officialItemWidths.value,
|
|
})
|
|
|
|
// 모바일(1024px 미만)에서는 overflowNam을 0으로 설정
|
|
if (screenWidth < 1024) {
|
|
overflowNam.value = 0
|
|
console.log('Mobile view - overflowNam set to 0')
|
|
return
|
|
}
|
|
|
|
// 해상도가 navWidth + startWidth보다 작은 경우
|
|
if (screenWidth < totalNavWidth) {
|
|
let removedCount = 0
|
|
let currentTotal = totalNavWidth
|
|
|
|
// officialItemWidths를 하나씩 빼면서 해상도보다 작아지는지 확인
|
|
for (let i = 0; i < officialItemWidths.value.length; i++) {
|
|
currentTotal -= officialItemWidths.value[i]
|
|
removedCount++
|
|
|
|
// 해상도보다 작아지면 중단
|
|
if (currentTotal <= screenWidth) {
|
|
break
|
|
}
|
|
}
|
|
|
|
overflowNam.value = removedCount
|
|
console.log('Overflow calculated:', overflowNam.value)
|
|
} else {
|
|
overflowNam.value = 0
|
|
console.log('No overflow needed, setting to 0')
|
|
}
|
|
}
|
|
|
|
// 컴포넌트 마운트 후 한 번만 계산
|
|
onMounted(() => {
|
|
// 초기화
|
|
overflowNam.value = 0
|
|
console.log('onMounted - overflowNam 초기화:', overflowNam.value)
|
|
|
|
nextTick(() => {
|
|
if (navAreaRef.value && startRef.value) {
|
|
navWidth.value = calculateNavWidth()
|
|
startWidth.value = calculateStartWidth()
|
|
calculateOfficialItemWidths()
|
|
}
|
|
})
|
|
})
|
|
|
|
// 화면 크기 변경 시 오버플로우 재계산
|
|
const { width } = useWindowSize()
|
|
watch(width, () => {
|
|
calculateOverflow()
|
|
})
|
|
|
|
onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
|
|
</script>
|
|
|
|
<template>
|
|
<header class="header">
|
|
<BlocksStoveGnb class="min-h-[48px]" />
|
|
|
|
<div class="game-wrapper" :class="{ 'is-fixed': isPassedStoveGnb }">
|
|
<AtomsLocaleLink to="/brand" class="mx-auto md:hidden">
|
|
<img
|
|
:src="gameData?.gnb?.bi_path"
|
|
:alt="gameData?.game_name"
|
|
class="h-[30px]"
|
|
/>
|
|
</AtomsLocaleLink>
|
|
<button class="btn-open" @click="handleMenuOpen">
|
|
<span class="sr-only">menu open</span>
|
|
</button>
|
|
<div
|
|
:class="['gnb-game', { 'is-open': isMenuOpen }]"
|
|
:style="{ '--scroll-position': scrollPositionHeight }"
|
|
>
|
|
<div ref="navAreaRef" class="nav-area">
|
|
<div class="nav-logo">
|
|
<AtomsLocaleLink to="/brand">
|
|
<img
|
|
:src="gameData?.gnb?.bi_path"
|
|
:alt="gameData?.game_name"
|
|
class="h-[30px]"
|
|
/>
|
|
</AtomsLocaleLink>
|
|
</div>
|
|
<nav class="nav-list">
|
|
<div v-if="gnbList" class="official">
|
|
<div
|
|
v-for="(gnbItem, key) in gnbList"
|
|
:key="key"
|
|
class="nav-item"
|
|
:class="{
|
|
'is-hidden':
|
|
overflowNam > 0 &&
|
|
Number(key) >= Object.keys(gnbList).length - overflowNam,
|
|
}"
|
|
>
|
|
<BlocksHybridLink
|
|
:to="gnbItem.url_path"
|
|
:target="gnbItem.link_target"
|
|
:class="`nav-1depth ${isNavItemActive(gnbItem) ? 'active' : ''}`"
|
|
>
|
|
<span>{{ gnbItem.menu_name }}</span>
|
|
<AtomsIconsArrowDown v-if="gnbItem.children" />
|
|
</BlocksHybridLink>
|
|
<div v-if="gnbItem.children" class="nav-2depth">
|
|
<ul>
|
|
<li
|
|
v-for="child in gnbItem.children"
|
|
:key="child.menu_name"
|
|
>
|
|
<BlocksHybridLink
|
|
:to="child.url_path"
|
|
:target="child.link_target"
|
|
>
|
|
<span>{{ child.menu_name }}</span>
|
|
<AtomsIconsLinkOut
|
|
v-if="child.link_target === '_blank'"
|
|
/>
|
|
</BlocksHybridLink>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="gnbList && overflowNam > 0" class="more">
|
|
<button class="btn-more">
|
|
<span class="sr-only">more</span>
|
|
</button>
|
|
<div class="more-list">
|
|
<div
|
|
v-for="(gnbItem, key) in gnbList"
|
|
:key="key"
|
|
:class="{
|
|
hidden:
|
|
Number(key) < Object.keys(gnbList).length - overflowNam,
|
|
}"
|
|
>
|
|
<BlocksHybridLink
|
|
:to="gnbItem.url_path"
|
|
:target="gnbItem.link_target"
|
|
:class="`${isNavItemActive(gnbItem) ? 'active' : ''}`"
|
|
>
|
|
<span>{{ gnbItem.menu_name }}</span>
|
|
<AtomsIconsArrowDown v-if="gnbItem.children" />
|
|
</BlocksHybridLink>
|
|
<div v-if="gnbItem.children">
|
|
<ul>
|
|
<li
|
|
v-for="child in gnbItem.children"
|
|
:key="child.menu_name"
|
|
>
|
|
<BlocksHybridLink
|
|
:to="child.url_path"
|
|
:target="child.link_target"
|
|
>
|
|
<span>{{ child.menu_name }}</span>
|
|
<AtomsIconsLinkOut
|
|
v-if="child.link_target === '_blank'"
|
|
/>
|
|
</BlocksHybridLink>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="event">
|
|
<div class="nav-item">
|
|
<BlocksHybridLink
|
|
:to="'/event'"
|
|
:target="'_self'"
|
|
class="nav-1depth text-gradient-pink"
|
|
>
|
|
<span>이벤트</span>
|
|
</BlocksHybridLink>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<div ref="startRef" class="btn-start">
|
|
<AtomsButton size="small" class="w-full md:w-auto">
|
|
게임 시작
|
|
</AtomsButton>
|
|
</div>
|
|
<button class="btn-close" @click="handleMenuClose">
|
|
<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 z-50;
|
|
}
|
|
.game-wrapper {
|
|
@apply absolute flex w-full h-[48px] items-center whitespace-nowrap px-[64px] bg-theme-foreground 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-wrapper.is-fixed {
|
|
@apply fixed top-0 left-0;
|
|
}
|
|
.game-logo {
|
|
@apply mx-auto shrink-0 md:mx-0;
|
|
}
|
|
|
|
.btn-open,
|
|
.btn-close {
|
|
@apply absolute w-[40px] h-[40px] md:hidden bg-[red];
|
|
}
|
|
.btn-open {
|
|
@apply top-[4px] left-[12px];
|
|
}
|
|
.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;
|
|
}
|
|
.btn-more {
|
|
@apply w-[40px] h-[40px] bg-[red];
|
|
}
|
|
|
|
.gnb-game {
|
|
@apply absolute top-0 left-0 w-0 md:relative md:w-full md:!h-full;
|
|
height: calc(100vh - var(--scroll-position));
|
|
}
|
|
.gnb-game.is-open {
|
|
@apply w-full;
|
|
}
|
|
.gnb-game.is-open::before {
|
|
@apply content-[''] absolute inset-0 w-[100vw] h-full bg-[rgba(0,0,0,0.6)] md:hidden;
|
|
}
|
|
.gnb-game.is-open .nav-area {
|
|
@apply h-full translate-x-0 transition-transform duration-300 md:translate-x-0;
|
|
}
|
|
|
|
.nav-area {
|
|
@apply flex flex-col w-[360px] bg-theme-foreground-10 translate-x-[-100%]
|
|
md:inline-flex md:flex-row md:w-auto md:h-full md:pl-[40px] md:items-center md:bg-transparent 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-4 px-2
|
|
md:flex-row md:order-none md:h-full md:my-0 md:mx-10 md:px-0 md:overflow-visible;
|
|
}
|
|
|
|
.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-item:hover .nav-1depth::after,
|
|
.nav-1depth.active::after {
|
|
@apply opacity-100 md:transition-opacity;
|
|
}
|
|
.nav-item:hover .nav-2depth {
|
|
@apply md:block;
|
|
}
|
|
|
|
.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-1depth.active {
|
|
@apply bg-theme-foreground-reversal-8 md:bg-transparent;
|
|
}
|
|
|
|
.nav-2depth {
|
|
@apply block text-[15px] md:hidden md:absolute md:top-[64px] md:left-[-28px] md:min-w-[190px] md:pt-1 md:z-50;
|
|
}
|
|
.nav-2depth ul {
|
|
@apply bg-theme-foreground-10 rounded-[20px] md:shadow-lg md:p-3;
|
|
}
|
|
.nav-2depth a {
|
|
@apply flex items-center gap-1 px-5 py-[9px] rounded-[12px] transition-colors
|
|
hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10
|
|
md:px-4 md:py-[11px];
|
|
}
|
|
|
|
.official {
|
|
@apply overflow-x-hidden overflow-y-auto pb-2 md:flex md:items-center md:space-x-8 md:pb-0 md:overflow-visible;
|
|
}
|
|
|
|
.more {
|
|
@apply relative hidden md:block;
|
|
}
|
|
.more-list {
|
|
@apply absolute;
|
|
}
|
|
|
|
.event {
|
|
@apply ml-[100px];
|
|
}
|
|
|
|
.is-hidden {
|
|
@apply hidden;
|
|
}
|
|
</style>
|