feat. lnb컴포넌트
This commit is contained in:
172
layers/components/blocks/Lnb.vue
Normal file
172
layers/components/blocks/Lnb.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageDataLnbMenu } from '#layers/types/api/pageData'
|
||||
|
||||
const { y: windowY } = useWindowScroll({ behavior: 'smooth' })
|
||||
const pageDataStore = usePageDataStore()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
|
||||
const { pageData } = storeToRefs(pageDataStore)
|
||||
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-20% 0px -60% 0px', // 상단 20%, 하단 60% 마진
|
||||
threshold: 0,
|
||||
}
|
||||
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 중 하나가 활성화된 경우
|
||||
if (lnbItem.children && Object.keys(lnbItem.children).length > 0) {
|
||||
return Object.values(lnbItem.children).some(
|
||||
child => activeSection.value === child.page_ver_tmpl_name_en
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
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) {
|
||||
activeSection.value = visibleEntries[0].target.id
|
||||
}
|
||||
}, observerOptions)
|
||||
|
||||
const observeSections = () => {
|
||||
if (import.meta.server) return
|
||||
Object.values(lnbList.value).forEach(lnbItem => {
|
||||
// 1depth 관찰
|
||||
const el = document.getElementById(lnbItem.page_ver_tmpl_name_en)
|
||||
if (el) {
|
||||
observer.observe(el)
|
||||
}
|
||||
|
||||
// 2depth 관찰
|
||||
if (lnbItem.children && Object.keys(lnbItem.children).length > 0) {
|
||||
Object.values(lnbItem.children).forEach(childItem => {
|
||||
const childEl = document.getElementById(childItem.page_ver_tmpl_name_en)
|
||||
if (childEl) {
|
||||
observer.observe(childEl)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleLnbClick = (lnbItem: PageDataLnbMenu) => {
|
||||
if (import.meta.server) return
|
||||
|
||||
const id = lnbItem.page_ver_tmpl_name_en
|
||||
const el = document.getElementById(id)
|
||||
if (!el) return
|
||||
|
||||
const elementTop = el.getBoundingClientRect().top
|
||||
const currentScrollY = window.scrollY
|
||||
const headerHeight = 64
|
||||
const targetScrollY = currentScrollY + elementTop - headerHeight
|
||||
|
||||
windowY.value = targetScrollY
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
observeSections()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="isShowLnb"
|
||||
class="lnb-wrap"
|
||||
:style="{
|
||||
'--lnb-active-color': activeColor,
|
||||
'--lnb-disable-color': disableColor,
|
||||
}"
|
||||
>
|
||||
<ul class="main-list">
|
||||
<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="sub-list"
|
||||
>
|
||||
<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>
|
||||
|
||||
<style scoped>
|
||||
.lnb-wrap {
|
||||
@apply fixed top-1/2 right-0 py-12 pr-4 text-right -translate-y-1/2 bg-[radial-gradient(100%_50%_at_100%_50%,rgba(0,0,0,0.5)_25%,rgba(0,0,0,0)_100%)] z-50;
|
||||
}
|
||||
.main-list {
|
||||
@apply flex flex-col gap-4 items-end;
|
||||
}
|
||||
.btn-1depth {
|
||||
@apply text-[15px] leading-[26px] tracking-[-0.54px];
|
||||
}
|
||||
.sub-list {
|
||||
@apply flex flex-col gap-2 items-end mt-4 mb-1 pr-[46px];
|
||||
}
|
||||
.btn-2depth {
|
||||
@apply text-[14px] leading-[20px] tracking-[-0.42px];
|
||||
}
|
||||
|
||||
button {
|
||||
@apply font-[500] text-[var(--lnb-disable-color)] transition-all duration-300 ease-in-out;
|
||||
}
|
||||
button:hover,
|
||||
button.is-active {
|
||||
@apply text-[var(--lnb-active-color)];
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user