feat. home 버튼 컴포넌트 추가

This commit is contained in:
clkim
2025-11-11 18:50:14 +09:00
parent 25ea7925ea
commit 4ca00e4028
20 changed files with 216 additions and 317 deletions

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
interface props {
type?: 'button' | 'link'
to?: string
target?: '_self' | '_blank'
srOnly?: string
}
const props = withDefaults(defineProps<props>(), {
type: 'button',
to: '',
target: '_self',
srOnly: '',
})
const componentTag = computed((): string => {
switch (props.type) {
case 'link':
return 'AtomsLocaleLink'
default:
return 'button'
}
})
const componentProps = computed(() => {
switch (props.type) {
case 'link':
return { to: props.to, target: props.target }
default:
return {}
}
})
</script>
<template>
<component :is="componentTag" v-bind="componentProps" class="btn-circle">
<slot />
<span class="sr-only">{{ props.srOnly }}</span>
</component>
</template>
<style scoped>
.btn-circle {
@apply relative flex items-center justify-center rounded-full
w-[40px] h-[40px] md:w-[48px] md:h-[48px]
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-[rgba(255,255,255,0.06)] before:rounded-full
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:rounded-full after:opacity-0 after:transition-all after:duration-300 after:ease-in-out
hover:after:opacity-10;
}
</style>

View File

@@ -1,28 +0,0 @@
<script setup lang="ts">
const { y: windowY } = useWindowScroll({ behavior: 'smooth' })
const showBtn = computed(() => windowY.value > 0)
const handleScrollToTop = () => {
windowY.value = 0
}
</script>
<template>
<Transition name="fade">
<button v-show="showBtn" class="btn-top" @click="handleScrollToTop">
<span class="sr-only">top</span>
</button>
</Transition>
</template>
<style scoped>
.btn-top {
@apply relative rounded-full bg-[image:var(--button-top)] bg-center bg-cover bg-no-repeat
w-[40px] h-[40px] md:w-[48px] md:h-[48px]
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:rounded-full after:opacity-0 after:transition-all after:duration-300 after:ease-in-out;
}
.btn-top:hover {
@apply after:opacity-10;
}
</style>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
interface Props {
size?: number | string
color?: string
}
withDefaults(defineProps<Props>(), {
size: 24,
color: 'white',
})
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
viewBox="0 0 24 24"
:fill="color"
>
<path
d="M10.7388 3.23622C11.4736 2.63919 12.5264 2.63919 13.2612 3.23622L20.6306 9.22387C20.8643 9.41376 21 9.69885 21 9.99998L21 19C21 20.1045 20.1046 21 19 21L14 21C13.4477 21 13 20.5523 13 20V14.125C13 13.5037 12.5523 13 12 13C11.4477 13 11 13.5037 11 14.125L11 20C11 20.5523 10.5523 21 10 21H5C3.89543 21 3 20.1045 3 19L3 9.99998C3 9.69885 3.1357 9.41376 3.36941 9.22387L10.7388 3.23622Z"
/>
</svg>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
const scrollStore = useScrollStore()
const { isPassedStoveGnb } = storeToRefs(scrollStore)
</script>
<template>
<AtomsButtonCircle
sr-only="home"
type="link"
to="/"
:class="['btn-home', { 'is-fixed': isPassedStoveGnb }]"
>
<AtomsIconsHomeFill />
</AtomsButtonCircle>
</template>
<style scoped>
.btn-home {
@apply absolute top-3 right-3 mt-[48px] bg-black/20 shadow-[0_1.667px_3.333px_0_rgba(0,0,0,0.06)] backdrop-blur-[12.5px] z-[100]
sm:top-5 md:top-6 md:right-8 md:mt-[64px];
}
.btn-home.is-fixed {
@apply fixed;
}
</style>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
const { y: windowY } = useWindowScroll({ behavior: 'smooth' })
const showBtn = computed(() => windowY.value > 0)
const handleScrollToTop = () => {
windowY.value = 0
}
</script>
<template>
<Transition name="fade">
<AtomsButtonCircle
v-show="showBtn"
class="btn-top"
sr-only="top"
@click="handleScrollToTop"
/>
</Transition>
</template>
<style scoped>
.btn-top {
@apply bg-[image:var(--button-top)] bg-center bg-cover bg-no-repeat;
}
</style>

