Resolve merge conflicts in ButtonList.vue and pageData.ts

This commit is contained in:
“hyeonggkim”
2025-10-17 11:15:48 +09:00
81 changed files with 1090 additions and 802 deletions

View File

@@ -1,28 +1,19 @@
<script setup lang="ts">
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
const props = defineProps<{ resourcesData: PageDataResourceGroup }>()
const emit = defineEmits<{
(e: 'click'): void
}>()
const bgStyles = getResponsiveSrc(props.resourcesData?.res_path, {
resourcesType: 'bg',
})
</script>
<template>
<button
v-if="resourcesData && bgStyles"
class="relative group bg-cover bg-center bg-no-repeat w-[66px] h-[66px] md:w-[100px] md:h-[100px]"
:class="getResponsiveClass()"
:style="bgStyles"
@click="emit('click')"
>
<span
class="absolute inset-0 m-[10px] bg-white opacity-0 group-hover:opacity-10 transition-opacity duration-300 ease-in-out rounded-[50%]"
/>
<button class="btn-play" @click="emit('click')">
<span class="sr-only">Play</span>
</button>
</template>
<style scoped>
.btn-play {
@apply relative w-[66px] h-[66px] bg-[image:var(--video-play)] bg-cover bg-center bg-no-repeat md:w-[100px] md:h-[100px]
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:rounded-[50%] after:opacity-0 after:transition-opacity after:duration-300 after:ease-in-out
hover:after:opacity-10;
}
</style>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import type { ButtonVariant } from '#layers/types/components/button'
interface props {
variant?: ButtonVariant
disabled?: boolean
}
const props = withDefaults(defineProps<props>(), {
variant: 'filled',
disabled: false,
})
</script>
<template>
<button :class="['btn-base', props.variant]" :disabled="props.disabled">
<slot />
</button>
</template>
<style scoped>
.btn-base {
@apply relative w-full py-[14px] px-5 text-sm font-medium rounded-lg
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-black/10 before:rounded-lg before:transition-all before:duration-300 before:ease-in-out;
}
.btn-base.filled {
@apply bg-[var(--primary)] text-[var(--text-secondary)]
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:rounded-lg after:transition-opacity after:duration-300 after:ease-in-out after:opacity-0
hover:after:opacity-20;
}
.btn-base.outlined {
@apply bg-white text-[#333333]
hover:before:border-[#999];
}
.btn-base:disabled {
@apply cursor-default bg-[#EBEBEB] text-[#999]
before:border-[#D9D9D9]
after:hidden
hover:before:border-[#D9D9D9];
}
</style>

View File

@@ -1,45 +1,25 @@
<script setup lang="ts">
import type { ButtonType } from '#layers/types/components/button'
interface ButtonProps {
interface props {
type?: ButtonType
icon?: string
buttonSize?: string
target?: '_self' | '_blank'
href?: string
rel?: string
backgroundColor?: string
backgroundImage?: string
textColor?: string
disabled?: boolean
class?: string
}
const props = withDefaults(defineProps<ButtonProps>(), {
const props = withDefaults(defineProps<props>(), {
type: 'action',
buttonSize: 'size-extra-small md:size-large',
backgroundColor: 'var(--primary)',
textColor: 'var(--alternative-02)',
disabled: false,
})
const buttonClasses = computed(() =>
['btn-base group', props.class].filter(Boolean)
)
const buttonStyles = computed(() => {
const styles: Record<string, string> = {
backgroundColor: props.backgroundColor,
color: props.textColor,
'--text-color': props.textColor,
}
if (props.backgroundImage) {
styles.backgroundImage = `url(${props.backgroundImage})`
styles.backgroundSize = 'contain'
styles.backgroundPosition = 'center'
styles.backgroundRepeat = 'no-repeat'
}
return styles
})
const componentTag = computed((): string => {
switch (props.type) {
case 'download':
@@ -77,13 +57,41 @@ const componentProps = computed(() => {
<template>
<component
:is="componentTag"
v-bind="componentProps"
:class="buttonClasses"
:style="buttonStyles"
v-bind="{ ...componentProps }"
:class="['btn-base', props.buttonSize]"
:style="{
backgroundColor: props.backgroundColor,
color: props.textColor,
}"
>
<span class="relative flex items-center gap-2 z-[1]">
<span class="btn-content">
<slot />
<span v-if="props.icon" class="flex-shrink-0" v-html="props.icon" />
<AtomsIconsLongArrowRightLine v-if="props.type === 'internal'" />
<AtomsIconsWebLinkLine
v-if="props.type === 'external'"
size="24"
color="#ebebeb"
/>
<AtomsIconsDownloadLine v-if="props.type === 'download'" />
</span>
</component>
</template>
<style scoped>
.btn-base {
@apply overflow-hidden relative inline-flex items-center justify-center font-medium cursor-pointer
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-white/10
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:transition-opacity after:duration-300 after:ease-in-out after:opacity-0;
}
.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 .btn-content {
@apply relative flex items-center gap-1 z-[1];
}
</style>

View File

@@ -2,13 +2,11 @@
interface Props {
size?: number | string
color?: string
className?: string
}
withDefaults(defineProps<Props>(), {
size: 12,
color: 'var(--foreground-gray-500)',
className: '',
})
</script>
@@ -19,7 +17,6 @@ withDefaults(defineProps<Props>(), {
:height="size"
viewBox="0 0 12 12"
:fill="color"
:class="className"
>
<path
d="M5.29499 7.715L2.39999 4.875C2.07499 4.555 2.29999 4 2.75999 4L9.23499 4C9.69499 4 9.91999 4.555 9.59499 4.875L6.69999 7.715C6.30999 8.095 5.68999 8.095 5.29999 7.715H5.29499Z"

View File

@@ -2,13 +2,11 @@
interface Props {
size?: number | string
color?: string
className?: string
}
withDefaults(defineProps<Props>(), {
size: 32,
color: '#EBEBEB',
className: '',
})
</script>
@@ -19,7 +17,6 @@ withDefaults(defineProps<Props>(), {
:height="size"
viewBox="0 0 32 32"
fill="none"
:class="className"
>
<path
d="M26.2768 8.10947C26.7975 7.58877 26.7975 6.74455 26.2768 6.22385C25.7561 5.70315 24.9119 5.70315 24.3912 6.22385L16.0007 14.6144L7.61013 6.22385C7.08943 5.70315 6.24521 5.70315 5.72451 6.22385C5.20381 6.74455 5.20381 7.58877 5.72451 8.10947L14.115 16.5L5.72451 24.8905C5.20381 25.4112 5.20381 26.2554 5.72451 26.7761C6.24521 27.2968 7.08943 27.2968 7.61013 26.7761L16.0007 18.3856L24.3912 26.7761C24.9119 27.2968 25.7561 27.2968 26.2768 26.7761C26.7975 26.2554 26.7975 25.4112 26.2768 24.8905L17.8863 16.5L26.2768 8.10947Z"

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
interface Props {
size?: number | string
color?: string
}
withDefaults(defineProps<Props>(), {
size: 24,
color: '#EBEBEB',
})
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M10.75 3.25L10.75 12.2322L6.88391 8.36611C6.39576 7.87796 5.6043 7.87796 5.11615 8.36611C4.62799 8.85427 4.62799 9.64573 5.11615 10.1339L11.1161 16.1339C11.3506 16.3683 11.6685 16.5 12 16.5C12.3316 16.5 12.6495 16.3683 12.8839 16.1339L18.8839 10.1339C19.3721 9.64573 19.3721 8.85427 18.8839 8.36611C18.3958 7.87796 17.6043 7.87796 17.1161 8.36611L13.25 12.2322L13.25 3.25C13.25 2.55964 12.6904 2 12 2C11.3097 2 10.75 2.55964 10.75 3.25Z"
:fill="color"
/>
<path
d="M20 21C20.6904 21 21.25 20.4404 21.25 19.75L21.25 17.75C21.25 17.0596 20.6904 16.5 20 16.5C19.3097 16.5 18.75 17.0596 18.75 17.75L18.75 18.5L5.25003 18.5L5.25003 17.75C5.25003 17.0596 4.69039 16.5 4.00003 16.5C3.30967 16.5 2.75003 17.0596 2.75003 17.75L2.75003 19.75C2.75003 20.4404 3.30967 21 4.00003 21L20 21Z"
:fill="color"
/>
</svg>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
interface Props {
size?: number | string
color?: string
}
withDefaults(defineProps<Props>(), {
size: 24,
color: '#EBEBEB',
})
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
viewBox="0 0 24 24"
fill="none"
>
<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>

View File

@@ -2,13 +2,11 @@
interface Props {
size?: number | string
color?: string
className?: string
}
withDefaults(defineProps<Props>(), {
size: 24,
color: 'var(--foreground-reversal)',
className: '',
})
</script>
@@ -19,7 +17,6 @@ withDefaults(defineProps<Props>(), {
:height="size"
viewBox="0 0 24 24"
fill="none"
:class="className"
>
<path
d="M20 16C20.7594 16 21.375 16.6156 21.375 17.375C21.375 18.1344 20.7594 18.75 20 18.75H4C3.24061 18.75 2.625 18.1344 2.625 17.375C2.625 16.6156 3.24061 16 4 16H20ZM20 10.5C20.7594 10.5 21.375 11.1156 21.375 11.875C21.375 12.6344 20.7594 13.25 20 13.25H4C3.24061 13.25 2.625 12.6344 2.625 11.875C2.625 11.1156 3.24061 10.5 4 10.5H20ZM20 5C20.7594 5 21.375 5.61561 21.375 6.375C21.375 7.13439 20.7594 7.75 20 7.75H4C3.24061 7.75 2.625 7.13439 2.625 6.375C2.625 5.61561 3.24061 5 4 5H20Z"

View File

@@ -2,13 +2,11 @@
interface Props {
size?: number | string
color?: string
className?: string
}
withDefaults(defineProps<Props>(), {
size: 16,
color: 'var(--foreground-reversal)',
className: '',
})
</script>
@@ -19,7 +17,6 @@ withDefaults(defineProps<Props>(), {
:height="size"
viewBox="0 0 16 16"
fill="none"
:class="className"
>
<path
d="M15.7071 1.70711C16.0976 1.31658 16.0976 0.683417 15.7071 0.292893C15.3166 -0.0976311 14.6834 -0.0976311 14.2929 0.292893L8 6.58579L1.70711 0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292894 0.292893C-0.0976304 0.683417 -0.0976304 1.31658 0.292894 1.70711L6.58579 8L0.292893 14.2929C-0.0976311 14.6834 -0.0976311 15.3166 0.292893 15.7071C0.683417 16.0976 1.31658 16.0976 1.70711 15.7071L8 9.41421L14.2929 15.7071C14.6834 16.0976 15.3166 16.0976 15.7071 15.7071C16.0976 15.3166 16.0976 14.6834 15.7071 14.2929L9.41421 8L15.7071 1.70711Z"

View File

@@ -2,13 +2,11 @@
interface Props {
size?: number | string
color?: string
className?: string
}
withDefaults(defineProps<Props>(), {
size: 24,
color: 'var(--foreground-reversal)',
className: '',
})
</script>
@@ -19,7 +17,6 @@ withDefaults(defineProps<Props>(), {
:height="size"
viewBox="0 0 24 24"
fill="none"
:class="className"
>
<path
d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z"

View File

@@ -2,13 +2,11 @@
interface Props {
size?: number | string
color?: string
className?: string
}
withDefaults(defineProps<Props>(), {
size: 12,
color: '#FD3886',
className: '',
})
</script>
@@ -19,7 +17,6 @@ withDefaults(defineProps<Props>(), {
:height="size"
viewBox="0 0 12 12"
fill="none"
:class="className"
>
<path
d="M6.37364 3.24966C6.22658 2.91678 5.77344 2.91678 5.62638 3.24966L4.95508 4.76916L3.36352 4.96618C3.01484 5.00934 2.87482 5.4593 3.1326 5.7082L4.30928 6.84431L3.99693 8.48558C3.9285 8.84514 4.2951 9.12323 4.60148 8.94417L6.00001 8.12684L7.39853 8.94417C7.70491 9.12323 8.07151 8.84514 8.00308 8.48558L7.69074 6.84431L8.8674 5.70819C9.12518 5.4593 8.98515 5.00934 8.63648 4.96618L7.04492 4.76916L6.37364 3.24966Z"

View File

@@ -2,13 +2,11 @@
interface Props {
size?: number | string
color?: string
className?: string
}
withDefaults(defineProps<Props>(), {
size: 16,
color: 'var(--foreground-gray-500)',
className: '',
})
</script>
@@ -19,7 +17,6 @@ withDefaults(defineProps<Props>(), {
:height="size"
viewBox="0 0 16 16"
fill="none"
:class="className"
>
<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"

View File

@@ -0,0 +1,85 @@
<script setup>
const {
isProcessing,
isShowCheckLauncher,
isShowDownloadLauncher,
validateLauncher,
downloadLauncher,
} = useCheckGameStart()
</script>
<template>
<AtomsButton
:class="$attrs?.class"
:disabled="isProcessing"
@click="validateLauncher"
>
<slot />
</AtomsButton>
<ClientOnly>
<Teleport to="#teleports">
<BlocksModalLayer
v-model:is-open="isShowCheckLauncher"
:is-show-dimmed="true"
:is-outside-close="false"
:modal-name="'launcher'"
area-class="max-w-[480px] pt-[56px] px-[24px] pb-[24px] rounded-[8px]"
close-class="absolute top-[16px] right-[24px]"
>
<span class="ico-loading"></span>
<!-- [TODO] i18n 적용 -->
<!-- <p class="text-check">{{ tm('Common_Message_Check_Client').txt }}</p> -->
<p class="text-check">pc 클라이언트 실행 ...</p>
<Transition name="fade">
<div v-if="isShowDownloadLauncher" class="client-area">
<!-- <p
v-dompurify-html="tm('Common_Message_Download_Client').txt"
class="text-info"
></p>
<button type="button" class="btn-download" @click="downloadLauncher">
{{ tm('Common_Message_Install').txt }}
</button>
<p
v-dompurify-html="tm('Common_Message_Download_Close').txt"
class="text-tip"
></p> -->
<p class="text-info">
PC 클라이언트가 실행되지 않나요?
<br />
다운로드 전이라면 다운로드 진행해주세요
</p>
<AtomsButtonVariant class="max-w-[300px]" @click="downloadLauncher">
다운로드
</AtomsButtonVariant>
<p
v-dompurify-html="
'*PC 클라이언트가 정상 실행되었다면 팝업을 닫아 주세요.'
"
class="text-tip"
></p>
</div>
</Transition>
</BlocksModalLayer>
</Teleport>
</ClientOnly>
</template>
<style scoped>
.ico-loading {
@apply block mx-auto mb-4 w-[80px] h-[80px] bg-[url('/images/common/stove_loading_light.png')] bg-contain bg-center bg-no-repeat;
}
.text-check {
@apply mb-6 text-center text-[20px] font-bold leading-[30px] tracking-[-0.6px] text-[#333333];
}
.client-area {
@apply pt-4 border-t border-[rgba(0,0,0,0.08)] text-center;
}
.text-info {
@apply mb-3 text-[14px] font-medium leading-[24px] tracking-[-0.42px] text-[#333333];
}
.text-tip {
@apply mt-4 text-[14px] leading-[20px] tracking-[-0.42px] text-[#999999];
}
</style>

View File

@@ -33,7 +33,7 @@ const componentProps = computed(() => {
</script>
<template>
<component :is="componentTag" v-bind="{ ...$attrs, ...componentProps }">
<component :is="componentTag" v-bind="{ ...componentProps }">
<slot />
</component>
</template>

View File

@@ -1,10 +1,8 @@
<template>
<div id="stove-wrapper" class="relative z-[5]" />
</template>
<script setup lang="ts">
import { useGameDataStore } from '#layers/stores/useGameDataStore'
let cpHeader: any = null
const runtimeConfig = useRuntimeConfig()
const { locale, availableLocales } = useI18n()
const { gameData } = useGameDataStore()
@@ -22,7 +20,7 @@ const languageCodes = computed(() => {
return [locale]
})
function loadGnb(locale: string) {
const loadGnb = (locale: string) => {
locale = locale.toLowerCase()
const gnbOption = {
@@ -51,11 +49,22 @@ function loadGnb(locale: string) {
},
}
const cpHeader = new (window as any).cp.Header(gnbOption)
cpHeader = new (window as any).cp.Header(gnbOption)
cpHeader.render()
}
onMounted(() => {
loadGnb(locale.value)
})
onBeforeUnmount(() => {
if (cpHeader && typeof cpHeader.destroy === 'function') {
cpHeader.destroy()
}
cpHeader = null
})
</script>
<template>
<div id="stove-wrapper" class="relative z-[5]" />
</template>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
interface props {
isShowDimmed?: boolean
contentText?: string
confirmButtonText?: string
isOutsideClose?: boolean
modalName?: string
}
const props = withDefaults(defineProps<props>(), {
isShowDimmed: false,
isOutsideClose: false,
})
const emit = defineEmits(['confirmButtonEvent'])
const isOpen = defineModel<boolean>('isOpen', { default: false })
const setButtonEvent = (event?: () => void | void) => {
if (typeof event === 'function') {
return event()
}
isOpen.value = false
}
const handleOutsideClick = () => {
if (props.isOutsideClose) {
isOpen.value = false
}
}
</script>
<template>
<Transition name="fade">
<div
v-if="isOpen"
:class="['modal-wrap', { dimmed: props.isShowDimmed }, props.modalName]"
@click="handleOutsideClick"
>
<div class="modal-area" @click.stop>
<div class="modal-content">
<p
v-if="props.contentText"
v-dompurify-html="props.contentText"
class="content-text"
></p>
<slot></slot>
<div class="content-btns">
<AtomsButtonVariant
@click="setButtonEvent(() => emit('confirmButtonEvent'))"
>
{{ props.confirmButtonText || '확인' }}
</AtomsButtonVariant>
</div>
</div>
</div>
</div>
</Transition>
</template>
<style scoped>
.modal-area {
@apply max-w-[312px] p-6 bg-white rounded-[20px];
}
</style>

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
interface props {
isShowDimmed?: boolean
contentText?: string
confirmButtonText?: string
cancelButtonText?: string
isOutsideClose?: boolean
modalName?: string
}
const props = withDefaults(defineProps<props>(), {
isShowDimmed: false,
isOutsideClose: false,
})
const emit = defineEmits(['cancelButtonEvent', 'confirmButtonEvent'])
const isOpen = defineModel<boolean>('isOpen', { default: false })
const setButtonEvent = (event?: () => void) => {
if (event) {
event()
}
isOpen.value = false
}
const handleOutsideClick = () => {
if (props.isOutsideClose) {
isOpen.value = false
}
}
</script>
<template>
<Transition name="fade">
<div
v-if="isOpen"
:class="['modal-wrap', { dimmed: props.isShowDimmed }, props.modalName]"
@click="handleOutsideClick"
>
<div class="modal-area" @click.stop>
<div class="modal-content">
<p
v-if="props.contentText"
v-dompurify-html="props.contentText"
class="content-text"
></p>
<slot></slot>
<div class="content-btns">
<AtomsButtonVariant
variant="outlined"
@click="setButtonEvent(() => emit('cancelButtonEvent'))"
>
{{ props.cancelButtonText || '취소' }}
</AtomsButtonVariant>
<AtomsButtonVariant
@click="setButtonEvent(() => emit('confirmButtonEvent'))"
>
{{ props.confirmButtonText || '확인' }}
</AtomsButtonVariant>
</div>
</div>
</div>
</div>
</Transition>
</template>
<style scoped>
.modal-area {
@apply max-w-[312px] p-6 bg-white rounded-[20px];
}
</style>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
interface props {
isShowDimmed?: boolean
isOutsideClose?: boolean
modalName?: string
areaClass?: string
closeClass?: string
}
const props = withDefaults(defineProps<props>(), {
isShowDimmed: false,
isOutsideClose: false,
})
const isOpen = defineModel<boolean>('isOpen', { default: false })
const handleCloseModal = () => {
isOpen.value = false
}
const handleOutsideClick = () => {
if (props.isOutsideClose) {
handleCloseModal()
}
}
</script>
<template>
<Transition name="fade">
<div
v-if="isOpen"
:class="['modal-wrap', { dimmed: props.isShowDimmed }, props.modalName]"
@click="handleOutsideClick"
>
<div :class="['modal-area', props.areaClass]" @click.stop>
<div class="modal-content">
<slot></slot>
</div>
<button
type="button"
:class="['modal-close', props.closeClass]"
@click="handleCloseModal"
>
<span class="sr-only">close</span>
<AtomsIconsCloseLine size="24" color="#333333" />
</button>
</div>
</div>
</Transition>
</template>

View File

@@ -1,19 +1,61 @@
<script setup lang="ts">
import { getYouTubeEmbedUrl } from '#layers/utils/youtube'
interface Props {
youtubeUrl: string
isOutsideClose?: boolean
modalName?: string
}
const props = withDefaults(defineProps<Props>(), {
youtubeUrl: '',
isOutsideClose: false,
})
const emit = defineEmits(['closeButtonEvent'])
const isOpen = defineModel<boolean>('isOpen', { default: false })
const embedUrl = computed(() => {
return getYouTubeEmbedUrl(props.youtubeUrl)
})
const handleCloseModal = () => {
isOpen.value = false
emit('closeButtonEvent')
}
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen.value) {
handleCloseModal()
}
}
const handleOutsideClick = () => {
if (props.isOutsideClose) {
handleCloseModal()
}
}
// 키보드 이벤트 리스너 등록/해제
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
</script>
<template>
<Transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<Transition name="fade">
<div
v-if="isOpen"
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75"
@click="handleBackdropClick"
:class="props.modalName"
@click="handleOutsideClick"
>
<div
v-if="isOpen"
class="relative mx-4 my-4"
style="
width: min(896px, 90vw, calc((90vh - 2rem) * 16 / 9));
@@ -23,11 +65,7 @@
>
<!-- 헤더 -->
<div class="flex justify-end mb-3 md:mb-4">
<button
class="text-white rounded-full transition-colors"
aria-label="모달 닫기"
@click="closeModal"
>
<button type="button" @click="handleCloseModal">
<AtomsIconsCloseLine />
</button>
</div>
@@ -48,61 +86,3 @@
</div>
</Transition>
</template>
<script setup lang="ts">
import { getYouTubeEmbedUrl } from '#layers/utils/youtube'
interface Props {
isOpen: boolean
youtubeUrl: string
title?: string
description?: string
closeOnBackdrop?: boolean
}
interface Emits {
(e: 'closeButtonEvent'): void
}
const props = withDefaults(defineProps<Props>(), {
isOpen: false,
youtubeUrl: '',
title: '',
description: '',
closeOnBackdrop: true,
})
const emit = defineEmits<Emits>()
const embedUrl = computed(() => {
return getYouTubeEmbedUrl(props.youtubeUrl)
})
// ESC 키로 모달 닫기
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && props.isOpen) {
closeModal()
}
}
// 배경 클릭으로 모달 닫기
const handleBackdropClick = () => {
if (props.closeOnBackdrop) {
closeModal()
}
}
// 모달 닫기 함수
const closeModal = () => {
emit('closeButtonEvent')
}
// 키보드 이벤트 리스너 등록/해제
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
</script>

View File

@@ -125,19 +125,27 @@ const handleMoved = (
width: var(--banner-width-mo);
height: var(--banner-height-mo-active);
margin-right: var(--banner-gap-mo);
opacity: 0.5;
}
.center-highlight:deep(.splide__slide) .slide-inner {
width: var(--banner-width-mo);
height: var(--banner-height-mo);
opacity: 0.5;
}
.center-highlight:deep(.splide__slide.is-active) {
width: var(--banner-width-mo-container);
opacity: 1;
}
.center-highlight:deep(.splide__slide.is-active) .slide-inner {
width: var(--banner-width-mo-active);
height: var(--banner-height-mo-active);
opacity: 1;
transition: all 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.center-highlight:deep(.splide__slide.is-next),
.center-highlight:deep(.splide__slide.is-prev) {
opacity: 1;
}
/* PC 스타일 */
@media (min-width: 1024px) {

View File

@@ -1,15 +1,19 @@
<script setup lang="ts">
import { Splide, SplideSlide } from '@splidejs/vue-splide'
import { getFirstGroup, isTypeVideo } from '#layers/utils/dataUtil'
import { getMediaSrc, getYouTubeEmbedUrl } from '#layers/utils/youtube'
import { getComponentGroup, isTypeVideo } from '#layers/utils/dataUtil'
import {
getMediaSrc,
getYouTubeEmbedUrl,
getMediaImgSrc,
} from '#layers/utils/youtube'
import type { Splide as SplideType, Options } from '@splidejs/splide'
import type {
PageDataResourceGroups,
PageDataTemplateComponentSet,
PageDataResourceGroup,
} from '#layers/types/api/pageData'
interface Props {
slideData: { media: PageDataResourceGroups; set_order: number }[]
slideData: PageDataTemplateComponentSet[]
videoPlay?: PageDataResourceGroup
arrows?: boolean
pagination?: boolean
@@ -19,6 +23,9 @@ interface Props {
const props = defineProps<Props>()
let mainInst: SplideType | null = null
let thumbsInst: SplideType | null = null
const mainRef = ref<InstanceType<typeof Splide> | null>(null)
const thumbsRef = ref<InstanceType<typeof Splide> | null>(null)
const playingSlideIndex = ref<number | null>(null)
@@ -33,6 +40,7 @@ const mainOptions = computed<Options>(() => ({
pagination: false,
drag: false,
}))
const thumbOptions = computed<Options>(() => ({
type: 'slide',
rewind: true,
@@ -51,39 +59,46 @@ const thumbOptions = computed<Options>(() => ({
},
}))
const isPassVideo = (groups: PageDataResourceGroups, index: number) => {
const firstGroup = getFirstGroup(groups)
return (
firstGroup &&
isTypeVideo(firstGroup?.resource_type) &&
index !== playingSlideIndex.value
)
const getMediaComponent = (item: PageDataTemplateComponentSet) => {
return getComponentGroup(item, 'media')
}
const getMediaImgSrcFromItem = (item: PageDataTemplateComponentSet) => {
const mediaComponent = getMediaComponent(item)
return mediaComponent ? getMediaImgSrc(mediaComponent) : ''
}
const getYouTubeEmbedUrlFromMedia = (item: PageDataTemplateComponentSet) => {
const mediaComponent = getMediaComponent(item)
if (!mediaComponent) return ''
const mediaSrc = getMediaSrc(mediaComponent)
return mediaSrc ? getYouTubeEmbedUrl(mediaSrc, true) : ''
}
const isPassVideo = (item: PageDataTemplateComponentSet, index: number) => {
const mediaComponent = getMediaComponent(item)
const isNotPlaying = index !== playingSlideIndex.value
const isVideoType =
mediaComponent && isTypeVideo(mediaComponent?.resource_type)
return isVideoType && isNotPlaying
}
const handleVideoClick = (index: number) => {
playingSlideIndex.value = index
}
const getYouTubeEmbedUrlFromMedia = (
resourceGroups: PageDataResourceGroup[]
) => {
const resourceGroup = getFirstGroup(resourceGroups)
const mediaSrc = getMediaSrc(resourceGroup)
return mediaSrc ? getYouTubeEmbedUrl(mediaSrc, true) : ''
const stopVideo = () => {
playingSlideIndex.value = null
}
let mainInst: SplideType | null = null
let thumbsInst: SplideType | null = null
onMounted(() => {
mainInst = mainRef.value?.splide ?? null
thumbsInst = thumbsRef.value?.splide ?? null
if (mainInst && thumbsInst) {
mainInst.sync(thumbsInst)
mainInst.on('moved', () => {
playingSlideIndex.value = null
})
mainInst.on('moved', stopVideo)
}
})
@@ -103,21 +118,21 @@ onBeforeUnmount(() => {
class="main-slide"
>
<img
:src="getMediaImgSrc(item.media)"
:src="getMediaImgSrcFromItem(item)"
alt="main image"
class="slide-image"
:class="{ 'opacity-0': playingSlideIndex === index }"
/>
<AtomsButtonPlay
v-if="isPassVideo(item.media, index)"
v-if="isPassVideo(item, index)"
:resources-data="videoPlay"
class="btn-play"
@click="handleVideoClick(index)"
/>
<iframe
v-if="playingSlideIndex === index"
:src="getYouTubeEmbedUrlFromMedia(item.media)"
class="absolute top-0 left-0 w-full h-full"
:src="getYouTubeEmbedUrlFromMedia(item)"
class="video-iframe"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
@@ -133,7 +148,7 @@ onBeforeUnmount(() => {
class="thumbnail-slide"
>
<img
:src="getMediaImgSrc(item.media)"
:src="getMediaImgSrcFromItem(item)"
alt="thumbnail image"
class="slide-image"
/>
@@ -143,8 +158,6 @@ onBeforeUnmount(() => {
</template>
<style scoped>
/* 비디오 iframe 전환 애니메이션 */
.thumbnail-carousel {
@apply w-full md:max-w-[944px];
}
@@ -161,6 +174,9 @@ onBeforeUnmount(() => {
.btn-play {
@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
}
.video-iframe {
@apply absolute top-0 left-0 w-full h-full;
}
.thumbnail-splide {
@apply overflow-hidden flex justify-center w-screen mt-[20px] mx-[-20px] sm:mx-[-40px] md:w-auto md:mx-0 md:px-[120px] md:mt-[28px];

View File

@@ -59,7 +59,7 @@ const handleMenuClose = () => (isMenuOpen.value = false)
// navAreaRef의 넓이를 구하는 함수
const calculateNavWidth = () => {
if (!navAreaRef.value) return 0
if (!navAreaRef.value || !gnbData) return 0
const navAreaWidth = navAreaRef.value.offsetWidth
const moreWidth = 72 // 더보기 버튼 넓이 + 마진
@@ -68,7 +68,7 @@ const calculateNavWidth = () => {
// startRef의 넓이를 구하는 함수
const calculateStartWidth = () => {
if (!startRef.value) return 0
if (!startRef.value || !gnbData) return 0
const startWidth = startRef.value.offsetWidth
const headerRightPadding = 40 // 헤더 오른쪽 마진
@@ -98,7 +98,7 @@ const calculateOfficialItemWidths = () => {
// 오버플로우 계산 함수
const calculateOverflow = () => {
if (!navAreaRef.value) return
if (!navAreaRef.value || !startRef.value) return
const totalNavWidth = navWidth.value + startWidth.value
const screenWidth = width.value
@@ -129,7 +129,10 @@ const calculateOverflow = () => {
}
}
onClickOutside(navAreaRef, () => (isMenuOpen.value = false))
const stopClickOutside = onClickOutside(
navAreaRef,
() => (isMenuOpen.value = false)
)
// 화면 크기 변경 시 오버플로우 재계산
watch(width, () => {
@@ -147,6 +150,12 @@ onMounted(() => {
}
})
})
onBeforeUnmount(() => {
if (stopClickOutside) {
stopClickOutside()
}
})
</script>
<template>
@@ -171,7 +180,7 @@ onMounted(() => {
>
<div ref="navAreaRef" class="nav-area">
<div class="nav-logo">
<AtomsLocaleLink to="/brand">
<AtomsLocaleLink to="/brand" @click="handleMenuClose">
<img
:src="gnbData?.bi_path"
:alt="gameData?.game_name"
@@ -203,24 +212,26 @@ onMounted(() => {
class="hidden md:block"
/>
</BlocksHybridLink>
<div v-if="gnbItem.children" class="nav-2depth">
<ul>
<li
v-for="child in gnbItem.children"
:key="child.menu_name"
>
<BlocksHybridLink
:to="child.url_path"
:target="child.link_target"
<Transition name="fade">
<div v-if="gnbItem.children" class="nav-2depth">
<ul>
<li
v-for="child in gnbItem.children"
:key="child.menu_name"
>
<span>{{ child.menu_name }}</span>
<AtomsIconsWebLinkLine
v-if="child.link_target === '_blank'"
/>
</BlocksHybridLink>
</li>
</ul>
</div>
<BlocksHybridLink
:to="child.url_path"
:target="child.link_target"
>
<span>{{ child.menu_name }}</span>
<AtomsIconsWebLinkLine
v-if="child.link_target === '_blank'"
/>
</BlocksHybridLink>
</li>
</ul>
</div>
</Transition>
</div>
</div>
<div v-if="gnbData?.menus && overflowNam > 0" class="more">
@@ -283,9 +294,9 @@ onMounted(() => {
</div>
</nav>
<div ref="startRef" class="btn-start">
<AtomsButton size="small" class="w-full md:w-auto">
<BlocksButtonLuncher class="w-full md:w-auto">
게임 시작
</AtomsButton>
</BlocksButtonLuncher>
</div>
<button class="btn-close" @click="handleMenuClose">
<AtomsIconsMenuCloseLine class="mx-auto" />
@@ -341,8 +352,8 @@ onMounted(() => {
}
.nav-area {
@apply flex flex-col w-[360px] bg-theme-foreground-10 translate-x-[-100%]
md:inline-flex md:flex-row md:w-auto md:h-full md:pl-[40px] md:items-center md:bg-transparent md:transform-none;
@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;
}
.nav-logo {

View File

@@ -1,9 +1,8 @@
<script setup lang="ts">
import { templateRegistry } from '#layers/registry'
import { useTemplateRegistry } from '#layers/composables/useTemplateRegistry'
import type {
PageDataValue,
PageDataTemplate,
PageDataComponent,
PageDataMetaTag,
} from '#layers/types/api/pageData'
@@ -13,11 +12,8 @@ interface Props {
const props = defineProps<Props>()
// 템플릿 레지스트리 타입 캐스팅
const registry = templateRegistry as unknown as Record<
string,
{ component: PageDataComponent }
>
// 템플릿 레지스트리 사용
const { getTemplateComponent } = useTemplateRegistry()
// 개별 메타 태그 표시 여부 확인
const shouldShowMetaTag = computed(() => props.pageData.meta_tag_type === 2)
@@ -63,7 +59,7 @@ watchEffect(() => {
:key="template.template_code ?? index"
>
<component
:is="registry[template.template_code]?.component"
:is="getTemplateComponent(template.template_code)"
:components="template.components"
:page-ver-tmpl-seq="template.page_ver_tmpl_seq.toString()"
/>

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import { SplideSlide } from '@splidejs/vue-splide'
import type { ListOperateGroupItem } from '#layers/types/api/resourcesData'
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
import type { SlideItemSize } from '#layers/types/components/slide'
interface BannerListProps {
resourcesData: ListOperateGroupItem[]
resourcesData: OperateGroupItem[]
slideItemSize: SlideItemSize
arrows?: boolean
pagination?: boolean

View File

@@ -2,6 +2,7 @@
import type {
PageDataResourceGroup,
PageDataResourceGroupBtnInfo,
PageDataTracking,
} from '#layers/types/api/pageData'
import type { ButtonType } from '#layers/types/components/button'
@@ -11,8 +12,11 @@ interface ButtonListProps {
}
const props = defineProps<ButtonListProps>()
const { sendLog, useAnalyticsLogDataDirect } = useAnalytics()
const {locale} = useI18n()
const { gameData } = useGameDataStore()
// 상수 정의
const BUTTON_TYPE_MAP = {
URL: {
_self: 'internal' as const,
@@ -37,25 +41,36 @@ const getButtonType = (btnInfo: PageDataResourceGroupBtnInfo): ButtonType => {
return DEFAULT_BUTTON_TYPE
}
const getButtonProps = (button: PageDataResourceGroup) => ({
type: getButtonType(button.btn_info),
target: button.btn_info?.detail?.action?.link_target,
href: button.btn_info?.detail?.action?.url,
rel: button.btn_info?.detail?.action?.rel,
backgroundColor: getColorCode({
colorName: button.btn_info?.color_name_btn,
colorCode: button.btn_info?.color_code_btn,
}),
textColor: getColorCode({
colorName: button.btn_info?.color_name_txt,
colorCode: button.btn_info?.color_code_txt,
}),
disabled: button.btn_info?.disabled,
text: button.btn_info?.txt_btn_name,
tracking: button.tracking,
})
const getButtonBackgroundImage = (
btnInfo: PageDataResourceGroupBtnInfo
): string => {
const marketType = btnInfo?.detail?.market_type
const marketImageMap: Record<string, string> = {
google_play: '/images/common/btn_logo-google.svg',
app_store: '/images/common/btn_logo-app.svg',
pc: '/images/common/btn_logo-pc.svg',
}
const { useAnalyticsLogDataDirect } = useAnalytics()
if (marketType && marketImageMap[marketType]) {
return marketImageMap[marketType]
}
return ''
}
const handleButtonClick = (btnInfo: PageDataResourceGroupBtnInfo) => {
const marketType = btnInfo?.detail?.market_type
if (marketType) {
const url = gameData?.market[marketType]?.url
window.open(url, '_blank')
return
}
// sendLog(locale.value, useAnalyticsLogDataDirect(btnInfo, props.pageVerTmplSeq))
// v-analytics="useAnalyticsLogDataDirect(props.resourcesData[0].tracking, props.pageVerTmplSeq)"
}
</script>
<template>
@@ -66,11 +81,39 @@ const { useAnalyticsLogDataDirect } = useAnalytics()
<AtomsButton
v-for="(button, index) in props.resourcesData"
:key="index"
v-bind="getButtonProps(button)"
v-analytics="useAnalyticsLogDataDirect(getButtonProps(button), props.pageVerTmplSeq)"
class="size-extra-small md:size-medium"
v-analytics="useAnalyticsLogDataDirect(props.resourcesData[index].tracking, props.pageVerTmplSeq)"
:type="getButtonType(button.btn_info)"
:target="button.btn_info?.detail?.action?.link_target"
:href="button.btn_info?.detail?.action?.url"
:rel="button.btn_info?.detail?.action?.rel"
:background-color="
getColorCode({
colorName: button.btn_info?.color_name_btn,
colorCode: button.btn_info?.color_code_btn,
})
"
:text-color="
getColorCode({
colorName: button.btn_info?.color_name_txt,
colorCode: button.btn_info?.color_code_txt,
})
"
:disabled="button.btn_info?.disabled"
:class="button.btn_info?.detail?.market_type ? 'btn-market' : ''"
:style="{
backgroundImage: `url(${getButtonBackgroundImage(button.btn_info)})`,
}"
@click="handleButtonClick(button.btn_info)"
>
{{ button.btn_info?.txt_btn_name }}
</AtomsButton>
</div>
</template>
<style scoped>
:deep(.btn-market) {
@apply flex items-start bg-[16px_50%] bg-[length:auto_34px] bg-no-repeat
min-w-[113px] pt-[23px] pl-[44px] pr-[22px] text-[11px]
md:min-w-[150px] md:pt-[30px] md:pl-[64px] md:pr-[28px] md:text-[12px] md:bg-[20px_50%] md:bg-[length:auto_40px];
}
</style>