Merge branch 'feature/202501107-all' into feature/20251001-gil
This commit is contained in:
@@ -3,39 +3,47 @@
|
|||||||
<LayoutsStoveHeader />
|
<LayoutsStoveHeader />
|
||||||
|
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="!isLoading" class="flex-1 flex items-center justify-center p-25">
|
<div
|
||||||
|
v-if="!isLoading"
|
||||||
|
class="flex-1 flex items-center justify-center p-25"
|
||||||
|
>
|
||||||
<div class="flex flex-col items-center gap-6 w-full">
|
<div class="flex flex-col items-center gap-6 w-full">
|
||||||
<!-- Stove Logo -->
|
<!-- Stove Logo -->
|
||||||
<div class="flex items-center justify-center h-7">
|
<div class="flex items-center justify-center h-7">
|
||||||
<img
|
<img
|
||||||
src="/images/common/logo-stove.svg"
|
src="/images/common/logo-stove.svg"
|
||||||
alt="Stove"
|
alt="Stove"
|
||||||
class="h-full w-auto"
|
class="h-full w-auto"
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error Icon and Text -->
|
|
||||||
<div class="flex flex-col items-center gap-4 w-full">
|
|
||||||
<!-- Error Icon -->
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<img
|
|
||||||
src="/images/common/img_error.png"
|
|
||||||
alt="Error"
|
|
||||||
class="w-40 h-40 md:w-60 md:h-60"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error Text -->
|
<!-- Error Icon and Text -->
|
||||||
<div class="flex flex-col items-center gap-2 w-full">
|
<div class="flex flex-col items-center gap-4 w-full">
|
||||||
<h1 class="font-medium text-xl md:text-2xl leading-[1.5] tracking-[-0.03em] text-center text-[#1F1F1F] m-0">
|
<!-- Error Icon -->
|
||||||
<span v-dompurify-html="errorTitle"></span>
|
<div class="flex items-center justify-center">
|
||||||
</h1>
|
<img
|
||||||
<p v-dompurify-html="errorMsg" class="font-normal text-sm md:text-base leading-[1.7] md:leading-[1.625] tracking-[-0.03em] text-center text-[#666666] m-0"></p>
|
src="/images/common/img_error.png"
|
||||||
</div>
|
alt="Error"
|
||||||
</div>
|
class="w-40 h-40 md:w-60 md:h-60"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Home Button -->
|
<!-- Error Text -->
|
||||||
<AtomsButton
|
<div class="flex flex-col items-center gap-2 w-full">
|
||||||
|
<h1
|
||||||
|
class="font-medium text-xl md:text-2xl leading-[1.5] tracking-[-0.03em] text-center text-[#1F1F1F] m-0"
|
||||||
|
>
|
||||||
|
<span v-dompurify-html="errorTitle"></span>
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
v-dompurify-html="errorMsg"
|
||||||
|
class="font-normal text-sm md:text-base leading-[1.7] md:leading-[1.625] tracking-[-0.03em] text-center text-[#666666] m-0"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Home Button -->
|
||||||
|
<AtomsButton
|
||||||
type="action"
|
type="action"
|
||||||
button-size="size-small md:size-large"
|
button-size="size-small md:size-large"
|
||||||
background-color="#FC4420"
|
background-color="#FC4420"
|
||||||
@@ -50,7 +58,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
interface ErrorProps {
|
interface ErrorProps {
|
||||||
error?: {
|
error?: {
|
||||||
@@ -62,7 +69,7 @@ interface ErrorProps {
|
|||||||
|
|
||||||
const { tm } = useI18n()
|
const { tm } = useI18n()
|
||||||
const props = withDefaults(defineProps<ErrorProps>(), {
|
const props = withDefaults(defineProps<ErrorProps>(), {
|
||||||
error: () => ({})
|
error: () => ({}),
|
||||||
})
|
})
|
||||||
const nuxtError = useError()
|
const nuxtError = useError()
|
||||||
const currentError = computed(() => props.error || nuxtError.value)
|
const currentError = computed(() => props.error || nuxtError.value)
|
||||||
@@ -72,13 +79,12 @@ const isLoading = ref(true)
|
|||||||
const errorTitle = ref('')
|
const errorTitle = ref('')
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
|
|
||||||
//error clear 함수 생성
|
//error clear 함수 생성
|
||||||
const localePath = useLocalePath()
|
const localePath = useLocalePath()
|
||||||
|
|
||||||
// const handleError = () => clearError({ redirect: '/' })
|
// const handleError = () => clearError({ redirect: '/' })
|
||||||
const handleError = () => {
|
const handleError = () => {
|
||||||
window.location.href = localePath('/')
|
window.location.href = localePath('/')
|
||||||
// clearError({ redirect: `${localePath('/brand')}` })
|
|
||||||
}
|
}
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
// 에러 상태를 클리어하고 이전 페이지로 이동
|
// 에러 상태를 클리어하고 이전 페이지로 이동
|
||||||
@@ -90,15 +96,16 @@ const handleBack = () => {
|
|||||||
|
|
||||||
// 백스페이스 키 처리
|
// 백스페이스 키 처리
|
||||||
const handleKeydown = (e: KeyboardEvent) => {
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Backspace' &&
|
if (
|
||||||
!['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) {
|
e.key === 'Backspace' &&
|
||||||
|
!['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)
|
||||||
|
) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleBack()
|
handleBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 500 에러 발생 시 /error 페이지로 리다이렉트
|
// 500 에러 발생 시 /error 페이지로 리다이렉트
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
||||||
const statusCode = currentError.value?.statusCode
|
const statusCode = currentError.value?.statusCode
|
||||||
|
|
||||||
if (statusCode === 500) {
|
if (statusCode === 500) {
|
||||||
@@ -108,19 +115,16 @@ onMounted(() => {
|
|||||||
errorTitle.value = tm('Error_404_Not_Found')
|
errorTitle.value = tm('Error_404_Not_Found')
|
||||||
errorMsg.value = tm('Error_404_Not_Found2')
|
errorMsg.value = tm('Error_404_Not_Found2')
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeydown)
|
window.addEventListener('keydown', handleKeydown)
|
||||||
window.addEventListener('popstate', handleBack)
|
window.addEventListener('popstate', handleBack)
|
||||||
|
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('keydown', handleKeydown)
|
window.removeEventListener('keydown', handleKeydown)
|
||||||
window.removeEventListener('popstate', handleBack)
|
window.removeEventListener('popstate', handleBack)
|
||||||
})
|
})
|
||||||
|
</script>
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@import './base/_theme.css';
|
|
||||||
@import './base/_reset.css';
|
@import './base/_reset.css';
|
||||||
|
@import './base/_theme.css';
|
||||||
|
@import './base/_font.css';
|
||||||
@import './base/_transition.css';
|
@import './base/_transition.css';
|
||||||
|
|
||||||
|
@import './components/_scrollbar.css';
|
||||||
@import './components/_button.css';
|
@import './components/_button.css';
|
||||||
@import './components/_layout.css';
|
@import './components/_layout.css';
|
||||||
@import './components/_modal.css';
|
@import './components/_modal.css';
|
||||||
|
|||||||
92
layers/assets/css/base/_font.css
Normal file
92
layers/assets/css/base/_font.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
@import url(https://static-cdn.onstove.com/0.0.1/font/SpoqaSans/StoveUIFont-KR.css);
|
||||||
|
@import url(https://static-cdn.onstove.com/0.0.4/font/Inter/StoveFont-Global.css);
|
||||||
|
@import url(https://static-cdn.onstove.com/0.0.4/font-icon/StoveFont-Icon.css);
|
||||||
|
/* @import url(https://static-cdn.onstove.com/0.0.1/font/SpoqaSans/StoveFont-KR.css); */
|
||||||
|
|
||||||
|
:lang(ko) {
|
||||||
|
font-family:
|
||||||
|
Spoqa Han Sans Neo KR,
|
||||||
|
-apple-system,
|
||||||
|
Segoe UI,
|
||||||
|
Helvetica,
|
||||||
|
Arial,
|
||||||
|
sans-serif,
|
||||||
|
Apple Color Emoji,
|
||||||
|
Segoe UI Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
:lang(en) {
|
||||||
|
font-family:
|
||||||
|
inter Global,
|
||||||
|
-apple-system,
|
||||||
|
Segoe UI,
|
||||||
|
Helvetica,
|
||||||
|
Arial,
|
||||||
|
sans-serif,
|
||||||
|
Apple Color Emoji,
|
||||||
|
Segoe UI Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
:lang(zh),
|
||||||
|
:lang(zh-cn) {
|
||||||
|
font-family:
|
||||||
|
Microsoft YaHei UI,
|
||||||
|
-apple-system,
|
||||||
|
Segoe UI,
|
||||||
|
Helvetica,
|
||||||
|
Arial,
|
||||||
|
sans-serif,
|
||||||
|
Apple Color Emoji,
|
||||||
|
Segoe UI Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
:lang(zh-tw) {
|
||||||
|
font-family:
|
||||||
|
Microsoft JhengHei UI,
|
||||||
|
-apple-system,
|
||||||
|
Segoe UI,
|
||||||
|
Helvetica,
|
||||||
|
Arial,
|
||||||
|
sans-serif,
|
||||||
|
Apple Color Emoji,
|
||||||
|
Segoe UI Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
:lang(ja) {
|
||||||
|
font-family:
|
||||||
|
Meiryo Yu Gothic UI,
|
||||||
|
-apple-system,
|
||||||
|
Segoe UI,
|
||||||
|
Helvetica,
|
||||||
|
Arial,
|
||||||
|
sans-serif,
|
||||||
|
Apple Color Emoji,
|
||||||
|
Segoe UI Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
:lang(de),
|
||||||
|
:lang(es),
|
||||||
|
:lang(fr),
|
||||||
|
:lang(pt) {
|
||||||
|
font-family:
|
||||||
|
inter Global,
|
||||||
|
-apple-system,
|
||||||
|
Segoe UI,
|
||||||
|
Helvetica,
|
||||||
|
Arial,
|
||||||
|
sans-serif,
|
||||||
|
Apple Color Emoji,
|
||||||
|
Segoe UI Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
:lang(th) {
|
||||||
|
font-family:
|
||||||
|
Leelawadee UI,
|
||||||
|
-apple-system,
|
||||||
|
Segoe UI,
|
||||||
|
Helvetica,
|
||||||
|
Arial,
|
||||||
|
sans-serif,
|
||||||
|
Apple Color Emoji,
|
||||||
|
Segoe UI Emoji;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply min-w-[320px] bg-black;
|
@apply min-w-[320px] bg-black;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
body.scroll-lock {
|
body.scroll-lock {
|
||||||
@apply overflow-hidden;
|
@apply overflow-hidden;
|
||||||
|
|||||||
@@ -28,9 +28,6 @@
|
|||||||
.use-base th {
|
.use-base th {
|
||||||
@apply font-semibold bg-gray-50;
|
@apply font-semibold bg-gray-50;
|
||||||
}
|
}
|
||||||
.use-base tbody tr:nth-child(even) {
|
|
||||||
@apply bg-gray-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.use-base blockquote {
|
.use-base blockquote {
|
||||||
@apply border-l-4 border-gray-300 pl-4 italic text-gray-700;
|
@apply border-l-4 border-gray-300 pl-4 italic text-gray-700;
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
|
|
||||||
/* 표준형 Title Classes */
|
/* 표준형 Title Classes */
|
||||||
.title-xlg {
|
.title-xlg {
|
||||||
@apply line-clamp-4 text-[24px] font-[700] leading-[34px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-3 md:text-[50px] md:leading-[70px];
|
@apply line-clamp-4 text-[24px] font-[700] tracking-[-0.82px] leading-[34px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:tracking-[-1.5px] md:line-clamp-3 md:text-[50px] md:leading-[70px];
|
||||||
}
|
}
|
||||||
.title-lg {
|
.title-lg {
|
||||||
@apply line-clamp-4 text-[20px] font-[700] leading-[30px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-3 md:text-[42px] md:leading-[56px];
|
@apply line-clamp-4 text-[20px] font-[700] tracking-[-0.6px] leading-[30px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:tracking-[-1.26px] md:line-clamp-3 md:text-[42px] md:leading-[56px];
|
||||||
}
|
}
|
||||||
.title-md {
|
.title-md {
|
||||||
@apply line-clamp-2 text-[16px] font-[500] leading-[24px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-1 md:text-[24px] md:leading-[34px];
|
@apply line-clamp-2 text-[16px] font-[500] tracking-[-0.48px] leading-[24px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:tracking-[-0.72px] md:line-clamp-1 md:text-[24px] md:leading-[34px];
|
||||||
}
|
}
|
||||||
.title-sm {
|
.title-sm {
|
||||||
@apply text-[15px] font-[500] leading-[24px] tracking-[-0.45px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:text-[20px] md:leading-[30px] md:tracking-[-0.6px];
|
@apply text-[15px] font-[500] leading-[24px] tracking-[-0.45px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:text-[20px] md:leading-[30px] md:tracking-[-0.6px];
|
||||||
|
|||||||
14
layers/assets/css/components/_scrollbar.css
Normal file
14
layers/assets/css/components/_scrollbar.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
body ::-webkit-scrollbar-track {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
body ::-webkit-scrollbar {
|
||||||
|
@apply w-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body ::-webkit-scrollbar-thumb {
|
||||||
|
@apply w-1 bg-[#D9D9D9] rounded-full bg-clip-padding border-solid border-transparent border-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-theme-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-[var(--foreground-reversal-15)];
|
||||||
|
}
|
||||||
@@ -1,26 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
const scrollStore = useScrollStore()
|
|
||||||
|
|
||||||
const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AtomsButtonCircle
|
<AtomsButtonCircle sr-only="home" type="link" to="/" class="btn-home">
|
||||||
sr-only="home"
|
|
||||||
type="link"
|
|
||||||
to="/"
|
|
||||||
:class="['btn-home', { 'is-fixed': isPassedStoveGnb }]"
|
|
||||||
>
|
|
||||||
<AtomsIconsHomeFill />
|
<AtomsIconsHomeFill />
|
||||||
</AtomsButtonCircle>
|
</AtomsButtonCircle>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.btn-home {
|
.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]
|
@apply fixed top-3 right-3 mt-[calc(var(--scroll-position,48px)+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];
|
sm:top-5 md:top-6 md:right-8 md:mt-[calc(var(--scroll-position,64px)+64px)];
|
||||||
}
|
|
||||||
.btn-home.is-fixed {
|
|
||||||
@apply fixed;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PageDataLnbMenu } from '#layers/types/api/pageData'
|
import type { PageDataLnbMenu } from '#layers/types/api/pageData'
|
||||||
|
|
||||||
const { y: windowY } = useWindowScroll({ behavior: 'smooth' })
|
const { y: windowY, directions } = useWindowScroll({ behavior: 'smooth' })
|
||||||
const pageDataStore = usePageDataStore()
|
const pageDataStore = usePageDataStore()
|
||||||
const breakpoints = useResponsiveBreakpoints()
|
const breakpoints = useResponsiveBreakpoints()
|
||||||
|
|
||||||
const { pageData } = storeToRefs(pageDataStore)
|
const { pageData } = storeToRefs(pageDataStore)
|
||||||
|
|
||||||
const observerOptions = {
|
// 상수 정의
|
||||||
|
const HEADER_HEIGHT = 64
|
||||||
|
const OBSERVER_OPTIONS = {
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '-20% 0px -60% 0px', // 상단 20%, 하단 60% 마진
|
rootMargin: '-20% 0px -60% 0px', // 상단 20%, 하단 60% 마진
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
}
|
} as const
|
||||||
|
|
||||||
|
const isShowLnbWithScroll = ref(false)
|
||||||
const activeSection = ref<string>('')
|
const activeSection = ref<string>('')
|
||||||
|
|
||||||
const lnbList = computed<Record<string, PageDataLnbMenu>>(
|
const lnbList = computed<Record<string, PageDataLnbMenu>>(
|
||||||
@@ -39,8 +43,9 @@ const is1DepthActive = (lnbItem: PageDataLnbMenu): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// children 중 하나가 활성화된 경우
|
// children 중 하나가 활성화된 경우
|
||||||
if (lnbItem.children && Object.keys(lnbItem.children).length > 0) {
|
const children = lnbItem.children
|
||||||
return Object.values(lnbItem.children).some(
|
if (children && Object.keys(children).length > 0) {
|
||||||
|
return Object.values(children).some(
|
||||||
child => activeSection.value === child.page_ver_tmpl_name_en
|
child => activeSection.value === child.page_ver_tmpl_name_en
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -48,54 +53,73 @@ const is1DepthActive = (lnbItem: PageDataLnbMenu): boolean => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const observer = new IntersectionObserver(entries => {
|
// IntersectionObserver 콜백: 교차하는 섹션들 중 가장 위에 있는 것을 활성화
|
||||||
|
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
|
||||||
if (import.meta.server) return
|
if (import.meta.server) return
|
||||||
// 교차하는 섹션들 중 가장 위에 있는 것을 활성화
|
|
||||||
const visibleEntries = entries
|
const visibleEntries = entries
|
||||||
.filter(entry => entry.isIntersecting)
|
.filter(entry => entry.isIntersecting)
|
||||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)
|
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)
|
||||||
|
|
||||||
if (visibleEntries.length > 0) {
|
if (visibleEntries.length > 0) {
|
||||||
activeSection.value = visibleEntries[0].target.id
|
const topEntry = visibleEntries[0]
|
||||||
|
activeSection.value = topEntry.target.id
|
||||||
}
|
}
|
||||||
}, observerOptions)
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(handleIntersection, OBSERVER_OPTIONS)
|
||||||
|
|
||||||
|
// 요소 관찰 헬퍼 함수
|
||||||
|
const observeElement = (elementId: string) => {
|
||||||
|
const element = document.getElementById(elementId)
|
||||||
|
if (element) {
|
||||||
|
observer.observe(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 섹션들을 관찰 시작
|
||||||
const observeSections = () => {
|
const observeSections = () => {
|
||||||
if (import.meta.server) return
|
if (import.meta.server) return
|
||||||
|
|
||||||
Object.values(lnbList.value).forEach(lnbItem => {
|
Object.values(lnbList.value).forEach(lnbItem => {
|
||||||
// 1depth 관찰
|
// 1depth 관찰
|
||||||
const el = document.getElementById(lnbItem.page_ver_tmpl_name_en)
|
observeElement(lnbItem.page_ver_tmpl_name_en)
|
||||||
if (el) {
|
|
||||||
observer.observe(el)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2depth 관찰
|
// 2depth 관찰
|
||||||
if (lnbItem.children && Object.keys(lnbItem.children).length > 0) {
|
const children = lnbItem.children
|
||||||
Object.values(lnbItem.children).forEach(childItem => {
|
if (children && Object.keys(children).length > 0) {
|
||||||
const childEl = document.getElementById(childItem.page_ver_tmpl_name_en)
|
Object.values(children).forEach(childItem => {
|
||||||
if (childEl) {
|
observeElement(childItem.page_ver_tmpl_name_en)
|
||||||
observer.observe(childEl)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LNB 클릭 핸들러: 해당 섹션으로 스크롤
|
||||||
const handleLnbClick = (lnbItem: PageDataLnbMenu) => {
|
const handleLnbClick = (lnbItem: PageDataLnbMenu) => {
|
||||||
if (import.meta.server) return
|
if (import.meta.server) return
|
||||||
|
|
||||||
const id = lnbItem.page_ver_tmpl_name_en
|
const targetId =
|
||||||
const el = document.getElementById(id)
|
lnbItem.page_ver_tmpl_name_en === ''
|
||||||
if (!el) return
|
? lnbItem?.children?.['1']?.page_ver_tmpl_name_en
|
||||||
|
: lnbItem.page_ver_tmpl_name_en
|
||||||
|
|
||||||
const elementTop = el.getBoundingClientRect().top
|
const targetElement = document.getElementById(targetId)
|
||||||
|
if (!targetElement) return
|
||||||
|
|
||||||
|
// 헤더 높이를 고려한 스크롤 위치 계산
|
||||||
|
const elementTop = targetElement.getBoundingClientRect().top
|
||||||
const currentScrollY = window.scrollY
|
const currentScrollY = window.scrollY
|
||||||
const headerHeight = 64
|
const targetScrollY = currentScrollY + elementTop - HEADER_HEIGHT
|
||||||
const targetScrollY = currentScrollY + elementTop - headerHeight
|
|
||||||
|
|
||||||
windowY.value = targetScrollY
|
windowY.value = targetScrollY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(directions, newVal => {
|
||||||
|
// 스크롤 업일 때만 표시, 다운이거나 멈춘 상태에서는 숨김
|
||||||
|
isShowLnbWithScroll.value = newVal.top === true
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
observeSections()
|
observeSections()
|
||||||
})
|
})
|
||||||
@@ -106,79 +130,109 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<template v-if="isShowLnb">
|
||||||
v-if="isShowLnb"
|
<div
|
||||||
class="lnb-wrap"
|
:class="['lnb-wrap', { 'is-hidden': !isShowLnbWithScroll }]"
|
||||||
:style="{
|
:style="{
|
||||||
'--lnb-active-color': activeColor,
|
'--lnb-active-color': activeColor,
|
||||||
'--lnb-disable-color': disableColor,
|
'--lnb-disable-color': disableColor,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ul class="main-list">
|
<ul class="lnb-main">
|
||||||
<li v-for="lnbItem in lnbList" :key="lnbItem.path_code">
|
<li v-for="lnbItem in lnbList" :key="lnbItem.path_code">
|
||||||
<button
|
<button
|
||||||
v-dompurify-html="lnbItem.menu_name"
|
v-dompurify-html="lnbItem.menu_name"
|
||||||
type="button"
|
type="button"
|
||||||
:class="['btn-1depth', { 'is-active': is1DepthActive(lnbItem) }]"
|
:class="['btn-1depth', { 'is-active': is1DepthActive(lnbItem) }]"
|
||||||
@click="handleLnbClick(lnbItem)"
|
@click="handleLnbClick(lnbItem)"
|
||||||
></button>
|
></button>
|
||||||
<ul
|
<ul
|
||||||
v-if="Object.keys(lnbItem.children || {}).length > 0"
|
v-if="Object.keys(lnbItem.children || {}).length > 0"
|
||||||
class="sub-list"
|
class="lnb-sub"
|
||||||
>
|
>
|
||||||
<li v-for="subItem in lnbItem.children" :key="subItem.path_code">
|
<li v-for="subItem in lnbItem.children" :key="subItem.path_code">
|
||||||
<button
|
<button
|
||||||
v-dompurify-html="subItem.menu_name"
|
v-dompurify-html="subItem.menu_name"
|
||||||
type="button"
|
type="button"
|
||||||
:class="[
|
:class="[
|
||||||
'btn-2depth',
|
'btn-2depth',
|
||||||
{
|
{
|
||||||
'is-active': activeSection === subItem.page_ver_tmpl_name_en,
|
'is-active':
|
||||||
},
|
activeSection === subItem.page_ver_tmpl_name_en,
|
||||||
]"
|
},
|
||||||
@click="handleLnbClick(subItem)"
|
]"
|
||||||
></button>
|
@click="handleLnbClick(subItem)"
|
||||||
</li>
|
></button>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.lnb-wrap {
|
.lnb-wrap {
|
||||||
@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;
|
@apply fixed top-0 right-0 mt-[calc(var(--scroll-position,48px)+64px)] py-8 pr-10 bg-[radial-gradient(100%_50%_at_100%_50%,rgba(0,0,0,0.4)_25%,rgba(0,0,0,0)_100%)] transition-transform duration-[400ms] ease-in-out z-50;
|
||||||
}
|
}
|
||||||
.main-list {
|
.lnb-wrap:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -5%;
|
||||||
|
width: 105%;
|
||||||
|
height: 100%;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
mask-image: radial-gradient(
|
||||||
|
circle at right center,
|
||||||
|
#000 0%,
|
||||||
|
#000 40%,
|
||||||
|
transparent 80%
|
||||||
|
);
|
||||||
|
mask-size: 100% 100%;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.lnb-wrap.is-hidden {
|
||||||
|
@apply translate-x-[110%] delay-[800ms];
|
||||||
|
}
|
||||||
|
.lnb-main {
|
||||||
@apply flex flex-col gap-4 items-end;
|
@apply flex flex-col gap-4 items-end;
|
||||||
}
|
}
|
||||||
|
.lnb-main > li {
|
||||||
|
@apply flex flex-col items-end;
|
||||||
|
}
|
||||||
.btn-1depth {
|
.btn-1depth {
|
||||||
@apply text-[15px] leading-[26px] tracking-[-0.54px];
|
@apply text-[15px] leading-[26px] tracking-[-0.54px];
|
||||||
}
|
}
|
||||||
.sub-list {
|
.lnb-sub {
|
||||||
@apply flex flex-col gap-2 items-end mt-4 mb-1 pr-[46px];
|
@apply flex flex-col gap-2 items-end mt-4 mb-1 pr-[16px];
|
||||||
}
|
}
|
||||||
.btn-2depth {
|
.btn-2depth {
|
||||||
@apply text-[14px] leading-[20px] tracking-[-0.42px];
|
@apply text-[14px] leading-[20px] tracking-[-0.42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@apply relative font-[500] text-[var(--lnb-disable-color)] transition-all duration-300 ease-in-out;
|
@apply flex items-center font-[500] text-[var(--lnb-disable-color)] transition-all duration-300 ease-in-out;
|
||||||
}
|
}
|
||||||
button:hover,
|
button:hover,
|
||||||
button.is-active {
|
button.is-active {
|
||||||
@apply text-[var(--lnb-active-color)];
|
@apply text-[var(--lnb-active-color)];
|
||||||
}
|
}
|
||||||
button::before {
|
button::after {
|
||||||
@apply content-[''] absolute top-1/2 rounded-full -translate-y-1/2 bg-transparent transition-all duration-300 ease-in-out;
|
@apply content-[''] rounded-full ml-2 bg-[var(--lnb-disable-color)] transition-all duration-300 ease-in-out;
|
||||||
}
|
}
|
||||||
button.is-active::before {
|
button.is-active::after {
|
||||||
@apply bg-[var(--lnb-active-color)];
|
@apply bg-[var(--lnb-active-color)];
|
||||||
}
|
}
|
||||||
.btn-1depth::before {
|
.btn-1depth::after {
|
||||||
@apply -right-4 w-1.5 h-1.5;
|
@apply -right-4 w-1.5 h-1.5;
|
||||||
}
|
}
|
||||||
.btn-2depth::before {
|
.btn-2depth::after {
|
||||||
@apply -right-3.5 w-1 h-1;
|
@apply -right-3.5 w-1 h-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-promotion .lnb-wrap {
|
||||||
|
@apply mt-[calc(var(--scroll-position,48px)+64px+72px)];
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const loadGnb = (locale: string) => {
|
|||||||
const gnbOption = {
|
const gnbOption = {
|
||||||
wrapper: '#stove-wrap',
|
wrapper: '#stove-wrap',
|
||||||
isResponsive: true,
|
isResponsive: true,
|
||||||
skin: stoveGnbData?.skin_type || 'gnb-dark-mini',
|
skin: 'gnb-dark-mini',
|
||||||
widget: {
|
widget: {
|
||||||
gameListAndService: false,
|
gameListAndService: false,
|
||||||
languageSelect: false,
|
languageSelect: false,
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ onMounted(() => {
|
|||||||
mobile: '',
|
mobile: '',
|
||||||
},
|
},
|
||||||
widget: {
|
widget: {
|
||||||
notification: true,
|
notification: stoveGnbData?.notify_icon_visible || true,
|
||||||
stoveDownload: true,
|
stoveDownload: stoveGnbData?.stove_install_button_visible || true,
|
||||||
languageSelect: false,
|
languageSelect: false,
|
||||||
themeSelect: false,
|
themeSelect: false,
|
||||||
stoveMenu: {
|
stoveMenu: {
|
||||||
@@ -40,9 +40,8 @@ onMounted(() => {
|
|||||||
},
|
},
|
||||||
mode: {
|
mode: {
|
||||||
theme: {
|
theme: {
|
||||||
support: ['light', 'dark'],
|
support: designTheme === 1 ? ['light'] : ['dark'],
|
||||||
default: designTheme === 1 ? 'light' : 'dark',
|
default: designTheme === 1 ? 'light' : 'dark',
|
||||||
// support: designTheme === 1 ? ['light'] : ['dark'],
|
|
||||||
},
|
},
|
||||||
mini: true,
|
mini: true,
|
||||||
layout: 'wide',
|
layout: 'wide',
|
||||||
@@ -54,7 +53,7 @@ onMounted(() => {
|
|||||||
stoveGnbOptions
|
stoveGnbOptions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if(mountedInstance){
|
if (mountedInstance) {
|
||||||
//Stove GNB에서도 쿠키를 굽고 있어 소문자로 통일
|
//Stove GNB에서도 쿠키를 굽고 있어 소문자로 통일
|
||||||
//LOCALE 쿠키를 가져온 후 소문자로 변경해서 다시 LOCALE 쿠키를 설정
|
//LOCALE 쿠키를 가져온 후 소문자로 변경해서 다시 LOCALE 쿠키를 설정
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@@ -66,10 +65,8 @@ onMounted(() => {
|
|||||||
localeCookie.value = localeCookie.value.toLowerCase()
|
localeCookie.value = localeCookie.value.toLowerCase()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (mountedInstance && typeof mountedInstance.destroy === 'function') {
|
if (mountedInstance && typeof mountedInstance.destroy === 'function') {
|
||||||
mountedInstance.destroy()
|
mountedInstance.destroy()
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ watch(isOpen, newVal => {
|
|||||||
:class="[
|
:class="[
|
||||||
'content-tex',
|
'content-tex',
|
||||||
'use-base',
|
'use-base',
|
||||||
{ 'is-visible': isVisible(index) },
|
{ 'is-hidden': !isVisible(index) },
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -153,13 +153,4 @@ watch(isOpen, newVal => {
|
|||||||
.content-tex {
|
.content-tex {
|
||||||
@apply overflow-y-auto mb-4 px-6 sm:mb-6 sm:px-8;
|
@apply overflow-y-auto mb-4 px-6 sm:mb-6 sm:px-8;
|
||||||
}
|
}
|
||||||
.content-tex::-webkit-scrollbar-track {
|
|
||||||
@apply bg-transparent;
|
|
||||||
}
|
|
||||||
.content-tex::-webkit-scrollbar {
|
|
||||||
@apply w-5;
|
|
||||||
}
|
|
||||||
.content-tex::-webkit-scrollbar-thumb {
|
|
||||||
@apply w-1 bg-[#D9D9D9] rounded-full bg-clip-padding border-solid border-transparent border-8;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ const mainOptions = computed<Options>(() => ({
|
|||||||
const thumbOptions = computed<Options>(() => ({
|
const thumbOptions = computed<Options>(() => ({
|
||||||
type: 'slide',
|
type: 'slide',
|
||||||
rewind: true,
|
rewind: true,
|
||||||
// focus: 'center',
|
|
||||||
autoWidth: true,
|
autoWidth: true,
|
||||||
perMove: 1,
|
perMove: 1,
|
||||||
arrows: true,
|
arrows: true,
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import type {
|
|||||||
|
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
const gameDomain = useGetGameDomain()
|
const gameDomain = useGetGameDomain()
|
||||||
const scrollStore = useScrollStore()
|
|
||||||
|
|
||||||
const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
|
||||||
|
|
||||||
const isEventNavigationOpen = ref(true)
|
const isEventNavigationOpen = ref(true)
|
||||||
const eventNavigationList = ref<Record<string, EventNavigation>>({})
|
const eventNavigationList = ref<Record<string, EventNavigation>>({})
|
||||||
@@ -52,7 +49,6 @@ onMounted(async () => {
|
|||||||
class="event-navigation"
|
class="event-navigation"
|
||||||
:class="{
|
:class="{
|
||||||
'is-closed': !isEventNavigationOpen,
|
'is-closed': !isEventNavigationOpen,
|
||||||
'is-fixed': isPassedStoveGnb,
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="navigation-wrapper">
|
<div class="navigation-wrapper">
|
||||||
@@ -90,10 +86,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.event-navigation {
|
.event-navigation {
|
||||||
@apply absolute top-0 left-0 bottom-0 mt-[48px] md:mt-[64px] z-[100] transition-transform duration-300 ease-in-out;
|
@apply fixed top-0 left-0 bottom-0 mt-[calc(var(--scroll-position,48px)+48px)] md:mt-[calc(var(--scroll-position,64px)+64px)] z-[100] transition-transform duration-300 ease-in-out;
|
||||||
}
|
|
||||||
.event-navigation.is-fixed {
|
|
||||||
@apply fixed;
|
|
||||||
}
|
}
|
||||||
.navigation-wrapper {
|
.navigation-wrapper {
|
||||||
@apply relative h-full p-3 sm:p-5 sm:pr-3
|
@apply relative h-full p-3 sm:p-5 sm:pr-3
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onClickOutside, useWindowSize } from '@vueuse/core'
|
|
||||||
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
import { useGameDataStore } from '#layers/stores/useGameDataStore'
|
||||||
import type {
|
import type {
|
||||||
GameDataMenu,
|
GameDataMenu,
|
||||||
@@ -9,6 +8,14 @@ import type {
|
|||||||
PlatformTransformType,
|
PlatformTransformType,
|
||||||
} from '#layers/types/api/gameData'
|
} from '#layers/types/api/gameData'
|
||||||
|
|
||||||
|
const MORE_WIDTH = 72
|
||||||
|
const START_WIDTH_MARGIN = 40
|
||||||
|
const PLATFORM_LABEL_KEY: Record<PlatformTransformType, string> = {
|
||||||
|
pc: 'PC',
|
||||||
|
google_play: 'Google Play',
|
||||||
|
app_store: 'App Store',
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { tm } = useI18n()
|
const { tm } = useI18n()
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
@@ -21,21 +28,23 @@ const modalStore = useModalStore()
|
|||||||
const { gameData } = storeToRefs(gameDataStore)
|
const { gameData } = storeToRefs(gameDataStore)
|
||||||
const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
||||||
|
|
||||||
const navAreaRef = ref<HTMLElement>()
|
const navAreaRef = ref<HTMLElement | null>(null)
|
||||||
const startRef = ref<HTMLElement>()
|
const startRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const gnbData = gameData.value?.gnb
|
const { width: startWidth } = useElementSize(startRef)
|
||||||
|
|
||||||
|
const isMounted = ref(false)
|
||||||
const isMenuOpen = ref(false)
|
const isMenuOpen = ref(false)
|
||||||
const navWidth = ref(0)
|
const navWidth = ref(0)
|
||||||
const startWidth = ref(0)
|
|
||||||
const officialItemWidths = ref<number[]>([])
|
const officialItemWidths = ref<number[]>([])
|
||||||
const overflowNam = ref<number>(0)
|
const overflowNam = ref<number>(0)
|
||||||
|
|
||||||
|
const gnbData = computed(() => gameData.value?.gnb)
|
||||||
const gnb1depthButtonData = computed(
|
const gnb1depthButtonData = computed(
|
||||||
() => gnbData?.buttons[0]?.button_json as GameDataResourceGroup
|
() => gnbData.value?.buttons[0]?.button_json as GameDataResourceGroup
|
||||||
)
|
)
|
||||||
const gnb2depthButtonData = computed(
|
const gnb2depthButtonData = computed(
|
||||||
() => gnbData?.buttons[1]?.button_json as GameDataResourceGroupSet
|
() => gnbData.value?.buttons[1]?.button_json as GameDataResourceGroupSet
|
||||||
)
|
)
|
||||||
const currentPath = computed(() => formatPathWithoutLocale(route.path))
|
const currentPath = computed(() => formatPathWithoutLocale(route.path))
|
||||||
const supportedPlatforms = computed(
|
const supportedPlatforms = computed(
|
||||||
@@ -47,7 +56,7 @@ const supportedPlatforms = computed(
|
|||||||
|
|
||||||
const pathMatches = (base: string, current: string) => {
|
const pathMatches = (base: string, current: string) => {
|
||||||
if (!base || base === '/') return current === '/'
|
if (!base || base === '/') return current === '/'
|
||||||
return current === base || current.startsWith(base + '/')
|
return current === base
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 자식 중 활성 링크 존재 여부 */
|
/** 자식 중 활성 링크 존재 여부 */
|
||||||
@@ -76,20 +85,10 @@ const isNavItemActive = (gnbItem: GameDataMenu): boolean => {
|
|||||||
|
|
||||||
// navAreaRef의 넓이를 구하는 함수
|
// navAreaRef의 넓이를 구하는 함수
|
||||||
const calculateNavWidth = () => {
|
const calculateNavWidth = () => {
|
||||||
if (!navAreaRef.value || !gnbData) return 0
|
if (!navAreaRef.value || !gnbData.value) return 0
|
||||||
|
|
||||||
const navAreaWidth = navAreaRef.value.offsetWidth
|
const navAreaWidth = navAreaRef.value.offsetWidth
|
||||||
const moreWidth = 72 // 더보기 버튼 넓이 + 마진
|
navWidth.value = navAreaWidth + MORE_WIDTH
|
||||||
return navAreaWidth + moreWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
// startRef의 넓이를 구하는 함수
|
|
||||||
const calculateStartWidth = () => {
|
|
||||||
if (!startRef.value || !gnbData) return 0
|
|
||||||
|
|
||||||
const startWidth = startRef.value.offsetWidth
|
|
||||||
const headerRightPadding = 40 // 헤더 오른쪽 마진
|
|
||||||
return startWidth + headerRightPadding
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// official 자식들의 넓이를 구하는 함수
|
// official 자식들의 넓이를 구하는 함수
|
||||||
@@ -108,24 +107,20 @@ const calculateOfficialItemWidths = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
officialItemWidths.value = widths
|
officialItemWidths.value = widths
|
||||||
|
|
||||||
// 해상도 체크 및 오버플로우 계산
|
|
||||||
calculateOverflow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 오버플로우 계산 함수
|
// 오버플로우 계산 함수
|
||||||
const calculateOverflow = () => {
|
const calculateOverflow = () => {
|
||||||
if (!navAreaRef.value || !startRef.value) return
|
if (!navAreaRef.value || !startRef.value) return
|
||||||
|
|
||||||
const totalNavWidth = navWidth.value + startWidth.value
|
if (breakpoints.value.isMobile) {
|
||||||
const screenWidth = width.value
|
|
||||||
|
|
||||||
// 모바일(1024px 미만)에서는 overflowNam을 0으로 설정
|
|
||||||
if (screenWidth < 1024) {
|
|
||||||
overflowNam.value = 0
|
overflowNam.value = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const screenWidth = width.value
|
||||||
|
const totalNavWidth = navWidth.value + startWidth.value + START_WIDTH_MARGIN
|
||||||
|
|
||||||
// 해상도가 navWidth + startWidth보다 작은 경우
|
// 해상도가 navWidth + startWidth보다 작은 경우
|
||||||
if (screenWidth < totalNavWidth) {
|
if (screenWidth < totalNavWidth) {
|
||||||
let removedCount = 0
|
let removedCount = 0
|
||||||
@@ -166,12 +161,6 @@ const has2depthButton = (gnbItem: GameDataMenu) => {
|
|||||||
|
|
||||||
const highlight = (text: string) => `<span class="highlight">${text}</span>`
|
const highlight = (text: string) => `<span class="highlight">${text}</span>`
|
||||||
|
|
||||||
const PLATFORM_LABEL_KEY: Record<PlatformTransformType, string> = {
|
|
||||||
pc: 'PC',
|
|
||||||
google_play: 'Google Play',
|
|
||||||
app_store: 'App Store',
|
|
||||||
}
|
|
||||||
|
|
||||||
const tmWithGameName = (key: string): string => {
|
const tmWithGameName = (key: string): string => {
|
||||||
const raw = tm(key)
|
const raw = tm(key)
|
||||||
if (typeof raw !== 'string') return ''
|
if (typeof raw !== 'string') return ''
|
||||||
@@ -215,7 +204,10 @@ const handleStartClick = () => {
|
|||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopClickOutside = onClickOutside(navAreaRef, () => handleMenuClose())
|
watchEffect(() => {
|
||||||
|
if (!startWidth.value) return // 0, null, undefined면 스킵
|
||||||
|
calculateOverflow()
|
||||||
|
})
|
||||||
|
|
||||||
// 화면 크기 변경 시 오버플로우 재계산
|
// 화면 크기 변경 시 오버플로우 재계산
|
||||||
watch(width, () => {
|
watch(width, () => {
|
||||||
@@ -224,51 +216,47 @@ watch(width, () => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
overflowNam.value = 0
|
overflowNam.value = 0
|
||||||
|
isMounted.value = true
|
||||||
|
|
||||||
|
// 초기 계산 시도
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (navAreaRef.value && startRef.value) {
|
calculateNavWidth()
|
||||||
navWidth.value = calculateNavWidth()
|
calculateOfficialItemWidths()
|
||||||
startWidth.value = calculateStartWidth()
|
calculateOverflow()
|
||||||
calculateOfficialItemWidths()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (stopClickOutside) {
|
|
||||||
stopClickOutside()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<BlocksStoveGnbNew class="h-[48px]" />
|
<BlocksStoveGnbNew class="h-[48px]" />
|
||||||
<div :class="['game-wrap', { 'is-fixed': isPassedStoveGnb }]">
|
<div :class="['game-wrap', { 'is-fixed': isPassedStoveGnb }]">
|
||||||
<NuxtLinkLocale to="/brand" class="mx-auto md:hidden router-link-active router-link-exact-active">
|
<AtomsLocaleLink to="/" class="mx-auto md:hidden">
|
||||||
<img
|
<img
|
||||||
:src="getImageHost(gnbData?.bi_path)"
|
:src="getImageHost(gnbData?.bi_path)"
|
||||||
:alt="gameData?.game_name"
|
:alt="gameData?.game_name"
|
||||||
class="h-[30px]"
|
class="h-[30px]"
|
||||||
/>
|
/>
|
||||||
</NuxtLinkLocale>
|
</atomslocalelink>
|
||||||
<button class="btn-open" @click="handleMenuOpen">
|
<button class="btn-open" @click="handleMenuOpen">
|
||||||
<AtomsIconsMenuBoldLine class="mx-auto" />
|
<AtomsIconsMenuBoldLine class="mx-auto" />
|
||||||
<span class="sr-only">menu open</span>
|
<span class="sr-only">menu open</span>
|
||||||
</button>
|
</button>
|
||||||
<div :class="['nav-wrap', { 'is-open': isMenuOpen }]">
|
<div
|
||||||
<div ref="navAreaRef" class="nav-area">
|
:class="['nav-wrap', { 'is-open': isMenuOpen }]"
|
||||||
|
@click="handleMenuClose"
|
||||||
|
>
|
||||||
|
<div ref="navAreaRef" class="nav-area" @click.stop>
|
||||||
<div class="nav-logo">
|
<div class="nav-logo">
|
||||||
<NuxtLinkLocale to="/brand" class="router-link-active router-link-exact-active" @click="handleMenuClose">
|
<AtomsLocaleLink to="/" @click="handleMenuClose">
|
||||||
<img
|
<img
|
||||||
:src="getImageHost(gnbData?.bi_path)"
|
:src="getImageHost(gnbData?.bi_path)"
|
||||||
:alt="gameData?.game_name"
|
:alt="gameData?.game_name"
|
||||||
class="h-[30px]"
|
class="h-[30px]"
|
||||||
/>
|
/>
|
||||||
</NuxtLinkLocale>
|
</AtomsLocaleLink>
|
||||||
</div>
|
<nav :class="['nav-list', { 'is-mounted': isMounted }]">
|
||||||
<nav class="nav-list">
|
<div v-if="gnbData?.menus" class="official custom-theme-scrollbar">
|
||||||
<div v-if="gnbData?.menus" class="official">
|
|
||||||
<div
|
<div
|
||||||
v-for="(gnbItem, key) in gnbData?.menus"
|
v-for="(gnbItem, key) in gnbData?.menus"
|
||||||
:key="key"
|
:key="key"
|
||||||
@@ -376,45 +364,48 @@ onBeforeUnmount(() => {
|
|||||||
class="nav-1depth text-gradient-pink"
|
class="nav-1depth text-gradient-pink"
|
||||||
>
|
>
|
||||||
<AtomsIconsStarFill />
|
<AtomsIconsStarFill />
|
||||||
<span>이벤트</span>
|
<span>{{ tm('Gnb_Event') }}</span>
|
||||||
<AtomsIconsStarFill />
|
<AtomsIconsStarFill />
|
||||||
</AtomsLocaleLink>
|
</AtomsLocaleLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div ref="startRef" class="btn-start">
|
<ClientOnly>
|
||||||
<ClientOnly>
|
<div ref="startRef" class="btn-start">
|
||||||
<component
|
<template v-if="gnb1depthButtonData">
|
||||||
:is="
|
<component
|
||||||
breakpoints.isDesktop ? 'BlocksButtonLauncher' : 'AtomsButton'
|
:is="
|
||||||
"
|
breakpoints.isDesktop
|
||||||
type="custom"
|
? 'BlocksButtonLauncher'
|
||||||
platform="pc"
|
: 'AtomsButton'
|
||||||
:background-color="
|
"
|
||||||
getColorCodeFromData(gnb1depthButtonData?.btn_info, 'btn')
|
type="custom"
|
||||||
"
|
platform="pc"
|
||||||
:text-color="
|
:background-color="
|
||||||
getColorCodeFromData(gnb1depthButtonData?.btn_info, 'txt')
|
getColorCodeFromData(gnb1depthButtonData?.btn_info, 'btn')
|
||||||
"
|
"
|
||||||
@click="handleStartClick"
|
:text-color="
|
||||||
>
|
getColorCodeFromData(gnb1depthButtonData?.btn_info, 'txt')
|
||||||
{{ gnb1depthButtonData?.btn_info?.txt_btn_name }}
|
"
|
||||||
</component>
|
@click="handleStartClick"
|
||||||
|
>
|
||||||
<div
|
{{ gnb1depthButtonData?.btn_info?.txt_btn_name }}
|
||||||
v-if="breakpoints.isDesktop && gnb2depthButtonData"
|
</component>
|
||||||
class="nav-2depth"
|
<div
|
||||||
>
|
v-if="breakpoints.isDesktop && gnb2depthButtonData"
|
||||||
<ul>
|
class="nav-2depth"
|
||||||
<li v-for="(item, key) in gnb2depthButtonData" :key="key">
|
>
|
||||||
<BlocksButtonLauncher type="custom" :platform="key">
|
<ul>
|
||||||
{{ item.btn_info?.txt_btn_name }}
|
<li v-for="(item, key) in gnb2depthButtonData" :key="key">
|
||||||
</BlocksButtonLauncher>
|
<BlocksButtonLauncher type="custom" :platform="key">
|
||||||
</li>
|
{{ item.btn_info?.txt_btn_name }}
|
||||||
</ul>
|
</BlocksButtonLauncher>
|
||||||
</div>
|
</li>
|
||||||
</ClientOnly>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</ClientOnly>
|
||||||
<button class="btn-close" @click="handleMenuClose">
|
<button class="btn-close" @click="handleMenuClose">
|
||||||
<AtomsIconsCloseLine
|
<AtomsIconsCloseLine
|
||||||
size="24"
|
size="24"
|
||||||
@@ -426,12 +417,12 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div></header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
.header {
|
||||||
@apply bg-theme-foreground text-theme-foreground-reversal relative z-[200];
|
@apply bg-theme-foreground text-theme-foreground-reversal relative font-[500] tracking-[-0.48px] z-[200];
|
||||||
}
|
}
|
||||||
.game-wrap {
|
.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 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]
|
||||||
@@ -471,7 +462,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.nav-area {
|
.nav-area {
|
||||||
@apply flex flex-col w-[100vw] max-w-[360px] min-w-[320px] bg-theme-foreground-10 translate-x-[-100%]
|
@apply flex flex-col w-[100vw] max-w-[360px] min-w-[320px] bg-theme-foreground-10 translate-x-[-100%]
|
||||||
md:inline-flex md:flex-row md:w-auto md:max-w-none md:h-full md:pl-[40px] md:items-center md:bg-transparent md:transform-none;
|
md:inline-flex md:flex-row md:w-auto md:max-w-[100%] md:h-full md:pl-[40px] md:items-center md:bg-transparent md:transform-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-logo {
|
.nav-logo {
|
||||||
@@ -480,7 +471,10 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.nav-list {
|
.nav-list {
|
||||||
@apply overflow-hidden flex flex-col order-1 h-full mt-2 mb-4 px-2
|
@apply overflow-hidden flex flex-col order-1 h-full mt-2 mb-4 px-2
|
||||||
md:flex-row md:order-none md:h-full md:my-0 md:ml-10 md:mr-6 md:px-0 md:overflow-visible;
|
md:flex-row md:order-none md:h-full md:my-0 md:ml-10 md:mr-6 md:px-0;
|
||||||
|
}
|
||||||
|
.nav-list.is-mounted {
|
||||||
|
@apply md:overflow-visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
@@ -528,6 +522,12 @@ onBeforeUnmount(() => {
|
|||||||
.official {
|
.official {
|
||||||
@apply overflow-x-hidden overflow-y-auto pb-2 md:flex md:items-center md:space-x-8 md:pb-0 md:overflow-visible;
|
@apply overflow-x-hidden overflow-y-auto pb-2 md:flex md:items-center md:space-x-8 md:pb-0 md:overflow-visible;
|
||||||
}
|
}
|
||||||
|
.custom-theme-scrollbar::-webkit-scrollbar {
|
||||||
|
@apply w-1;
|
||||||
|
}
|
||||||
|
.custom-theme-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
@apply border-0;
|
||||||
|
}
|
||||||
|
|
||||||
.more {
|
.more {
|
||||||
@apply relative hidden ml-[32px] pt-[11px] md:block;
|
@apply relative hidden ml-[32px] pt-[11px] md:block;
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ const props = defineProps<{
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.description {
|
.description {
|
||||||
@apply line-clamp-4 text-[15px] font-[400] leading-[24px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-3 md:text-[20px] md:leading-[30px];
|
@apply line-clamp-4 text-[15px] font-[400] tracking-[-0.45px] leading-[24px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:tracking-[-0.6px] md:line-clamp-3 md:text-[20px] md:leading-[30px];
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ const componentProps = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap items-end justify-between mb-[16px] md:mb-[24px]">
|
<div class="flex items-end justify-between mb-[16px] md:mb-[24px]">
|
||||||
<h3
|
<h3
|
||||||
class="text-[#1F1F1F] text-[18px] font-bold leading-[26px] tracking-[-0.54px] md:text-[24px] md:leading-[34px] md:tracking-[0.72px] shrink-0"
|
class="text-[#1F1F1F] text-[18px] font-bold leading-[26px] tracking-[-0.54px] md:text-[24px] md:leading-[34px] md:tracking-[0.72px] shrink-0"
|
||||||
>
|
>
|
||||||
<span>{{ props.title }}</span>
|
<span>{{ props.title }}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex items-center justify-between w-full">
|
<div class="flex items-center justify-between">
|
||||||
<slot />
|
<slot />
|
||||||
<p
|
<p
|
||||||
v-if="props.description && !props.link"
|
v-if="props.description && !props.link"
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ defineExpose({
|
|||||||
class="absolute left-0 right-0 top-0 bg-gradient-to-b from-[#292929] to-transparent z-[1] h-[24px] md:h-[32px]"
|
class="absolute left-0 right-0 top-0 bg-gradient-to-b from-[#292929] to-transparent z-[1] h-[24px] md:h-[32px]"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="overflow-y-auto h-full py-[24px] px-5 md:py-[32px] md:px-10"
|
class="overflow-y-auto h-full py-[24px] px-5 md:py-[32px] md:px-10 custom-theme-scrollbar"
|
||||||
>
|
>
|
||||||
<div class="px-3 py-4 md:px-6">
|
<div class="px-3 py-4 md:px-6">
|
||||||
<div class="flex cursor-pointer items-center gap-3 md:gap-4">
|
<div class="flex cursor-pointer items-center gap-3 md:gap-4">
|
||||||
@@ -308,7 +308,7 @@ defineExpose({
|
|||||||
<!-- Marketing Detail Content -->
|
<!-- Marketing Detail Content -->
|
||||||
<div
|
<div
|
||||||
v-if="isExpandedMarketing"
|
v-if="isExpandedMarketing"
|
||||||
class="mt-4 max-h-[160px] overflow-y-auto rounded-lg bg-white/[0.04] px-4 py-3"
|
class="mt-4 max-h-[160px] overflow-y-auto rounded-lg bg-white/[0.04] px-4 py-3 custom-theme-scrollbar"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
v-dompurify-html="tmWithGameName('Preregist_Agree_News_Info')"
|
v-dompurify-html="tmWithGameName('Preregist_Agree_News_Info')"
|
||||||
@@ -426,27 +426,4 @@ defineExpose({
|
|||||||
.modal-wrap:deep(.modal-close) svg {
|
.modal-wrap:deep(.modal-close) svg {
|
||||||
@apply fill-white;
|
@apply fill-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar for accordion content */
|
|
||||||
:deep(.overflow-y-auto) {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.overflow-y-auto::-webkit-scrollbar) {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.overflow-y-auto::-webkit-scrollbar-track) {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.overflow-y-auto::-webkit-scrollbar-thumb) {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.overflow-y-auto::-webkit-scrollbar-thumb:hover) {
|
|
||||||
background: rgba(255, 255, 255, 0.25);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|||||||
|
|
||||||
const serverGameData = getGameDataFromServer()
|
const serverGameData = getGameDataFromServer()
|
||||||
const gameDataStore = useGameDataStore()
|
const gameDataStore = useGameDataStore()
|
||||||
|
|
||||||
const { setGameData } = gameDataStore
|
const { setGameData } = gameDataStore
|
||||||
|
|
||||||
if (serverGameData) {
|
if (serverGameData) {
|
||||||
setGameData(serverGameData)
|
setGameData(serverGameData)
|
||||||
}
|
}
|
||||||
try{
|
try {
|
||||||
// 서버 사이드에서는 스킵
|
// 서버 사이드에서는 스킵
|
||||||
if (!import.meta.client) {
|
if (!import.meta.client) {
|
||||||
return
|
return
|
||||||
@@ -34,7 +35,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|||||||
|
|
||||||
// 쿼리스트링에서 f 파라미터 값 추출 (CSR용)
|
// 쿼리스트링에서 f 파라미터 값 추출 (CSR용)
|
||||||
// const fValue = (to.query.f as string) || ''
|
// const fValue = (to.query.f as string) || ''
|
||||||
|
|
||||||
// 미리보기 API 호출 처리
|
// 미리보기 API 호출 처리
|
||||||
// let finalGameDomain = currentDomain
|
// let finalGameDomain = currentDomain
|
||||||
// if (fValue === 'preview') {
|
// if (fValue === 'preview') {
|
||||||
@@ -52,9 +53,12 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|||||||
// const { getGameDataExternal } = useGetGameDataExternal()
|
// const { getGameDataExternal } = useGetGameDataExternal()
|
||||||
// await getGameDataExternal(req)
|
// await getGameDataExternal(req)
|
||||||
|
|
||||||
|
|
||||||
// error 페이지는 API 호출하지 않음
|
// error 페이지는 API 호출하지 않음
|
||||||
if (pageUrl === '/error' || to.path.includes('/error') || to.path.includes('/inspection')) {
|
if (
|
||||||
|
pageUrl === '/error' ||
|
||||||
|
to.path.includes('/error') ||
|
||||||
|
to.path.includes('/inspection')
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,17 +68,17 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|||||||
// 현재 언어가 허용된 언어 목록에 없으면 에러 페이지로 이동
|
// 현재 언어가 허용된 언어 목록에 없으면 에러 페이지로 이동
|
||||||
if (currentLangCode && !allowedLangCodes.includes(currentLangCode)) {
|
if (currentLangCode && !allowedLangCodes.includes(currentLangCode)) {
|
||||||
return navigateTo(`/${currentLangCode}/error`, { external: true })
|
return navigateTo(`/${currentLangCode}/error`, { external: true })
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
showError(createError({
|
showError(
|
||||||
statusCode: error.statusCode,
|
createError({
|
||||||
statusMessage: error.message,
|
statusCode: error?.statusCode || error?.status || 500,
|
||||||
fatal: false, // 즉시 에러 페이지로
|
statusMessage:
|
||||||
data: { reason: 'post-not-found' }
|
error?.statusMessage || error?.message || 'Internal Server Error',
|
||||||
}))
|
fatal: false, // 즉시 에러 페이지로
|
||||||
|
data: { reason: 'post-not-found' },
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|||||||
const loadingStore = useLoadingStore()
|
const loadingStore = useLoadingStore()
|
||||||
const { getPathAfterLanguage } = usePathResolver()
|
const { getPathAfterLanguage } = usePathResolver()
|
||||||
|
|
||||||
|
const langCode = csrGetFinalLocale(to.path, gameData.value?.lang_codes)
|
||||||
const langCode = csrGetFinalLocale(
|
|
||||||
to.path,
|
|
||||||
gameData.value?.lang_codes,
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (to.path.includes('inspection')) {
|
if (to.path.includes('inspection')) {
|
||||||
@@ -33,18 +29,16 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pageUrl = getPathAfterLanguage(to.path)
|
const pageUrl = getPathAfterLanguage(to.path)
|
||||||
console.log("🚀 ~ pageUrl:", pageUrl)
|
|
||||||
|
|
||||||
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
|
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
|
||||||
if (
|
// if (
|
||||||
!pageUrl ||
|
// !pageUrl ||
|
||||||
pageUrl === '' ||
|
// pageUrl === '' ||
|
||||||
pageUrl === '/' ||
|
// pageUrl === '/' ||
|
||||||
pageUrl === `/${langCode}/`
|
// pageUrl === `/${langCode}/`
|
||||||
) {
|
// ) {
|
||||||
return navigateTo(`/${langCode}/brand`, { external: false })
|
// return navigateTo(`/${langCode}/brand`, { external: false })
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
// error 페이지는 API 호출하지 않음
|
// error 페이지는 API 호출하지 않음
|
||||||
if (pageUrl === '/error' || to.path.includes('/error')) {
|
if (pageUrl === '/error' || to.path.includes('/error')) {
|
||||||
@@ -61,7 +55,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|||||||
|
|
||||||
// 쿼리스트링에서 f 파라미터 값 추출 (CSR용)
|
// 쿼리스트링에서 f 파라미터 값 추출 (CSR용)
|
||||||
// const fValue = (to.query.f as string) || ''
|
// const fValue = (to.query.f as string) || ''
|
||||||
|
|
||||||
// // 미리보기 API 호출 처리
|
// // 미리보기 API 호출 처리
|
||||||
// let finalGameDomain = gameDomain
|
// let finalGameDomain = gameDomain
|
||||||
// if (fValue === 'preview') {
|
// if (fValue === 'preview') {
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ const cache = new LRUCache({
|
|||||||
* @param finalLocale - 최종 언어
|
* @param finalLocale - 최종 언어
|
||||||
* @param baseDomain - 기본 도메인
|
* @param baseDomain - 기본 도메인
|
||||||
*/
|
*/
|
||||||
function setFinalLocaleCookie(event: any, finalLocale: string, baseDomain: string) {
|
function setFinalLocaleCookie(
|
||||||
|
event: any,
|
||||||
|
finalLocale: string,
|
||||||
|
baseDomain: string
|
||||||
|
) {
|
||||||
setCookie(event, 'LOCALE', finalLocale.toLowerCase(), {
|
setCookie(event, 'LOCALE', finalLocale.toLowerCase(), {
|
||||||
domain: baseDomain,
|
domain: baseDomain,
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -95,7 +99,6 @@ function fnLocaleMiddleware(event: any, finalLocale: string) {
|
|||||||
// 쿼리스트링 포함 시 순수 경로만 추출
|
// 쿼리스트링 포함 시 순수 경로만 추출
|
||||||
arrPath = path.split('?')[0].split('/')
|
arrPath = path.split('?')[0].split('/')
|
||||||
queryString = path.split('?')[1]
|
queryString = path.split('?')[1]
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
arrPath = path.split('/')
|
arrPath = path.split('/')
|
||||||
queryString = ''
|
queryString = ''
|
||||||
@@ -134,7 +137,7 @@ export default defineEventHandler(async event => {
|
|||||||
if (event.node.res.headersSent || event.node.res.writableEnded) {
|
if (event.node.res.headersSent || event.node.res.writableEnded) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// const runType = `${config.public.runType}`
|
// const runType = `${config.public.runType}`
|
||||||
// console.log("🚀 ~ baseDomain:", config.public.baseDomain)
|
// console.log("🚀 ~ baseDomain:", config.public.baseDomain)
|
||||||
// const url = getRequestURL(event)
|
// const url = getRequestURL(event)
|
||||||
@@ -235,9 +238,14 @@ export default defineEventHandler(async event => {
|
|||||||
if (event.node.res.headersSent || event.node.res.writableEnded) {
|
if (event.node.res.headersSent || event.node.res.writableEnded) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 언어 코드 추출
|
// 2. 언어 코드 추출
|
||||||
finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers, initLangCodes, initDefaultLocale)
|
finalLocale = ssrGetFinalLocale(
|
||||||
|
event?.node.req.url,
|
||||||
|
event.node.req.headers,
|
||||||
|
initLangCodes,
|
||||||
|
initDefaultLocale
|
||||||
|
)
|
||||||
|
|
||||||
const path = event?.node.req.url || ''
|
const path = event?.node.req.url || ''
|
||||||
let queryStringF = ''
|
let queryStringF = ''
|
||||||
@@ -255,21 +263,22 @@ export default defineEventHandler(async event => {
|
|||||||
console.error('쿼리스트링 파싱 에러:', e)
|
console.error('쿼리스트링 파싱 에러:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 테스트용 500 에러 발생
|
// 테스트용 500 에러 발생
|
||||||
if (test500) {
|
if (test500) {
|
||||||
throw new Error('테스트용 500 에러 발생')
|
throw new Error('테스트용 500 에러 발생')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 미리보기 API 호출 처리
|
// 미리보기 API 호출 처리
|
||||||
if (fValue === 'preview') {
|
if (fValue === 'preview') {
|
||||||
cleanHost = 'samplegame.onstove.com'
|
cleanHost = 'samplegame.onstove.com'
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryParams: Record<string, string> = {
|
const queryParams: Record<string, string> = {
|
||||||
game_domain: cleanHost || '',
|
game_domain: cleanHost || '',
|
||||||
lang_code: finalLocale,
|
lang_code: finalLocale,
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = (await $fetch(apiUrl, {
|
const response = (await $fetch(apiUrl, {
|
||||||
query: queryParams,
|
query: queryParams,
|
||||||
})) as GameDataResponse | null
|
})) as GameDataResponse | null
|
||||||
@@ -283,7 +292,7 @@ export default defineEventHandler(async event => {
|
|||||||
event.context.gameData = response.value
|
event.context.gameData = response.value
|
||||||
event.context.googleAnalyticsId = response.value?.ga_code
|
event.context.googleAnalyticsId = response.value?.ga_code
|
||||||
|
|
||||||
// console.log('🚀 ~ gameData:', response.value)
|
console.log('🚀 ~ gameData:', response.value)
|
||||||
|
|
||||||
// 점검 데이터 조회
|
// 점검 데이터 조회
|
||||||
let inspectionData
|
let inspectionData
|
||||||
@@ -305,7 +314,6 @@ export default defineEventHandler(async event => {
|
|||||||
)
|
)
|
||||||
inspectionData = inspectionResponse?.value?.inspection
|
inspectionData = inspectionResponse?.value?.inspection
|
||||||
cache.set(cacheKey, inspectionData) // 캐시에 저장
|
cache.set(cacheKey, inspectionData) // 캐시에 저장
|
||||||
// console.log("🚀 ~ inspectionData:", inspectionData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +354,10 @@ export default defineEventHandler(async event => {
|
|||||||
// 허용된 IP 목록 확인
|
// 허용된 IP 목록 확인
|
||||||
if (!inspectionData?.ip_filter_list?.includes(clientIP)) {
|
if (!inspectionData?.ip_filter_list?.includes(clientIP)) {
|
||||||
// 허용되지 않은 IP인 경우 점검 페이지로 이동
|
// 허용되지 않은 IP인 경우 점검 페이지로 이동
|
||||||
if (!event.node.res.headersSent && !event.node.res.writableEnded) {
|
if (
|
||||||
|
!event.node.res.headersSent &&
|
||||||
|
!event.node.res.writableEnded
|
||||||
|
) {
|
||||||
event.node.res.statusCode = 302
|
event.node.res.statusCode = 302
|
||||||
event.node.res.setHeader('Location', inspectionPath)
|
event.node.res.setHeader('Location', inspectionPath)
|
||||||
event.node.res.end()
|
event.node.res.end()
|
||||||
@@ -404,6 +415,28 @@ export default defineEventHandler(async event => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// [Root Path Redirect to /home]
|
||||||
|
// 언어 코드만 있는 경로(예: /ko, /ko/)를 /home 리다이렉트
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
const normalizedPath = fullPath.endsWith('/')
|
||||||
|
? fullPath.slice(0, -1)
|
||||||
|
: fullPath
|
||||||
|
const localePath = `/${finalLocale}`
|
||||||
|
if (normalizedPath === localePath) {
|
||||||
|
const defaultPath = `/${finalLocale}/home`
|
||||||
|
const queryString = event?.node.req.url?.includes('?')
|
||||||
|
? '?' + event.node.req.url.split('?')[1]
|
||||||
|
: ''
|
||||||
|
|
||||||
|
if (!event.node.res.headersSent && !event.node.res.writableEnded) {
|
||||||
|
event.node.res.statusCode = 302
|
||||||
|
event.node.res.setHeader('Location', defaultPath + queryString)
|
||||||
|
event.node.res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------
|
||||||
// [Locale Middleware]
|
// [Locale Middleware]
|
||||||
// -------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------
|
||||||
@@ -412,7 +445,7 @@ export default defineEventHandler(async event => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('gameData load error:', error)
|
console.error('gameData load error:', error)
|
||||||
|
|
||||||
// 500 에러 발생 시 /error 페이지로 리다이렉트
|
// 500 에러 발생 시 /error 페이지로 리다이렉트
|
||||||
if (!event.node.res.headersSent && !event.node.res.writableEnded) {
|
if (!event.node.res.headersSent && !event.node.res.writableEnded) {
|
||||||
// 언어 코드 추출 시도
|
// 언어 코드 추출 시도
|
||||||
@@ -429,7 +462,7 @@ export default defineEventHandler(async event => {
|
|||||||
}
|
}
|
||||||
// finalLocale이 undefined인 경우 기본값으로 'ko' 설정
|
// finalLocale이 undefined인 경우 기본값으로 'ko' 설정
|
||||||
|
|
||||||
console.log("🚀 ~ 여기도 타? error:", error)
|
console.log('🚀 ~ 여기도 타? error:', error)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: error.statusCode,
|
statusCode: error.statusCode,
|
||||||
statusMessage: error.statusMessage,
|
statusMessage: error.statusMessage,
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ const handleLoadMoreRecent = () => {
|
|||||||
class="relative content-static bg-[#fff] rounded-[12px] md:rounded-[16px]"
|
class="relative content-static bg-[#fff] rounded-[12px] md:rounded-[16px]"
|
||||||
>
|
>
|
||||||
<BlocksSlideFade
|
<BlocksSlideFade
|
||||||
|
:arrows="recommendedVideos.length > 1"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
:drag="false"
|
:drag="false"
|
||||||
@move="handleSplideMove"
|
@move="handleSplideMove"
|
||||||
|
|||||||
@@ -70,9 +70,11 @@ const onArrowClick = (direction, targetIndex) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="pt-[80px] pb-[100px] md:pt-[120px] md:pb-[140px]">
|
<section class="section-standard">
|
||||||
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
|
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
|
||||||
<div class="content-standard px-0 max-w-[2043px] mx-auto">
|
<div
|
||||||
|
class="content-standard justify-start pt-[80px] px-0 max-w-[2043px] mx-auto md:pt-[120px]"
|
||||||
|
>
|
||||||
<WidgetsMainTitle
|
<WidgetsMainTitle
|
||||||
v-if="mainTitleData"
|
v-if="mainTitleData"
|
||||||
:resources-data="mainTitleData"
|
:resources-data="mainTitleData"
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ const onArrowClick = direction => {
|
|||||||
<section class="section-standard">
|
<section class="section-standard">
|
||||||
<BlocksSlideFade
|
<BlocksSlideFade
|
||||||
v-if="slideData"
|
v-if="slideData"
|
||||||
:arrows="true"
|
:arrows="slideData.length > 1"
|
||||||
:pagination="true"
|
:pagination="slideData.length > 1"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
:pagination-data="paginationData"
|
:pagination-data="paginationData"
|
||||||
@arrow-click="onArrowClick"
|
@arrow-click="onArrowClick"
|
||||||
|
|||||||
@@ -212,8 +212,8 @@ export type StoveGnbSkinType =
|
|||||||
|
|
||||||
// Stove GNB 타입
|
// Stove GNB 타입
|
||||||
export interface GameDataStoveGnb {
|
export interface GameDataStoveGnb {
|
||||||
skin_type: StoveGnbSkinType
|
notify_icon_visible: boolean
|
||||||
stove_install_button_visible: string
|
stove_install_button_visible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// 이벤트 배너 타입
|
// 이벤트 배너 타입
|
||||||
|
|||||||
BIN
public/images/sample/GR_BOARD_01/common/bg.jpg
Normal file
BIN
public/images/sample/GR_BOARD_01/common/bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/images/sample/GR_BOARD_01/common/bg_m.jpg
Normal file
BIN
public/images/sample/GR_BOARD_01/common/bg_m.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 396 KiB |
BIN
public/images/sample/GR_CONTENTS_01/common/bg.jpg
Normal file
BIN
public/images/sample/GR_CONTENTS_01/common/bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
BIN
public/images/sample/GR_CONTENTS_01/common/bg_m.jpg
Normal file
BIN
public/images/sample/GR_CONTENTS_01/common/bg_m.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/images/sample/GR_CONTENTS_01/ko/img_slide01.png
Normal file
BIN
public/images/sample/GR_CONTENTS_01/ko/img_slide01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 563 KiB |
BIN
public/images/sample/GR_CONTENTS_01/ko/img_slide01_m.png
Normal file
BIN
public/images/sample/GR_CONTENTS_01/ko/img_slide01_m.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Reference in New Issue
Block a user