View File

@@ -42,14 +42,14 @@ const handleCopy = async () => {
<template>
<div v-if="Object.keys(snsList).length > 0" class="sns-container">
<button
<AtomsButtonCircle
class="btn-sns"
sr-only="sns"
:style="{ backgroundColor: snsBackgroundColor }"
@click="handleMouseEnter"
>
<AtomsIconsShareLine class="icon-share" />
<span class="sr-only">sns</span>
</button>
</AtomsButtonCircle>
<transition name="fade">
<div
v-if="showSnsList"
@@ -95,15 +95,6 @@ const handleCopy = async () => {
</template>
<style scoped>
.btn-sns {
@apply relative rounded-full flex items-center justify-center
w-[40px] h-[40px] md:w-[48px] md:h-[48px]
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-[rgba(255,255,255,0.06)] before:rounded-full
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:rounded-full after:opacity-0 after:transition-all after:duration-300 after:ease-in-out;
}
.btn-sns:hover {
@apply after:opacity-10;
}
.btn-sns:hover .icon-share {
@apply fill-white;
}

View File

@@ -147,7 +147,7 @@ onUnmounted(() => {
<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;
@apply fixed top-1/2 right-0 py-12 pr-[42px] 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;
@@ -163,10 +163,22 @@ onUnmounted(() => {
}
button {
@apply font-[500] text-[var(--lnb-disable-color)] transition-all duration-300 ease-in-out;
@apply relative 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)];
}
button::before {
@apply content-[''] absolute top-1/2 rounded-full -translate-y-1/2 bg-transparent transition-all duration-300 ease-in-out;
}
button.is-active::before {
@apply bg-[var(--lnb-active-color)];
}
.btn-1depth::before {
@apply -right-4 w-1.5 h-1.5;
}
.btn-2depth::before {
@apply -right-3.5 w-1 h-1;
}
</style>

View File

@@ -1,211 +0,0 @@
<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>

View File

@@ -58,16 +58,13 @@ onMounted(async () => {
}"
>
<div class="navigation-wrapper">
<button class="btn-control" @click="toggleEventNavigation">
<AtomsButtonCircle
sr-only="event navigation control"
class="btn-control"
@click="toggleEventNavigation"
>
<AtomsIconsArrowRightLine size="24" color="#ffffff" />
<span class="sr-only">
{{
isEventNavigationOpen
? 'event navigation close'
: 'event navigation open'
}}
</span>
</button>
</AtomsButtonCircle>
<ul class="navigation-list">
<li v-for="item in eventNavigationList" :key="item.banner_seq">
<AtomsLocaleLink
@@ -95,7 +92,7 @@ onMounted(async () => {
<style scoped>
.event-navigation {
@apply absolute top-0 left-0 bottom-0 pt-[48px] md:pt-[64px] z-[100] transition-transform duration-300 ease-in-out;
@apply absolute top-0 left-0 bottom-0 mt-[48px] md:mt-[64px] z-[100] transition-transform duration-300 ease-in-out;
}
.event-navigation.is-fixed {
@apply fixed;
@@ -116,9 +113,9 @@ onMounted(async () => {
@apply block mt-2 text-center line-clamp-2 text-[#ebebeb] text-[14px] font-normal leading-[20px] tracking-[-0.42px] opacity-50;
}
.btn-control {
@apply absolute top-3 right-[-40px] flex items-center justify-center w-[40px] h-[40px] transition-transform duration-300 ease-in-out
bg-black/20 shadow-[0_1.667px_3.333px_0_rgba(0,0,0,0.06)] backdrop-blur-[12.5px] rounded-full
sm:top-5 md:top-6 md:right-[-48px] md:w-[48px] md:h-[48px];
@apply absolute top-3 right-[-40px]
bg-black/20 shadow-[0_1.667px_3.333px_0_rgba(0,0,0,0.06)] backdrop-blur-[12.5px]
sm:top-5 md:top-6 md:right-[-48px];
}
.event-navigation.is-closed {

View File

@@ -317,7 +317,7 @@ onBeforeUnmount(() => {
</div>
</nav>
<div ref="startRef" class="btn-start">
<AtomsButtonLauncher
<BlocksButtonLauncher
type="custom"
platform="pc"
:background-color="
@@ -334,13 +334,13 @@ onBeforeUnmount(() => {
"
>
{{ gnb1depthButtonData?.btn_info?.txt_btn_name }}
</AtomsButtonLauncher>
</BlocksButtonLauncher>
<div v-if="gnb2depthButtonData" class="nav-2depth hidden md:block">
<ul>
<li v-for="(item, key) in gnb2depthButtonData" :key="key">
<AtomsButtonLauncher type="custom" :platform="key">
<BlocksButtonLauncher type="custom" :platform="key">
{{ item.btn_info?.txt_btn_name }}
</AtomsButtonLauncher>
</BlocksButtonLauncher>
</li>
</ul>
</div>
@@ -361,7 +361,7 @@ onBeforeUnmount(() => {
<style scoped>
.header {
@apply bg-theme-foreground text-theme-foreground-reversal relative z-[110];
@apply bg-theme-foreground text-theme-foreground-reversal relative z-[100];
}
.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]

View File

@@ -52,7 +52,7 @@ const setupSeoMeta = (metaTag: PageDataMetaTag) => {
onMounted(() => {
const { sendLog } = useAnalytics()
sendLog(locale.value, useAnalyticsLogDataDirect('view', 1))
// sendLog(locale.value, useAnalyticsLogDataDirect('view', 1))
})
// 메타 태그 설정 감시
@@ -82,8 +82,8 @@ watchEffect(() => {
v-if="isShowTopBtn || isShowSnsBtn"
:class="['utile-wrap', { 'is-stop': pinToMain }]"
>
<AtomsButtonScrollTop v-if="isShowTopBtn" />
<AtomsButtonSns v-if="isShowSnsBtn" />
<BlocksButtonScrollTop v-if="isShowTopBtn" />
<BlocksButtonSns v-if="isShowSnsBtn" />
</div>
</ClientOnly>
</div>

View File

@@ -12,6 +12,7 @@ interface Props {
const props = defineProps<Props>()
const { locale } = useI18n()
const modalStore = useModalStore()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const getBtnType = (item?: PageDataResourceGroupBtnInfo): ButtonType => {
@@ -35,11 +36,16 @@ const getTextColor = (item?: PageDataResourceGroupBtnInfo): string =>
colorCode: item?.color_code_txt,
})
const handleLogClick = (index: number) => {
sendLog(
locale.value,
useAnalyticsLogDataDirect(props.resourcesData[index], props.pageVerTmplSeq)
)
const handleLogClick = (button: PageDataResourceGroup) => {
sendLog(locale.value, useAnalyticsLogDataDirect(button, props.pageVerTmplSeq))
if (button.btn_info?.detail?.btn_type === 'POP') {
const popupSize = button.btn_info?.detail?.size_info
const popupTitle = button.btn_info?.detail?.title
const popupContent = button.btn_info?.detail?.tab_info[0].title
modalStore.handleOpenAlert({
contentText: popupContent,
})
}
}
// 편의상
@@ -52,17 +58,17 @@ const buttonList = computed(() => props.resourcesData || [])
class="flex flex-wrap justify-center gap-3 md:gap-4"
>
<template v-for="(button, index) in buttonList" :key="index">
<AtomsButtonLauncher
<BlocksButtonLauncher
v-if="button.btn_info?.detail?.btn_type === 'RUN'"
type="duplication"
:platform="button.btn_info?.detail?.market_type"
:background-color="getBgColor(button.btn_info)"
:text-color="getTextColor(button.btn_info)"
:disabled="button?.btn_info?.disabled"
@click="handleLogClick(index)"
@click="handleLogClick(button)"
>
{{ button.btn_info?.txt_btn_name }}
</AtomsButtonLauncher>
</BlocksButtonLauncher>
<AtomsButton
v-else
:type="getBtnType(button.btn_info)"
@@ -72,7 +78,7 @@ const buttonList = computed(() => props.resourcesData || [])
:background-color="getBgColor(button.btn_info)"
:text-color="getTextColor(button.btn_info)"
:disabled="button?.btn_info?.disabled"
@click="handleLogClick(index)"
@click="handleLogClick(button)"
>
{{ button.btn_info?.txt_btn_name }}
</AtomsButton>