Files
web-temp/layers/components/blocks/Lnb.vue
2026-01-21 15:57:52 +09:00

270 lines
7.1 KiB
Vue

<script setup lang="ts">
import type { TrackingObject } from '#layers/types/api/common'
import type { PageDataLnbMenu } from '#layers/types/api/pageData'
const { directions } = useWindowScroll({ behavior: 'smooth' })
const { locale } = useI18n()
const pageDataStore = usePageDataStore()
const scrollStore = useScrollStore()
const breakpoints = useResponsiveBreakpoints()
const { sendLog } = useAnalytics()
const { pageData } = storeToRefs(pageDataStore)
// 상수 정의
const OBSERVER_OPTIONS = {
root: null,
rootMargin: '-20% 0px -60% 0px', // 상단 20%, 하단 60% 마진
threshold: 0,
} as const
const AUTO_HIDE_MS = 5000
let autoHideTimer: ReturnType<typeof setTimeout> | null = null
const isShowLnbWithScroll = ref(true)
const activeSection = ref<string>('')
const lnbList = computed<Record<string, PageDataLnbMenu>>(
() => pageData.value?.lnb_menus || {}
)
const isShowLnb = computed(() => {
return Boolean(
pageData.value?.use_lnb &&
breakpoints.value.isDesktop &&
Object.keys(lnbList.value).length > 0
)
})
const activeColor = computed(
() => pageData.value?.lnb_text_color_code_active || 'var(--text-primary)'
)
const disableColor = computed(
() => pageData.value?.lnb_text_color_code_deactive || 'var(--text-secondary)'
)
// 1depth가 활성화되었는지 확인 (자신 또는 자식 중 하나가 활성화된 경우)
const is1DepthActive = (lnbItem: PageDataLnbMenu): boolean => {
// 자신이 활성화된 경우
if (activeSection.value === lnbItem.page_ver_tmpl_name_en) {
return true
}
// children 중 하나가 활성화된 경우
const children = lnbItem.children
if (children && Object.keys(children).length > 0) {
return Object.values(children).some(
child => activeSection.value === child.page_ver_tmpl_name_en
)
}
return false
}
// IntersectionObserver 콜백: 교차하는 섹션들 중 가장 위에 있는 것을 활성화
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
if (import.meta.server) return
const visibleEntries = entries
.filter(entry => entry.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)
if (visibleEntries.length > 0) {
const topEntry = visibleEntries[0]
activeSection.value = topEntry.target.id
}
}
const observer = new IntersectionObserver(handleIntersection, OBSERVER_OPTIONS)
const clearAutoHide = () => {
if (autoHideTimer) {
clearTimeout(autoHideTimer)
autoHideTimer = null
}
}
const scheduleAutoHide = () => {
clearAutoHide()
autoHideTimer = setTimeout(() => {
isShowLnbWithScroll.value = false
}, AUTO_HIDE_MS)
}
// 요소 관찰 헬퍼 함수
const observeElement = (elementId: string) => {
const element = document.getElementById(elementId)
if (element) {
observer.observe(element)
}
}
// 섹션들을 관찰 시작
const observeSections = () => {
if (import.meta.server) return
Object.values(lnbList.value).forEach(lnbItem => {
// 1depth 관찰
observeElement(lnbItem.page_ver_tmpl_name_en)
// 2depth 관찰
const children = lnbItem.children
if (children && Object.keys(children).length > 0) {
Object.values(children).forEach(childItem => {
observeElement(childItem.page_ver_tmpl_name_en)
})
}
})
}
// LNB 클릭 핸들러: 해당 섹션으로 스크롤
const handleLnbClick = (lnbItem: PageDataLnbMenu) => {
if (import.meta.server) return
const targetId =
lnbItem.page_ver_tmpl_name_en === ''
? lnbItem?.children?.['1']?.page_ver_tmpl_name_en
: lnbItem.page_ver_tmpl_name_en
scrollStore.scrollToAnchor(targetId)
sendLog(locale.value, lnbItem.tracking_json as TrackingObject)
}
watch(directions, newVal => {
// 스크롤 위로: 즉시 노출 + 5초 후 자동 숨김
if (newVal.top === true) {
isShowLnbWithScroll.value = true
scheduleAutoHide()
return
}
// 스크롤 아래로: 즉시 숨김 (딜레이 없음)
if (newVal.bottom === true) {
clearAutoHide()
isShowLnbWithScroll.value = false
}
})
onMounted(() => {
observeSections()
isShowLnbWithScroll.value = true
scheduleAutoHide()
})
onUnmounted(() => {
clearAutoHide()
observer.disconnect()
})
</script>
<template>
<template v-if="isShowLnb">
<div
:class="['lnb-wrap', { 'is-hidden': !isShowLnbWithScroll }]"
:style="{
'--lnb-active-color': activeColor,
'--lnb-disable-color': disableColor,
}"
>
<ul class="lnb-main">
<li v-for="lnbItem in lnbList" :key="lnbItem.path_code">
<button
v-dompurify-html="lnbItem.menu_name"
type="button"
:class="['btn-1depth', { 'is-active': is1DepthActive(lnbItem) }]"
@click="handleLnbClick(lnbItem)"
></button>
<ul
v-if="Object.keys(lnbItem.children || {}).length > 0"
class="lnb-sub"
>
<li v-for="subItem in lnbItem.children" :key="subItem.path_code">
<button
v-dompurify-html="subItem.menu_name"
type="button"
:class="[
'btn-2depth',
{
'is-active':
activeSection === subItem.page_ver_tmpl_name_en,
},
]"
@click="handleLnbClick(subItem)"
></button>
</li>
</ul>
</li>
</ul>
</div>
</template>
</template>
<style scoped>
.empty-game + main .lnb-wrap {
@apply mt-[var(--scroll-position,48px)];
}
.lnb-wrap {
@apply fixed top-0 right-0 mt-[calc(var(--scroll-position,48px)+64px)] py-8 pr-10 bg-[radial-gradient(100%_50%_at_100%_50%,rgba(0,0,0,0.4)_25%,rgba(0,0,0,0)_100%)] transition-transform duration-[400ms] ease-in-out z-50;
}
.lnb-wrap:before {
content: '';
position: absolute;
top: 0;
left: -5%;
width: 105%;
height: 100%;
backdrop-filter: blur(5px);
mask-image: radial-gradient(
circle at right center,
#000 0%,
#000 40%,
transparent 80%
);
mask-size: 100% 100%;
mask-repeat: no-repeat;
z-index: -1;
}
.lnb-wrap.is-hidden {
@apply translate-x-[110%];
}
.lnb-main {
@apply flex flex-col gap-4 items-end;
}
.lnb-main > li {
@apply flex flex-col items-end;
}
.btn-1depth {
@apply text-[15px] leading-[26px] tracking-[-0.54px];
}
.lnb-sub {
@apply flex flex-col gap-2 items-end mt-4 mb-1 pr-[16px];
}
.btn-2depth {
@apply text-[14px] leading-[20px] tracking-[-0.42px];
}
button {
@apply flex items-center font-[500] text-[var(--lnb-disable-color)] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] transition-all duration-300 ease-in-out;
}
button:hover,
button.is-active {
@apply text-[var(--lnb-active-color)];
}
button::after {
@apply content-[''] rounded-full ml-2 bg-[var(--lnb-disable-color)] transition-all duration-300 ease-in-out;
}
button.is-active::after {
@apply bg-[var(--lnb-active-color)];
}
.btn-1depth::after {
@apply -right-4 w-1.5 h-1.5;
}
.btn-2depth::after {
@apply -right-3.5 w-1 h-1;
}
.main-promotion .lnb-wrap {
@apply mt-[calc(var(--scroll-position,48px)+64px+72px)];
}
.empty-game + .main-promotion .lnb-wrap {
@apply mt-[calc(var(--scroll-position,48px)+72px)];
}
</style>