feat. lnb컴포넌트
This commit is contained in:
@@ -10,7 +10,7 @@ const handleScrollToTop = () => {
|
||||
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<button v-if="showBtn" class="btn-top" @click="handleScrollToTop">
|
||||
<button v-show="showBtn" class="btn-top" @click="handleScrollToTop">
|
||||
<span class="sr-only">top</span>
|
||||
</button>
|
||||
</Transition>
|
||||
|
||||
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>
|
||||
211
layers/components/blocks/Lnb2.vue
Normal file
211
layers/components/blocks/Lnb2.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageDataLnbMenu } from '#layers/types/api/pageData'
|
||||
|
||||
const HEADER_HEIGHT = 64
|
||||
|
||||
const { y: windowY } = useWindowScroll({ behavior: 'smooth' })
|
||||
const pageDataStore = usePageDataStore()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
|
||||
const { pageData } = storeToRefs(pageDataStore)
|
||||
|
||||
const activeSection = ref<string>('')
|
||||
const observerRef = shallowRef<IntersectionObserver | null>(null)
|
||||
|
||||
/** 1뎁스 LNB 배열로 정규화 */
|
||||
const lnbRoot = computed<PageDataLnbMenu[]>(() =>
|
||||
Object.values(pageData.value?.lnb_menus || {}).filter(Boolean)
|
||||
)
|
||||
const isShowLnb = computed(() => {
|
||||
const pageDataUseLnb = pageData.value?.use_lnb ?? false
|
||||
const isDesktop = breakpoints.value.isDesktop
|
||||
const lnbRootLength = lnbRoot.value.length
|
||||
return Boolean(pageDataUseLnb && isDesktop && lnbRootLength)
|
||||
})
|
||||
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)'
|
||||
)
|
||||
|
||||
const getChildren = (item?: PageDataLnbMenu) =>
|
||||
item?.children ? Object.values(item.children) : []
|
||||
|
||||
/** 1뎁스 활성 판정(자신 또는 자식) */
|
||||
const is1DepthActive = (lnbItem: PageDataLnbMenu): boolean => {
|
||||
if (!lnbItem) return false
|
||||
if (activeSection.value === lnbItem.page_ver_tmpl_name_en) return true
|
||||
return getChildren(lnbItem).some(
|
||||
c => activeSection.value === c.page_ver_tmpl_name_en
|
||||
)
|
||||
}
|
||||
|
||||
/** 스크롤 이동 */
|
||||
const scrollToLnb = async (id: string) => {
|
||||
if (import.meta.server) return
|
||||
await nextTick()
|
||||
|
||||
const el = document.getElementById(id)
|
||||
if (!el) return
|
||||
|
||||
const rect = el.getBoundingClientRect()
|
||||
const targetY = window.scrollY + rect.top - HEADER_HEIGHT
|
||||
|
||||
windowY.value = targetY
|
||||
}
|
||||
|
||||
/** 관찰 대상 id 리스트(1뎁스 + 2뎁스) */
|
||||
const sectionIds = computed<string[]>(() => {
|
||||
const ids: string[] = []
|
||||
for (const item of lnbRoot.value) {
|
||||
if (item?.page_ver_tmpl_name_en) ids.push(item.page_ver_tmpl_name_en)
|
||||
for (const c of getChildren(item)) {
|
||||
if (c?.page_ver_tmpl_name_en) ids.push(c.page_ver_tmpl_name_en)
|
||||
}
|
||||
}
|
||||
// 중복 제거
|
||||
return Array.from(new Set(ids))
|
||||
})
|
||||
|
||||
const createObserver = () => {
|
||||
if (observerRef.value) observerRef.value.disconnect()
|
||||
|
||||
observerRef.value = new IntersectionObserver(
|
||||
entries => {
|
||||
// 보이는 섹션 중 화면 상단에 가장 가까운 요소를 활성화
|
||||
const visibles = entries
|
||||
.filter(e => e.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)
|
||||
|
||||
if (visibles.length > 0) {
|
||||
activeSection.value = (visibles[0].target as HTMLElement).id
|
||||
return
|
||||
}
|
||||
|
||||
// 가끔 빠르게 스크롤 시 entries가 비어있을 수 있으므로
|
||||
// 현재 모든 섹션 중에서 '상단 기준으로 가장 가까운' 것을 폴백 계산
|
||||
const candidates: Array<{ id: string; top: number }> = []
|
||||
for (const id of sectionIds.value) {
|
||||
const el = document.getElementById(id)
|
||||
if (!el) continue
|
||||
const top = el.getBoundingClientRect().top - HEADER_HEIGHT
|
||||
candidates.push({ id, top })
|
||||
}
|
||||
// 0에 가장 가까운 양수/음수 모두 고려(위/아래 가장 가까운)
|
||||
candidates.sort((a, b) => Math.abs(a.top) - Math.abs(b.top))
|
||||
if (candidates.length) activeSection.value = candidates[0].id
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
// 상단 20%, 하단 60% 마진은 유지하되 헤더 보정치 반영
|
||||
rootMargin: `-${Math.max(HEADER_HEIGHT, 20)}px 0px -60% 0px`,
|
||||
threshold: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const handleLnbClick = (lnbItem: PageDataLnbMenu) => {
|
||||
if (!lnbItem?.page_ver_tmpl_name_en) return
|
||||
scrollToLnb(lnbItem.page_ver_tmpl_name_en)
|
||||
}
|
||||
|
||||
const observeSections = () => {
|
||||
if (import.meta.server) return
|
||||
if (!observerRef.value) createObserver()
|
||||
const obs = observerRef.value!
|
||||
obs.disconnect() // 기존 관찰 해제
|
||||
|
||||
// DOM 렌더 후 관찰 등록
|
||||
requestAnimationFrame(() => {
|
||||
for (const id of sectionIds.value) {
|
||||
const el = document.getElementById(id)
|
||||
if (el) obs.observe(el)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (import.meta.server) return
|
||||
createObserver()
|
||||
observeSections()
|
||||
})
|
||||
|
||||
// lnb 데이터/DOM이 바뀌면 재관찰
|
||||
watchEffect(async () => {
|
||||
if (!import.meta.client) return
|
||||
if (isShowLnb.value && sectionIds.value.length) {
|
||||
await nextTick()
|
||||
observeSections()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
observerRef.value?.disconnect()
|
||||
observerRef.value = null
|
||||
})
|
||||
</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 lnbRoot" :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="getChildren(lnbItem).length > 0" class="sub-list">
|
||||
<li v-for="subItem in getChildren(lnbItem)" :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>
|
||||
@@ -1,32 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
isShowTopBtn: boolean
|
||||
isShowSnsBtn: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isShowTopBtn: false,
|
||||
isShowSnsBtn: false,
|
||||
})
|
||||
|
||||
const pinToMain = inject('pinToMain')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<div :class="['utile-container', { 'is-stop': pinToMain }]">
|
||||
<AtomsButtonScrollTop v-if="props.isShowTopBtn" />
|
||||
<AtomsButtonSns v-if="props.isShowSnsBtn" />
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.utile-container {
|
||||
@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-stop {
|
||||
@apply absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,9 @@ import type {
|
||||
|
||||
const { locale } = useI18n()
|
||||
const gameDomain = useGetGameDomain()
|
||||
const scrollStore = useScrollStore()
|
||||
|
||||
const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
||||
|
||||
const isEventNavigationOpen = ref(true)
|
||||
const eventNavigationList = ref<Record<string, EventNavigation>>({})
|
||||
@@ -51,6 +54,7 @@ onMounted(async () => {
|
||||
class="event-navigation"
|
||||
:class="{
|
||||
'is-closed': !isEventNavigationOpen,
|
||||
'is-fixed': isPassedStoveGnb,
|
||||
}"
|
||||
>
|
||||
<div class="navigation-wrapper">
|
||||
@@ -91,7 +95,10 @@ onMounted(async () => {
|
||||
|
||||
<style scoped>
|
||||
.event-navigation {
|
||||
@apply fixed top-0 left-0 bottom-0 mt-[var(--scroll-position,48px)] pt-[48px] md:pt-[64px] z-[100] transition-transform duration-300 ease-in-out;
|
||||
@apply absolute top-0 left-0 bottom-0 pt-[48px] md:pt-[64px] z-[100] transition-transform duration-300 ease-in-out;
|
||||
}
|
||||
.event-navigation.is-fixed {
|
||||
@apply fixed;
|
||||
}
|
||||
.navigation-wrapper {
|
||||
@apply relative h-full p-3 sm:p-5 sm:pr-3
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { onClickOutside, useWindowSize } from '@vueuse/core'
|
||||
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
||||
import type {
|
||||
GameDataValue,
|
||||
GameDataMenu,
|
||||
GameDataMenuChildren,
|
||||
GameDataResourceGroup,
|
||||
@@ -12,12 +11,15 @@ import type {
|
||||
const route = useRoute()
|
||||
const { width } = useWindowSize()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const scrollStore = useScrollStore()
|
||||
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
||||
|
||||
const navAreaRef = ref<HTMLElement>()
|
||||
const startRef = ref<HTMLElement>()
|
||||
|
||||
const gameData = gameDataStore.gameData as GameDataValue
|
||||
const gnbData = gameData?.gnb
|
||||
const gnbData = gameData.value?.gnb
|
||||
const isMenuOpen = ref(false)
|
||||
const navWidth = ref(0)
|
||||
const startWidth = ref(0)
|
||||
@@ -57,9 +59,6 @@ const isNavItemActive = (gnbItem: GameDataMenu): boolean => {
|
||||
return selfActive || hasActiveChild(gnbItem.children)
|
||||
}
|
||||
|
||||
const handleMenuOpen = () => (isMenuOpen.value = true)
|
||||
const handleMenuClose = () => (isMenuOpen.value = false)
|
||||
|
||||
// navAreaRef의 넓이를 구하는 함수
|
||||
const calculateNavWidth = () => {
|
||||
if (!navAreaRef.value || !gnbData) return 0
|
||||
@@ -132,15 +131,26 @@ const calculateOverflow = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const stopClickOutside = onClickOutside(
|
||||
navAreaRef,
|
||||
() => (isMenuOpen.value = false)
|
||||
)
|
||||
const handleMenuOpen = () => {
|
||||
isMenuOpen.value = true
|
||||
scrollStore.controlScrollLock(true)
|
||||
}
|
||||
|
||||
const handleMenuClose = () => {
|
||||
isMenuOpen.value = false
|
||||
scrollStore.controlScrollLock(false)
|
||||
}
|
||||
|
||||
const isNotClickable = (gnbItem: GameDataMenu) => {
|
||||
return gnbItem.click_action_type === 0
|
||||
}
|
||||
|
||||
const has2depthButton = (gnbItem: GameDataMenu) => {
|
||||
return gnbItem.children && Object.keys(gnbItem.children).length > 0
|
||||
}
|
||||
|
||||
const stopClickOutside = onClickOutside(navAreaRef, () => handleMenuClose())
|
||||
|
||||
// 화면 크기 변경 시 오버플로우 재계산
|
||||
watch(width, () => {
|
||||
calculateOverflow()
|
||||
@@ -168,7 +178,7 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<header class="header">
|
||||
<BlocksStoveGnbNew class="h-[48px]" />
|
||||
<div class="game-wrap">
|
||||
<div :class="['game-wrap', { 'is-fixed': isPassedStoveGnb }]">
|
||||
<AtomsLocaleLink to="/brand" class="mx-auto md:hidden">
|
||||
<img
|
||||
:src="getImageHost(gnbData?.bi_path)"
|
||||
@@ -205,11 +215,11 @@ onBeforeUnmount(() => {
|
||||
}"
|
||||
>
|
||||
<AtomsLocaleLink
|
||||
:to="gnbItem.url_path"
|
||||
:to="isNotClickable(gnbItem) ? '#' : gnbItem.url_path"
|
||||
:target="gnbItem.link_target"
|
||||
:class="[
|
||||
'nav-1depth',
|
||||
{ 'has-link': !!gnbItem.url_path },
|
||||
{ 'has-link': !isNotClickable(gnbItem) },
|
||||
{ active: isNavItemActive(gnbItem) },
|
||||
]"
|
||||
>
|
||||
@@ -354,9 +364,13 @@ onBeforeUnmount(() => {
|
||||
@apply bg-theme-foreground text-theme-foreground-reversal relative z-[110];
|
||||
}
|
||||
.game-wrap {
|
||||
@apply fixed top-0 flex w-full h-[48px] items-center whitespace-nowrap mt-[var(--scroll-position,48px)] px-[52px] bg-theme-foreground sm:px-[72px] md:h-16 md:pl-0 md:pr-[40px]
|
||||
@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;
|
||||
}
|
||||
@@ -426,7 +440,7 @@ onBeforeUnmount(() => {
|
||||
@apply bg-theme-foreground-reversal-8 md:bg-transparent;
|
||||
}
|
||||
.nav-1depth.has-link {
|
||||
@apply hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10 md:hover:bg-transparent md:active:bg-transparent;
|
||||
@apply cursor-pointer hover:bg-theme-foreground-reversal-4 active:bg-theme-foreground-reversal-10 md:hover:bg-transparent md:active:bg-transparent;
|
||||
}
|
||||
|
||||
.nav-2depth {
|
||||
|
||||
@@ -15,6 +15,8 @@ const props = defineProps<Props>()
|
||||
const { locale } = useI18n()
|
||||
const { getTemplateComponent } = useTemplateRegistry()
|
||||
|
||||
const pinToMain = inject('pinToMain')
|
||||
|
||||
// 개별 메타 태그 표시 여부 확인
|
||||
const shouldShowMetaTag = computed(() => props.pageData?.meta_tag_type === 2)
|
||||
|
||||
@@ -30,6 +32,9 @@ const isTemplateVisible = (template: PageDataTemplate): boolean => {
|
||||
const visibleTemplates = computed(() =>
|
||||
Object.values(props.pageData?.templates).filter(isTemplateVisible)
|
||||
)
|
||||
const isShowTopBtn = computed(() => props.pageData?.use_top_btn ?? false)
|
||||
const isShowSnsBtn = computed(() => props.pageData?.use_sns_btn ?? false)
|
||||
const isShowLnb = computed(() => props.pageData?.use_lnb ?? false)
|
||||
|
||||
// SEO 메타 태그 설정
|
||||
const setupSeoMeta = (metaTag: PageDataMetaTag) => {
|
||||
@@ -66,14 +71,21 @@ watchEffect(() => {
|
||||
>
|
||||
<component
|
||||
:is="getTemplateComponent(template.template_code)"
|
||||
:id="template.page_ver_tmpl_name_en"
|
||||
:components="template.page_ver_tmpl_json"
|
||||
:page-ver-tmpl-seq="template.page_ver_tmpl_seq"
|
||||
/>
|
||||
</template>
|
||||
<BlocksUtileContainer
|
||||
:is-show-top-btn="pageData.use_top_btn ?? false"
|
||||
:is-show-sns-btn="pageData.use_sns_btn ?? false"
|
||||
/>
|
||||
<ClientOnly>
|
||||
<BlocksLnb v-if="isShowLnb" />
|
||||
<div
|
||||
v-if="isShowTopBtn || isShowSnsBtn"
|
||||
:class="['utile-wrap', { 'is-stop': pinToMain }]"
|
||||
>
|
||||
<AtomsButtonScrollTop v-if="isShowTopBtn" />
|
||||
<AtomsButtonSns v-if="isShowSnsBtn" />
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -81,6 +93,13 @@ watchEffect(() => {
|
||||
.main-content {
|
||||
@apply relative min-h-[200px] pt-[48px] md:min-h-[800px] md:pt-[64px];
|
||||
}
|
||||
.utile-wrap {
|
||||
@apply fixed flex flex-col z-[100]
|
||||
bottom-[12px] right-[12px] gap-2 md:bottom-[40px] md:right-[40px] md:gap-3;
|
||||
}
|
||||
.utile-wrap.is-stop {
|
||||
@apply absolute;
|
||||
}
|
||||
|
||||
[data-theme='light'] {
|
||||
.main-content {
|
||||
|
||||
@@ -15,11 +15,11 @@ const { locale } = useI18n()
|
||||
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
|
||||
|
||||
const getBtnType = (item?: PageDataResourceGroupBtnInfo): ButtonType => {
|
||||
const t = item?.detail?.btn_type
|
||||
const type = item?.detail?.btn_type
|
||||
const target = item?.detail?.action?.link_target
|
||||
if (t === 'URL' && target)
|
||||
if (type === 'URL' && target)
|
||||
return target === '_blank' ? 'external' : 'internal'
|
||||
if (t === 'DOWNLOAD') return 'download'
|
||||
if (type === 'DOWNLOAD') return 'download'
|
||||
return 'action'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
const isLoading = ref(true)
|
||||
const maintRef = ref<HTMLElement>()
|
||||
const mainRef = ref<HTMLElement>()
|
||||
|
||||
const { height: viewportH } = useWindowSize()
|
||||
const { bottom: mainBottom } = useElementBounding(maintRef)
|
||||
const { bottom: mainBottom } = useElementBounding(mainRef)
|
||||
|
||||
const pinToMain = computed(() => {
|
||||
if (!mainBottom.value) return false
|
||||
@@ -20,7 +20,7 @@ onMounted(() => {
|
||||
<template>
|
||||
<LayoutsHeader />
|
||||
<AtomsLoadingSimple :is-loading="isLoading" />
|
||||
<main id="LayoutsMain" class="relative">
|
||||
<main id="LayoutsMain" ref="mainRef" class="relative">
|
||||
<slot />
|
||||
</main>
|
||||
<LayoutsFooter />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
const maintRef = ref<HTMLElement>()
|
||||
const mainRef = ref<HTMLElement>()
|
||||
|
||||
const { height: viewportH } = useWindowSize()
|
||||
const { bottom: mainBottom } = useElementBounding(maintRef)
|
||||
const { bottom: mainBottom } = useElementBounding(mainRef)
|
||||
|
||||
const pinToMain = computed(() => {
|
||||
if (!mainBottom.value) return false
|
||||
@@ -14,7 +14,7 @@ provide('pinToMain', pinToMain)
|
||||
|
||||
<template>
|
||||
<LayoutsHeader />
|
||||
<main id="LayoutsMain" ref="maintRef" class="relative">
|
||||
<main id="LayoutsMain" ref="mainRef" class="relative">
|
||||
<LayoutsEventNavigation />
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface PageDataValue {
|
||||
use_lnb: boolean
|
||||
lnb_text_color_code_active: string
|
||||
lnb_text_color_code_deactive: string
|
||||
lnb_menus: PageDataLnbMenu[]
|
||||
lnb_menus: Record<string, PageDataLnbMenu>
|
||||
meta_tag_json: PageDataMetaTag
|
||||
templates: Record<string, PageDataTemplate>
|
||||
}
|
||||
@@ -51,6 +51,7 @@ export interface PageDataLnbMenu {
|
||||
target_type: number
|
||||
page_ver_tmpl_name_en: string
|
||||
tracking_json: Record<string, PageDataTracking>
|
||||
children?: Record<string, PageDataLnbMenu>
|
||||
}
|
||||
|
||||
// 메타 태그 타입
|
||||
|
||||
Reference in New Issue
Block a user