feat. 이벤트 네비게이션 추가

This commit is contained in:
clkim
2025-11-10 21:11:39 +09:00
parent 65c79eb689
commit 60b306ca90
24 changed files with 647 additions and 422 deletions

View File

@@ -0,0 +1,135 @@
<script setup lang="ts">
import type {
EventNavigationResponse,
EventNavigation,
} from '#layers/types/api/eventNavigation'
const { locale } = useI18n()
const gameDomain = useGetGameDomain()
const isEventNavigationOpen = ref(true)
const eventNavigationList = ref<Record<string, EventNavigation>>({})
// const pinToMain = inject('pinToMain')
const getEventNavigation = async (): Promise<Record<
string,
EventNavigation
> | null> => {
const runtimeConfig = useRuntimeConfig()
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/eventBanners`
const queryParams: Record<string, string> = {
game_domain: gameDomain,
lang_code: locale.value,
_t: Date.now().toString(),
}
const response = (await commonFetch('GET', apiUrl, {
query: queryParams,
loading: false,
})) as EventNavigationResponse | null
if (response?.code === 0 && 'value' in response) {
return response.value
}
return null
}
const toggleEventNavigation = () => {
isEventNavigationOpen.value = !isEventNavigationOpen.value
}
onMounted(async () => {
eventNavigationList.value = await getEventNavigation()
})
</script>
<template>
<div
v-if="Object.keys(eventNavigationList).length > 1"
class="event-navigation"
:class="{
'is-closed': !isEventNavigationOpen,
}"
>
<div class="navigation-wrapper">
<button class="btn-control" @click="toggleEventNavigation">
<AtomsIconsArrowRightLine size="24" color="#ffffff" />
<span class="sr-only">
{{
isEventNavigationOpen
? 'event navigation close'
: 'event navigation open'
}}
</span>
</button>
<ul class="navigation-list">
<li v-for="item in eventNavigationList" :key="item.banner_seq">
<AtomsLocaleLink
:to="item.page_url"
:target="item.link_type === 2 ? '_blank' : '_self'"
class="item-link"
>
<div class="item-thumbnail">
<img
v-if="item.thumbnail"
:src="item.thumbnail"
:alt="item.banner_title || item.promotion_name"
class="w-full h-full object-cover"
/>
</div>
<span class="item-title">
{{ item.banner_title || item.promotion_name }}
</span>
</AtomsLocaleLink>
</li>
</ul>
</div>
</div>
</template>
<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;
}
.navigation-wrapper {
@apply relative h-full p-3 sm:p-5 sm:pr-3
md:p-8 md:pt-6 md:pr-4;
}
.navigation-list {
@apply flex flex-col gap-4 h-full overflow-y-auto rounded-[20px] p-4 bg-[rgba(25,25,25,0.5)] shadow-[0_2px_4px_0_rgba(0,0,0,0.06)] backdrop-blur-[25px] transition-opacity duration-300 ease-in-out;
}
.item-thumbnail {
@apply overflow-hidden relative w-[148px] h-[75px] rounded-[10px]
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:rounded-[10px] before:border-[1px] before:border-white/[0.08]
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-[lightgray]/50;
}
.item-title {
@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];
}
.event-navigation.is-closed {
@apply translate-x-[calc(-100%+20px)] sm:translate-x-[calc(-100%+40px)];
}
.event-navigation.is-closed .btn-control {
@apply rotate-180;
}
.event-navigation.is-closed .navigation-list {
@apply pointer-events-none opacity-0;
}
.router-link-active .item-thumbnail,
.item-link:hover .item-thumbnail {
@apply before:border-[var(--primary)] after:opacity-0;
}
.router-link-active .item-title,
.item-link:hover .item-title {
@apply text-[var(--primary)] opacity-100;
}
</style>

View File

@@ -1,144 +1,215 @@
<template>
<footer id="footer" ref="footerRef" class="relative bg-black z-[90]">
<footer id="footer" ref="footerRef" class="relative bg-blac">
<div
class="inner relative max-w-7xl mx-auto px-5 md:px-10 py-4 text-[12px] text-gray-400 md:px-4 md:py-9 md:text-[12px]"
class="inner relative max-w-7xl mx-auto px-5 py-4 text-[12px] text-gray-400 md:px-4 md:py-9 md:text-[12px]"
>
<ClientOnly>
<div class="menu-area py-4 pb-4">
<ul class="flex items-center flex-wrap gap-x-6 gap-y-2">
<li
v-for="(footerMenuItem, index) in footerLinks"
:key="index"
class="text-[15px] text-white/50 md:tracking-[-0.5px] relative flex items-center"
>
<NuxtLink
:to="footerMenuItem.url"
:target="footerMenuItem.target"
:class="[
footerMenuItem.active === 'y' && 'text-white/50',
index === 2 && 'text-[#fff]',
'hover:text-gray-600 transition-colors',
]"
<ClientOnly>
<div class="menu-area py-4 pb-4">
<ul class="flex items-center flex-wrap gap-x-6 gap-y-2">
<li
v-for="(footerMenuItem, index) in footerLinks"
:key="index"
class="text-[15px] text-white/50 md:tracking-[-0.5px] relative flex items-center"
>
{{ footerMenuItem.title }}
</NuxtLink>
</li>
<li class="relative">
<button class="text-[15px] text-white/50 hover:text-gray-600 transition-colors" @click="toggleAgeRating">
<em v-dompurify-html="tm('Footer_AgeRating')"></em>
</button>
<div v-if="showAgeRating" class="game-rating-card absolute bottom-6 left-1 md:left-1/2 md:-translate-x-1/2 bg-[#383838] rounded-lg border border-white/30 w-[340px] mx-auto z-10">
<!-- 헤더 -->
<div class="px-6 py-4 rounded-t-lg flex justify-between items-center">
<h3 class="text-white text-base">{{ tm('Footer_AgeRating') }}</h3>
<button class="text-white hover:text-gray-300 transition-colors" @click="toggleAgeRating">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<NuxtLink
:to="footerMenuItem.url"
:target="footerMenuItem.target"
:class="[
footerMenuItem.active === 'y' && 'text-white/50',
index === 2 && 'text-[#fff]',
'hover:text-gray-600 transition-colors',
]"
>
{{ footerMenuItem.title }}
</NuxtLink>
</li>
<li class="relative">
<button
class="text-[15px] text-white/50 hover:text-gray-600 transition-colors"
@click="toggleAgeRating"
>
<em v-dompurify-html="tm('Footer_AgeRating')"></em>
</button>
<div
v-if="showAgeRating"
class="game-rating-card absolute bottom-6 left-1 md:left-1/2 md:-translate-x-1/2 bg-[#383838] rounded-lg border border-white/30 w-[340px] mx-auto z-10"
>
<!-- 헤더 -->
<div
class="px-6 py-4 rounded-t-lg flex justify-between items-center"
>
<h3 class="text-white text-base">
{{ tm('Footer_AgeRating') }}
</h3>
<button
class="text-white hover:text-gray-300 transition-colors"
@click="toggleAgeRating"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
</div>
<!-- 등급 아이콘 그리드 -->
<div class="px-6 pt-2 pb-6">
<!-- 아이콘은 52x60 사이즈 갭은 4px 4개씩 그리드 레이아웃 -->
<div class="grid grid-cols-4 gap-[4px] mb-4 max-w-[220px] justify-start items-start">
<!-- 19 등급 -->
<div v-for="image in getGameRatingImage" :key="image" class="text-center">
<img :src="image" alt="게임이용등급안내" class="w-full h-full object-contain" />
<!-- 등급 아이콘 그리드 -->
<div class="px-6 pt-2 pb-6">
<!-- 아이콘은 52x60 사이즈 갭은 4px 4개씩 그리드 레이아웃 -->
<div
class="grid grid-cols-4 gap-[4px] mb-4 max-w-[220px] justify-start items-start"
>
<!-- 19 등급 -->
<div
v-for="image in getGameRatingImage"
:key="image"
class="text-center"
>
<img
:src="image"
alt="게임이용등급안내"
class="w-full h-full object-contain"
/>
</div>
<div
v-for="image in getContentInfoImage"
:key="image"
class="text-center"
>
<img
:src="image"
alt="게임이용등급안내"
class="w-full h-full object-contain"
/>
</div>
</div>
<div v-for="image in getContentInfoImage" :key="image" class="text-center">
<img :src="image" alt="게임이용등급안내" class="w-full h-full object-contain" />
</div>
</div>
</div>
<!-- 정보 테이블 -->
<div class="px-6 py-6 rounded-b-lg bg-[#A31639]">
<div class="space-y-2">
<div class="flex flex-start border-b border-white/10 pb-2">
<span class="text-white text-sm flex-1">{{ footerAgeRatingInfo[0] }}</span>
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.title }}</span>
</div>
<div class="flex flex-start border-b border-white/10 pb-2">
<span class="text-white text-sm flex-1">{{ footerAgeRatingInfo[1] }}</span>
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.rating_grade }}</span>
</div>
<div class="flex flex-start border-b border-white/10 pb-2">
<span class="text-white text-sm flex-1">{{ footerAgeRatingInfo[2] }}</span>
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.reg_no }}</span>
</div>
<div class="flex flex-start border-b border-white/10 pb-2">
<span class="text-white text-sm flex-1">{{ footerAgeRatingInfo[3] }}</span>
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.prod_date }}</span>
</div>
<div class="flex flex-start">
<span class="text-white text-sm flex-1">{{ footerAgeRatingInfo[4] }}</span>
<span class="text-white text-sm flex-1">{{ footerData.game_rating_info.rating_class_no }}</span>
</div>
<!-- 정보 테이블 -->
<div class="px-6 py-6 rounded-b-lg bg-[#A31639]">
<div class="space-y-2">
<div class="flex flex-start border-b border-white/10 pb-2">
<span class="text-white text-sm flex-1">
{{ footerAgeRatingInfo[0] }}
</span>
<span class="text-white text-sm flex-1">
{{ footerData.game_rating_info.title }}
</span>
</div>
<div class="flex flex-start border-b border-white/10 pb-2">
<span class="text-white text-sm flex-1">
{{ footerAgeRatingInfo[1] }}
</span>
<span class="text-white text-sm flex-1">
{{ footerData.game_rating_info.rating_grade }}
</span>
</div>
<div class="flex flex-start border-b border-white/10 pb-2">
<span class="text-white text-sm flex-1">
{{ footerAgeRatingInfo[2] }}
</span>
<span class="text-white text-sm flex-1">
{{ footerData.game_rating_info.reg_no }}
</span>
</div>
<div class="flex flex-start border-b border-white/10 pb-2">
<span class="text-white text-sm flex-1">
{{ footerAgeRatingInfo[3] }}
</span>
<span class="text-white text-sm flex-1">
{{ footerData.game_rating_info.prod_date }}
</span>
</div>
<div class="flex flex-start">
<span class="text-white text-sm flex-1">
{{ footerAgeRatingInfo[4] }}
</span>
<span class="text-white text-sm flex-1">
{{ footerData.game_rating_info.rating_class_no }}
</span>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="address-area mt-4 hidden sm:block">
<address class="not-italic text-white/50">
<div class="row my-1.5 leading-5">
<span
v-dompurify-html="tm('Footer_Address')"
class="text-[13px] [&_a]:cursor-pointer [&_a]:text-white/50 [&_a]:underline"
></span>
</div>
</li>
</ul>
</div>
<div class="address-area mt-4 hidden sm:block">
<address class="not-italic text-white/50">
<div class="row my-1.5 leading-5">
<span
v-dompurify-html="tm('Footer_Address')"
class="text-[13px] [&_a]:cursor-pointer [&_a]:text-white/50 [&_a]:underline"
></span>
</div>
</address>
</div>
<div class="language-area static md:absolute bottom-7 right-10 text-white mt-5 md:mt-0 md:bottom-5.5 md:right-4">
<BlocksLanguageSwitcher />
</div>
<div class="mt-6 md:mt-6 hidden sm:block">
<div v-dompurify-html="tm('Footer_caution')" class="text-xs text-white/30"></div>
</div>
<div class="copyright-area mt-6 text-[13px] text-white/50 md:mt-4">
<span v-dompurify-html="tm('Footer_Copyright')"></span>
</div>
<div class="logo-area flex mt-6 md:mt-6">
<a
href="https://www.smilegate.com"
target="_blank"
class="smilegate"
</address>
</div>
<div
class="language-area static md:absolute bottom-7 right-10 text-white mt-5 md:mt-0 md:bottom-5.5 md:right-4"
>
<img
:src="getImageHost(`/images/common/logo_smilegate.png`, { imageType: 'common' })"
alt="스마일게이트 로고"
class="w-auto h-auto"
/>
</a>
<a
v-if="setDevCi.dev_ci_yn"
:href="footerData.use_dev_ci_url ? footerData.dev_ci_url : '#'"
target="_blank"
class="nx3 ml-2.5 md:ml-4"
>
<img
:src="getImageHost(`${setDevCi.dev_ci_img_path}`, { imageType: 'game' })"
alt="CI"
class="block w-auto h-[22px]"
/>
</a>
</div>
</ClientOnly>
<BlocksLanguageSwitcher />
</div>
<div class="mt-6 md:mt-6 hidden sm:block">
<div
v-dompurify-html="tm('Footer_caution')"
class="text-xs text-white/30"
></div>
</div>
<div class="copyright-area mt-6 text-[13px] text-white/50 md:mt-4">
<span v-dompurify-html="tm('Footer_Copyright')"></span>
</div>
<div class="logo-area flex mt-6 md:mt-6">
<a href="https://www.smilegate.com" target="_blank" class="smilegate">
<img
:src="
getImageHost(`/images/common/logo_smilegate.png`, {
imageType: 'common',
})
"
alt="스마일게이트 로고"
class="w-auto h-auto"
/>
</a>
<a
v-if="setDevCi.dev_ci_yn"
:href="footerData.use_dev_ci_url ? footerData.dev_ci_url : '#'"
target="_blank"
class="nx3 ml-2.5 md:ml-4"
>
<img
:src="
getImageHost(`${setDevCi.dev_ci_img_path}`, {
imageType: 'game',
})
"
alt="CI"
class="block w-auto h-[22px]"
/>
</a>
</div>
</ClientOnly>
</div>
</footer>
</template>
<script setup lang="ts">
import type { FooterMenuItem, FooterData, DevCiConfig } from '#layers/types/Common'
import type {
FooterMenuItem,
FooterData,
DevCiConfig,
} from '#layers/types/Common'
const { tm }: any = useI18n()
@@ -149,7 +220,7 @@ const { gameData } = storeToRefs(gameDataStore)
// 공통다국어 data
const footerLinks = computed((): FooterMenuItem[] => {
const menu = (tm as any)('Footer_Menu')
return Array.isArray(menu) ? menu as FooterMenuItem[] : []
return Array.isArray(menu) ? (menu as FooterMenuItem[]) : []
})
const footerData = ref(gameData.value?.footer_json as unknown as FooterData)
const setDevCi = ref<DevCiConfig>({
@@ -158,18 +229,20 @@ const setDevCi = ref<DevCiConfig>({
})
///local/template/common/grades_age
const getGameRatingImage = computed((): string[] => {
const getGameRatingImage = computed((): string[] => {
const contentInfo = footerData.value.game_rating_info.rating_type.split(',')
const ageTypeMap: Record<string, string> = {
'12': 'Type12',
'15': 'Type15',
'19': 'Type19',
'all': 'TypeAll',
'e': 'TypeExempt'
all: 'TypeAll',
e: 'TypeExempt',
}
return contentInfo.map(item => {
const type = ageTypeMap[item] || 'TypeTest'
return getImageHost(`/images/common/grades_age/${type}.svg`, { imageType: 'common' })
return getImageHost(`/images/common/grades_age/${type}.svg`, {
imageType: 'common',
})
})
})
@@ -187,10 +260,16 @@ const getContentInfoImage = computed((): string[] => {
'7': 'Type-speculation',
}
return contentInfo.map(item => {
const type = contentTypeMap[item]
return type ? getImageHost(`/images/common/grades_use/${type}.svg`, { imageType: 'common' }) : ''
}).filter(Boolean)
return contentInfo
.map(item => {
const type = contentTypeMap[item]
return type
? getImageHost(`/images/common/grades_use/${type}.svg`, {
imageType: 'common',
})
: ''
})
.filter(Boolean)
})
const showAgeRating = ref<boolean>(false)
@@ -202,7 +281,6 @@ const footerAgeRatingInfo = computed((): string[] => {
const info = (tm as any)('Footer_AgeRating_Info')
return Array.isArray(info) ? info : []
})
</script>
<style scoped>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { onClickOutside, useWindowSize } from '@vueuse/core'
import { useGameDataStore } from '#layers/stores/useGameDataStore'
import { useScrollStore } from '#layers/stores/useScrollStore'
import type {
GameDataValue,
GameDataMenu,
@@ -13,9 +12,6 @@ import type {
const route = useRoute()
const { width } = useWindowSize()
const gameDataStore = useGameDataStore()
const scrollStore = useScrollStore()
const { isPassedStoveGnb, scrollGnbPosition } = storeToRefs(scrollStore)
const navAreaRef = ref<HTMLElement>()
const startRef = ref<HTMLElement>()
@@ -172,8 +168,7 @@ onBeforeUnmount(() => {
<template>
<header class="header">
<BlocksStoveGnbNew class="h-[48px]" />
<div class="game-wrap" :class="{ 'is-fixed': isPassedStoveGnb }">
<div class="game-wrap">
<AtomsLocaleLink to="/brand" class="mx-auto md:hidden">
<img
:src="getImageHost(gnbData?.bi_path)"
@@ -185,10 +180,7 @@ onBeforeUnmount(() => {
<AtomsIconsMenuBoldLine class="mx-auto" />
<span class="sr-only">menu open</span>
</button>
<div
:class="['gnb-game', { 'is-open': isMenuOpen }]"
:style="{ '--scroll-position': scrollGnbPosition + 'px' }"
>
<div :class="['nav-wrap', { 'is-open': isMenuOpen }]">
<div ref="navAreaRef" class="nav-area">
<div class="nav-logo">
<AtomsLocaleLink to="/brand" @click="handleMenuClose">
@@ -359,15 +351,12 @@ onBeforeUnmount(() => {
<style scoped>
.header {
@apply bg-theme-foreground text-theme-foreground-reversal relative z-[100];
@apply bg-theme-foreground text-theme-foreground-reversal relative z-[110];
}
.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]
@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]
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 left-0;
}
.game-logo {
@apply mx-auto shrink-0 md:mx-0;
}
@@ -383,17 +372,16 @@ onBeforeUnmount(() => {
@apply top-[11px] left-[12px];
}
.gnb-game {
@apply absolute top-0 left-0 w-0 md:relative md:w-full md:!h-full;
height: calc(100vh - var(--scroll-position));
.nav-wrap {
@apply fixed top-0 left-0 bottom-0 w-0 mt-[var(--scroll-position,48px)] md:relative md:w-full md:h-full md:mt-0;
}
.gnb-game.is-open {
.nav-wrap.is-open {
@apply w-full;
}
.gnb-game.is-open::before {
.nav-wrap.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 {
.nav-wrap.is-open .nav-area {
@apply h-full translate-x-0 transition-transform duration-300 md:transform-none;
}

View File

@@ -11,11 +11,8 @@ interface Props {
}
const props = defineProps<Props>()
const { locale } = useI18n()
const mainRef = ref<HTMLElement>()
// 템플릿 레지스트리 사용
const { getTemplateComponent } = useTemplateRegistry()
// 개별 메타 태그 표시 여부 확인
@@ -62,7 +59,7 @@ watchEffect(() => {
</script>
<template>
<div ref="mainRef" class="main">
<div class="main-content">
<template
v-for="(template, index) in visibleTemplates"
:key="template.template_code ?? index"
@@ -74,7 +71,6 @@ watchEffect(() => {
/>
</template>
<BlocksUtileContainer
:parent-ref="mainRef"
:is-show-top-btn="pageData.use_top_btn ?? false"
:is-show-sns-btn="pageData.use_sns_btn ?? false"
/>
@@ -82,12 +78,12 @@ watchEffect(() => {
</template>
<style scoped>
.main {
@apply relative min-h-[200px] pt-[48px] md:min-h-[600px] md:pt-[64px] z-[91];
.main-content {
@apply relative min-h-[200px] pt-[48px] md:min-h-[800px] md:pt-[64px];
}
[data-theme='light'] {
.main {
.main-content {
@apply bg-theme-foreground;
}
}

View File

@@ -1,11 +1,8 @@
<script setup lang="ts">
</script>
<script setup lang="ts"></script>
<template>
<header class="header">
<BlocksStoveGnbNew class="min-h-[48px]" />
</header>
</template>
@@ -13,6 +10,4 @@
.header {
@apply bg-theme-foreground text-theme-foreground-reversal relative z-[100];
}
</style>
c