feat. 이벤트 네비게이션 추가
This commit is contained in:
54
app/app.vue
54
app/app.vue
@@ -16,6 +16,7 @@ const scrollStore = useScrollStore()
|
||||
const { setGameData } = gameDataStore
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
const { youtube, confirm, alert, toast, handleResetYoutube } = modalStore
|
||||
const { scrollGnbPosition } = storeToRefs(scrollStore)
|
||||
|
||||
const metaData = ref<GameDataMetaTag | null>(null)
|
||||
|
||||
@@ -51,13 +52,22 @@ const setupAllMetaData = (data: GameDataValue) => {
|
||||
]
|
||||
|
||||
// 색상 CSS 변수 생성
|
||||
const cssVariables = Object.entries(data.key_color_json ?? {})
|
||||
const cssColorVariables = Object.entries(data.key_color_json ?? {})
|
||||
.map(([key, value]) => `--${key}: ${value};`)
|
||||
.join('\n ')
|
||||
.join('\n ')
|
||||
|
||||
const commImgVariables =
|
||||
data.comm_img_json?.groups
|
||||
?.map(
|
||||
({ img_name, img_path }) =>
|
||||
`--${img_name}: url(${getImageHost(img_path?.comm ?? '')});`
|
||||
)
|
||||
.join('\n ') ?? ''
|
||||
|
||||
const cssContent = `
|
||||
:root {
|
||||
${cssVariables}
|
||||
${cssColorVariables}
|
||||
${commImgVariables}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -81,7 +91,7 @@ const setupAllMetaData = (data: GameDataValue) => {
|
||||
style: [
|
||||
{
|
||||
innerHTML: cssContent,
|
||||
id: 'game-color-variables',
|
||||
id: 'game-css-variables',
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -109,21 +119,34 @@ gtag('event', 'screen_view', {
|
||||
screen_name: 'Home',
|
||||
})
|
||||
|
||||
let rafId: number | null = null
|
||||
|
||||
onMounted(() => {
|
||||
useEventListener('scroll', scrollStore.updateScrollValue)
|
||||
if (gameData.value?.comm_img_json?.groups) {
|
||||
const groups = gameData.value.comm_img_json.groups
|
||||
groups.forEach(({ img_name, img_path }) => {
|
||||
document.documentElement.style.setProperty(
|
||||
`--${img_name}`,
|
||||
`url(${getImageHost(img_path?.comm ?? '')})`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
scrollGnbPosition,
|
||||
newValue => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
rafId = requestAnimationFrame(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
'--scroll-position',
|
||||
`${newValue}px`
|
||||
)
|
||||
rafId = null
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeEventListener('scroll', scrollStore.updateScrollValue)
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -160,10 +183,7 @@ onBeforeUnmount(() => {
|
||||
:modal-name="alert.storeModalName"
|
||||
@confirm-button-event="alert.storeConfirmButtonEvent"
|
||||
/>
|
||||
<BlocksModalToast
|
||||
v-model:is-open="toast.storeIsOpen"
|
||||
:content-text="toast.storeContentText"
|
||||
/>
|
||||
<BlocksModalToast />
|
||||
|
||||
<!-- 로딩 컴포넌트 -->
|
||||
<AtomsLoadingFull />
|
||||
|
||||
@@ -46,10 +46,12 @@ onMounted(() => {
|
||||
mini: true,
|
||||
layout: 'wide',
|
||||
fixed: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
mountedInstance = (window as any).StoveGnb.mount('#stove-wrap', stoveGnbOptions)
|
||||
|
||||
mountedInstance = (window as any).StoveGnb.mount(
|
||||
'#stove-wrap',
|
||||
stoveGnbOptions
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -61,22 +63,17 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="stove-gnb-new">
|
||||
<div id="stove-wrap" class="relative z-[5]" />
|
||||
</div>
|
||||
<div id="stove-wrap" class="relative h-[48px] z-[5]" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stove-gnb-new {
|
||||
@apply h-[48px];
|
||||
}
|
||||
[data-theme='light'] {
|
||||
.stove-gnb-new {
|
||||
#stove-wrap {
|
||||
@apply bg-white;
|
||||
}
|
||||
}
|
||||
[data-theme='dark'] {
|
||||
.stove-gnb-new {
|
||||
#stove-wrap {
|
||||
@apply bg-black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
parentRef: HTMLElement | null
|
||||
isShowTopBtn: boolean
|
||||
isShowSnsBtn: boolean
|
||||
}
|
||||
|
||||
const { height: viewportH } = useWindowSize()
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isShowTopBtn: false,
|
||||
isShowSnsBtn: false,
|
||||
})
|
||||
|
||||
const parentEl = toRef(props, 'parentRef')
|
||||
const { bottom: parentBottom } = useElementBounding(parentEl)
|
||||
|
||||
const pinToParent = computed(() => {
|
||||
if (!parentBottom.value) return false
|
||||
return parentBottom.value <= viewportH.value
|
||||
})
|
||||
const pinToMain = inject('pinToMain')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<div :class="['utile-container', { 'is-fixed': pinToParent }]">
|
||||
<div :class="['utile-container', { 'is-stop': pinToMain }]">
|
||||
<AtomsButtonScrollTop v-if="props.isShowTopBtn" />
|
||||
<AtomsButtonSns v-if="props.isShowSnsBtn" />
|
||||
</div>
|
||||
@@ -35,7 +26,7 @@ const pinToParent = computed(() => {
|
||||
@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-fixed {
|
||||
.utile-container.is-stop {
|
||||
@apply absolute;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,39 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
contentText: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isOpen: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const isVisible = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
newVal => {
|
||||
if (newVal) {
|
||||
isVisible.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
isVisible.value = false
|
||||
emit('close')
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
)
|
||||
const modalStore = useModalStore()
|
||||
const { toast } = modalStore
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div v-if="isVisible" class="toast-container">
|
||||
<div v-if="toast.storeIsOpen" class="toast-container">
|
||||
<p class="toast-text">
|
||||
{{ contentText }}
|
||||
{{ toast.storeContentText }}
|
||||
</p>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
135
layers/components/layouts/EventNavigation.vue
Normal file
135
layers/components/layouts/EventNavigation.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
63
layers/composables/useCwmsArticle.ts
Normal file
63
layers/composables/useCwmsArticle.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type {
|
||||
getCwmsArticleParams,
|
||||
CwmsArticleResponse,
|
||||
CwmsArticle,
|
||||
} from '#layers/types/api/cwmsArticle'
|
||||
|
||||
export const useCwmsArticle = () => {
|
||||
/**
|
||||
*
|
||||
* @param articleGroupCode 게시판 그룹 코드 (예: 128093)
|
||||
* @param options 옵션 객체
|
||||
* @returns 게시판 글 목록 응답
|
||||
*/
|
||||
const getCwmsArticle = async (
|
||||
articleGroupCode: string,
|
||||
articleGroupSeq: number,
|
||||
params: getCwmsArticleParams
|
||||
): Promise<CwmsArticle | null> => {
|
||||
const {
|
||||
lang,
|
||||
sortTypeCode,
|
||||
interactionTypeCodes,
|
||||
handleCode,
|
||||
contentYn,
|
||||
summaryYn,
|
||||
headlineTitleYn,
|
||||
translationYn,
|
||||
page,
|
||||
size,
|
||||
} = params
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const apiUrl = `${stoveApiBaseUrl}/cwms/v3.0/article_group/${articleGroupCode}/${articleGroupSeq}/article/list`
|
||||
|
||||
const queryParams: Record<string, string | number | boolean> = {
|
||||
lang: lang,
|
||||
sort_type_code: sortTypeCode,
|
||||
interaction_type_code: interactionTypeCodes?.join(','),
|
||||
handle_code: handleCode ? 'Y' : 'N',
|
||||
content_yn: contentYn ? 'Y' : 'N',
|
||||
summary_yn: summaryYn ? 'Y' : 'N',
|
||||
headline_title_yn: headlineTitleYn ? 'Y' : 'N',
|
||||
translation_yn: translationYn ? 'Y' : 'N',
|
||||
page: page,
|
||||
size: size,
|
||||
}
|
||||
|
||||
const response = (await commonFetch('GET', apiUrl, {
|
||||
query: queryParams,
|
||||
loading: true,
|
||||
})) as CwmsArticleResponse | null
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
return response.value
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
getCwmsArticle,
|
||||
}
|
||||
}
|
||||
47
layers/composables/useOperateResources.ts
Normal file
47
layers/composables/useOperateResources.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type {
|
||||
getOperateResourcesParams,
|
||||
OperateResourcesResponse,
|
||||
OperateComponents,
|
||||
} from '#layers/types/api/operateResources'
|
||||
|
||||
export const useOperateResources = () => {
|
||||
/**
|
||||
*
|
||||
* @param params getOperateResourcesDataParams
|
||||
* @returns OperateComponents | null
|
||||
* @description 운영 리소스 데이터 조회
|
||||
*/
|
||||
const getOperateResources = async (
|
||||
params: getOperateResourcesParams
|
||||
): Promise<OperateComponents | null> => {
|
||||
const { pageSeq, pageVer, pageVerTmplSeq, langCode, q, qc } = params
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/operateResources`
|
||||
|
||||
const queryParams: Record<string, string | number> = {
|
||||
page_seq: pageSeq,
|
||||
page_ver: pageVer,
|
||||
page_ver_tmpl_seq: pageVerTmplSeq,
|
||||
lang_code: langCode,
|
||||
q: q || '',
|
||||
qc: qc || '',
|
||||
_t: Date.now().toString(), // 캐시 무효화를 위한 타임스탬프
|
||||
}
|
||||
|
||||
const response = (await commonFetch('GET', apiUrl, {
|
||||
query: queryParams,
|
||||
loading: true,
|
||||
})) as OperateResourcesResponse | null
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
return response.value
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
getOperateResources,
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import type {
|
||||
getOperateResourcesDataParams,
|
||||
OperateResourcesDataResponse,
|
||||
OperateComponents,
|
||||
getCwmsArticleDataParams,
|
||||
CwmsArticleDataResponse,
|
||||
CwmsArticleData,
|
||||
} from '#layers/types/api/resourcesData'
|
||||
|
||||
export const useResourcesData = () => {
|
||||
/**
|
||||
*
|
||||
* @param params getOperateResourcesDataParams
|
||||
* @returns OperateComponents | null
|
||||
* @description 운영 리소스 데이터 조회
|
||||
*/
|
||||
const getOperateResourcesData = async (
|
||||
params: getOperateResourcesDataParams
|
||||
): Promise<OperateComponents | null> => {
|
||||
const { pageSeq, pageVer, pageVerTmplSeq, langCode, q, qc } = params
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/operateResources`
|
||||
|
||||
const queryParams: Record<string, string | number> = {
|
||||
page_seq: pageSeq,
|
||||
page_ver: pageVer,
|
||||
page_ver_tmpl_seq: pageVerTmplSeq,
|
||||
lang_code: langCode,
|
||||
q: q || '',
|
||||
qc: qc || '',
|
||||
_t: Date.now().toString(), // 캐시 무효화를 위한 타임스탬프
|
||||
}
|
||||
|
||||
const response = (await commonFetch('GET', apiUrl, {
|
||||
query: queryParams,
|
||||
loading: true,
|
||||
})) as OperateResourcesDataResponse | null
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
return response.value
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param articleGroupCode 게시판 그룹 코드 (예: 128093)
|
||||
* @param options 옵션 객체
|
||||
* @returns 게시판 글 목록 응답
|
||||
*/
|
||||
const getCwmsArticleData = async (
|
||||
articleGroupCode: string,
|
||||
articleGroupSeq: number,
|
||||
params: getCwmsArticleDataParams
|
||||
): Promise<CwmsArticleData | null> => {
|
||||
const {
|
||||
lang,
|
||||
sortTypeCode,
|
||||
interactionTypeCodes,
|
||||
handleCode,
|
||||
contentYn,
|
||||
summaryYn,
|
||||
headlineTitleYn,
|
||||
translationYn,
|
||||
page,
|
||||
size,
|
||||
} = params
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const apiUrl = `${stoveApiBaseUrl}/cwms/v3.0/article_group/${articleGroupCode}/${articleGroupSeq}/article/list`
|
||||
|
||||
const queryParams: Record<string, string | number | boolean> = {
|
||||
lang: lang,
|
||||
sort_type_code: sortTypeCode,
|
||||
interaction_type_code: interactionTypeCodes?.join(','),
|
||||
handle_code: handleCode ? 'Y' : 'N',
|
||||
content_yn: contentYn ? 'Y' : 'N',
|
||||
summary_yn: summaryYn ? 'Y' : 'N',
|
||||
headline_title_yn: headlineTitleYn ? 'Y' : 'N',
|
||||
translation_yn: translationYn ? 'Y' : 'N',
|
||||
page: page,
|
||||
size: size,
|
||||
}
|
||||
|
||||
const response = (await commonFetch('GET', apiUrl, {
|
||||
query: queryParams,
|
||||
loading: true,
|
||||
})) as CwmsArticleDataResponse | null
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
return response.value
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
getOperateResourcesData,
|
||||
getCwmsArticleData,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const isLoading = ref(true)
|
||||
const maintRef = ref<HTMLElement>()
|
||||
|
||||
const { height: viewportH } = useWindowSize()
|
||||
const { bottom: mainBottom } = useElementBounding(maintRef)
|
||||
|
||||
const pinToMain = computed(() => {
|
||||
if (!mainBottom.value) return false
|
||||
return mainBottom.value <= viewportH.value
|
||||
})
|
||||
|
||||
provide('pinToMain', pinToMain)
|
||||
|
||||
onMounted(() => {
|
||||
isLoading.value = false
|
||||
@@ -9,15 +20,8 @@ onMounted(() => {
|
||||
<template>
|
||||
<LayoutsHeader />
|
||||
<AtomsLoadingSimple :is-loading="isLoading" />
|
||||
<main id="LayoutsMain">
|
||||
<main id="LayoutsMain" class="relative">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<LayoutsFooter />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#LayoutsMain {
|
||||
@apply relative min-h-[200px] md:min-h-[800px] z-[90];
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
const maintRef = ref<HTMLElement>()
|
||||
|
||||
const { height: viewportH } = useWindowSize()
|
||||
const { bottom: mainBottom } = useElementBounding(maintRef)
|
||||
|
||||
const pinToMain = computed(() => {
|
||||
if (!mainBottom.value) return false
|
||||
return mainBottom.value <= viewportH.value
|
||||
})
|
||||
|
||||
provide('pinToMain', pinToMain)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutsHeader />
|
||||
<slot />
|
||||
<main id="LayoutsMain" ref="maintRef" class="relative">
|
||||
<LayoutsEventNavigation />
|
||||
<slot />
|
||||
</main>
|
||||
<LayoutsFooter />
|
||||
</template>
|
||||
|
||||
@@ -44,7 +44,12 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
}
|
||||
|
||||
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
|
||||
if (!pageUrl || pageUrl === '' || pageUrl === '/' || pageUrl === `/${langCode}/`) {
|
||||
if (
|
||||
!pageUrl ||
|
||||
pageUrl === '' ||
|
||||
pageUrl === '/' ||
|
||||
pageUrl === `/${langCode}/`
|
||||
) {
|
||||
return navigateTo(`/${langCode}/brand`, { replace: true })
|
||||
}
|
||||
|
||||
@@ -54,15 +59,14 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
page_url: pageUrl,
|
||||
_t: Date.now().toString(), // 캐시 무효화를 위한 타임스탬프
|
||||
}
|
||||
// console.log('🚀 ~ queryParams:', queryParams)
|
||||
|
||||
const response = (await commonFetch('GET', apiUrl, {
|
||||
query: queryParams,
|
||||
loading: true,
|
||||
})) as PageDataResponse | null
|
||||
console.log('🚀 ~ response?.code:', response?.code)
|
||||
console.log('🚀 ~ pageData.global response:', response.value)
|
||||
|
||||
if(response?.code === 91003) {
|
||||
if (response?.code === 91003) {
|
||||
return navigateTo(`/${langCode}/error`, { external: false })
|
||||
}
|
||||
|
||||
|
||||
@@ -101,14 +101,14 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
storeContentText: ref(''),
|
||||
}
|
||||
|
||||
const handleOpenToast = (contentText: string) => {
|
||||
const handleOpenToast = (contentText: string, duration: number = 2000) => {
|
||||
toast.storeIsOpen.value = true
|
||||
toast.storeContentText.value = contentText
|
||||
}
|
||||
|
||||
const handleCloseToast = () => {
|
||||
toast.storeIsOpen.value = false
|
||||
toast.storeContentText.value = ''
|
||||
setTimeout(() => {
|
||||
toast.storeIsOpen.value = false
|
||||
toast.storeContentText.value = ''
|
||||
}, duration)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -121,6 +121,5 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
handleOpenYoutube,
|
||||
handleResetYoutube,
|
||||
handleOpenToast,
|
||||
handleCloseToast,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import { getComponentGroup, getComponentGroupAry } from '#layers/utils/dataUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/operateResources'
|
||||
import type { Platform } from '#layers/types/components/button'
|
||||
|
||||
interface Props {
|
||||
@@ -27,7 +27,7 @@ const { tm, locale }: any = useI18n({
|
||||
messages: Object(resultGetMultilingual?.value?.multilingual),
|
||||
})
|
||||
|
||||
const { getOperateResourcesData } = useResourcesData()
|
||||
const { getOperateResources } = useOperateResources()
|
||||
const { gameData } = storeToRefs(useGameDataStore())
|
||||
const { pageData } = storeToRefs(usePageDataStore())
|
||||
|
||||
@@ -136,7 +136,7 @@ const { data: rewardCompletedData } = await useAsyncData(
|
||||
if (!page_seq || !page_ver) return []
|
||||
|
||||
try {
|
||||
const operateGroupList = await getOperateResourcesData({
|
||||
const operateGroupList = await getOperateResources({
|
||||
pageSeq: page_seq,
|
||||
pageVer: page_ver,
|
||||
pageVerTmplSeq: props.pageVerTmplSeq,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { getYouTubeThumbnail } from '#layers/utils/youtubeUtil'
|
||||
import type { Splide as SplideType } from '@splidejs/splide'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/operateResources'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
@@ -19,7 +19,7 @@ const props = defineProps<Props>()
|
||||
|
||||
const pageDataStore = usePageDataStore()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
const { getOperateResourcesData } = useResourcesData()
|
||||
const { getOperateResources } = useOperateResources()
|
||||
|
||||
const { pageData } = storeToRefs(pageDataStore)
|
||||
|
||||
@@ -53,7 +53,7 @@ const { data: slideData } = await useAsyncData(
|
||||
return []
|
||||
}
|
||||
|
||||
const operateGroupList = await getOperateResourcesData({
|
||||
const operateGroupList = await getOperateResources({
|
||||
pageSeq: pageData.value.page_seq,
|
||||
pageVer: pageData.value.page_ver,
|
||||
pageVerTmplSeq: props.pageVerTmplSeq,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import { globalDateFormat } from '@seed-next/date'
|
||||
import { getComponentGroup } from '#layers/utils/dataUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { CwmsArticleItem } from '#layers/types/api/resourcesData'
|
||||
import type { CwmsArticleItem } from '#layers/types/api/cwmsArticle'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
@@ -14,7 +14,7 @@ const props = defineProps<Props>()
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
const pageDataStore = usePageDataStore()
|
||||
const { getCwmsArticleData } = useResourcesData()
|
||||
const { getCwmsArticle } = useCwmsArticle()
|
||||
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
|
||||
const { locale } = useI18n()
|
||||
|
||||
@@ -45,7 +45,7 @@ const { data: slideData } = await useAsyncData(
|
||||
return []
|
||||
}
|
||||
|
||||
const cwmsArticleData = await getCwmsArticleData('BOARD', boardId.value, {
|
||||
const cwmsArticleData = await getCwmsArticle('BOARD', boardId.value, {
|
||||
sortTypeCode: 'LATEST',
|
||||
translationYn: true,
|
||||
page: 1,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '#layers/utils/dataUtil'
|
||||
import { getImageHost } from '#layers/utils/styleUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/operateResources'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const pageDataStore = usePageDataStore()
|
||||
const { getOperateResourcesData } = useResourcesData()
|
||||
const { getOperateResources } = useOperateResources()
|
||||
const { locale } = useI18n()
|
||||
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
|
||||
|
||||
@@ -43,7 +43,7 @@ const { data: slideData } = await useAsyncData(
|
||||
return []
|
||||
}
|
||||
|
||||
const operateGroupList = await getOperateResourcesData({
|
||||
const operateGroupList = await getOperateResources({
|
||||
pageSeq: pageData.value.page_seq,
|
||||
pageVer: pageData.value.page_ver,
|
||||
pageVerTmplSeq: props.pageVerTmplSeq,
|
||||
|
||||
@@ -1,44 +1,3 @@
|
||||
/**
|
||||
* Resources Data API 타입 정의
|
||||
*/
|
||||
|
||||
// OperateResources
|
||||
export interface OperateGroupItem {
|
||||
seq: number
|
||||
sort_order: number
|
||||
title: string
|
||||
img_path: string
|
||||
url: string
|
||||
link_target: string
|
||||
reg_dt: number
|
||||
option01: number
|
||||
option02: number
|
||||
option03: string
|
||||
}
|
||||
|
||||
export interface OperateGroupList {
|
||||
groups: OperateGroupItem[]
|
||||
}
|
||||
|
||||
export interface OperateComponents {
|
||||
[key: string]: OperateGroupList
|
||||
}
|
||||
|
||||
export interface OperateResourcesDataResponse {
|
||||
code: number
|
||||
message: string
|
||||
value: OperateComponents
|
||||
}
|
||||
|
||||
export interface getOperateResourcesDataParams {
|
||||
pageSeq: string
|
||||
pageVer: string
|
||||
pageVerTmplSeq: number
|
||||
langCode: string
|
||||
q?: string
|
||||
qc?: string
|
||||
}
|
||||
|
||||
// CwmsArticle
|
||||
export interface CwmsArticleAdminOptionSummary {
|
||||
notice_cancel_reservation_yn: 'Y' | 'N'
|
||||
@@ -93,20 +52,20 @@ export interface CwmsArticleItem {
|
||||
translation_status: string | null
|
||||
}
|
||||
|
||||
export interface CwmsArticleData {
|
||||
export interface CwmsArticle {
|
||||
total: number
|
||||
page: number
|
||||
size: number
|
||||
list: CwmsArticleItem[]
|
||||
}
|
||||
|
||||
export interface CwmsArticleDataResponse {
|
||||
export interface CwmsArticleResponse {
|
||||
code: number
|
||||
message: string
|
||||
value: CwmsArticleData
|
||||
value: CwmsArticle
|
||||
}
|
||||
|
||||
export interface getCwmsArticleDataParams {
|
||||
export interface getCwmsArticleParams {
|
||||
lang?: string
|
||||
sortTypeCode?: string
|
||||
interactionTypeCodes?: string[]
|
||||
23
layers/types/api/eventNavigation.ts
Normal file
23
layers/types/api/eventNavigation.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface EventNavigationParams {
|
||||
game_domain: string
|
||||
lang_code: string
|
||||
q?: string
|
||||
qc?: string
|
||||
}
|
||||
|
||||
export interface EventNavigationResponse {
|
||||
code: number
|
||||
message: string
|
||||
value: Record<string, EventNavigation>
|
||||
}
|
||||
|
||||
export interface EventNavigation {
|
||||
sort_order?: number
|
||||
banner_seq: number
|
||||
promotion_name: string
|
||||
thumbnail: string
|
||||
page_url_type?: number
|
||||
page_url?: string
|
||||
link_type?: number
|
||||
banner_title?: string
|
||||
}
|
||||
40
layers/types/api/operateResources.ts
Normal file
40
layers/types/api/operateResources.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Resources Data API 타입 정의
|
||||
*/
|
||||
|
||||
// OperateResources
|
||||
export interface OperateGroupItem {
|
||||
seq: number
|
||||
sort_order: number
|
||||
title: string
|
||||
img_path: string
|
||||
url: string
|
||||
link_target: string
|
||||
reg_dt: number
|
||||
option01: number
|
||||
option02: number
|
||||
option03: string
|
||||
}
|
||||
|
||||
export interface OperateGroupList {
|
||||
groups: OperateGroupItem[]
|
||||
}
|
||||
|
||||
export interface OperateComponents {
|
||||
[key: string]: OperateGroupList
|
||||
}
|
||||
|
||||
export interface OperateResourcesResponse {
|
||||
code: number
|
||||
message: string
|
||||
value: OperateComponents
|
||||
}
|
||||
|
||||
export interface getOperateResourcesParams {
|
||||
pageSeq: string
|
||||
pageVer: string
|
||||
pageVerTmplSeq: number
|
||||
langCode: string
|
||||
q?: string
|
||||
qc?: string
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
PageDataResourceGroup,
|
||||
PageDataResourceGroupType,
|
||||
} from '#layers/types/api/pageData'
|
||||
import type { OperateComponents } from '#layers/types/api/resourcesData'
|
||||
import type { OperateComponents } from '#layers/types/api/operateResources'
|
||||
|
||||
/**
|
||||
* 페이지 데이터를 기반으로 레이아웃 타입을 결정합니다.
|
||||
|
||||
Reference in New Issue
Block a user