Merge branch 'feature/202501107-all' into feature/20251001-gil
This commit is contained in:
@@ -22,9 +22,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
})
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const { gameData } = useGameDataStore()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { isProcessing, validateLauncher } = useCheckGameStart()
|
||||
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
const PLATFORM_ICON_MAP: Record<Platform, string> = {
|
||||
google_play: 'AtomsIconsLogoGoogle',
|
||||
app_store: 'AtomsIconsLogoApple',
|
||||
@@ -40,7 +42,7 @@ const DUP_IMAGE_MAP: Record<Platform, string> = {
|
||||
} as const
|
||||
|
||||
const componentTag = computed(() => {
|
||||
if (props.platform === 'stove') {
|
||||
if (props.type !== 'duplication' && props.platform === 'stove') {
|
||||
return 'a'
|
||||
}
|
||||
return 'button'
|
||||
@@ -75,7 +77,7 @@ const handleClick = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const url = gameData?.market_json[props.platform]?.url
|
||||
const url = gameData.value?.market_json[props.platform]?.url
|
||||
if (url) window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
const showSnsList = ref(false)
|
||||
const isForceClosed = ref(false)
|
||||
|
||||
const { gameData } = useGameDataStore()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
const { handleOpenToast } = modalStore
|
||||
|
||||
const snsBackgroundColor = computed(() => {
|
||||
const colorData = gameData?.comm_sns_bg_color_json?.display
|
||||
const colorData = gameData.value?.comm_sns_bg_color_json?.display
|
||||
const colorCode = getColorCode({
|
||||
colorName: colorData?.color_name,
|
||||
colorCode: colorData?.color_code,
|
||||
@@ -16,27 +16,15 @@ const snsBackgroundColor = computed(() => {
|
||||
return colorCode
|
||||
})
|
||||
const snsList = computed(() => {
|
||||
return gameData?.sns_json
|
||||
return gameData.value?.sns_json
|
||||
})
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (isForceClosed.value) return
|
||||
showSnsList.value = true
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (isForceClosed.value) return
|
||||
showSnsList.value = false
|
||||
}
|
||||
|
||||
const handleForceClose = () => {
|
||||
isForceClosed.value = true
|
||||
showSnsList.value = false
|
||||
|
||||
// 일정 시간 뒤 다시 hover 가능하도록 초기화
|
||||
setTimeout(() => {
|
||||
isForceClosed.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleCopy = async () => {
|
||||
@@ -53,14 +41,12 @@ const handleCopy = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="Object.keys(snsList).length > 0"
|
||||
class="sns-container"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@click="handleMouseEnter"
|
||||
>
|
||||
<button class="btn-sns" :style="{ backgroundColor: snsBackgroundColor }">
|
||||
<div v-if="Object.keys(snsList).length > 0" class="sns-container">
|
||||
<button
|
||||
class="btn-sns"
|
||||
:style="{ backgroundColor: snsBackgroundColor }"
|
||||
@click="handleMouseEnter"
|
||||
>
|
||||
<AtomsIconsShareLine class="icon-share" />
|
||||
<span class="sr-only">sns</span>
|
||||
</button>
|
||||
|
||||
@@ -68,6 +68,7 @@ const componentProps = computed(() => {
|
||||
:style="{
|
||||
backgroundColor: props.backgroundColor,
|
||||
color: props.textColor,
|
||||
'--text-color': props.textColor,
|
||||
}"
|
||||
>
|
||||
<span class="btn-content">
|
||||
@@ -100,6 +101,9 @@ const componentProps = computed(() => {
|
||||
after:bg-[var(--text-color)] after:opacity-20 after:z-[2];
|
||||
}
|
||||
|
||||
.btn-base:disabled .btn-content {
|
||||
@apply opacity-50;
|
||||
}
|
||||
.btn-base .btn-content {
|
||||
@apply relative flex items-center gap-1 z-[1];
|
||||
}
|
||||
|
||||
@@ -2,27 +2,37 @@
|
||||
interface Props {
|
||||
src: string | { pc?: string; mo?: string }
|
||||
alt?: string
|
||||
imageType?: 'common' | 'game'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
alt: 'image',
|
||||
imageType: 'game',
|
||||
})
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const rootPath = isDev ? '' : '/templates/brand'
|
||||
|
||||
const isResponsiveMode = computed(() => {
|
||||
return typeof props.src === 'object' && !!props.src.pc && !!props.src.mo
|
||||
})
|
||||
|
||||
const imagePaths = computed(() => {
|
||||
if (typeof props.src === 'string') {
|
||||
const resolved = getImageHost(`${rootPath}${props.src}`)
|
||||
return { pc: resolved, mo: '' }
|
||||
const resolved = getImageHost(props.src, {
|
||||
imageType: props.imageType,
|
||||
})
|
||||
return { pc: '', mo: resolved }
|
||||
}
|
||||
|
||||
return {
|
||||
pc: props.src.pc ? getImageHost(`${rootPath}${props.src.pc}`) : '',
|
||||
mo: props.src.mo ? getImageHost(`${rootPath}${props.src.mo}`) : '',
|
||||
pc: props.src.pc
|
||||
? getImageHost(props.src.pc, {
|
||||
imageType: props.imageType,
|
||||
})
|
||||
: '',
|
||||
mo: props.src.mo
|
||||
? getImageHost(props.src.mo, {
|
||||
imageType: props.imageType,
|
||||
})
|
||||
: '',
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -34,11 +44,5 @@ const imagePaths = computed(() => {
|
||||
<img :src="imagePaths.pc" :alt="alt" v-bind="$attrs" loading="lazy" />
|
||||
</picture>
|
||||
|
||||
<img
|
||||
v-else
|
||||
:src="imagePaths.pc || imagePaths.mo"
|
||||
:alt="alt"
|
||||
v-bind="$attrs"
|
||||
loading="lazy"
|
||||
/>
|
||||
<img v-else :src="imagePaths.mo" :alt="alt" v-bind="$attrs" loading="lazy" />
|
||||
</template>
|
||||
|
||||
@@ -16,10 +16,10 @@ withDefaults(defineProps<Props>(), {
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 12 12"
|
||||
:fill="color"
|
||||
>
|
||||
<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"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
27
layers/components/atoms/icons/ArrowDownLine.vue
Normal file
27
layers/components/atoms/icons/ArrowDownLine.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 16,
|
||||
color: '#7F7F7F',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 16 16"
|
||||
:fill="color"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.86201 5.19526C3.12236 4.93491 3.54447 4.93491 3.80482 5.19526L8.00008 9.39052L12.1953 5.19526C12.4557 4.93491 12.8778 4.93491 13.1382 5.19526C13.3985 5.45561 13.3985 5.87772 13.1382 6.13807L8.47149 10.8047C8.21114 11.0651 7.78903 11.0651 7.52868 10.8047L2.86201 6.13807C2.60166 5.87772 2.60166 5.45561 2.86201 5.19526Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -16,12 +16,12 @@ withDefaults(defineProps<Props>(), {
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 20 20"
|
||||
:fill="color"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.91073 3.57757C6.5853 3.90301 6.5853 4.43065 6.91073 4.75609L12.1548 10.0002L6.91073 15.2442C6.5853 15.5697 6.5853 16.0973 6.91073 16.4228C7.23617 16.7482 7.76381 16.7482 8.08924 16.4228L13.9226 10.5894C14.248 10.264 14.248 9.73634 13.9226 9.41091L8.08924 3.57757C7.76381 3.25214 7.23617 3.25214 6.91073 3.57757Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
27
layers/components/atoms/icons/CheckBoldLine.vue
Normal file
27
layers/components/atoms/icons/CheckBoldLine.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 24,
|
||||
color: '#666666',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 24 24"
|
||||
:fill="color"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M20.5607 5.93934C21.1465 6.52513 21.1465 7.47487 20.5607 8.06066L10.5607 18.0607C10.2652 18.3561 9.85997 18.5149 9.44246 18.4989C9.02495 18.4829 8.63305 18.2934 8.36114 17.9762L3.36114 12.1429C2.82201 11.5139 2.89485 10.5669 3.52384 10.0278C4.15283 9.48865 5.09978 9.56149 5.63891 10.1905L9.58475 14.794L18.4394 5.93934C19.0252 5.35355 19.9749 5.35355 20.5607 5.93934Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -16,11 +16,10 @@ withDefaults(defineProps<Props>(), {
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
:fill="color"
|
||||
>
|
||||
<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"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -1,95 +1,146 @@
|
||||
<template>
|
||||
<div class="select-language" :class="{ 'language-changing': isChanging }">
|
||||
|
||||
<button
|
||||
:disabled="isChanging"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg text-[#CCCCCC] transition-all duration-300 w-[180px] bg-[#292929] border border-[#595959]"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': isChanging }"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<!-- 지구본 아이콘 -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 지구본 아이콘 -->
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_5964_1685)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6666 8.00065C14.6666 11.6825 11.6818 14.6673 7.99992 14.6673C4.31802 14.6673 1.33325 11.6825 1.33325 8.00065C1.33325 4.31875 4.31802 1.33398 7.99992 1.33398C11.6818 1.33398 14.6666 4.31875 14.6666 8.00065ZM6.89756 13.2199C6.03596 11.8504 5.50924 10.2901 5.36895 8.66732H2.70785C2.99033 10.9326 4.69347 12.7567 6.89756 13.2199ZM2.70785 7.33398H5.36895C5.50924 5.71116 6.03596 4.15086 6.89756 2.78138C4.69347 3.24458 2.99033 5.06868 2.70785 7.33398ZM13.292 8.66732C13.0095 10.9326 11.3064 12.7567 9.10228 13.2199C9.96388 11.8504 10.4906 10.2901 10.6309 8.66732H13.292ZM13.292 7.33398C13.0095 5.06868 11.3064 3.24458 9.10228 2.78138C9.96388 4.15086 10.4906 5.71116 10.6309 7.33398H13.292ZM7.99992 12.468C7.28662 11.3201 6.84273 10.0202 6.70801 8.66732H9.29183C9.15711 10.0202 8.71322 11.3201 7.99992 12.468ZM6.70801 7.33398H9.29183C9.15711 5.98112 8.71322 4.68121 7.99992 3.5333C7.28662 4.68121 6.84273 5.98112 6.70801 7.33398Z" fill="#CCCCCC"/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.6666 8.00065C14.6666 11.6825 11.6818 14.6673 7.99992 14.6673C4.31802 14.6673 1.33325 11.6825 1.33325 8.00065C1.33325 4.31875 4.31802 1.33398 7.99992 1.33398C11.6818 1.33398 14.6666 4.31875 14.6666 8.00065ZM6.89756 13.2199C6.03596 11.8504 5.50924 10.2901 5.36895 8.66732H2.70785C2.99033 10.9326 4.69347 12.7567 6.89756 13.2199ZM2.70785 7.33398H5.36895C5.50924 5.71116 6.03596 4.15086 6.89756 2.78138C4.69347 3.24458 2.99033 5.06868 2.70785 7.33398ZM13.292 8.66732C13.0095 10.9326 11.3064 12.7567 9.10228 13.2199C9.96388 11.8504 10.4906 10.2901 10.6309 8.66732H13.292ZM13.292 7.33398C13.0095 5.06868 11.3064 3.24458 9.10228 2.78138C9.96388 4.15086 10.4906 5.71116 10.6309 7.33398H13.292ZM7.99992 12.468C7.28662 11.3201 6.84273 10.0202 6.70801 8.66732H9.29183C9.15711 10.0202 8.71322 11.3201 7.99992 12.468ZM6.70801 7.33398H9.29183C9.15711 5.98112 8.71322 4.68121 7.99992 3.5333C7.28662 4.68121 6.84273 5.98112 6.70801 7.33398Z"
|
||||
fill="#CCCCCC"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5964_1685">
|
||||
<rect width="16" height="16" fill="#CCCCCC"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip0_5964_1685">
|
||||
<rect width="16" height="16" fill="#CCCCCC" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="flex-1 text-sm text-left transition-all duration-300">
|
||||
{{ isChanging ? '언어 변경 중...' : getLanguageName(selectedLocale) }}
|
||||
</span>
|
||||
<!-- 로딩 스피너 -->
|
||||
<svg v-if="isChanging" class="w-3 h-3 animate-spin" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="#CCCCCC" stroke-width="2" stroke-linecap="round" stroke-dasharray="31.416" stroke-dashoffset="31.416">
|
||||
<animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/>
|
||||
<animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/>
|
||||
<svg
|
||||
v-if="isChanging"
|
||||
class="w-3 h-3 animate-spin"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="#CCCCCC"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="31.416"
|
||||
stroke-dashoffset="31.416"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dasharray"
|
||||
dur="2s"
|
||||
values="0 31.416;15.708 15.708;0 31.416"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
dur="2s"
|
||||
values="0;-15.708;-31.416"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
<!-- 드롭다운 화살표 -->
|
||||
<svg
|
||||
v-else
|
||||
class="w-3 h-3 text-gray-300 transition-transform duration-200"
|
||||
<svg
|
||||
v-else
|
||||
class="w-3 h-3 text-gray-300 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': isDropdownOpen }"
|
||||
viewBox="0 0 12 12" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.69999 4.285L9.59499 7.125C9.91999 7.445 9.69499 8 9.23499 8H2.75999C2.29999 8 2.07499 7.445 2.39999 7.125L5.29499 4.285C5.68499 3.905 6.30499 3.905 6.69499 4.285H6.69999Z" fill="#EBEBEB"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="isDropdownOpen" class="dropdown-menu">
|
||||
<div v-for="localeItem in availableLanguages" :key="localeItem.code" class="dropdown-menu-item">
|
||||
<button
|
||||
class="dropdown-menu-item-button"
|
||||
:class="{ 'current': localeItem.code === selectedLocale }"
|
||||
@click="selectLanguage(localeItem.code)"
|
||||
viewBox="0 0 12 12"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg
|
||||
width="15"
|
||||
height="11"
|
||||
viewBox="0 0 15 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="transition-opacity duration-200"
|
||||
<path
|
||||
d="M6.69999 4.285L9.59499 7.125C9.91999 7.445 9.69499 8 9.23499 8H2.75999C2.29999 8 2.07499 7.445 2.39999 7.125L5.29499 4.285C5.68499 3.905 6.30499 3.905 6.69499 4.285H6.69999Z"
|
||||
fill="#EBEBEB"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="isDropdownOpen" class="dropdown-menu">
|
||||
<div
|
||||
v-for="localeItem in availableLanguages"
|
||||
:key="localeItem.code"
|
||||
class="dropdown-menu-item"
|
||||
>
|
||||
<button
|
||||
class="dropdown-menu-item-button"
|
||||
:class="{ current: localeItem.code === selectedLocale }"
|
||||
@click="selectLanguage(localeItem.code)"
|
||||
>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6339 0.366117C15.1221 0.854272 15.1221 1.64573 14.6339 2.13388L6.30057 10.4672C6.05437 10.7134 5.71664 10.8458 5.36872 10.8324C5.0208 10.8191 4.69421 10.6612 4.46762 10.3968L0.300952 5.53571C-0.148326 5.01155 -0.0876239 4.22243 0.436533 3.77315C0.960691 3.32387 1.74982 3.38458 2.19909 3.90873L5.48729 7.74496L12.8661 0.366117C13.3543 -0.122039 14.1458 -0.122039 14.6339 0.366117Z" fill="#FC4420"/>
|
||||
</svg>
|
||||
<span class="text-sm">{{ localeItem.name }}</span>
|
||||
</button>
|
||||
<svg
|
||||
width="15"
|
||||
height="11"
|
||||
viewBox="0 0 15 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="transition-opacity duration-200"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.6339 0.366117C15.1221 0.854272 15.1221 1.64573 14.6339 2.13388L6.30057 10.4672C6.05437 10.7134 5.71664 10.8458 5.36872 10.8324C5.0208 10.8191 4.69421 10.6612 4.46762 10.3968L0.300952 5.53571C-0.148326 5.01155 -0.0876239 4.22243 0.436533 3.77315C0.960691 3.32387 1.74982 3.38458 2.19909 3.90873L5.48729 7.74496L12.8661 0.366117C13.3543 -0.122039 14.1458 -0.122039 14.6339 0.366117Z"
|
||||
fill="#FC4420"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm">{{ localeItem.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const config = useRuntimeConfig()
|
||||
const baseDomain = `${config.public.baseDomain}`
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const baseDomain = `${runtimeConfig.public.baseDomain}`
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
// 사용 가능한 언어 목록
|
||||
const availableLanguages = computed(() => {
|
||||
return gameData.value?.lang_codes?.map(localeCode => ({
|
||||
code: localeCode,
|
||||
name: getLanguageName(localeCode)
|
||||
})) || [{ code: 'ko', name: '한국어' }]
|
||||
return (
|
||||
gameData.value?.lang_codes?.map(localeCode => ({
|
||||
code: localeCode,
|
||||
name: getLanguageName(localeCode),
|
||||
})) || [{ code: 'ko', name: '한국어' }]
|
||||
)
|
||||
})
|
||||
|
||||
// 언어 코드를 한국어 이름으로 변환하는 함수
|
||||
const getLanguageName = (localeCode: string) => {
|
||||
const languageNames: Record<string, string> = {
|
||||
'ko': '한국어',
|
||||
'en': 'English',
|
||||
'ja': '日本語',
|
||||
ko: '한국어',
|
||||
en: 'English',
|
||||
ja: '日本語',
|
||||
'zh-cn': '简体中文',
|
||||
'zh-tw': '繁體中文',
|
||||
'es': 'Español',
|
||||
'fr': 'Français',
|
||||
'de': 'Deutsch',
|
||||
'pt': 'Português',
|
||||
'th': 'ไทย',
|
||||
'it': 'Italiano'
|
||||
es: 'Español',
|
||||
fr: 'Français',
|
||||
de: 'Deutsch',
|
||||
pt: 'Português',
|
||||
th: 'ไทย',
|
||||
it: 'Italiano',
|
||||
}
|
||||
return languageNames[localeCode] || localeCode
|
||||
}
|
||||
@@ -113,7 +164,7 @@ const selectLanguage = async (localeCode: string) => {
|
||||
isDropdownOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
selectedLocale.value = localeCode as any
|
||||
isDropdownOpen.value = false
|
||||
await switchLanguage()
|
||||
@@ -121,38 +172,37 @@ const selectLanguage = async (localeCode: string) => {
|
||||
// 언어 변경 함수 (서버 미드웨어를 통한 gameData 갱신)
|
||||
const switchLanguage = async () => {
|
||||
if (!selectedLocale.value || isChanging.value) return
|
||||
|
||||
|
||||
isChanging.value = true
|
||||
|
||||
|
||||
try {
|
||||
// URL 경로를 통해 언어 변경
|
||||
const path = switchLocalePath(selectedLocale.value as any)
|
||||
if (path) {
|
||||
// 언어 쿠키 설정 (클라이언트 사이드) - 페이지 이동 전에 설정
|
||||
const localeCookie = useCookie('LOCALE', {
|
||||
domain: baseDomain,
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 365, // 1년 (초 단위)
|
||||
sameSite: 'lax'
|
||||
})
|
||||
localeCookie.value = selectedLocale.value.toUpperCase()
|
||||
|
||||
// 페이지 데이터 초기화 (새로운 언어로 다시 로드되도록)
|
||||
pageDataStore.clearPageData()
|
||||
window.location.href = path
|
||||
// URL 경로를 통해 언어 변경
|
||||
const path = switchLocalePath(selectedLocale.value as any)
|
||||
if (path) {
|
||||
// 언어 쿠키 설정 (클라이언트 사이드) - 페이지 이동 전에 설정
|
||||
const localeCookie = useCookie('LOCALE', {
|
||||
domain: baseDomain,
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 365, // 1년 (초 단위)
|
||||
sameSite: 'lax',
|
||||
})
|
||||
localeCookie.value = selectedLocale.value.toUpperCase()
|
||||
|
||||
// 페이지 데이터 초기화 (새로운 언어로 다시 로드되도록)
|
||||
pageDataStore.clearPageData()
|
||||
window.location.href = path
|
||||
// 언어 변경 및 라우팅
|
||||
// await setLocale(selectedLocale.value as any)
|
||||
|
||||
|
||||
// 전체 페이지에 페이드 아웃 효과 적용
|
||||
// document.body.style.transition = 'opacity 0.1s ease-out'
|
||||
// document.body.style.opacity = '0'
|
||||
|
||||
|
||||
// // 페이드 아웃 완료 후 페이지 이동
|
||||
// await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
|
||||
// 서버 미드웨어를 통해 gameData 갱신을 위해 페이지 새로고침
|
||||
// 이렇게 하면 서버 미드웨어가 새로운 언어로 gameData를 다시 가져옴
|
||||
|
||||
}
|
||||
} catch {
|
||||
// 오류 발생 시 이전 언어로 복원
|
||||
@@ -248,5 +298,4 @@ body {
|
||||
.dropdown-menu-item-button.current svg {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -5,11 +5,13 @@ let cpHeader: any = null
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const { locale, availableLocales } = useI18n()
|
||||
const { gameData } = useGameDataStore()
|
||||
const gameDataStore = useGameDataStore()
|
||||
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
const stoveInflowPath = runtimeConfig.public.stoveInflowPath
|
||||
const stoveGameNo = runtimeConfig.public.stoveGameNo
|
||||
const stoveGnbData = gameData?.stove_gnb_json
|
||||
const stoveGnbData = gameData.value?.stove_gnb_json
|
||||
|
||||
const languageCodes = computed(() => {
|
||||
if (Array.isArray(availableLocales)) {
|
||||
|
||||
@@ -14,9 +14,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
})
|
||||
|
||||
const imagePaths = computed(() => getImagePaths(props.resourcesData))
|
||||
const displayText = computed(
|
||||
() => props.resourcesData?.display?.text || 'image'
|
||||
)
|
||||
const displayText = computed(() => props.resourcesData?.display?.text)
|
||||
const colorName = computed(() => props.resourcesData?.display?.color_name)
|
||||
const colorCode = computed(() => props.resourcesData?.display?.color_code)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ interface props {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<props>(), {
|
||||
isShowDimmed: false,
|
||||
isShowDimmed: true,
|
||||
isOutsideClose: false,
|
||||
})
|
||||
|
||||
@@ -18,9 +18,9 @@ const { tm } = useI18n()
|
||||
|
||||
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
||||
|
||||
const setButtonEvent = (event?: () => void | void) => {
|
||||
if (typeof event === 'function') {
|
||||
return event()
|
||||
const setButtonEvent = (event?: () => void) => {
|
||||
if (event) {
|
||||
event()
|
||||
}
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ interface props {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<props>(), {
|
||||
isShowDimmed: false,
|
||||
isShowDimmed: true,
|
||||
isOutsideClose: false,
|
||||
})
|
||||
|
||||
|
||||
@@ -8,13 +8,18 @@ interface props {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<props>(), {
|
||||
isShowDimmed: false,
|
||||
isShowDimmed: true,
|
||||
isOutsideClose: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
||||
|
||||
const handleCloseModal = () => {
|
||||
emit('close')
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ onUnmounted(() => {
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-75 z-[800]"
|
||||
class="modal-wrap dimmed overflow-hidden flex items-center justify-center"
|
||||
:class="props.modalName"
|
||||
@click="handleOutsideClick"
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@ interface Props {
|
||||
arrows?: boolean
|
||||
pagination?: boolean
|
||||
paginationData?: PageDataResourceGroups
|
||||
destroy?: boolean
|
||||
breakpoints?: ResponsiveOptions['breakpoints']
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
drag: true,
|
||||
arrows: true,
|
||||
pagination: true,
|
||||
destroy: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['mounted', 'move', 'arrowClick'])
|
||||
@@ -47,9 +49,9 @@ const options = computed((): ResponsiveOptions => {
|
||||
updateOnMove: true,
|
||||
autoplay: props.autoplay,
|
||||
drag: props.drag,
|
||||
trimSpace: false,
|
||||
arrows: props.arrows,
|
||||
pagination: props.pagination,
|
||||
destroy: props.destroy,
|
||||
classes: {
|
||||
arrows: 'splide-arrows',
|
||||
arrow: 'splide-arrow',
|
||||
|
||||
@@ -62,6 +62,20 @@ const thumbOptions = computed<Options>(() => ({
|
||||
prev: 'arrow-prev',
|
||||
next: 'arrow-next',
|
||||
},
|
||||
breakpoints: {
|
||||
[BREAKPOINTS.md - 1]: {
|
||||
padding: {
|
||||
left: 40,
|
||||
right: 40,
|
||||
},
|
||||
},
|
||||
[BREAKPOINTS.sm - 1]: {
|
||||
padding: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const getThumbnailSrc = (item: PageDataTemplateComponentSet) => {
|
||||
@@ -158,12 +172,12 @@ 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] md:bg-transparent
|
||||
@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];
|
||||
}
|
||||
.thumbnail-slide:hover,
|
||||
.thumbnail-slide.is-active {
|
||||
@apply bg-[var(--pagination-active)] md:bg-transparent;
|
||||
@apply bg-[var(--pagination-active)];
|
||||
}
|
||||
.thumbnail-slide::after {
|
||||
@apply border-[var(--pagination-disabled)];
|
||||
@@ -194,10 +208,10 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.thumbnail-carousel.thumbnail-default .thumbnail-slide:hover img,
|
||||
.thumbnail-carousel.thumbnail-default .thumbnail-slide.is-active img {
|
||||
@apply md:grayscale-0 md:opacity-100;
|
||||
@apply md:grayscale-0;
|
||||
}
|
||||
.thumbnail-carousel.thumbnail-default .thumbnail-slide img {
|
||||
@apply hidden md:block md:grayscale md:opacity-60;
|
||||
@apply hidden md:block md:grayscale;
|
||||
}
|
||||
|
||||
/* 미디어 버전 스타일 */
|
||||
@@ -205,16 +219,15 @@ onBeforeUnmount(() => {
|
||||
@apply flex flex-col items-center;
|
||||
}
|
||||
.thumbnail-carousel.thumbnail-media .thumbnail-splide {
|
||||
@apply w-screen mt-[20px] mx-[-20px] sm:mx-[-40px] md:w-auto md:max-w-[100%] md:mt-[28px] md:mx-auto md:px-[112px];
|
||||
}
|
||||
.thumbnail-carousel.thumbnail-media .thumbnail-splide:deep(.splide__track) {
|
||||
@apply !px-[20px] sm:!px-[40px] md:!px-[0];
|
||||
@apply max-w-[calc(100%+40px)] mt-[20px] mx-[-20px]
|
||||
sm:max-w-[calc(100%+80px)] sm:mx-[-40px]
|
||||
md:max-w-[100%] md:mt-[28px] md:mx-auto md:px-[64px];
|
||||
}
|
||||
.thumbnail-carousel.thumbnail-media:deep(.arrow-prev) {
|
||||
@apply left-[48px];
|
||||
@apply left-[0];
|
||||
}
|
||||
.thumbnail-carousel.thumbnail-media:deep(.arrow-next) {
|
||||
@apply right-[48px];
|
||||
@apply right-[0];
|
||||
}
|
||||
.thumbnail-carousel.thumbnail-media .thumbnail-slide {
|
||||
@apply aspect-[16/9] w-[92px] md:w-[128px];
|
||||
|
||||
@@ -333,7 +333,7 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ gnb1depthButtonData?.btn_info?.txt_btn_name }}
|
||||
</AtomsButtonLauncher>
|
||||
<div v-if="gnb2depthButtonData" class="nav-2depth">
|
||||
<div v-if="gnb2depthButtonData" class="nav-2depth hidden md:block">
|
||||
<ul>
|
||||
<li v-for="(item, key) in gnb2depthButtonData" :key="key">
|
||||
<AtomsButtonLauncher type="custom" :platform="key">
|
||||
|
||||
@@ -19,7 +19,7 @@ const mainRef = ref<HTMLElement>()
|
||||
const { getTemplateComponent } = useTemplateRegistry()
|
||||
|
||||
// 개별 메타 태그 표시 여부 확인
|
||||
const shouldShowMetaTag = computed(() => props.pageData.meta_tag_type === 2)
|
||||
const shouldShowMetaTag = computed(() => props.pageData?.meta_tag_type === 2)
|
||||
|
||||
// 템플릿 표시 여부 확인
|
||||
const isTemplateVisible = (template: PageDataTemplate): boolean => {
|
||||
@@ -31,7 +31,7 @@ const isTemplateVisible = (template: PageDataTemplate): boolean => {
|
||||
|
||||
// 템플릿 목록 계산
|
||||
const visibleTemplates = computed(() =>
|
||||
Object.values(props.pageData.templates).filter(isTemplateVisible)
|
||||
Object.values(props.pageData?.templates).filter(isTemplateVisible)
|
||||
)
|
||||
|
||||
// SEO 메타 태그 설정
|
||||
|
||||
@@ -31,7 +31,7 @@ const imageClasses = computed(() => [
|
||||
props.size === 'contain' ? 'bg-contain' : 'bg-cover',
|
||||
])
|
||||
const gradientClasses = computed(() => [
|
||||
'absolute bottom-0 left-0 right-0',
|
||||
'absolute bottom-[-2px] left-[-2px] right-[-2px]',
|
||||
props.gradient,
|
||||
])
|
||||
|
||||
@@ -45,7 +45,7 @@ watch(videoSrc, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0 w-full h-full">
|
||||
<div class="overflow-hidden absolute inset-0 w-full h-full">
|
||||
<!-- 이미지 타입 -->
|
||||
<div
|
||||
v-if="isTypeImage(resourcesData?.resource_type) && imageSrc"
|
||||
|
||||
@@ -23,8 +23,5 @@ const handleVideoPlayClick = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AtomsButtonPlay
|
||||
:resources-data="resourcesData"
|
||||
@click="handleVideoPlayClick"
|
||||
/>
|
||||
<AtomsButtonPlay @click="handleVideoPlayClick" />
|
||||
</template>
|
||||
|
||||
452
layers/components/widgets/modal/Preregist.vue
Normal file
452
layers/components/widgets/modal/Preregist.vue
Normal file
@@ -0,0 +1,452 @@
|
||||
<script setup lang="ts">
|
||||
import { globalDateFormat } from '@seed-next/date'
|
||||
import { PREREGIST_ERROR_CODE } from '#layers/composables/usePreregist'
|
||||
|
||||
interface Props {
|
||||
preregistCode?: string
|
||||
tm?: (key: string) => string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Composables
|
||||
const { locale } = useI18n()
|
||||
const device = useDevice()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const modalStore = useModalStore()
|
||||
const { handleTokenValidation } = useTokenValidation()
|
||||
const {
|
||||
countryCode,
|
||||
preregistDate,
|
||||
checkCountryByIp,
|
||||
getPreregist,
|
||||
setPreregist,
|
||||
} = usePreregist()
|
||||
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
// Constants
|
||||
const stoveCs = runtimeConfig.public.stoveCs
|
||||
const customerServiceUrl = `${stoveCs}/${gameData.value?.game_id}`
|
||||
|
||||
/**
|
||||
* 번역 함수 (Props로 전달받은 tm 또는 key 반환)
|
||||
*/
|
||||
const tm = (key: string): string => {
|
||||
return props.tm?.(key) ?? key
|
||||
}
|
||||
|
||||
const isModalOpen = ref(false)
|
||||
const currentStep = ref<1 | 2>(1)
|
||||
const isSubmitting = ref(false)
|
||||
const isCheckedMarketing = ref(false)
|
||||
const isExpandedMarketing = ref(false)
|
||||
const isValidated = ref(false) // 검증 완료 여부 (중복 검증 방지)
|
||||
|
||||
const canSubmit = computed(() => isCheckedMarketing.value)
|
||||
const errorMessages = computed<Record<number, string>>(() => ({
|
||||
[PREREGIST_ERROR_CODE.NOT_PERIOD]: tm('Preregist_Alert_Date'),
|
||||
[PREREGIST_ERROR_CODE.REQUIRED_TERMS]: tm('Preregist_Alert_Agree'),
|
||||
[PREREGIST_ERROR_CODE.AGE_RESTRICTION]: tm('Preregist_Alert_Age'),
|
||||
[PREREGIST_ERROR_CODE.ALREADY_REGISTERED]: tm('Preregist_Alert_Already'),
|
||||
}))
|
||||
|
||||
const tmWithGameName = (key: string): string => {
|
||||
const text = tm(key)
|
||||
if (typeof text === 'string' && text.includes('%게임명%')) {
|
||||
const gameName = gameData.value?.game_name ?? ''
|
||||
return text.replace(/%게임명%/g, gameName)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const toggleMarketing = () => {
|
||||
isCheckedMarketing.value = !isCheckedMarketing.value
|
||||
}
|
||||
|
||||
const toggleExpand = (event: Event) => {
|
||||
event.stopPropagation()
|
||||
isExpandedMarketing.value = !isExpandedMarketing.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 모달 표시
|
||||
*/
|
||||
const showErrorModal = (code: number): void => {
|
||||
if (!code) return
|
||||
|
||||
// 일반 에러 메시지
|
||||
const message = errorMessages.value[code]
|
||||
if (message) {
|
||||
modalStore.handleOpenAlert({ contentText: message })
|
||||
return
|
||||
}
|
||||
|
||||
// 로그인 필요
|
||||
if (code === PREREGIST_ERROR_CODE.LOGIN_REQUIRED) {
|
||||
modalStore.handleOpenConfirm({
|
||||
contentText: tm('Alert_StoveLogin'),
|
||||
confirmButtonText: tm('Text_StoveLogin'),
|
||||
confirmButtonEvent: () => {
|
||||
csrGoStoveLogin()
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 기타 오류
|
||||
modalStore.handleOpenConfirm({
|
||||
contentText: tm('Alert_Error'),
|
||||
confirmButtonText: tm('Text_Customer'),
|
||||
confirmButtonEvent: () => {
|
||||
window.open(customerServiceUrl, '_blank')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 및 사전등록 여부 검증
|
||||
*/
|
||||
const checkValidation = async (): Promise<boolean> => {
|
||||
if (!props.preregistCode) {
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[Preregist] preregistCode is required')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// 토큰 검증
|
||||
const accessToken = csrGetAccessToken()
|
||||
const isValidToken = await handleTokenValidation(accessToken)
|
||||
if (!isValidToken) return false
|
||||
|
||||
// 사전등록 여부 조회
|
||||
const result = await getPreregist({
|
||||
accessToken,
|
||||
event_code: props.preregistCode,
|
||||
lang: locale.value,
|
||||
terms_type: 3,
|
||||
})
|
||||
|
||||
// 사전등록 가능
|
||||
if (result.code === PREREGIST_ERROR_CODE.NO_DATA) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 이미 사전등록 완료
|
||||
if (result.code === PREREGIST_ERROR_CODE.SUCCESS) {
|
||||
showErrorModal(PREREGIST_ERROR_CODE.ALREADY_REGISTERED)
|
||||
return false
|
||||
}
|
||||
|
||||
// 기타 오류
|
||||
showErrorModal(result.code)
|
||||
return false
|
||||
} catch (error) {
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[Preregist.checkValidation]', error)
|
||||
}
|
||||
showErrorModal(PREREGIST_ERROR_CODE.UNKNOWN)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사전등록 모달 오픈
|
||||
*/
|
||||
const handleOpenPreregist = async (): Promise<void> => {
|
||||
if (isSubmitting.value) return
|
||||
|
||||
const isValid = await checkValidation()
|
||||
if (!isValid) return
|
||||
|
||||
// 국가 정보 조회
|
||||
if (!countryCode.value) {
|
||||
await checkCountryByIp({
|
||||
policy_grp: 'onstove',
|
||||
device_nation: csrGetCountry().toUpperCase(),
|
||||
client_lang: locale.value,
|
||||
include_coverages: false,
|
||||
qc: csrGetQc(),
|
||||
runType: runtimeConfig.public.runType,
|
||||
})
|
||||
}
|
||||
|
||||
isValidated.value = true // 검증 완료 플래그
|
||||
isModalOpen.value = true
|
||||
currentStep.value = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 사전등록 제출
|
||||
*/
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
if (!props.preregistCode) return
|
||||
if (isSubmitting.value || currentStep.value !== 1) return
|
||||
|
||||
// 유효성 검사
|
||||
if (!canSubmit.value) {
|
||||
showErrorModal(PREREGIST_ERROR_CODE.REQUIRED_TERMS)
|
||||
return
|
||||
}
|
||||
|
||||
// 이미 검증을 통과한 경우 재검증 스킵
|
||||
if (!isValidated.value) {
|
||||
const isValid = await checkValidation()
|
||||
if (!isValid) return
|
||||
}
|
||||
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
const result = await setPreregist({
|
||||
accessToken: csrGetAccessToken(),
|
||||
event_code: props.preregistCode,
|
||||
lang_code: locale.value,
|
||||
terms_type: 3,
|
||||
device_type: device.isMobile ? 'mobile' : 'pc',
|
||||
country_code: countryCode.value || 'KR',
|
||||
necessary_consent1: 'Y',
|
||||
necessary_consent2: 'Y',
|
||||
necessary_consent3: isCheckedMarketing.value ? 'Y' : 'N',
|
||||
birth_date: '',
|
||||
})
|
||||
|
||||
if (result.code === PREREGIST_ERROR_CODE.SUCCESS) {
|
||||
currentStep.value = 2
|
||||
return
|
||||
}
|
||||
|
||||
showErrorModal(result.code)
|
||||
} catch (error) {
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[Preregist.handleSubmit]', error)
|
||||
}
|
||||
showErrorModal(PREREGIST_ERROR_CODE.UNKNOWN)
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 닫기 및 상태 초기화
|
||||
*/
|
||||
const handleCloseModal = (): void => {
|
||||
if (isSubmitting.value) return
|
||||
|
||||
currentStep.value = 1
|
||||
isCheckedMarketing.value = false
|
||||
isExpandedMarketing.value = false
|
||||
isSubmitting.value = false
|
||||
isValidated.value = false // 검증 플래그도 초기화
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleOpenPreregist,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BlocksModalLayer
|
||||
v-model:is-open="isModalOpen"
|
||||
area-class="h-full bg-[#292929] pt-[60px] md:w-[476px] md:h-[680px] md:pt-[64px] md:rounded-[20px] md:shadow-[0_2px_4px_rgba(0,0,0,0.06)]"
|
||||
close-class="absolute top-[19px] right-[26px] md:top-[20px] md:right-[24px]"
|
||||
@close="handleCloseModal"
|
||||
>
|
||||
<!-- Step 1: Terms Agreement -->
|
||||
<div v-if="currentStep === 1" class="flex flex-col h-full">
|
||||
<div class="flex gap-5 px-5 pt-5 pb-[12px] md:px-10 md:pt-6 md:pb-[16px]">
|
||||
<h4
|
||||
class="flex-1 text-xl font-bold leading-[30px] tracking-[-0.6px] text-[#ebebeb] md:text-2xl md:leading-[34px] md:tracking-[-0.72px]"
|
||||
>
|
||||
{{ tm('Preregist_Modal_Title01') }}
|
||||
</h4>
|
||||
<div
|
||||
class="flex h-[30px] items-center gap-1 text-base leading-6 tracking-[-0.48px] md:h-[34px]"
|
||||
>
|
||||
<span class="font-bold text-[#b2b2b2]">1</span>
|
||||
<span class="text-[#666666]">/</span>
|
||||
<span class="text-[#666666]">2</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- content area -->
|
||||
<div class="overflow-hidden relative">
|
||||
<div
|
||||
class="absolute left-0 right-0 top-0 bg-gradient-to-b from-[#292929] to-transparent z-[1] h-[24px] md:h-[32px]"
|
||||
></div>
|
||||
<div
|
||||
class="overflow-y-auto h-full py-[24px] px-5 md:py-[32px] md:px-10"
|
||||
>
|
||||
<div class="px-3 py-4 md:px-6">
|
||||
<div class="flex cursor-pointer items-center gap-3 md:gap-4">
|
||||
<div class="shrink-0">
|
||||
<AtomsIconsCheckBoldLine
|
||||
:color="isCheckedMarketing ? 'var(--primary)' : '#666666'"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="flex-1 text-sm font-medium leading-6 tracking-[-0.42px] text-[#ebebeb] md:text-[15px] md:tracking-[-0.45px]"
|
||||
@click="toggleMarketing"
|
||||
>
|
||||
{{ tmWithGameName('Preregist_Agree_News') }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center transition-transform duration-200"
|
||||
:class="{ 'rotate-180': isExpandedMarketing }"
|
||||
@click="toggleExpand($event)"
|
||||
>
|
||||
<AtomsIconsArrowDownLine />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Marketing Detail Content -->
|
||||
<div
|
||||
v-if="isExpandedMarketing"
|
||||
class="mt-4 max-h-[160px] overflow-y-auto rounded-lg bg-white/[0.04] px-4 py-3"
|
||||
>
|
||||
<p
|
||||
v-dompurify-html="tmWithGameName('Preregist_Agree_News_Info')"
|
||||
class="text-[13px] font-normal leading-[22px] tracking-[-0.325px] text-[#b2b2b2]"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute left-0 right-0 bottom-0 bg-gradient-to-t from-[#292929] to-transparent z-[1] h-[24px] md:h-[32px]"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto px-5 pb-10 md:px-10 md:pb-12">
|
||||
<AtomsButton
|
||||
class="w-full"
|
||||
button-size="size-small md:size-medium"
|
||||
:disabled="!canSubmit || isSubmitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ tm('Preregist_Btn_Preegist') }}
|
||||
</AtomsButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Success -->
|
||||
<div v-if="currentStep === 2" class="flex flex-1 flex-col h-full">
|
||||
<div class="flex gap-5 px-5 pb-10 pt-5 md:px-10 md:pb-12 md:pt-6">
|
||||
<h4
|
||||
class="flex-1 text-xl font-bold leading-[30px] tracking-[-0.6px] text-[#ebebeb] md:text-2xl md:leading-[34px] md:tracking-[-0.72px]"
|
||||
>
|
||||
{{ tm('Preregist_Modal_Title02') }}
|
||||
</h4>
|
||||
<div
|
||||
class="flex h-[30px] items-center gap-1 text-base leading-6 tracking-[-0.48px] md:h-[34px]"
|
||||
>
|
||||
<span class="font-bold text-[#b2b2b2]">2</span>
|
||||
<span class="text-[#666666]">/</span>
|
||||
<span class="text-[#666666]">2</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-10 px-5 pb-10 md:px-10">
|
||||
<!-- Success Info -->
|
||||
<div
|
||||
class="flex flex-col gap-1 rounded-lg border border-white/10 bg-[#383838] px-5 py-4 md:gap-2 md:px-6"
|
||||
>
|
||||
<p
|
||||
class="text-[13px] font-normal leading-[22px] tracking-[-0.325px] text-[#b2b2b2] opacity-50 md:text-[15px] md:leading-6 md:tracking-[-0.45px]"
|
||||
>
|
||||
{{ globalDateFormat(preregistDate, locale) }}
|
||||
</p>
|
||||
<h3
|
||||
class="text-xl font-bold leading-[30px] tracking-[-0.6px] text-[#ebebeb] md:text-2xl md:leading-[34px] md:tracking-[-0.72px]"
|
||||
>
|
||||
{{ gameData?.game_name }}
|
||||
</h3>
|
||||
<p
|
||||
class="text-[13px] font-normal leading-[22px] tracking-[-0.325px] text-[#ebebeb] md:text-[15px] md:leading-6 md:tracking-[-0.45px]"
|
||||
>
|
||||
{{ tm('Preregist_Agree_News_Complete') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- STOVE App Download -->
|
||||
<div class="flex flex-col gap-5">
|
||||
<p
|
||||
class="text-left text-sm font-medium leading-6 tracking-[-0.42px] text-[#ebebeb] md:text-center md:text-base md:leading-[26px] md:tracking-[-0.48px]"
|
||||
>
|
||||
{{ tm('Preregist_Stove_Download') }}
|
||||
</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex size-[108px] p-4 shrink-0 items-center justify-center rounded-lg bg-white/[0.04] backdrop-blur-[15px] md:size-[124px] md:p-4.5"
|
||||
>
|
||||
<AtomsImg
|
||||
src="/images/common/stove_app_qr.png"
|
||||
alt="STOVE APP QR Code"
|
||||
image-type="common"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col gap-3">
|
||||
<a
|
||||
href="https://play.google.com/store/search?q=stove&c=apps"
|
||||
target="_blank"
|
||||
class="flex h-12 w-full items-center justify-center gap-1.5 rounded-lg bg-white/[0.04] px-8 pl-8 pr-10 text-sm font-medium leading-5 tracking-[-0.42px] text-white no-underline backdrop-blur-[15px] transition-colors duration-200 hover:bg-white/[0.08] md:h-14 md:gap-2 md:text-base md:leading-6 md:tracking-[-0.48px]"
|
||||
>
|
||||
<AtomsIconsLogoGoogle />
|
||||
<span>Google Play</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://apps.apple.com/app/stove-app-stove-app/id1342134971"
|
||||
target="_blank"
|
||||
class="flex h-12 w-full items-center justify-center gap-1.5 rounded-lg bg-white/[0.04] px-8 pl-8 pr-10 text-sm font-medium leading-5 tracking-[-0.42px] text-white no-underline backdrop-blur-[15px] transition-colors duration-200 hover:bg-white/[0.08] md:h-14 md:gap-2 md:text-base md:leading-6 md:tracking-[-0.48px]"
|
||||
>
|
||||
<AtomsIconsLogoApple />
|
||||
<span>App Store</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BlocksModalLayer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-wrap {
|
||||
@apply p-0 md:p-5;
|
||||
}
|
||||
.modal-wrap:deep(.modal-content) {
|
||||
@apply h-full;
|
||||
}
|
||||
.modal-wrap:deep(.modal-close) svg {
|
||||
@apply fill-white;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for accordion content */
|
||||
:deep(.overflow-y-auto) {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
|
||||
}
|
||||
|
||||
:deep(.overflow-y-auto::-webkit-scrollbar) {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
:deep(.overflow-y-auto::-webkit-scrollbar-track) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.overflow-y-auto::-webkit-scrollbar-thumb) {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
:deep(.overflow-y-auto::-webkit-scrollbar-thumb:hover) {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user