Merge branch 'feature/202501107-all' into feature/20251001-gil
This commit is contained in:
32
app/app.vue
32
app/app.vue
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useNuxtApp } from 'nuxt/app'
|
||||
import { getImageHost } from '#layers/utils/styleUtil'
|
||||
import type {
|
||||
GameDataFavicon,
|
||||
GameDataMetaTag,
|
||||
@@ -15,7 +14,7 @@ const scrollStore = useScrollStore()
|
||||
|
||||
const { setGameData } = gameDataStore
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
const { youtube, confirm, alert, content, handleResetYoutube } = modalStore
|
||||
const { confirm, alert } = modalStore
|
||||
const { scrollGnbPosition } = storeToRefs(scrollStore)
|
||||
|
||||
const metaData = ref<GameDataMetaTag | null>(null)
|
||||
@@ -38,16 +37,16 @@ const setupAllMetaData = (data: GameDataValue) => {
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: getImageHost(faviconPath[0]),
|
||||
href: getResourceHost(faviconPath[0]),
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
href: getImageHost(faviconPath[1]),
|
||||
href: getResourceHost(faviconPath[1]),
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
href: getImageHost(faviconPath[2]),
|
||||
href: getResourceHost(faviconPath[2]),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -60,7 +59,7 @@ const setupAllMetaData = (data: GameDataValue) => {
|
||||
data.comm_img_json?.groups
|
||||
?.map(
|
||||
({ img_name, img_path }) =>
|
||||
`--${img_name}: url(${getImageHost(img_path?.comm ?? '')});`
|
||||
`--${img_name}: url(${getResourceHost(img_path?.comm ?? '')});`
|
||||
)
|
||||
.join('\n ') ?? ''
|
||||
|
||||
@@ -126,7 +125,7 @@ let stopWatch: (() => void) | null = null
|
||||
|
||||
onMounted(() => {
|
||||
if (!import.meta.client) return
|
||||
|
||||
|
||||
useEventListener('scroll', scrollStore.updateScrollValue)
|
||||
|
||||
stopWatch = watch(
|
||||
@@ -153,7 +152,7 @@ onBeforeUnmount(() => {
|
||||
stopWatch()
|
||||
stopWatch = null
|
||||
}
|
||||
|
||||
|
||||
// requestAnimationFrame 정리
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
@@ -168,21 +167,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 공통 모달 컴포넌트 -->
|
||||
<WidgetsModalClient />
|
||||
<BlocksModalContent
|
||||
v-model:is-open="content.storeIsOpen"
|
||||
:is-outside-close="content.storeIsOutsideClose"
|
||||
:content-title="content.storeContentTitle"
|
||||
:tab-info="content.storeTabInfo"
|
||||
:tab-active-index="content.storeTabActiveIndex"
|
||||
:modal-name="content.storeModalName"
|
||||
/>
|
||||
<BlocksModalYouTube
|
||||
v-model:is-open="youtube.storeIsOpen"
|
||||
:youtube-url="youtube.storeYoutubeUrl"
|
||||
:is-outside-close="youtube.storeIsOutsideClose"
|
||||
:modal-name="youtube.storeModalName"
|
||||
@close-button-event="handleResetYoutube"
|
||||
/>
|
||||
<BlocksModalYouTube />
|
||||
<BlocksModalContent />
|
||||
<BlocksModalConfirm
|
||||
v-model:is-open="confirm.storeIsOpen"
|
||||
:is-show-dimmed="confirm.storeIsShowDimmed"
|
||||
|
||||
@@ -213,7 +213,7 @@ const enabledMarkets = computed(() => {
|
||||
const logoImgUrl = computed(() => {
|
||||
const currentLocale = locale.value || 'ko'
|
||||
const localeData = (webInspectionData.value as any)?.[currentLocale]
|
||||
return getImageHost(localeData.img_json.bi_large)
|
||||
return getResourceHost(localeData.img_json.bi_large)
|
||||
})
|
||||
|
||||
const communityUrl = computed(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@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); */
|
||||
@import url(https://static-cdn.onstove.com/0.0.1/font/SpoqaSans/StoveFont-KR.css);
|
||||
|
||||
:lang(ko) {
|
||||
font-family:
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
@layer base {
|
||||
body {
|
||||
@apply min-w-[320px] bg-black;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
body.scroll-lock {
|
||||
|
||||
@@ -18,19 +18,20 @@
|
||||
.use-base table {
|
||||
@apply w-full border-collapse;
|
||||
}
|
||||
.use-base thead {
|
||||
@apply bg-gray-100;
|
||||
.use-base thead,
|
||||
.use-base tfoot {
|
||||
@apply bg-[#FAFAFA];
|
||||
}
|
||||
.use-base th,
|
||||
.use-base td {
|
||||
@apply border border-gray-300 px-4 py-2 text-left;
|
||||
@apply border border-[#D9D9D9] px-4 py-2 text-left;
|
||||
}
|
||||
.use-base th {
|
||||
@apply font-semibold bg-gray-50;
|
||||
@apply font-semibold border-[#D9D9D9];
|
||||
}
|
||||
|
||||
.use-base blockquote {
|
||||
@apply border-l-4 border-gray-300 pl-4 italic text-gray-700;
|
||||
@apply border-l-4 border-[#D9D9D9] pl-4 italic;
|
||||
}
|
||||
|
||||
.use-base h1 {
|
||||
@@ -68,10 +69,7 @@
|
||||
}
|
||||
|
||||
.use-base a {
|
||||
@apply text-blue-600 underline;
|
||||
}
|
||||
.use-base a:hover {
|
||||
@apply text-blue-800;
|
||||
@apply text-[#3C75FF] underline;
|
||||
}
|
||||
|
||||
.use-base img {
|
||||
@@ -79,10 +77,10 @@
|
||||
}
|
||||
|
||||
.use-base pre {
|
||||
@apply bg-gray-100 p-4 rounded overflow-x-auto mb-4;
|
||||
@apply bg-[#FAFAFA] p-4 rounded overflow-x-auto mb-4;
|
||||
}
|
||||
.use-base code {
|
||||
@apply bg-gray-100 px-1 py-0.5 rounded text-sm;
|
||||
@apply bg-[#FAFAFA] px-1 py-0.5 rounded text-sm;
|
||||
}
|
||||
.use-base pre code {
|
||||
@apply bg-transparent p-0;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.splide-pagination-bullet {
|
||||
@apply relative w-2 h-2 rounded-full opacity-100 md:w-3 md:h-3 transition-all duration-300 ease-in-out;
|
||||
@apply relative w-2 h-2 rounded-full opacity-100 md:w-3 md:h-3 transition-all duration-200 ease-in-out;
|
||||
background-color: var(--pagination-disabled);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ interface props {
|
||||
buttonSize?: string
|
||||
target?: '_self' | '_blank'
|
||||
href?: string
|
||||
rel?: string
|
||||
backgroundColor?: string
|
||||
textColor?: string
|
||||
disabled?: boolean
|
||||
@@ -15,7 +14,7 @@ interface props {
|
||||
const props = withDefaults(defineProps<props>(), {
|
||||
type: 'action',
|
||||
buttonSize: 'size-small md:size-large',
|
||||
target: '_blank',
|
||||
target: '_self',
|
||||
backgroundColor: 'var(--primary)',
|
||||
textColor: 'var(--alternative-02)',
|
||||
disabled: false,
|
||||
@@ -24,7 +23,6 @@ const props = withDefaults(defineProps<props>(), {
|
||||
const componentTag = computed((): string => {
|
||||
switch (props.type) {
|
||||
case 'external':
|
||||
case 'download':
|
||||
case 'link':
|
||||
return 'a'
|
||||
case 'internal':
|
||||
@@ -34,29 +32,20 @@ const componentTag = computed((): string => {
|
||||
}
|
||||
})
|
||||
const componentProps = computed(() => {
|
||||
const baseProps = { disabled: props.disabled }
|
||||
|
||||
if (
|
||||
props.type === 'external' ||
|
||||
props.type === 'download' ||
|
||||
props.type === 'link'
|
||||
) {
|
||||
if (props.type === 'external' || props.type === 'link') {
|
||||
return {
|
||||
...baseProps,
|
||||
href: props.href,
|
||||
target: props.target,
|
||||
rel: props.rel,
|
||||
}
|
||||
}
|
||||
|
||||
if (props.type === 'internal') {
|
||||
return {
|
||||
...baseProps,
|
||||
to: props.href,
|
||||
}
|
||||
}
|
||||
|
||||
return baseProps
|
||||
return {}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -64,25 +53,31 @@ const componentProps = computed(() => {
|
||||
<component
|
||||
:is="componentTag"
|
||||
v-bind="{ ...componentProps }"
|
||||
:class="['btn-base', props.buttonSize]"
|
||||
:class="['btn-base', props.buttonSize, { disabled: props.disabled }]"
|
||||
:style="{
|
||||
backgroundColor: props.backgroundColor,
|
||||
color: props.textColor,
|
||||
'--text-color': props.textColor,
|
||||
}"
|
||||
:disabled="props.disabled"
|
||||
>
|
||||
<span class="btn-content">
|
||||
<slot />
|
||||
<AtomsIconsLongArrowRightLine
|
||||
v-if="props.type === 'internal'"
|
||||
:color="props.textColor"
|
||||
class="icon"
|
||||
/>
|
||||
<AtomsIconsWebLinkLine
|
||||
v-if="props.type === 'external'"
|
||||
color="#ebebeb"
|
||||
:color="props.textColor"
|
||||
class="icon"
|
||||
/>
|
||||
<AtomsIconsDownloadLine
|
||||
v-if="props.type === 'download'"
|
||||
:color="props.textColor"
|
||||
class="icon"
|
||||
/>
|
||||
<AtomsIconsDownloadLine v-if="props.type === 'download'" class="icon" />
|
||||
</span>
|
||||
</component>
|
||||
</template>
|
||||
@@ -96,12 +91,12 @@ const componentProps = computed(() => {
|
||||
.btn-base:hover {
|
||||
@apply after:opacity-20;
|
||||
}
|
||||
.btn-base:disabled {
|
||||
@apply cursor-default
|
||||
after:bg-[var(--text-color)] after:opacity-20 after:z-[2];
|
||||
.btn-base.disabled {
|
||||
@apply cursor-default pointer-events-none
|
||||
after:bg-[var(--text-color)] after:opacity-20 after:z-[2];
|
||||
}
|
||||
|
||||
.btn-base:disabled .btn-content {
|
||||
.btn-base.disabled .btn-content {
|
||||
@apply opacity-50;
|
||||
}
|
||||
.btn-base .btn-content {
|
||||
|
||||
@@ -16,7 +16,7 @@ const isResponsiveMode = computed(() => {
|
||||
|
||||
const imagePaths = computed(() => {
|
||||
if (typeof props.src === 'string') {
|
||||
const resolved = getImageHost(props.src, {
|
||||
const resolved = getResourceHost(props.src, {
|
||||
imageType: props.imageType,
|
||||
})
|
||||
return { pc: '', mo: resolved }
|
||||
@@ -24,12 +24,12 @@ const imagePaths = computed(() => {
|
||||
|
||||
return {
|
||||
pc: props.src.pc
|
||||
? getImageHost(props.src.pc, {
|
||||
? getResourceHost(props.src.pc, {
|
||||
imageType: props.imageType,
|
||||
})
|
||||
: '',
|
||||
mo: props.src.mo
|
||||
? getImageHost(props.src.mo, {
|
||||
? getResourceHost(props.src.mo, {
|
||||
imageType: props.imageType,
|
||||
})
|
||||
: '',
|
||||
|
||||
422
layers/components/atoms/Tooltip.vue
Normal file
422
layers/components/atoms/Tooltip.vue
Normal file
@@ -0,0 +1,422 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
type?: 'click' | 'hover'
|
||||
position?:
|
||||
| 'top'
|
||||
| 'bottom'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-right'
|
||||
offset?: number
|
||||
backgroundColor?: string
|
||||
textColor?: string
|
||||
textAlign?: string
|
||||
fontSize?: string
|
||||
fontWeight?: string
|
||||
lineHeight?: string
|
||||
letterSpacing?: string
|
||||
arrow?: boolean
|
||||
teleport?: boolean
|
||||
content?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'click',
|
||||
position: 'top',
|
||||
offset: 8,
|
||||
arrow: true,
|
||||
teleport: false,
|
||||
backgroundColor: '#666666',
|
||||
textColor: '#FFFFFF',
|
||||
textAlign: 'center',
|
||||
fontSize: '12px',
|
||||
fontWeight: '400',
|
||||
lineHeight: '18px',
|
||||
letterSpacing: '-0.24px',
|
||||
})
|
||||
|
||||
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
||||
|
||||
const isLeftAligned = ref(false) // 브라우저 화면 기준으로 툴팁의 왼쪽 가장자리가 화면 밖으로 나가는 경우 좌측 정렬을 합니다.
|
||||
const isRightAligned = ref(false) // 브라우저 화면 기준으로 툴팁의 오른쪽 가장자리가 화면 밖으로 나가는 경우 우측 정렬을 합니다.
|
||||
const tooltipOffsetX = ref(18) // 툴팁 오프셋 X 값
|
||||
const tooltipArrowOffsetX = ref(26) // 툴팁 화살표 오프셋 X 값
|
||||
const tooltipPosition = ref({
|
||||
// 툴팁 위치 값
|
||||
top: null,
|
||||
bottom: null,
|
||||
left: null,
|
||||
right: null,
|
||||
})
|
||||
const tooltipTriggerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const isPositions = computed(() => {
|
||||
return {
|
||||
isTop:
|
||||
props.position === 'top' && !isLeftAligned.value && !isRightAligned.value,
|
||||
isTopLeft:
|
||||
props.position === 'top-left' ||
|
||||
(props.position === 'top' && isRightAligned.value),
|
||||
isTopRight:
|
||||
props.position === 'top-right' ||
|
||||
(props.position === 'top' && isLeftAligned.value),
|
||||
isBottom:
|
||||
props.position === 'bottom' &&
|
||||
!isLeftAligned.value &&
|
||||
!isRightAligned.value,
|
||||
isBottomLeft:
|
||||
props.position === 'bottom-left' ||
|
||||
(props.position === 'bottom' && isRightAligned.value),
|
||||
isBottomRight:
|
||||
props.position === 'bottom-right' ||
|
||||
(props.position === 'bottom' && isLeftAligned.value),
|
||||
isLeft: props.position === 'left',
|
||||
isRight: props.position === 'right',
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 툴팁 초기화 함수입니다.
|
||||
*/
|
||||
const resetTooltipContent = () => {
|
||||
tooltipPosition.value.top = null
|
||||
tooltipPosition.value.bottom = null
|
||||
tooltipPosition.value.left = null
|
||||
tooltipPosition.value.right = null
|
||||
isOpen.value = false
|
||||
isLeftAligned.value = false
|
||||
isRightAligned.value = false
|
||||
tooltipTriggerRef.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 반응형 툴팁 위치 계산 함수입니다.
|
||||
* @param trigger 트리거 요소
|
||||
*/
|
||||
const calculateTooltipPosition = (trigger: HTMLElement) => {
|
||||
const triggerRect = trigger.getBoundingClientRect()
|
||||
const tooltipOffset = props.offset + 4 // 툴팁 오프셋
|
||||
const triggerCenterX = triggerRect.left + triggerRect.width / 2
|
||||
const triggerCenterY = triggerRect.top + triggerRect.height / 2
|
||||
|
||||
const isLefts =
|
||||
isPositions.value.isLeft ||
|
||||
isPositions.value.isTopLeft ||
|
||||
isPositions.value.isBottomLeft ||
|
||||
isLeftAligned.value
|
||||
const isOnlyLeft =
|
||||
isPositions.value.isLeft &&
|
||||
Object.values(isPositions.value).filter(Boolean).length === 1
|
||||
|
||||
const isRights =
|
||||
isPositions.value.isRight ||
|
||||
isPositions.value.isTopRight ||
|
||||
isPositions.value.isBottomRight ||
|
||||
isRightAligned.value
|
||||
const isOnlyRight =
|
||||
isPositions.value.isRight &&
|
||||
Object.values(isPositions.value).filter(Boolean).length === 1
|
||||
|
||||
let topPosition: number | null = null
|
||||
let bottomPosition: number | null = null
|
||||
let leftPosition: number | null = null
|
||||
let rightPosition: number | null = null
|
||||
|
||||
if (isLefts) {
|
||||
if (isOnlyLeft) {
|
||||
leftPosition = triggerRect.left - tooltipOffset
|
||||
} else {
|
||||
leftPosition = triggerRect.left + triggerRect.width + tooltipOffsetX.value
|
||||
}
|
||||
} else if (isRights) {
|
||||
if (isOnlyRight) {
|
||||
leftPosition = triggerRect.left + triggerRect.width + tooltipOffset
|
||||
} else {
|
||||
leftPosition = triggerRect.left - tooltipOffsetX.value
|
||||
}
|
||||
} else {
|
||||
leftPosition = triggerCenterX
|
||||
}
|
||||
|
||||
if (
|
||||
isPositions.value.isBottom ||
|
||||
isPositions.value.isBottomLeft ||
|
||||
isPositions.value.isBottomRight
|
||||
) {
|
||||
topPosition = triggerRect.top
|
||||
} else if (
|
||||
isPositions.value.isTop ||
|
||||
isPositions.value.isTopLeft ||
|
||||
isPositions.value.isTopRight
|
||||
) {
|
||||
topPosition = triggerRect.top - tooltipOffset
|
||||
} else {
|
||||
topPosition = triggerCenterY
|
||||
}
|
||||
|
||||
tooltipPosition.value = {
|
||||
top: topPosition,
|
||||
bottom: bottomPosition,
|
||||
left: leftPosition,
|
||||
right: rightPosition,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 좌측 정렬이 필요한지 확인 함수입니다.
|
||||
* 브라우저 화면 기준으로 툴팁의 왼쪽 가장자리가 화면 밖으로 나가는 경우 좌측 정렬을 합니다.
|
||||
*/
|
||||
const isValidLeftAligned = (trigger: HTMLElement): boolean => {
|
||||
const tooltipWidth = 280 // 툴팁 최대 너비
|
||||
const triggerRect = trigger.getBoundingClientRect()
|
||||
const tooltipOffset = props.offset + 4 // 툴팁 오프셋
|
||||
|
||||
// trigger의 중앙 위치 계산
|
||||
const triggerCenterX = triggerRect.left + triggerRect.width / 2
|
||||
|
||||
// 중앙 정렬 시 툴팁의 왼쪽 가장자리 위치
|
||||
const tooltipLeftEdge = triggerCenterX - tooltipWidth / 2
|
||||
|
||||
return tooltipLeftEdge < tooltipOffset
|
||||
}
|
||||
|
||||
/**
|
||||
* 우측 정렬이 필요한지 확인 함수입니다.
|
||||
* 브라우저 화면 기준으로 툴팁의 오른쪽 가장자리가 화면 밖으로 나가는 경우 우측 정렬을 합니다.
|
||||
*/
|
||||
const isValidRightAligned = (trigger: HTMLElement): boolean => {
|
||||
const tooltipWidth = 280 // 툴팁 최대 너비
|
||||
const triggerRect = trigger.getBoundingClientRect()
|
||||
const windowWidth = window.innerWidth
|
||||
const tooltipOffset = props.offset + 4 // 툴팁 오프셋
|
||||
|
||||
// trigger의 중앙 위치 계산
|
||||
const triggerCenterX = triggerRect.left + triggerRect.width / 2
|
||||
|
||||
// 중앙 정렬 시 툴팁의 오른쪽 가장자리 위치
|
||||
const tooltipRightEdge = triggerCenterX + tooltipWidth / 2
|
||||
|
||||
return tooltipRightEdge > windowWidth - tooltipOffset
|
||||
}
|
||||
|
||||
/**
|
||||
* 툴팁 표시 함수입니다.
|
||||
* @param event 이벤트 객체
|
||||
* @param text 툴팁 텍스트
|
||||
*/
|
||||
const handleTooltip = (event: MouseEvent, type: string) => {
|
||||
if (type !== props.type) {
|
||||
return
|
||||
}
|
||||
|
||||
const trigger = (event.currentTarget || event.target) as HTMLElement
|
||||
|
||||
isOpen.value = !isOpen.value
|
||||
|
||||
if (isOpen.value) {
|
||||
tooltipTriggerRef.value = trigger
|
||||
isLeftAligned.value = isValidLeftAligned(trigger)
|
||||
isRightAligned.value = isValidRightAligned(trigger)
|
||||
calculateTooltipPosition(trigger)
|
||||
} else {
|
||||
resetTooltipContent()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 브라우저 리사이즈 시 툴팁 위치 재계산 함수입니다.
|
||||
*/
|
||||
const handleResize = () => {
|
||||
if (isOpen.value && tooltipTriggerRef.value) {
|
||||
isLeftAligned.value = isValidLeftAligned(tooltipTriggerRef.value)
|
||||
isRightAligned.value = isValidRightAligned(tooltipTriggerRef.value)
|
||||
calculateTooltipPosition(tooltipTriggerRef.value)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative z-[100] inline-flex items-center justify-center"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="handleTooltip($event, 'click')"
|
||||
@mouseenter="handleTooltip($event, 'hover')"
|
||||
@mouseleave="handleTooltip($event, 'hover')"
|
||||
>
|
||||
<slot name="trigger" />
|
||||
|
||||
<template v-if="!$slots.trigger">
|
||||
<AtomsIconsStateInfoCircleLine :size="20" color="#7F7F7F" />
|
||||
</template>
|
||||
</button>
|
||||
<div
|
||||
v-if="!teleport && isOpen"
|
||||
:class="[
|
||||
'absolute z-[10] flex items-center justify-center w-[280px] py-[8px] px-[12px] rounded-[4px]',
|
||||
]"
|
||||
:style="[
|
||||
isPositions.isTop
|
||||
? `top: -${props.offset + 4}px; left: 50%; transform: translate(-50%, -100%)`
|
||||
: isPositions.isBottom
|
||||
? `bottom: -${props.offset + 4}px; left: 50%; transform: translate(-50%, 100%)`
|
||||
: isPositions.isLeft
|
||||
? `top: 50%; left: -${props.offset + 4}px; transform: translate(-100%, -50%)`
|
||||
: isPositions.isRight
|
||||
? `top: 50%; right: -${props.offset + 4}px; transform: translate(100%, -50%)`
|
||||
: isPositions.isTopLeft
|
||||
? `top: -${props.offset + 4}px; right: -${tooltipOffsetX}px; transform: translate(0, -100%)`
|
||||
: isPositions.isTopRight
|
||||
? `top: -${props.offset + 4}px; left: -${tooltipOffsetX}px; transform: translate(0, -100%)`
|
||||
: isPositions.isBottomLeft
|
||||
? `bottom: -${props.offset + 4}px; right: -${tooltipOffsetX}px; transform: translate(0, 100%)`
|
||||
: isPositions.isBottomRight
|
||||
? `bottom: -${props.offset + 4}px; left: -${tooltipOffsetX}px; transform: translate(0, 100%)`
|
||||
: '',
|
||||
`background-color: ${backgroundColor};`,
|
||||
]"
|
||||
>
|
||||
<slot name="panel" />
|
||||
|
||||
<p
|
||||
v-if="!$slots.panel"
|
||||
v-dompurify-html="props.content"
|
||||
:class="[
|
||||
`relative flex items-center justify-center w-full text-${props.textAlign} text-[${props.fontSize}] font-[${props.fontWeight}] leading-[${props.lineHeight}] tracking-[${props.letterSpacing}]`,
|
||||
]"
|
||||
:style="[`color: ${props.textColor};`]"
|
||||
></p>
|
||||
|
||||
<span
|
||||
v-if="arrow"
|
||||
class="absolute"
|
||||
:style="[
|
||||
isPositions.isTop
|
||||
? `bottom: -4px; left: 50%; transform: translate(-50%, 0)`
|
||||
: isPositions.isBottom
|
||||
? `top: -4px; left: 50%; transform: translate(-50%, 0) rotate(180deg)`
|
||||
: isPositions.isLeft
|
||||
? `right: -4px; top: 50%; transform: translate(0, -50%) rotate(-90deg)`
|
||||
: isPositions.isRight
|
||||
? `left: -4px; top: 50%; transform: translate(0, -50%) rotate(90deg)`
|
||||
: isPositions.isTopLeft
|
||||
? `bottom: -4px; right: ${tooltipArrowOffsetX}px; transform: translate(0, 0)`
|
||||
: isPositions.isTopRight
|
||||
? `bottom: -4px; left: ${tooltipArrowOffsetX}px; transform: translate(0, 0)`
|
||||
: isPositions.isBottomLeft
|
||||
? `top: -4px; right: ${tooltipArrowOffsetX}px; transform: translate(0, 0) rotate(180deg)`
|
||||
: isPositions.isBottomRight
|
||||
? `top: -4px; left: ${tooltipArrowOffsetX}px; transform: translate(0, 0) rotate(180deg)`
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
<svg
|
||||
width="6"
|
||||
height="4"
|
||||
viewBox="0 0 6 4"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.2 2.93333L0 0H6L3.8 2.93333C3.4 3.46667 2.6 3.46667 2.2 2.93333Z"
|
||||
:fill="backgroundColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ClientOnly>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="teleport && isOpen"
|
||||
:class="[
|
||||
'absolute z-[100] flex items-center justify-center w-[280px] py-[8px] px-[12px] rounded-[4px] text-center text-[12px] font-[400] leading-[18px] tracking-[-0.24px] sm:w-auto sm:max-w-[280px]',
|
||||
]"
|
||||
:style="[
|
||||
isPositions.isTop
|
||||
? `transform: translate(-50%, -100%)`
|
||||
: isPositions.isBottom
|
||||
? `transform: translate(-50%, 100%)`
|
||||
: isPositions.isLeft
|
||||
? `transform: translate(0, -50%)`
|
||||
: isPositions.isRight
|
||||
? `transform: translate(0, -50%)`
|
||||
: isPositions.isTopLeft
|
||||
? `transform: translate(-100%, -100%)`
|
||||
: isPositions.isTopRight
|
||||
? `transform: translate(0, -100%)`
|
||||
: isPositions.isBottomLeft
|
||||
? `transform: translate(-100%, 100%)`
|
||||
: isPositions.isBottomRight
|
||||
? `transform: translate(0, 100%)`
|
||||
: '',
|
||||
`top: ${tooltipPosition.top !== null ? `${tooltipPosition.top}px` : 'auto'}; bottom: ${tooltipPosition.bottom !== null ? `${tooltipPosition.bottom}px` : 'auto'}; left: ${tooltipPosition.left !== null ? `${tooltipPosition.left}px` : 'auto'}; right: ${tooltipPosition.right !== null ? `${tooltipPosition.right}px` : 'auto'}; background-color: ${backgroundColor};`,
|
||||
]"
|
||||
>
|
||||
<slot name="panel" />
|
||||
|
||||
<p
|
||||
v-if="!$slots.panel"
|
||||
v-dompurify-html="props.content"
|
||||
:class="[
|
||||
`relative flex items-center justify-center w-full text-${props.textAlign} text-[${props.fontSize}] font-[${props.fontWeight}] leading-[${props.lineHeight}] tracking-[${props.letterSpacing}]`,
|
||||
]"
|
||||
:style="[`color: ${props.textColor};`]"
|
||||
></p>
|
||||
|
||||
<span
|
||||
v-if="arrow"
|
||||
class="absolute"
|
||||
:style="[
|
||||
isPositions.isTop
|
||||
? `bottom: -4px; left: 50%; transform: translate(-50%, 0)`
|
||||
: isPositions.isBottom
|
||||
? `top: -4px; left: 50%; transform: translate(-50%, 0) rotate(180deg)`
|
||||
: isPositions.isLeft
|
||||
? `right: -4px; top: 50%; transform: translate(0, -50%) rotate(-90deg)`
|
||||
: isPositions.isRight
|
||||
? `left: -4px; top: 50%; transform: translate(0, -50%) rotate(90deg)`
|
||||
: isPositions.isTopLeft
|
||||
? `bottom: -4px; right: ${tooltipArrowOffsetX}px; transform: translate(0, 0)`
|
||||
: isPositions.isTopRight
|
||||
? `bottom: -4px; left: ${tooltipArrowOffsetX}px; transform: translate(0, 0)`
|
||||
: isPositions.isBottomLeft
|
||||
? `top: -4px; right: ${tooltipArrowOffsetX}px; transform: translate(0, 0) rotate(180deg)`
|
||||
: isPositions.isBottomRight
|
||||
? `top: -4px; left: ${tooltipArrowOffsetX}px; transform: translate(0, 0) rotate(180deg)`
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
<svg
|
||||
width="6"
|
||||
height="4"
|
||||
viewBox="0 0 6 4"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.2 2.93333L0 0H6L3.8 2.93333C3.4 3.46667 2.6 3.46667 2.2 2.93333Z"
|
||||
:fill="backgroundColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</Teleport>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -16,11 +16,10 @@ withDefaults(defineProps<Props>(), {
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
:fill="color"
|
||||
>
|
||||
<path
|
||||
d="M11.7929 18.2929C11.4024 18.6834 11.4024 19.3166 11.7929 19.7071C12.1834 20.0976 12.8166 20.0976 13.2071 19.7071L20.2071 12.7071C20.5976 12.3166 20.5976 11.6834 20.2071 11.2929L13.2071 4.29289C12.8166 3.90237 12.1834 3.90237 11.7929 4.29289C11.4024 4.68342 11.4024 5.31658 11.7929 5.70711L17.0858 11L4.5 11C3.94771 11 3.5 11.4477 3.5 12C3.5 12.5523 3.94771 13 4.5 13L17.0858 13L11.7929 18.2929Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
36
layers/components/atoms/icons/StateInfoCircleLine.vue
Normal file
36
layers/components/atoms/icons/StateInfoCircleLine.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 12,
|
||||
color: 'var(--foreground-gray-500)',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M8.75 13.3333C8.28976 13.3333 7.91667 12.9602 7.91667 12.5L7.91667 8.125C7.91667 7.66476 8.28976 7.29167 8.75 7.29167C9.21024 7.29167 9.58333 7.66476 9.58333 8.125V12.5C9.58333 12.9602 9.21024 13.3333 8.75 13.3333Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<path
|
||||
d="M8.75 4.21875C8.14594 4.21875 7.65625 4.70844 7.65625 5.3125C7.65625 5.91656 8.14594 6.40625 8.75 6.40625C9.35406 6.40625 9.84375 5.91656 9.84375 5.3125C9.84375 4.70844 9.35406 4.21875 8.75 4.21875Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M17.5 8.75C17.5 13.5825 13.5825 17.5 8.75 17.5C3.91751 17.5 0 13.5825 0 8.75C0 3.91751 3.91751 0 8.75 0C13.5825 0 17.5 3.91751 17.5 8.75ZM8.75 15.8333C12.662 15.8333 15.8333 12.662 15.8333 8.75C15.8333 4.83798 12.662 1.66667 8.75 1.66667C4.83798 1.66667 1.66667 4.83798 1.66667 8.75C1.66667 12.662 4.83798 15.8333 8.75 15.8333Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -16,15 +16,13 @@ withDefaults(defineProps<Props>(), {
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
:fill="color"
|
||||
>
|
||||
<path
|
||||
d="M3.63636 3.33333C3.469 3.33333 3.33333 3.469 3.33333 3.63636L3.33333 12.3636C3.33333 12.531 3.469 12.6667 3.63636 12.6667H12.3636C12.531 12.6667 12.6667 12.531 12.6667 12.3636V9.93939C12.6667 9.5712 12.9651 9.27273 13.3333 9.27273C13.7015 9.27273 14 9.5712 14 9.93939V12.3636C14 13.2674 13.2674 14 12.3636 14H3.63636C2.73262 14 2 13.2674 2 12.3636L2 3.63636C2 2.73263 2.73262 2 3.63636 2L6.06061 2C6.4288 2 6.72727 2.29848 6.72727 2.66667C6.72727 3.03486 6.4288 3.33333 6.06061 3.33333H3.63636Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<path
|
||||
d="M12.6667 4.27614V6.54545C12.6667 6.91364 12.9651 7.21212 13.3333 7.21212C13.7015 7.21212 14 6.91364 14 6.54545V2.66667C14 2.29848 13.7015 2 13.3333 2L9.45455 2C9.08636 2 8.78788 2.29848 8.78788 2.66667C8.78788 3.03486 9.08636 3.33333 9.45455 3.33333L11.7239 3.33333L7.28616 7.77103C7.02581 8.03138 7.02581 8.45349 7.28616 8.71384C7.54651 8.97419 7.96862 8.97419 8.22897 8.71384L12.6667 4.27614Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.empty-game + main .btn-home {
|
||||
@apply mt-[var(--scroll-position,48px)] md:mt-[var(--scroll-position,64px)];
|
||||
}
|
||||
.btn-home {
|
||||
@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-[calc(var(--scroll-position,64px)+64px)];
|
||||
|
||||
@@ -61,7 +61,7 @@ const inlineStyle = computed<CSSProperties>(() => {
|
||||
style.color = props.textColor
|
||||
}
|
||||
if (props.type === 'duplication') {
|
||||
style.backgroundImage = `url(${getImageHost(DUP_IMAGE_MAP[props.platform], { imageType: 'common' })})`
|
||||
style.backgroundImage = `url(${getResourceHost(DUP_IMAGE_MAP[props.platform], { imageType: 'common' })})`
|
||||
}
|
||||
return style
|
||||
})
|
||||
@@ -167,7 +167,7 @@ const handleClick = () => {
|
||||
|
||||
/* duplication */
|
||||
.btn-base.duplication {
|
||||
@apply bg-[16px_50%] bg-[length:auto_28px] bg-no-repeat
|
||||
@apply bg-[16px_50%] bg-[length:auto_28px] bg-no-repeat backdrop-blur-[15px]
|
||||
pt-[22px] pl-[47px] pr-[22px] pb-[7px] text-[11px]
|
||||
md:h-[64px] md:pt-[30px] md:pl-[64px] md:pr-[28px] md:pb-[11px] md:text-[12px] md:bg-[20px_50%] md:bg-[length:auto_40px];
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ const handleCopy = async () => {
|
||||
try {
|
||||
const url = window.location.href
|
||||
await navigator.clipboard.writeText(url)
|
||||
handleOpenToast(tm('Alert_Copy_Complete'))
|
||||
handleOpenToast({ contentText: tm('Alert_Copy_Complete') })
|
||||
} catch (error) {
|
||||
console.error('[handleCopy] Error:', error)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ const handleCopy = async () => {
|
||||
target="_blank"
|
||||
class="sns-item"
|
||||
:style="{
|
||||
backgroundImage: `url(${getImageHost(`/images/common/ic-v2-logo-${key}-fill.png`, { imageType: 'common' })})`,
|
||||
backgroundImage: `url(${getResourceHost(`/images/common/ic-v2-logo-${key}-fill.png`, { imageType: 'common' })})`,
|
||||
}"
|
||||
>
|
||||
<span class="sr-only">{{ key }}</span>
|
||||
@@ -74,7 +74,7 @@ const handleCopy = async () => {
|
||||
type="button"
|
||||
class="sns-item"
|
||||
:style="{
|
||||
backgroundImage: `url(${getImageHost('/images/common/ic-v2-community-link-line.png', { imageType: 'common' })})`,
|
||||
backgroundImage: `url(${getResourceHost('/images/common/ic-v2-community-link-line.png', { imageType: 'common' })})`,
|
||||
}"
|
||||
@click="handleCopy"
|
||||
>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageDataLnbMenu } from '#layers/types/api/pageData'
|
||||
|
||||
const { y: windowY, directions } = useWindowScroll({ behavior: 'smooth' })
|
||||
const { directions } = useWindowScroll({ behavior: 'smooth' })
|
||||
const pageDataStore = usePageDataStore()
|
||||
const scrollStore = useScrollStore()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
|
||||
const { pageData } = storeToRefs(pageDataStore)
|
||||
|
||||
// 상수 정의
|
||||
const HEADER_HEIGHT = 64
|
||||
const OBSERVER_OPTIONS = {
|
||||
root: null,
|
||||
rootMargin: '-20% 0px -60% 0px', // 상단 20%, 하단 60% 마진
|
||||
@@ -104,15 +104,7 @@ const handleLnbClick = (lnbItem: PageDataLnbMenu) => {
|
||||
? lnbItem?.children?.['1']?.page_ver_tmpl_name_en
|
||||
: lnbItem.page_ver_tmpl_name_en
|
||||
|
||||
const targetElement = document.getElementById(targetId)
|
||||
if (!targetElement) return
|
||||
|
||||
// 헤더 높이를 고려한 스크롤 위치 계산
|
||||
const elementTop = targetElement.getBoundingClientRect().top
|
||||
const currentScrollY = window.scrollY
|
||||
const targetScrollY = currentScrollY + elementTop - HEADER_HEIGHT
|
||||
|
||||
windowY.value = targetScrollY
|
||||
scrollStore.scrollToAnchor(targetId)
|
||||
}
|
||||
|
||||
watch(directions, newVal => {
|
||||
@@ -172,6 +164,9 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.empty-game + main .lnb-wrap {
|
||||
@apply mt-[var(--scroll-position,48px)];
|
||||
}
|
||||
.lnb-wrap {
|
||||
@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;
|
||||
}
|
||||
@@ -194,7 +189,7 @@ onUnmounted(() => {
|
||||
z-index: -1;
|
||||
}
|
||||
.lnb-wrap.is-hidden {
|
||||
@apply translate-x-[110%] delay-[800ms];
|
||||
@apply translate-x-[110%] delay-[5s];
|
||||
}
|
||||
.lnb-main {
|
||||
@apply flex flex-col gap-4 items-end;
|
||||
@@ -213,7 +208,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
button {
|
||||
@apply flex items-center 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)] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] transition-all duration-300 ease-in-out;
|
||||
}
|
||||
button:hover,
|
||||
button.is-active {
|
||||
@@ -235,4 +230,7 @@ button.is-active::after {
|
||||
.main-promotion .lnb-wrap {
|
||||
@apply mt-[calc(var(--scroll-position,48px)+64px+72px)];
|
||||
}
|
||||
.empty-game + .main-promotion .lnb-wrap {
|
||||
@apply mt-[calc(var(--scroll-position,48px)+72px)];
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,38 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContentParams } from '#layers/types/components/modal'
|
||||
|
||||
interface TabItem {
|
||||
title: string
|
||||
desc: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ContentParams>(), {
|
||||
isOutsideClose: false,
|
||||
contentTitle: '',
|
||||
tabActiveIndex: 0,
|
||||
tabInfo: () => [],
|
||||
})
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
|
||||
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
||||
const currentTab = ref<number>(props.tabActiveIndex)
|
||||
const { content } = modalStore
|
||||
|
||||
const currentTab = ref<number>(content.storeTabActiveIndex)
|
||||
|
||||
const responsiveTransition = computed(() =>
|
||||
breakpoints.value.isXs ? 'slide-up' : 'fade'
|
||||
)
|
||||
const tabInfo = computed<TabItem[]>(() => props.tabInfo ?? [])
|
||||
const tabInfo = computed<TabItem[]>(() => content.storeTabInfo ?? [])
|
||||
const isTab = computed(() => tabInfo.value.length >= 2)
|
||||
|
||||
const isVisible = (index: number) => currentTab.value === index
|
||||
|
||||
const handleCloseModal = () => {
|
||||
isOpen.value = false
|
||||
content.storeIsOpen = false
|
||||
}
|
||||
|
||||
const handleOutsideClick = () => {
|
||||
if (props.isOutsideClose) handleCloseModal()
|
||||
if (content.storeIsOutsideClose) handleCloseModal()
|
||||
}
|
||||
|
||||
const handleUpdateTab = (tabNumber: number) => {
|
||||
@@ -41,25 +33,33 @@ const handleUpdateTab = (tabNumber: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(isOpen, newVal => {
|
||||
if (newVal) {
|
||||
modalStore.handleControlDimmed(true)
|
||||
} else {
|
||||
modalStore.handleControlDimmed(false)
|
||||
watch(
|
||||
() => content.storeIsOpen,
|
||||
(newVal: boolean) => {
|
||||
if (newVal) {
|
||||
modalStore.handleControlDimmed(true)
|
||||
} else {
|
||||
modalStore.handleControlDimmed(false)
|
||||
currentTab.value = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition :name="responsiveTransition">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
:class="['modal-wrap', { 'is-open': isOpen }, props.modalName]"
|
||||
v-if="content.storeIsOpen"
|
||||
:class="[
|
||||
'modal-wrap',
|
||||
{ 'is-open': content.storeIsOpen },
|
||||
content.storeModalName,
|
||||
]"
|
||||
@click="handleOutsideClick"
|
||||
>
|
||||
<div class="modal-area" @click.stop>
|
||||
<div class="modal-header">
|
||||
<strong class="title">{{ props.contentTitle }}</strong>
|
||||
<strong class="title">{{ content.storeContentTitle }}</strong>
|
||||
|
||||
<button type="button" class="modal-close" @click="handleCloseModal">
|
||||
<span class="sr-only">close</span>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
const modalStore = useModalStore()
|
||||
const { toast } = modalStore
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div v-if="toast.storeIsOpen" class="toast-container">
|
||||
<p v-dompurify-html="toast.storeContentText" class="toast-text"></p>
|
||||
<div v-if="modalStore.toast.storeIsOpen" class="toast-container">
|
||||
<p
|
||||
v-dompurify-html="modalStore.toast.storeContentText"
|
||||
class="toast-text"
|
||||
></p>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
@@ -1,35 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { getYouTubeEmbedUrl } from '@/layers/utils/youtubeUtil'
|
||||
import type { YoutubeParams } from '#layers/types/components/modal'
|
||||
|
||||
const props = withDefaults(defineProps<YoutubeParams>(), {
|
||||
youtubeUrl: '',
|
||||
isOutsideClose: false,
|
||||
})
|
||||
const modalStore = useModalStore()
|
||||
const scrollStore = useScrollStore()
|
||||
|
||||
const emit = defineEmits(['closeButtonEvent'])
|
||||
|
||||
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
||||
const { youtube } = modalStore
|
||||
|
||||
const embedUrl = computed(() => {
|
||||
return getYouTubeEmbedUrl(props.youtubeUrl)
|
||||
return getYouTubeEmbedUrl(youtube.storeYoutubeUrl)
|
||||
})
|
||||
|
||||
const handleCloseModal = () => {
|
||||
isOpen.value = false
|
||||
emit('closeButtonEvent')
|
||||
const handleClose = () => {
|
||||
youtube.storeIsOpen = false
|
||||
scrollStore.controlScrollLock(false)
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpen.value) {
|
||||
handleCloseModal()
|
||||
if (event.key === 'Escape' && youtube.storeIsOpen) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleOutsideClick = () => {
|
||||
if (props.isOutsideClose) {
|
||||
handleCloseModal()
|
||||
}
|
||||
if (youtube.storeIsOutsideClose) handleClose()
|
||||
}
|
||||
|
||||
// 키보드 이벤트 리스너 등록/해제
|
||||
@@ -45,9 +38,8 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="modal-wrap dimmed overflow-hidden flex items-center justify-center"
|
||||
:class="props.modalName"
|
||||
v-if="youtube.storeIsOpen"
|
||||
:class="['modal-wrap', 'dimmed', youtube.storeModalName]"
|
||||
@click="handleOutsideClick"
|
||||
>
|
||||
<div
|
||||
@@ -60,7 +52,7 @@ onUnmounted(() => {
|
||||
>
|
||||
<!-- 헤더 -->
|
||||
<div class="flex justify-end mb-3 md:mb-4">
|
||||
<button type="button" @click="handleCloseModal">
|
||||
<button type="button" @click="handleClose">
|
||||
<AtomsIconsCloseLine />
|
||||
</button>
|
||||
</div>
|
||||
@@ -81,3 +73,9 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-wrap {
|
||||
@apply overflow-hidden flex items-center justify-center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,8 @@ const options = computed((): ResponsiveOptions => {
|
||||
focus: 'center',
|
||||
autoWidth: true,
|
||||
autoHeight: true,
|
||||
speed: 500,
|
||||
speed: 600,
|
||||
easing: 'ease-in-out',
|
||||
updateOnMove: true,
|
||||
arrows: props.arrows && isMultipleItems.value,
|
||||
pagination: props.pagination && isMultipleItems.value,
|
||||
|
||||
@@ -31,7 +31,8 @@ const options = computed((): ResponsiveOptions => {
|
||||
focus: 'center',
|
||||
autoWidth: true,
|
||||
autoHeight: true,
|
||||
speed: 500,
|
||||
speed: 600,
|
||||
easing: 'ease-in-out',
|
||||
updateOnMove: true,
|
||||
arrows: props.arrows && isMultipleItems.value,
|
||||
pagination: props.pagination && isMultipleItems.value,
|
||||
|
||||
@@ -45,7 +45,8 @@ const options = computed((): ResponsiveOptions => {
|
||||
autoHeight: true,
|
||||
gap: props.gap,
|
||||
perPage: props.perPage,
|
||||
speed: 500,
|
||||
speed: 600,
|
||||
easing: 'ease-in-out',
|
||||
updateOnMove: true,
|
||||
autoplay: props.autoplay,
|
||||
drag: props.drag,
|
||||
|
||||
@@ -32,7 +32,8 @@ const options = computed((): ResponsiveOptions => {
|
||||
rewind: true,
|
||||
perPage: 1,
|
||||
perMove: 1,
|
||||
speed: 500,
|
||||
speed: 600,
|
||||
easing: 'ease-in-out',
|
||||
updateOnMove: true,
|
||||
drag: props.drag,
|
||||
autoplay: props.autoplay,
|
||||
|
||||
@@ -41,7 +41,8 @@ const mainOptions = computed<Options>(() => ({
|
||||
rewind: true,
|
||||
perPage: 1,
|
||||
perMove: 1,
|
||||
speed: 500,
|
||||
speed: 600,
|
||||
easing: 'ease-in-out',
|
||||
arrows: false,
|
||||
pagination: false,
|
||||
drag: props.drag,
|
||||
@@ -81,7 +82,7 @@ const thumbOptions = computed<Options>(() => ({
|
||||
const getThumbnailSrc = (item: PageDataTemplateComponentSet) => {
|
||||
if (props.variant === 'media') {
|
||||
const mediaComponent = getComponentGroup(item, 'media')
|
||||
return mediaComponent ? getMediaImgSrc(mediaComponent) : ''
|
||||
return mediaComponent ? getMediaImgSrc(mediaComponent, 'high') : ''
|
||||
}
|
||||
|
||||
const thumbnailComponent = getComponentGroup(item, 'pagenaviThumbnail')
|
||||
@@ -111,9 +112,12 @@ onMounted(() => {
|
||||
mainInst.sync(thumbsInst)
|
||||
// 썸네일 슬라이드의 화살표 버튼에 이벤트 리스너 추가
|
||||
nextTick(() => {
|
||||
removeArrowListeners = addArrowClickListeners(thumbsInst, (direction, targetIndex) => {
|
||||
emit('arrowClick', direction, targetIndex)
|
||||
})
|
||||
removeArrowListeners = addArrowClickListeners(
|
||||
thumbsInst,
|
||||
(direction, targetIndex) => {
|
||||
emit('arrowClick', direction, targetIndex)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -124,7 +128,7 @@ onBeforeUnmount(() => {
|
||||
removeArrowListeners()
|
||||
removeArrowListeners = null
|
||||
}
|
||||
|
||||
|
||||
// Splide 인스턴스 정리
|
||||
mainInst?.destroy?.()
|
||||
thumbsInst?.destroy?.()
|
||||
@@ -181,8 +185,8 @@ onBeforeUnmount(() => {
|
||||
@apply md:w-[calc(100%-16px)];
|
||||
}
|
||||
.thumbnail-slide {
|
||||
@apply overflow-hidden relative mr-[12px] !border-none rounded-[4px] bg-[var(--pagination-disabled)] md:mr-[16px]
|
||||
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:border after:rounded-[4px];
|
||||
@apply overflow-hidden relative mr-[12px] !border-none rounded-[4px] bg-[var(--pagination-disabled)] md:mr-[16px] transition-all duration-200 ease-in-out
|
||||
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:border after:rounded-[4px] after:transition-all after:duration-200 after:ease-in-out;
|
||||
}
|
||||
.thumbnail-slide:hover,
|
||||
.thumbnail-slide.is-active {
|
||||
@@ -195,6 +199,13 @@ onBeforeUnmount(() => {
|
||||
.thumbnail-slide.is-active::after {
|
||||
@apply border-[var(--pagination-active)];
|
||||
}
|
||||
.thumbnail-slide:hover img,
|
||||
.thumbnail-slide.is-active img {
|
||||
@apply opacity-100;
|
||||
}
|
||||
.thumbnail-slide img {
|
||||
@apply opacity-50 transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
/* 기본 버전 스타일 */
|
||||
.thumbnail-carousel.thumbnail-default,
|
||||
@@ -212,7 +223,7 @@ onBeforeUnmount(() => {
|
||||
@apply right-0;
|
||||
}
|
||||
.thumbnail-carousel.thumbnail-default .thumbnail-slide {
|
||||
@apply aspect-[1/1] w-[8px] md:w-[80px]
|
||||
@apply aspect-[1/1] w-[8px] md:w-[80px] backdrop-blur-[15px]
|
||||
after:hidden md:after:block;
|
||||
}
|
||||
.thumbnail-carousel.thumbnail-default .thumbnail-slide:hover img,
|
||||
|
||||
@@ -85,6 +85,9 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.empty-game + main .event-navigation {
|
||||
@apply mt-[var(--scroll-position,48px)];
|
||||
}
|
||||
.event-navigation {
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
<a href="https://www.smilegate.com" target="_blank" class="smilegate">
|
||||
<img
|
||||
:src="
|
||||
getImageHost(`/images/common/logo_smilegate.png`, {
|
||||
getResourceHost(`/images/common/logo_smilegate.png`, {
|
||||
imageType: 'common',
|
||||
})
|
||||
"
|
||||
@@ -190,7 +190,7 @@
|
||||
>
|
||||
<img
|
||||
:src="
|
||||
getImageHost(`${setDevCi.dev_ci_img_path}`, {
|
||||
getResourceHost(`${setDevCi.dev_ci_img_path}`, {
|
||||
imageType: 'game',
|
||||
})
|
||||
"
|
||||
@@ -242,7 +242,7 @@ const getGameRatingImage = computed((): string[] => {
|
||||
}
|
||||
return contentInfo.map(item => {
|
||||
const type = ageTypeMap[item] || 'TypeTest'
|
||||
return getImageHost(`/images/common/grades_age/${locale.value}/${type}.svg`, {
|
||||
return getResourceHost(`/images/common/grades_age/${locale.value}/${type}.svg`, {
|
||||
imageType: 'common',
|
||||
})
|
||||
})
|
||||
@@ -266,7 +266,7 @@ const getContentInfoImage = computed((): string[] => {
|
||||
.map(item => {
|
||||
const type = contentTypeMap[item]
|
||||
return type
|
||||
? getImageHost(`/images/common/grades_use/${type}.svg`, {
|
||||
? getResourceHost(`/images/common/grades_use/${type}.svg`, {
|
||||
imageType: 'common',
|
||||
})
|
||||
: ''
|
||||
|
||||
@@ -61,8 +61,6 @@ const pathMatches = (base: string, current: string) => {
|
||||
|
||||
/** 자식 중 활성 링크 존재 여부 */
|
||||
const hasActiveChild = (children?: GameDataMenuChildren) => {
|
||||
if(!import.meta.client) return false
|
||||
|
||||
const cur = currentPath.value
|
||||
return formatToArray(children).some(child => {
|
||||
if (!child?.url_path || !isInternalUrl(child.url_path)) return false
|
||||
@@ -72,8 +70,6 @@ const hasActiveChild = (children?: GameDataMenuChildren) => {
|
||||
|
||||
/** 1Depth 활성화 여부 */
|
||||
const isNavItemActive = (gnbItem: GameDataMenu): boolean => {
|
||||
if(!import.meta.client) return false
|
||||
|
||||
const cur = currentPath.value
|
||||
const base = gnbItem?.url_path
|
||||
const selfActive =
|
||||
@@ -228,16 +224,19 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="header">
|
||||
<header :class="['header', { 'empty-game': !gnbData }]">
|
||||
<BlocksStoveGnbNew class="h-[48px]" />
|
||||
<div :class="['game-wrap', { 'is-fixed': isPassedStoveGnb }]">
|
||||
<div
|
||||
v-if="gnbData"
|
||||
:class="['game-wrap', { 'is-fixed': isPassedStoveGnb }]"
|
||||
>
|
||||
<AtomsLocaleLink to="/" class="mx-auto md:hidden">
|
||||
<img
|
||||
:src="getImageHost(gnbData?.bi_path)"
|
||||
:src="getResourceHost(gnbData?.bi_path)"
|
||||
:alt="gameData?.game_name"
|
||||
class="h-[30px]"
|
||||
/>
|
||||
</atomslocalelink>
|
||||
</AtomsLocaleLink>
|
||||
<button class="btn-open" @click="handleMenuOpen">
|
||||
<AtomsIconsMenuBoldLine class="mx-auto" />
|
||||
<span class="sr-only">menu open</span>
|
||||
@@ -250,11 +249,12 @@ onMounted(() => {
|
||||
<div class="nav-logo">
|
||||
<AtomsLocaleLink to="/" @click="handleMenuClose">
|
||||
<img
|
||||
:src="getImageHost(gnbData?.bi_path)"
|
||||
:src="getResourceHost(gnbData?.bi_path)"
|
||||
:alt="gameData?.game_name"
|
||||
class="h-[30px]"
|
||||
/>
|
||||
</AtomsLocaleLink>
|
||||
</AtomsLocaleLink>
|
||||
</div>
|
||||
<nav :class="['nav-list', { 'is-mounted': isMounted }]">
|
||||
<div v-if="gnbData?.menus" class="official custom-theme-scrollbar">
|
||||
<div
|
||||
@@ -276,8 +276,12 @@ onMounted(() => {
|
||||
{ 'has-link': !isNotClickable(gnbItem) },
|
||||
{ active: isNavItemActive(gnbItem) },
|
||||
]"
|
||||
@click="handleMenuClose"
|
||||
>
|
||||
<span>{{ gnbItem.menu_name }}</span>
|
||||
<AtomsIconsWebLinkLine
|
||||
v-if="gnbItem.link_target === '_blank'"
|
||||
/>
|
||||
<AtomsIconsArrowDownFill
|
||||
v-if="has2depthButton(gnbItem)"
|
||||
class="hidden md:block"
|
||||
@@ -297,6 +301,7 @@ onMounted(() => {
|
||||
<AtomsLocaleLink
|
||||
:to="child.url_path"
|
||||
:target="child.link_target"
|
||||
@click="handleMenuClose"
|
||||
>
|
||||
<span>{{ child.menu_name }}</span>
|
||||
<AtomsIconsWebLinkLine
|
||||
@@ -329,6 +334,7 @@ onMounted(() => {
|
||||
:to="gnbItem.url_path"
|
||||
:target="gnbItem.link_target"
|
||||
:class="`${isNavItemActive(gnbItem) ? 'active' : ''}`"
|
||||
@click="handleMenuClose"
|
||||
>
|
||||
<span>{{ gnbItem.menu_name }}</span>
|
||||
</AtomsLocaleLink>
|
||||
@@ -362,10 +368,12 @@ onMounted(() => {
|
||||
gameData.event_banner?.link_type === 1 ? '_self' : '_blank'
|
||||
"
|
||||
class="nav-1depth text-gradient-pink"
|
||||
@click="handleMenuClose"
|
||||
>
|
||||
<AtomsIconsStarFill />
|
||||
<span>{{ tm('Gnb_Event') }}</span>
|
||||
<AtomsIconsStarFill />
|
||||
<AtomsIconsArrowRightLine class="ml-auto md:hidden" />
|
||||
</AtomsLocaleLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -417,7 +425,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></header>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -470,7 +478,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.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-6 pl-2 pr-1
|
||||
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 {
|
||||
@@ -520,7 +528,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.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-scroll 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;
|
||||
@@ -553,7 +561,9 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.event {
|
||||
@apply relative md:ml-[64px] md:after:content-[''] md:after:absolute md:after:top-[50%] md:after:left-[-32px] md:after:w-[1px] md:after:h-[16px] md:after:bg-theme-foreground-gray-750 md:after:translate-y-[-50%];
|
||||
@apply relative pr-1 md:ml-[64px] md:pr-0
|
||||
before:content-[''] before:block before:h-px before:mb-2 before:mx-3 before:bg-theme-foreground-reversal-8 md:before:hidden
|
||||
after:content-[''] after:absolute md:after:top-[50%] md:after:left-[-32px] md:after:w-[1px] md:after:h-[16px] md:after:bg-theme-foreground-gray-750 md:after:translate-y-[-50%];
|
||||
}
|
||||
|
||||
.is-hidden {
|
||||
@@ -561,13 +571,13 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.btn-start {
|
||||
@apply relative mt-2 px-5 md:absolute md:top-[0] md:right-0 md:flex md:items-center md:h-full md:mt-0 md:px-0;
|
||||
@apply relative py-2 px-5 md:absolute md:top-[0] md:right-0 md:flex md:items-center md:h-full md:py-0 md:px-0;
|
||||
}
|
||||
.btn-start:hover .nav-2depth {
|
||||
@apply md:block;
|
||||
}
|
||||
.btn-start:deep(> .btn-base) {
|
||||
@apply w-full h-[48px] px-10;
|
||||
@apply w-full h-[48px] px-10 font-[700];
|
||||
}
|
||||
.btn-start:deep(> .btn-base) .icon-platform {
|
||||
@apply hidden;
|
||||
|
||||
@@ -112,6 +112,9 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.empty-game + main .main-content {
|
||||
@apply pt-0;
|
||||
}
|
||||
.main-content {
|
||||
@apply relative pt-[48px] md:pt-[64px];
|
||||
}
|
||||
|
||||
@@ -3,13 +3,15 @@ import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
resourcesData: PageDataResourceGroup
|
||||
gradient?: string
|
||||
size?: 'contain' | 'cover'
|
||||
gradient?: string
|
||||
dimmed?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
gradient: '',
|
||||
size: 'cover',
|
||||
gradient: '',
|
||||
dimmed: false,
|
||||
})
|
||||
|
||||
const { getCurrentSrc } = useResponsiveSrc()
|
||||
@@ -35,6 +37,16 @@ const gradientClasses = computed(() => [
|
||||
props.gradient,
|
||||
])
|
||||
|
||||
// 비디오를 처음부터 재생하는 메서드
|
||||
const restartVideo = () => {
|
||||
if (!videoRef.value) return
|
||||
|
||||
videoRef.value.currentTime = 0
|
||||
videoRef.value.play().catch(err => {
|
||||
console.warn('Video play failed:', err)
|
||||
})
|
||||
}
|
||||
|
||||
// src 변경 시 비디오 다시 로드
|
||||
watch(videoSrc, () => {
|
||||
if (!videoRef.value) return
|
||||
@@ -42,10 +54,14 @@ watch(videoSrc, () => {
|
||||
videoRef.value.currentTime = 0
|
||||
videoRef.value.load()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
restartVideo,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-hidden absolute inset-0 w-full h-full">
|
||||
<div v-if="resPath" class="overflow-hidden absolute inset-0 w-full h-full">
|
||||
<!-- 이미지 타입 -->
|
||||
<div
|
||||
v-if="isTypeImage(resourcesData?.resource_type) && imageSrc"
|
||||
@@ -67,7 +83,7 @@ watch(videoSrc, () => {
|
||||
<source :src="videoSrc" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
<!-- 그라디언트 오버레이 -->
|
||||
<div v-if="gradient" :class="gradientClasses" />
|
||||
<i v-if="dimmed" class="absolute inset-0 bg-black/50" />
|
||||
<i v-if="gradient" :class="gradientClasses" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,33 +9,93 @@ interface Props {
|
||||
resourcesData: PageDataResourceGroup[]
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { locale } = useI18n()
|
||||
const modalStore = useModalStore()
|
||||
const scrollStore = useScrollStore()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
|
||||
const { tm } = useI18n()
|
||||
|
||||
const getBtnType = (item?: PageDataResourceGroupBtnInfo): ButtonType => {
|
||||
const type = item?.detail?.btn_type
|
||||
const target = item?.detail?.action?.link_target
|
||||
if (type === 'URL' && target)
|
||||
const buttonList = computed<PageDataResourceGroup[]>(
|
||||
() => props.resourcesData ?? []
|
||||
)
|
||||
|
||||
const getButtonType = (btnInfo?: PageDataResourceGroupBtnInfo): ButtonType => {
|
||||
const btnType = btnInfo?.detail?.btn_type
|
||||
const target = btnInfo?.detail?.action?.link_target
|
||||
|
||||
if (btnType === 'URL' && target) {
|
||||
return target === '_blank' ? 'external' : 'internal'
|
||||
if (type === 'DOWNLOAD') return 'download'
|
||||
}
|
||||
|
||||
if (btnType === 'DOWNLOAD') return 'download'
|
||||
|
||||
return 'action'
|
||||
}
|
||||
|
||||
const handleLogClick = (button: PageDataResourceGroup) => {
|
||||
sendLog(locale.value, useAnalyticsLogDataDirect(button, props.pageVerTmplSeq))
|
||||
if (button.btn_info?.detail?.btn_type === 'POP') {
|
||||
modalStore.handleOpenContent({
|
||||
contentTitle: button.btn_info?.detail?.title,
|
||||
tabInfo: button.btn_info?.detail?.tab_info,
|
||||
const downloadZip = async (url: string, osType: number) => {
|
||||
if (osType === 1 && breakpoints.value?.isMobile) {
|
||||
modalStore.handleOpenAlert({ contentText: tm('Alert_Download_PC') })
|
||||
return
|
||||
}
|
||||
|
||||
const fileUrl = getResourceHost(url)
|
||||
|
||||
try {
|
||||
const res = await $fetch(fileUrl, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
const blob = new Blob([res as BlobPart], { type: 'application/zip' })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const pathPart = fileUrl.split('/').pop()
|
||||
const a = document.createElement('a')
|
||||
|
||||
a.href = blobUrl
|
||||
a.download = pathPart ?? 'download.zip'
|
||||
a.click()
|
||||
|
||||
// 메모리 정리
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
|
||||
modalStore.handleOpenAlert({ contentText: tm('Alert_Download_Success') })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
modalStore.handleOpenAlert({ contentText: tm('Alert_Download_Error') })
|
||||
}
|
||||
}
|
||||
|
||||
// 편의상
|
||||
const buttonList = computed(() => props.resourcesData || [])
|
||||
const handleButtonClick = (button: PageDataResourceGroup) => {
|
||||
// 로그
|
||||
sendLog(locale.value, useAnalyticsLogDataDirect(button, props.pageVerTmplSeq))
|
||||
|
||||
const btnDetail = button.btn_info?.detail
|
||||
|
||||
switch (btnDetail?.btn_type) {
|
||||
case 'POP':
|
||||
modalStore.handleOpenContent({
|
||||
contentTitle: btnDetail?.title,
|
||||
tabInfo: btnDetail?.tab_info,
|
||||
})
|
||||
return
|
||||
case 'ANCHOR':
|
||||
scrollStore.scrollToAnchor(btnDetail?.page_ver_tmpl_name_en ?? '')
|
||||
return
|
||||
case 'MOV':
|
||||
modalStore.handleOpenYoutube({ youtubeUrl: btnDetail.url ?? '' })
|
||||
return
|
||||
case 'DOWNLOAD':
|
||||
downloadZip(btnDetail?.file_path ?? '', btnDetail?.os_type ?? 0)
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,21 +110,20 @@ const buttonList = computed(() => props.resourcesData || [])
|
||||
:platform="button.btn_info?.detail?.market_type"
|
||||
:background-color="getColorCodeFromData(button.btn_info, 'btn')"
|
||||
:text-color="getColorCodeFromData(button.btn_info, 'txt')"
|
||||
:disabled="button?.btn_info?.disabled"
|
||||
@click="handleLogClick(button)"
|
||||
@click="handleButtonClick(button)"
|
||||
>
|
||||
{{ button.btn_info?.txt_btn_name }}
|
||||
</BlocksButtonLauncher>
|
||||
|
||||
<AtomsButton
|
||||
v-else
|
||||
:type="getBtnType(button.btn_info)"
|
||||
:type="getButtonType(button.btn_info)"
|
||||
:href="button.btn_info?.detail?.action?.url"
|
||||
:target="button.btn_info?.detail?.action?.link_target"
|
||||
:rel="button.btn_info?.detail?.action?.rel"
|
||||
:background-color="getColorCodeFromData(button.btn_info, 'btn')"
|
||||
:text-color="getColorCodeFromData(button.btn_info, 'txt')"
|
||||
:disabled="button?.btn_info?.disabled"
|
||||
@click="handleLogClick(button)"
|
||||
:disabled="button.btn_info?.detail?.btn_type === 'DEACTIVE'"
|
||||
@click="handleButtonClick(button)"
|
||||
>
|
||||
{{ button.btn_info?.txt_btn_name }}
|
||||
</AtomsButton>
|
||||
|
||||
@@ -41,9 +41,9 @@ const componentProps = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-end justify-between mb-[16px] md:mb-[24px]">
|
||||
<div class="flex justify-between mb-[16px] md:mb-[24px]">
|
||||
<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>
|
||||
</h3>
|
||||
|
||||
@@ -13,12 +13,14 @@ const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
|
||||
// 비디오 플레이 버튼 클릭 핸들러
|
||||
const handleVideoPlayClick = () => {
|
||||
const youtubeUrl = props.resourcesData?.display?.text ?? ''
|
||||
modalStore.handleOpenYoutube({ youtubeUrl })
|
||||
if (youtubeUrl) {
|
||||
modalStore.handleOpenYoutube({ youtubeUrl })
|
||||
|
||||
sendLog(
|
||||
locale.value,
|
||||
useAnalyticsLogDataDirect(props.resourcesData, props.pageVerTmplSeq)
|
||||
)
|
||||
sendLog(
|
||||
locale.value,
|
||||
useAnalyticsLogDataDirect(props.resourcesData, props.pageVerTmplSeq)
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -31,21 +31,19 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
|
||||
const pageUrl = getPathAfterLanguage(to.path)
|
||||
|
||||
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
|
||||
// if (
|
||||
// !pageUrl ||
|
||||
// pageUrl === '' ||
|
||||
// pageUrl === '/' ||
|
||||
// pageUrl === `/${langCode}/`
|
||||
// ) {
|
||||
// return navigateTo(`/${langCode}/brand`, { external: false })
|
||||
// }
|
||||
// pageUrl이 빈값이거나 null이면 /home로 리다이렉트
|
||||
if (
|
||||
!pageUrl ||
|
||||
pageUrl === '' ||
|
||||
pageUrl === '/' ||
|
||||
pageUrl === `/${langCode}/`
|
||||
) {
|
||||
return navigateTo(`/${langCode}/home`, { external: false })
|
||||
}
|
||||
|
||||
// error 페이지는 API 호출하지 않음
|
||||
if (pageUrl === '/error' || to.path.includes('/error')) {
|
||||
console.log("🚀 ~pageData.global error 페이지는 API 호출하지 않음")
|
||||
return
|
||||
}
|
||||
if (pageUrl === '/error' || to.path.includes('/error')) return
|
||||
|
||||
// 페이지 이동 시 로딩 상태 시작
|
||||
loadingStore.startFullLoading()
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from 'h3'
|
||||
import { ssrGetFinalLocale } from '../../utils/localeUtil'
|
||||
import type { GameDataResponse } from '../../types/api/gameData'
|
||||
import type { ResGetInspectionData, WebInspectionData } from '../../types/InspectionType'
|
||||
import type {
|
||||
ResGetInspectionData,
|
||||
WebInspectionData,
|
||||
} from '../../types/InspectionType'
|
||||
import { isStaticFile } from '#layers/utils/commonUtil'
|
||||
import { getTrueClientIp } from '#layers/utils/apiUtil'
|
||||
|
||||
@@ -262,6 +265,8 @@ export default defineEventHandler(async event => {
|
||||
query: queryParams,
|
||||
})) as GameDataResponse | null
|
||||
|
||||
console.log('🚀 ~ gameData response:', response)
|
||||
|
||||
// 언어패스 쿠키 굽기 - 장기방안에서는 굽지않음
|
||||
if (initLangCodes?.includes(finalLocale)) {
|
||||
setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
*/
|
||||
const handleControlDimmed = (state: boolean) => {
|
||||
if (!import.meta.client) return
|
||||
|
||||
|
||||
if (state) {
|
||||
document.body.classList.add('dimmed')
|
||||
} else {
|
||||
@@ -104,14 +104,6 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
scrollStore.controlScrollLock(true)
|
||||
}
|
||||
|
||||
const handleResetYoutube = () => {
|
||||
youtube.storeIsOpen.value = false
|
||||
youtube.storeYoutubeUrl.value = ''
|
||||
youtube.storeIsOutsideClose.value = false
|
||||
youtube.storeModalName.value = ''
|
||||
scrollStore.controlScrollLock(false)
|
||||
}
|
||||
|
||||
// toast ------------------
|
||||
const toast = {
|
||||
storeIsOpen: ref(false),
|
||||
@@ -152,7 +144,7 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
modalName = '',
|
||||
contentTitle,
|
||||
tabInfo,
|
||||
tabActiveIndex,
|
||||
tabActiveIndex = 0,
|
||||
}: ContentParams) => {
|
||||
content.storeIsOpen.value = true
|
||||
content.storeModalName.value = modalName
|
||||
@@ -171,7 +163,6 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
handleOpenAlert,
|
||||
handleOpenConfirm,
|
||||
handleOpenYoutube,
|
||||
handleResetYoutube,
|
||||
handleOpenToast,
|
||||
handleOpenContent,
|
||||
handleControlDimmed,
|
||||
|
||||
@@ -3,7 +3,11 @@ import { useWindowScroll } from '@vueuse/core'
|
||||
|
||||
export const useScrollStore = defineStore('scrollStore', () => {
|
||||
const { y: windowY } = useWindowScroll({ behavior: 'smooth' })
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
|
||||
const headerHeight = computed(() => {
|
||||
return breakpoints.value.isMobile ? 48 : 64
|
||||
})
|
||||
const stoveGnbHeight = 48 as number
|
||||
const isPassedStoveGnb = ref(false)
|
||||
const scrollGnbPosition = ref(stoveGnbHeight)
|
||||
@@ -24,7 +28,7 @@ export const useScrollStore = defineStore('scrollStore', () => {
|
||||
|
||||
const controlScrollLock = (state: boolean) => {
|
||||
if (!import.meta.client) return
|
||||
|
||||
|
||||
if (state) {
|
||||
document.body.classList.add('scroll-lock')
|
||||
} else {
|
||||
@@ -32,6 +36,17 @@ export const useScrollStore = defineStore('scrollStore', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const scrollToAnchor = (targetId: string) => {
|
||||
const targetElement = document.getElementById(targetId)
|
||||
if (!targetElement) return
|
||||
|
||||
const elementTop = targetElement.getBoundingClientRect().top
|
||||
const currentScrollY = window.scrollY
|
||||
const targetScrollY = currentScrollY + elementTop - headerHeight.value
|
||||
|
||||
windowY.value = targetScrollY
|
||||
}
|
||||
|
||||
return {
|
||||
stoveGnbHeight,
|
||||
isPassedStoveGnb,
|
||||
@@ -39,5 +54,6 @@ export const useScrollStore = defineStore('scrollStore', () => {
|
||||
|
||||
updateScrollValue,
|
||||
controlScrollLock,
|
||||
scrollToAnchor,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -546,7 +546,10 @@ onMounted(async () => {
|
||||
</section>
|
||||
|
||||
<section class="section-static">
|
||||
<WidgetsFixSubTitle :title="tm('Coupon_Section_History_Title')">
|
||||
<WidgetsFixSubTitle
|
||||
:title="tm('Coupon_Section_History_Title')"
|
||||
class="flex-col"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-col items-start justify-start gap-[12px] w-full mt-[16px] md:gap-[16px] md:mt-[24px] lg:flex-row lg:items-end lg:justify-between lg:gap-[0]"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import { getComponentGroup, getComponentGroupAry } from '#layers/utils/dataUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { Platform } from '#layers/types/components/button'
|
||||
@@ -16,6 +15,8 @@ const props = defineProps<Props>()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
|
||||
const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_download.json'
|
||||
const stoveClientDownloadUrl = runtimeConfig.public
|
||||
.stoveClientDownloadUrl as string
|
||||
|
||||
// Multilingual
|
||||
const resultGetMultilingual = await useGetMultilingual({
|
||||
@@ -70,13 +71,6 @@ const mobileSpecArray = ref<Array<string>>(['Android', 'Ios'])
|
||||
const mobileOSArray = ref<Array<string>>(['AOS', 'iOS'])
|
||||
|
||||
// Computed
|
||||
const platformList = computed(() => {
|
||||
if (breakpoints.value.isMobile) {
|
||||
return ['MOBILE', 'PC', 'STOVE']
|
||||
} else {
|
||||
return ['PC', 'STOVE', 'MOBILE']
|
||||
}
|
||||
})
|
||||
const driverList = computed(() =>
|
||||
driverArray.value.map(driver => ({
|
||||
id: `DRIVER_${driver}`,
|
||||
@@ -103,6 +97,7 @@ const mobileSpecList = computed(() =>
|
||||
const mobileOSList = computed(() =>
|
||||
mobileOSArray.value.map(os => ({
|
||||
id: `MO_OS_${os}`,
|
||||
osType: tm(`Download_${os}_Type`),
|
||||
osCode: os,
|
||||
osText: tm(`Download_${os}_OS`),
|
||||
platformCode: tm(`Download_${os}_Platform`),
|
||||
@@ -141,116 +136,164 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
|
||||
<section class="section-static">
|
||||
<WidgetsFixSubTitle :title="tm('Download_Section_Platform_Title')" />
|
||||
|
||||
<BlocksSlideDefault
|
||||
:per-page="platformList.length"
|
||||
:gap="20"
|
||||
:arrows="false"
|
||||
:pagination="false"
|
||||
:drag="false"
|
||||
:breakpoints="{
|
||||
1023: {
|
||||
perPage: 'auto',
|
||||
gap: 12,
|
||||
focus: 0,
|
||||
drag: true,
|
||||
padding: { left: 0, right: 0 },
|
||||
},
|
||||
}"
|
||||
class="min-w-[320px] w-[100vw] px-[20px] ml-[-20px] sm:px-[40px] sm:ml-[-40px] md:w-full md:px-0 md:ml-0"
|
||||
<div
|
||||
class="relative flex flex-col-reverse sm:flex-row-reverse items-stretch justify-center gap-[20px] w-full md:flex-row"
|
||||
>
|
||||
<SplideSlide
|
||||
v-for="platform in platformList"
|
||||
:key="platform"
|
||||
class="flex flex-col items-center justify-between shrink-0 whitespace-normal w-[295px] h-[280px] bg-[#FFFFFF] p-[20px] rounded-[12px] text-left md:w-[calc((100%-40px)/3)] md:h-[314px] md:p-[24px] md:rounded-[16px] lg:w-[420px] lg:h-[340px] lg:p-[32px]"
|
||||
<div
|
||||
v-if="gameData?.platform_type !== '2'"
|
||||
class="relative flex flex-1 flex-col items-start justify-start h-[270px] py-[20px] rounded-[12px] bg-white sm:h-auto md:flex-[2] md:pt-[24px] md:pb-0 lg:pt-[32px]"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-start justify-start gap-[8px] w-full md:gap-[12px]"
|
||||
class="relative flex flex-col items-start justify-start w-full max-w-[800px] h-full px-[20px] mx-auto md:h-auto md:px-[28px] lg:px-[40px]"
|
||||
>
|
||||
<h4
|
||||
class="relative flex justify-left items-center w-full text-left text-[#1F1F1F] text-[18px] font-bold leading-[26px] tracking-[-0.54px] md:text-[24px] md:leading-[34px] md:tracking-[0.72px]"
|
||||
<span
|
||||
v-if="!breakpoints.isMobile"
|
||||
class="absolute top-0 right-[28px] w-[212px] h-[212px] lg:right-[40px]"
|
||||
>
|
||||
<span>{{ tm(`Download_Box_${platform}_Title`) }}</span>
|
||||
<img
|
||||
:src="
|
||||
getResourceHost('/images/common/img_desktop.png', {
|
||||
imageType: 'common',
|
||||
})
|
||||
"
|
||||
:alt="tm('Download_Box_PC_Title')"
|
||||
loading="lazy"
|
||||
draggable="false"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<h4
|
||||
class="relative flex justify-left items-center w-full gap-[4px] text-left text-[#1F1F1F] text-[18px] font-bold leading-[26px] tracking-[-0.54px] md:text-[24px] md:leading-[34px] md:tracking-[0.72px]"
|
||||
>
|
||||
<span>{{ tm('Download_Box_PC_Title') }}</span>
|
||||
|
||||
<AtomsTooltip
|
||||
position="top"
|
||||
:offset="8"
|
||||
background-color="#666666"
|
||||
text-color="#FFFFFF"
|
||||
:arrow="true"
|
||||
:content="tm('Download_Box_PC_Tooltip')"
|
||||
/>
|
||||
</h4>
|
||||
|
||||
<p
|
||||
v-if="
|
||||
tm(`Download_Box_${platform}_Description_List`).length === 1
|
||||
"
|
||||
v-dompurify-html="tm(`Download_Box_${platform}_Description01`)"
|
||||
class="text-[#999999] text-[14px] font-[400] leading-[24px] tracking-[-0.42px] md:text-[15px] md:tracking-[-0.45px]"
|
||||
></p>
|
||||
<ul
|
||||
v-else-if="
|
||||
tm(`Download_Box_${platform}_Description_List`).length > 1
|
||||
"
|
||||
<template
|
||||
v-for="(description, dIndex) in tm(
|
||||
'Download_Box_PC_Description_List'
|
||||
)"
|
||||
:key="dIndex"
|
||||
>
|
||||
<li
|
||||
v-for="description in tm(
|
||||
`Download_Box_${platform}_Description_List`
|
||||
)"
|
||||
:key="description"
|
||||
v-dompurify-html="tm(description)"
|
||||
class="relative pl-[22px] before:content-[''] before:absolute before:top-[10px] before:left-[9px] before:w-[3px] before:h-[3px] before:rounded-full before:bg-[#999999] text-[#999999] text-[14px] font-[400] leading-[24px] tracking-[-0.42px] md:text-[15px] md:tracking-[-0.45px]"
|
||||
></li>
|
||||
</ul>
|
||||
<p
|
||||
class="relative flex items-center justify-start w-full mt-[12px] text-left text-[#999999] text-[14px] font-[400] leading-[24px] tracking-[-0.42px] md:text-[15px] md:tracking-[-0.45px]"
|
||||
v-dompurify-html="tm(description as string)"
|
||||
></p>
|
||||
</template>
|
||||
|
||||
<AtomsButton
|
||||
v-if="platform !== 'STOVE'"
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="transparent"
|
||||
text-color="#1F1F1F"
|
||||
class="relative w-auto h-auto px-0 text-[16px] font-[500] leading-[24px] tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-p before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20"
|
||||
@click="
|
||||
handleMoveFocus(platform.toLowerCase() as 'pc' | 'mobile')
|
||||
"
|
||||
class="relative w-auto h-auto px-0 mt-[12px] text-[16px] font-[500] leading-[24px] tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-p before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20"
|
||||
@click="handleMoveFocus('pc')"
|
||||
>
|
||||
<span>{{ tm(`Download_Box_${platform}_SpecCheck`) }}</span>
|
||||
<span>{{ tm('Download_Box_PC_SpecCheck') }}</span>
|
||||
<AtomsIconsLongArrowRightLine
|
||||
:size="20"
|
||||
color="#1F1F1F"
|
||||
class="relative rotate-90"
|
||||
/>
|
||||
</AtomsButton>
|
||||
|
||||
<div
|
||||
v-if="breakpoints.isMobile"
|
||||
class="relative flex items-center justify-center w-full h-auto min-h-[48px] py-[14px] px-[40px] mt-[48px] border border-solid border-[rgba(0,0,0,0.1)] rounded-[8px] bg-[#EBEBEB] text-center text-[#999999] text-[14px] font-[500] leading-[20px] tracking-[-0.42px]"
|
||||
>
|
||||
<span>{{ tm('Download_Button_PC_Mobile') }}</span>
|
||||
</div>
|
||||
|
||||
<BlocksButtonLauncher
|
||||
v-else-if="breakpoints.isMd || breakpoints.isDesktop"
|
||||
platform="pc"
|
||||
class="!w-full !max-w-[300px] mt-[32px] lg:mt-[48px]"
|
||||
>
|
||||
<span>{{ tm('Download_Button_PC') }}</span>
|
||||
</BlocksButtonLauncher>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-center justify-center gap-[8px] w-full md:gap-[12px]"
|
||||
v-if="!breakpoints.isMobile"
|
||||
class="relative flex items-center justify-center w-full p-[25px] mt-[24px] rounded-b-[12px] bg-[#FAFAFA] lg:mt-[32px]"
|
||||
>
|
||||
<template v-if="platform === 'MOBILE'">
|
||||
<p
|
||||
class="relative flex items-center justify-center w-full gap-[8px] text-[#999999] text-[16px] font-[400] leading-[26px] tracking-[-0.48px]"
|
||||
>
|
||||
<span>{{ tm('Download_Text_Not_STOVE_Client') }}</span>
|
||||
<NuxtLink
|
||||
:href="stoveClientDownloadUrl"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center justify-start gap-[4px] text-[#3C75FF] text-[16px] font-[500] reading-[24px] tracking-[-0.48px]"
|
||||
>
|
||||
<span>{{ tm('Download_Button_STOVE') }}</span>
|
||||
<AtomsIconsDownloadLine color="#3C75FF" />
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="gameData?.platform_type !== '1'"
|
||||
class="relative flex flex-1 flex-col items-start justify-start h-[270px] p-[20px] rounded-[12px] bg-white sm:h-auto md:py-[24px] md:px-[28px] lg:pt-[32px] lg:pb-[40px] lg:px-[40px]"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-col items-start justify-start w-full max-w-[800px] mx-auto"
|
||||
>
|
||||
<h4
|
||||
class="relative flex justify-left items-center w-full text-left text-[#1F1F1F] text-[18px] font-bold leading-[26px] tracking-[-0.54px] md:text-[24px] md:leading-[34px] md:tracking-[0.72px]"
|
||||
>
|
||||
<span>{{ tm('Download_Box_MOBILE_Title') }}</span>
|
||||
</h4>
|
||||
|
||||
<AtomsButton
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="transparent"
|
||||
text-color="#1F1F1F"
|
||||
class="relative w-auto h-auto px-0 mt-[8px] mb-[48px] text-[16px] font-[500] leading-[24px] tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-p before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20 md:mt-[12px]"
|
||||
@click="handleMoveFocus('mobile')"
|
||||
>
|
||||
<span>{{ tm('Download_Box_MOBILE_SpecCheck') }}</span>
|
||||
<AtomsIconsLongArrowRightLine
|
||||
:size="20"
|
||||
color="#1F1F1F"
|
||||
class="relative rotate-90"
|
||||
/>
|
||||
</AtomsButton>
|
||||
|
||||
<div
|
||||
class="relative flex flex-col items-start justify-start w-full gap-[8px] mt-auto md:gap-[12px]"
|
||||
>
|
||||
<template v-for="os in mobileOSList" :key="os.id">
|
||||
<BlocksButtonLauncher
|
||||
v-if="device.isMobile ? os.isValue : true"
|
||||
v-if="
|
||||
device.isMobile
|
||||
? os.isValue
|
||||
: gameData?.os_type !== '3'
|
||||
? gameData?.os_type === os.osType
|
||||
: true
|
||||
"
|
||||
:platform="`${os.platformCode as Platform}`"
|
||||
class="!w-full"
|
||||
>
|
||||
<span>{{ os.platformText }}</span>
|
||||
</BlocksButtonLauncher>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AtomsButton
|
||||
v-if="breakpoints.isMobile"
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="#EBEBEB"
|
||||
text-color="#999999"
|
||||
:disabled="true"
|
||||
class="w-full px-0 border border-solid border-[rgba(0,0,0,0.1)] cursor-default"
|
||||
>
|
||||
<span>{{ tm(`Download_Button_${platform}_Mobile`) }}</span>
|
||||
</AtomsButton>
|
||||
<BlocksButtonLauncher
|
||||
v-else-if="breakpoints.isMd || breakpoints.isDesktop"
|
||||
:platform="`${platform.toLowerCase() as Platform}`"
|
||||
class="!w-full"
|
||||
>
|
||||
<span>{{ tm(`Download_Button_${platform}`) }}</span>
|
||||
</BlocksButtonLauncher>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
</BlocksSlideDefault>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section ref="specPCRef" class="section-static">
|
||||
@@ -336,7 +379,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
|
||||
<div class="flex items-center justify-center w-full">
|
||||
<img
|
||||
:src="
|
||||
getImageHost(
|
||||
getResourceHost(
|
||||
`/images/common/grades_driver/Type-${driver.driverCode}.svg`,
|
||||
{ imageType: 'common' }
|
||||
)
|
||||
@@ -426,4 +469,8 @@ table td {
|
||||
.splide :deep(.splide__track) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
::v-deep([data-platform='stove']) .icon-platform {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { getComponentGroup } from '#layers/utils/dataUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import { getImageHost } from '#layers/utils/styleUtil'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
@@ -233,7 +232,7 @@ onMounted(() => {
|
||||
>
|
||||
<img
|
||||
:src="
|
||||
getImageHost(card.benefitIcon, { imageType: 'common' })
|
||||
getResourceHost(card.benefitIcon, { imageType: 'common' })
|
||||
"
|
||||
:alt="card.benefitTitle"
|
||||
class="w-[48px] h-[48px] object-contain rounded-2xl"
|
||||
|
||||
@@ -19,6 +19,7 @@ interface Props {
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const pageDataStore = usePageDataStore()
|
||||
const modalStore = useModalStore()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
const { getOperateResources } = useOperateResources()
|
||||
|
||||
@@ -111,6 +112,10 @@ const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
currentRecommendedIndex.value = newIndex + 1
|
||||
}
|
||||
|
||||
const handleVideoClick = (url: string) => {
|
||||
modalStore.handleOpenYoutube({ youtubeUrl: url })
|
||||
}
|
||||
|
||||
const handleLoadMoreRecent = () => {
|
||||
if (hasMore.value) {
|
||||
currentRecentPage.value++
|
||||
@@ -144,12 +149,12 @@ const handleLoadMoreRecent = () => {
|
||||
v-for="(item, index) in recommendedVideos"
|
||||
:key="`recommended-${item.url}-${index}`"
|
||||
>
|
||||
<SplideSlide>
|
||||
<SplideSlide @click="handleVideoClick(item.url)">
|
||||
<div
|
||||
class="overflow-hidden aspect-[16/9] flex-shrink-0 w-full rounded-[4px] sm:w-[60.3%] sm:rounded-[8px] md:w-[56%] lg:w-[710px] lg:rounded-[12px]"
|
||||
>
|
||||
<img
|
||||
:src="getYouTubeThumbnail(item.url)"
|
||||
:src="getYouTubeThumbnail(item.url, 'maxres')"
|
||||
:alt="item.title || 'Video thumbnail'"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
@@ -189,7 +194,8 @@ const handleLoadMoreRecent = () => {
|
||||
<li
|
||||
v-for="(item, index) in visibleVideos"
|
||||
:key="`recent-${item.url}-${index}`"
|
||||
class="p-3 rounded-[12px] bg-white md:p-4 md:rounded-[16px] lg:p-5"
|
||||
class="p-3 rounded-[12px] bg-white md:p-4 md:rounded-[16px] lg:p-5 cursor-pointer"
|
||||
@click="handleVideoClick(item.url)"
|
||||
>
|
||||
<div
|
||||
class="overflow-hidden aspect-[16/9] w-full rounded-[4px] sm:rounded-[8px] lg:rounded-[12px]"
|
||||
@@ -256,7 +262,7 @@ const handleLoadMoreRecent = () => {
|
||||
lg:left-[850px];
|
||||
}
|
||||
.splide__slide {
|
||||
@apply flex flex-col p-3 gap-4 sm:flex-row sm:gap-6 md:gap-10 md:p-4 lg:gap-[60px] lg:p-5;
|
||||
@apply flex flex-col p-3 gap-4 sm:flex-row sm:gap-6 md:gap-10 md:p-4 lg:gap-[60px] lg:p-5 cursor-pointer;
|
||||
}
|
||||
.splide-pagination {
|
||||
@apply absolute bottom-[28px] left-[80px] right-[80px] text-center font-[500] text-[16px] leading-[24px] tracking-[-0.48px] text-[#999]
|
||||
|
||||
@@ -139,7 +139,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
<BlocksCardNews
|
||||
:title="item.title"
|
||||
:description="globalDateFormat(item.create_datetime, locale)"
|
||||
:img-path="getImageHost(item.media_thumbnail_url)"
|
||||
:img-path="getResourceHost(item.media_thumbnail_url)"
|
||||
:url="getArticleUrl(item.article_id)"
|
||||
link-target="_blank"
|
||||
/>
|
||||
|
||||
@@ -56,7 +56,6 @@ const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
/>
|
||||
<WidgetsBackground
|
||||
v-if="hasComponentGroup(item, 'foreground')"
|
||||
size="contain"
|
||||
:resources-data="getComponentGroup(item, 'foreground')"
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -45,7 +45,7 @@ const getMediaComponent = (item: PageDataTemplateComponentSet) => {
|
||||
|
||||
const getMediaImgSrcFromItem = (item: PageDataTemplateComponentSet) => {
|
||||
const mediaComponent = getMediaComponent(item)
|
||||
return mediaComponent ? getMediaImgSrc(mediaComponent) : ''
|
||||
return mediaComponent ? getMediaImgSrc(mediaComponent, 'maxres') : ''
|
||||
}
|
||||
|
||||
const getYouTubeEmbedUrlFromMedia = (item: PageDataTemplateComponentSet) => {
|
||||
@@ -95,7 +95,7 @@ const stopVideo = () => {
|
||||
clearTimeout(stopVideoTimeoutId)
|
||||
stopVideoTimeoutId = null
|
||||
}
|
||||
|
||||
|
||||
// 전환 시간 후 완전히 제거
|
||||
stopVideoTimeoutId = setTimeout(() => {
|
||||
playingSlideIndex.value = null
|
||||
@@ -170,27 +170,16 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
|
||||
<style scoped>
|
||||
.thumbnail-carousel {
|
||||
@apply w-full md:max-w-[944px];
|
||||
@apply w-full max-w-[688px] md:max-w-[944px];
|
||||
}
|
||||
.thumbnail-carousel:deep(.main-splide) {
|
||||
@apply overflow-hidden rounded-lg border border-white/10 shadow-[0_4px_20px_0_rgba(0,0,0,0.5)];
|
||||
}
|
||||
.thumbnail-carousel:deep(.thumbnail-slide) {
|
||||
@apply opacity-50;
|
||||
}
|
||||
.thumbnail-carousel:deep(.thumbnail-slide:hover),
|
||||
.thumbnail-carousel:deep(.thumbnail-slide.is-active) {
|
||||
@apply opacity-100;
|
||||
}
|
||||
.thumbnail-carousel:deep(.thumbnail-splide .splide__track) {
|
||||
@apply md:max-w-[720px];
|
||||
}
|
||||
|
||||
.main-slide {
|
||||
@apply relative aspect-[16/9];
|
||||
}
|
||||
.slide-image {
|
||||
@apply transition-opacity duration-500 ease-in-out;
|
||||
.thumbnail-carousel:deep(.thumbnail-splide .splide__track) {
|
||||
@apply md:max-w-[720px];
|
||||
}
|
||||
.btn-play {
|
||||
@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
|
||||
|
||||
@@ -60,7 +60,11 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
|
||||
<template>
|
||||
<section class="section-standard">
|
||||
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
|
||||
<WidgetsBackground
|
||||
v-if="backgroundData"
|
||||
:resources-data="backgroundData"
|
||||
dimmed
|
||||
/>
|
||||
<div class="content-standard px-0">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
getComponentGroup,
|
||||
getComponentContainer,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import { getImageHost } from '#layers/utils/styleUtil'
|
||||
import { getResourceHost } from '#layers/utils/styleUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/operateResources'
|
||||
|
||||
@@ -118,7 +118,7 @@ const onArrowClick = direction => {
|
||||
<BlocksCardNews
|
||||
:title="item.title"
|
||||
:description="globalDateFormat(item.reg_dt, locale)"
|
||||
:img-path="getImageHost(item.img_path)"
|
||||
:img-path="getResourceHost(item.img_path)"
|
||||
:url="item.url"
|
||||
:link-target="item.link_target"
|
||||
class="slide-inner"
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface YoutubeParams {
|
||||
|
||||
export interface ToastParams {
|
||||
contentText: string
|
||||
duration: number
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export interface ContentParams {
|
||||
|
||||
@@ -13,11 +13,11 @@ import type {
|
||||
} from '#layers/types/api/pageData'
|
||||
|
||||
/**
|
||||
* 이미지 경로를 완전한 호스트 URL로 변환합니다.
|
||||
* @param path 이미지 경로
|
||||
* @returns 완전한 이미지 URL
|
||||
* 리소스 경로를 완전한 호스트 URL로 변환합니다.
|
||||
* @param path 리소스 경로
|
||||
* @returns 완전한 리소스 URL
|
||||
*/
|
||||
export const getImageHost = (
|
||||
export const getResourceHost = (
|
||||
path: string,
|
||||
options: { imageType?: 'common' | 'game' } = {}
|
||||
): string => {
|
||||
@@ -71,8 +71,8 @@ export const getDeviceSrc = (
|
||||
if (!pcPath && !mobilePath) return null
|
||||
|
||||
const resolvedImages = {
|
||||
pc: pcPath ? getImageHost(pcPath, { imageType }) : '',
|
||||
mobile: mobilePath ? getImageHost(mobilePath, { imageType }) : '',
|
||||
pc: pcPath ? getResourceHost(pcPath, { imageType }) : '',
|
||||
mobile: mobilePath ? getResourceHost(mobilePath, { imageType }) : '',
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -156,7 +156,7 @@ export const getPaginationClass = (
|
||||
*/
|
||||
export const getMediaImgSrc = (
|
||||
resourceGroups: PageDataResourceGroup,
|
||||
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'high'
|
||||
quality?
|
||||
): string => {
|
||||
if (!resourceGroups) return ''
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export const getYouTubeEmbedUrl = (
|
||||
*/
|
||||
export const getYouTubeThumbnail = (
|
||||
url: string,
|
||||
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'high'
|
||||
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'standard'
|
||||
): string => {
|
||||
const videoId = getYouTubeId(url)
|
||||
if (!videoId) return ''
|
||||
|
||||
BIN
public/images/common/img_desktop.png
Normal file
BIN
public/images/common/img_desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Reference in New Issue
Block a user