feat. FX_PREREGIST_01 템플릿 제작
This commit is contained in:
44
layers/assets/data/countryData.ts
Normal file
44
layers/assets/data/countryData.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 국가 번호 정의
|
||||
*/
|
||||
const countryDialingCodes: Record<string, string> = {
|
||||
KR: '82', // 대한민국
|
||||
US: '1', // 미국
|
||||
GB: '44',
|
||||
CA: '1', // 캐나다
|
||||
AU: '61',
|
||||
IE: '353',
|
||||
NZ: '64',
|
||||
ZA: '27',
|
||||
IN: '91',
|
||||
SG: '65',
|
||||
PH: '63',
|
||||
JP: '81',
|
||||
DE: '49',
|
||||
AT: '43',
|
||||
CH: '41',
|
||||
LI: '423',
|
||||
FR: '33',
|
||||
BE: '32',
|
||||
LU: '352',
|
||||
MC: '377',
|
||||
PT: '351',
|
||||
BR: '55',
|
||||
ES: '34',
|
||||
MX: '52',
|
||||
AR: '54',
|
||||
CO: '57',
|
||||
PE: '51',
|
||||
VE: '58',
|
||||
CL: '56',
|
||||
EC: '593',
|
||||
GT: '502',
|
||||
CU: '53',
|
||||
TH: '66',
|
||||
TW: '886', // 대만
|
||||
HK: '852', // 홍콩
|
||||
MO: '853', // 마카오
|
||||
CN: '86',
|
||||
}
|
||||
|
||||
export { countryDialingCodes }
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 메타 태그 설정
|
||||
|
||||
660
layers/components/widgets/modal/Preregist.vue
Normal file
660
layers/components/widgets/modal/Preregist.vue
Normal file
@@ -0,0 +1,660 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
preregistCode?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { locale, tm } = useI18n()
|
||||
const modalStore = useModalStore()
|
||||
const { gameData } = useGameDataStore()
|
||||
const { handleTokenValidation } = useTokenValidation()
|
||||
const { getPreregist, setPreregist } = usePreregist()
|
||||
const { isNorthAmerica, countryCode } = useGds()
|
||||
|
||||
// State
|
||||
const isOpen = ref(false)
|
||||
const step = ref<1 | 2>(1)
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const checks = ref({
|
||||
age: false,
|
||||
privacy: false,
|
||||
marketing: false,
|
||||
})
|
||||
const birthdate = ref({
|
||||
month: '',
|
||||
day: '',
|
||||
year: '',
|
||||
})
|
||||
const expanded = ref({
|
||||
privacy: false,
|
||||
marketing: false,
|
||||
})
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
const currentDate = computed(() => new Date().toISOString().split('T')[0])
|
||||
const allChecked = computed(() => {
|
||||
const { age, privacy, marketing } = checks.value
|
||||
return isNorthAmerica.value
|
||||
? privacy && marketing
|
||||
: age && privacy && marketing
|
||||
})
|
||||
const isBirthdateValid = computed(() => {
|
||||
if (!isNorthAmerica.value) return true
|
||||
|
||||
const { month, day, year } = birthdate.value
|
||||
if (!month || !day || !year) return false
|
||||
|
||||
const monthNum = Number(month)
|
||||
const dayNum = Number(day)
|
||||
const yearNum = Number(year)
|
||||
|
||||
return (
|
||||
monthNum >= 1 &&
|
||||
monthNum <= 12 &&
|
||||
dayNum >= 1 &&
|
||||
dayNum <= 31 &&
|
||||
yearNum >= 1900 &&
|
||||
yearNum <= currentYear
|
||||
)
|
||||
})
|
||||
const canSubmit = computed(() => {
|
||||
const { age, privacy, marketing } = checks.value
|
||||
if (!privacy || !marketing) return false
|
||||
|
||||
return isNorthAmerica.value ? isBirthdateValid.value : age
|
||||
})
|
||||
|
||||
const handleCheckAll = () => {
|
||||
const newValue = !allChecked.value
|
||||
checks.value = {
|
||||
age: isNorthAmerica.value ? false : newValue,
|
||||
privacy: newValue,
|
||||
marketing: newValue,
|
||||
}
|
||||
}
|
||||
const handleCheck = (key: keyof typeof checks.value) => {
|
||||
checks.value[key] = !checks.value[key]
|
||||
}
|
||||
const toggleExpand = (key: keyof typeof expanded.value, event: Event) => {
|
||||
event.stopPropagation()
|
||||
expanded.value[key] = !expanded.value[key]
|
||||
}
|
||||
|
||||
// 에러 코드 매핑
|
||||
const ERROR_MESSAGES: Record<number, string> = {
|
||||
'-90002': '사전 등록 기간이 아닙니다.',
|
||||
'-90018': '생년월일을 입력해주세요.',
|
||||
'-90022': '사전 등록 가능한 연령이 아닙니다.',
|
||||
'-90000': '필수 약관을 모두 선택해 주세요.',
|
||||
'-90023': '이미 사전 등록을 완료한 계정입니다',
|
||||
}
|
||||
|
||||
const showErrorModal = (code: number) => {
|
||||
if (!code) return
|
||||
|
||||
const message = ERROR_MESSAGES[code]
|
||||
|
||||
if (message) {
|
||||
modalStore.handleOpenAlert({ contentText: message })
|
||||
return
|
||||
}
|
||||
|
||||
// 기타 오류
|
||||
modalStore.handleOpenConfirm({
|
||||
contentText: tm('Alert_Error'),
|
||||
confirmButtonText: tm('Text_Customer'),
|
||||
confirmButtonEvent: () => {
|
||||
window.open('aa', '_blank')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 토큰 및 사전등록 여부 검증
|
||||
const checkValidation = async () => {
|
||||
if (!props.preregistCode) 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 === -1) {
|
||||
// 사전등록 가능
|
||||
return true
|
||||
}
|
||||
|
||||
if (result.code === 0) {
|
||||
// 이미 사전등록 완료
|
||||
showErrorModal(-90023)
|
||||
return false
|
||||
}
|
||||
|
||||
// 기타 오류
|
||||
showErrorModal(result.code)
|
||||
return false
|
||||
} catch (error) {
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[checkValidation]', error)
|
||||
}
|
||||
showErrorModal(-99999)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 사전등록 모달 오픈
|
||||
const handlePreregist = async () => {
|
||||
if (isSubmitting.value) return
|
||||
|
||||
const isValid = await checkValidation()
|
||||
if (isValid) {
|
||||
isOpen.value = true
|
||||
step.value = 1
|
||||
}
|
||||
}
|
||||
|
||||
// 사전등록 제출
|
||||
const handleSubmit = async () => {
|
||||
if (!props.preregistCode) return
|
||||
if (isSubmitting.value || step.value !== 1) return
|
||||
|
||||
// 유효성 검사
|
||||
if (!canSubmit.value) {
|
||||
showErrorModal(-90000)
|
||||
return
|
||||
}
|
||||
|
||||
const isValid = await checkValidation()
|
||||
if (!isValid) return
|
||||
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
// 생년월일 포맷팅
|
||||
const birthDate = isNorthAmerica.value
|
||||
? `${birthdate.value.year}-${birthdate.value.month.padStart(2, '0')}-${birthdate.value.day.padStart(2, '0')}`
|
||||
: undefined
|
||||
|
||||
// API 호출
|
||||
const result = await setPreregist({
|
||||
accessToken: csrGetAccessToken(),
|
||||
event_code: props.preregistCode,
|
||||
lang_code: locale.value,
|
||||
terms_type: 3,
|
||||
device_type: 'WEB',
|
||||
country_code: countryCode.value || 'KR',
|
||||
necessary_consent1: checks.value.age || isNorthAmerica.value ? 'Y' : 'N',
|
||||
necessary_consent2: checks.value.privacy ? 'Y' : 'N',
|
||||
necessary_consent3: checks.value.marketing ? 'Y' : 'N',
|
||||
birth_date: birthDate,
|
||||
})
|
||||
|
||||
if (result.code === 0) {
|
||||
step.value = 2
|
||||
return
|
||||
}
|
||||
|
||||
showErrorModal(result.code)
|
||||
} catch (error) {
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[handleSubmit]', error)
|
||||
}
|
||||
showErrorModal(-99999)
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 모달 닫기 및 초기화
|
||||
const resetForm = () => {
|
||||
checks.value = {
|
||||
age: false,
|
||||
privacy: false,
|
||||
marketing: false,
|
||||
}
|
||||
birthdate.value = {
|
||||
month: '',
|
||||
day: '',
|
||||
year: '',
|
||||
}
|
||||
expanded.value = {
|
||||
privacy: false,
|
||||
marketing: false,
|
||||
}
|
||||
isSubmitting.value = false
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
if (isSubmitting.value) return
|
||||
|
||||
isOpen.value = false
|
||||
step.value = 1
|
||||
|
||||
// 애니메이션 완료 후 초기화
|
||||
setTimeout(resetForm, 300)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handlePreregist,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BlocksModalLayer
|
||||
:is-open="isOpen"
|
||||
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="handleClose"
|
||||
>
|
||||
<!-- Step 1: Terms Agreement -->
|
||||
<div v-if="step === 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]">
|
||||
<h2
|
||||
class="flex-1 text-xl font-bold leading-[30px] tracking-[-0.6px] text-[#ebebeb] md:text-2xl md:leading-[34px] md:tracking-[-0.72px]"
|
||||
>
|
||||
사전 등록 이용 약관에
|
||||
<br />
|
||||
동의해주세요
|
||||
</h2>
|
||||
<div class="flex h-[30px] items-center gap-1 md:h-[34px]">
|
||||
<span
|
||||
class="text-base font-bold leading-6 tracking-[-0.48px] text-[#b2b2b2]"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
<span
|
||||
class="text-xs font-bold leading-[18px] tracking-[-0.24px] text-[#666666]"
|
||||
>
|
||||
/
|
||||
</span>
|
||||
<span
|
||||
class="text-base font-medium leading-6 tracking-[-0.48px] 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="flex h-12 cursor-pointer items-center rounded-lg border border-white/10 bg-[#383838] px-4 md:px-6 gap-2 md:gap-3"
|
||||
@click="handleCheckAll"
|
||||
>
|
||||
<div class="shrink-0">
|
||||
<AtomsIconsCheckBoldLine
|
||||
:color="allChecked ? 'var(--primary)' : '#666666'"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="flex-1 text-base font-bold leading-6 tracking-[-0.48px] text-[#ebebeb] md:text-lg md:leading-[26px] md:tracking-[-0.54px]"
|
||||
>
|
||||
아래 내용에 모두 동의 합니다.
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<!-- Age Check (북미 제외) -->
|
||||
<div v-if="!isNorthAmerica" class="px-3 py-4 md:px-6">
|
||||
<div
|
||||
class="flex cursor-pointer items-center gap-3 md:gap-4"
|
||||
@click="handleCheck('age')"
|
||||
>
|
||||
<div class="shrink-0">
|
||||
<AtomsIconsCheckBoldLine
|
||||
:color="checks.age ? '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]"
|
||||
>
|
||||
[필수] 18세 이상입니다.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Privacy Check with Accordion -->
|
||||
<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="checks.privacy ? '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="handleCheck('privacy')"
|
||||
>
|
||||
[필수] 개인정보 수집 및 이용동의
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center transition-transform duration-200"
|
||||
:class="{ 'rotate-180': expanded.privacy }"
|
||||
@click="toggleExpand('privacy', $event)"
|
||||
>
|
||||
<AtomsIconsArrowDownLine />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Detail Content -->
|
||||
<div
|
||||
v-if="expanded.privacy"
|
||||
class="mt-4 h-[160px] overflow-y-auto rounded-lg bg-white/[0.04] px-4 py-3"
|
||||
>
|
||||
<p
|
||||
class="text-[13px] font-normal leading-[22px] tracking-[-0.325px] text-[#b2b2b2]"
|
||||
>
|
||||
본 약관은 [게임명] (이하 "회사")이 제공하는 사전등록 서비스와
|
||||
관련하여 회사와 이용자 간의 권리, 의무 및 책임사항, 서비스
|
||||
이용조건 및 절차 등 기본적인 사항을 규정함을 목적으로 합니다.
|
||||
<br />
|
||||
<br />
|
||||
회사는 이용자의 개인정보를 중요시하며, 「개인정보 보호법」,
|
||||
「정보통신망 이용촉진 및 정보보호 등에 관한 법률」을 준수하고
|
||||
있습니다.
|
||||
<br />
|
||||
<br />
|
||||
수집하는 개인정보 항목: 이메일 주소, 휴대전화번호, 게임 계정
|
||||
정보
|
||||
<br />
|
||||
개인정보 수집 목적: 사전등록 확인, 게임 출시 안내, 마케팅 활용
|
||||
<br />
|
||||
개인정보 보유 및 이용기간: 회원 탈퇴 시까지
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Marketing Check with Accordion -->
|
||||
<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="checks.marketing ? '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="handleCheck('marketing')"
|
||||
>
|
||||
[필수] 게임 및 서비스의 유용한 소식 받기
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center transition-transform duration-200"
|
||||
:class="{ 'rotate-180': expanded.marketing }"
|
||||
@click="toggleExpand('marketing', $event)"
|
||||
>
|
||||
<AtomsIconsArrowDownLine />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Marketing Detail Content -->
|
||||
<div
|
||||
v-if="expanded.marketing"
|
||||
class="mt-4 h-[160px] overflow-y-auto rounded-lg bg-white/[0.04] px-4 py-3"
|
||||
>
|
||||
<p
|
||||
class="text-[13px] font-normal leading-[22px] tracking-[-0.325px] text-[#b2b2b2]"
|
||||
>
|
||||
회사는 다음의 목적으로 광고성 정보를 전송합니다:
|
||||
<br />
|
||||
<br />
|
||||
• 게임 출시 안내 및 업데이트 정보
|
||||
<br />
|
||||
• 이벤트, 프로모션 등의 마케팅 정보
|
||||
<br />
|
||||
• 게임 내 혜택 및 쿠폰 제공
|
||||
<br />
|
||||
• 신규 콘텐츠 및 패치 정보
|
||||
<br />
|
||||
<br />
|
||||
전송방법: 이메일, SMS, 푸시 알림, 앱 내 알림
|
||||
<br />
|
||||
<br />
|
||||
이용자는 언제든지 수신 동의를 철회할 수 있으며, 수신 거부
|
||||
시에도 서비스 이용에는 제한이 없습니다. 단, 거래 관련 정보,
|
||||
고객문의 답변 등 의무적으로 안내되어야 하는 정보는 수신동의
|
||||
여부와 무관하게 제공됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Birthdate Input (북미 등) -->
|
||||
<div
|
||||
v-if="isNorthAmerica"
|
||||
class="flex flex-col gap-6 pb-4 pl-[44px] pr-3 pt-2 md:pl-[60px] md:pr-6"
|
||||
>
|
||||
<!-- Divider -->
|
||||
<div class="h-px w-full bg-white/[0.06]"></div>
|
||||
|
||||
<!-- Birthdate Form -->
|
||||
<div class="flex flex-col gap-3 md:gap-4">
|
||||
<p
|
||||
class="text-sm font-medium leading-6 tracking-[-0.42px] text-[#ebebeb] md:text-[15px] md:tracking-[-0.45px]"
|
||||
>
|
||||
[필수] 생년월일을 입력해주세요
|
||||
</p>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<!-- Month -->
|
||||
<label class="flex-1">
|
||||
<span class="sr-only">Month</span>
|
||||
<input
|
||||
id="birthdate-month"
|
||||
v-model="birthdate.month"
|
||||
type="number"
|
||||
name="birthdate-month"
|
||||
placeholder="Month"
|
||||
min="1"
|
||||
max="12"
|
||||
aria-label="Birth month"
|
||||
class="h-12 w-full rounded-lg border border-[#595959] bg-[#292929] px-4 py-3 text-sm font-normal leading-5 tracking-[-0.42px] text-[#ebebeb] placeholder:text-[#666666] focus:border-[#7f7f7f] focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- Day -->
|
||||
<label class="flex-1">
|
||||
<span class="sr-only">Day</span>
|
||||
<input
|
||||
id="birthdate-day"
|
||||
v-model="birthdate.day"
|
||||
type="number"
|
||||
name="birthdate-day"
|
||||
placeholder="Day"
|
||||
min="1"
|
||||
max="31"
|
||||
aria-label="Birth day"
|
||||
class="h-12 w-full rounded-lg border border-[#595959] bg-[#292929] px-4 py-3 text-sm font-normal leading-5 tracking-[-0.42px] text-[#ebebeb] placeholder:text-[#666666] focus:border-[#7f7f7f] focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- Year -->
|
||||
<label class="flex-1">
|
||||
<span class="sr-only">Year</span>
|
||||
<input
|
||||
id="birthdate-year"
|
||||
v-model="birthdate.year"
|
||||
type="number"
|
||||
name="birthdate-year"
|
||||
placeholder="Year"
|
||||
min="1900"
|
||||
:max="currentYear"
|
||||
aria-label="Birth year"
|
||||
class="h-12 w-full rounded-lg border border-[#595959] bg-[#292929] px-4 py-3 text-sm font-normal leading-5 tracking-[-0.42px] text-[#ebebeb] placeholder:text-[#666666] focus:border-[#7f7f7f] focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
사전 등록하기
|
||||
</AtomsButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Success -->
|
||||
<div v-if="step === 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">
|
||||
<p
|
||||
class="flex-1 text-xl font-bold leading-[30px] tracking-[-0.6px] text-[#ebebeb] md:text-2xl md:leading-[34px] md:tracking-[-0.72px]"
|
||||
>
|
||||
사전 등록이 완료되었습니다.
|
||||
</p>
|
||||
<div class="flex h-[30px] items-center gap-1 md:h-[34px]">
|
||||
<span
|
||||
class="text-base font-bold leading-6 tracking-[-0.48px] text-[#b2b2b2]"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<span
|
||||
class="text-xs font-bold leading-[18px] tracking-[-0.24px] text-[#666666]"
|
||||
>
|
||||
/
|
||||
</span>
|
||||
<span
|
||||
class="text-base font-medium leading-6 tracking-[-0.48px] 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]"
|
||||
>
|
||||
{{ currentDate }}
|
||||
</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]"
|
||||
>
|
||||
광고성 정보 수신에 동의하였습니다.
|
||||
</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]"
|
||||
>
|
||||
STOVE APP 다운로드 하고 정식 런칭 및 소식 알림 받기!
|
||||
</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);
|
||||
}
|
||||
|
||||
/* Remove number input spinner */
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
119
layers/composables/useGds.ts
Normal file
119
layers/composables/useGds.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type {
|
||||
ReqGetGdsClientPolicyTotal,
|
||||
ResGetGdsClientPolicyTotal,
|
||||
} from '#layers/types/GdsType'
|
||||
import { DEFAULT_LOCALE_CODE } from '@/i18n.config'
|
||||
|
||||
/**
|
||||
* GDS
|
||||
*
|
||||
* @description https://wiki.smilegate.net/display/SDKAPIDOCU/51-09.+gds
|
||||
*/
|
||||
const useGds = () => {
|
||||
const logPrefix = {
|
||||
exception: '[Exception] /composables/useGds',
|
||||
failure: '[Failure] /composables/useGds',
|
||||
}
|
||||
|
||||
const countryCode = ref('') // 국가 코드
|
||||
const isKorea = ref(false) // 대한민국 여부
|
||||
const isTaiwanHongKongMacau = ref(false) // 대홍마 여부
|
||||
const isNorthAmerica = ref(false) // 북미 여부
|
||||
|
||||
// [Setter] 사전등록 - 국가 코드 세팅
|
||||
const setCountryCode = (newCountryCode: string) => {
|
||||
countryCode.value = newCountryCode
|
||||
}
|
||||
// [Setter] 사전등록 - 대한민국 여부 세팅
|
||||
const setIsKorea = (newIsKorea: boolean) => {
|
||||
isKorea.value = newIsKorea
|
||||
}
|
||||
// [Setter] 사전등록 - 대홍마 여부 세팅
|
||||
const setIsTaiwanHongKongMacau = (newIsTaiwanHongKongMacau: boolean) => {
|
||||
isTaiwanHongKongMacau.value = newIsTaiwanHongKongMacau
|
||||
}
|
||||
// [Setter] 사전등록 - 북미 여부 세팅
|
||||
const setIsNorthAmerica = (newIsNorthAmerica: boolean) => {
|
||||
isNorthAmerica.value = newIsNorthAmerica
|
||||
}
|
||||
|
||||
// 사전 등록 - 특정 국가 여부 조회 (IP 기반)
|
||||
const checkCountryByIp = async (req: ReqGetGdsClientPolicyTotal) => {
|
||||
let res: ResGetGdsClientPolicyTotal
|
||||
const arrKorea = ['KR'] // cf. UM: 미국령 군소 제도의 기 미국령 군소 제도, VI: 미국령 버진아일랜드의 기 미국령 버진아일랜드
|
||||
const arrNorthAmerica = ['US', 'CA'] // cf. UM: 미국령 군소 제도의 기 미국령 군소 제도, VI: 미국령 버진아일랜드의 기 미국령 버진아일랜드
|
||||
const arrTaiwanHongKongMacau = ['TW', 'HK', 'MO'] // 대만, 홍콩, 마카오 국가 코드
|
||||
try {
|
||||
if (`${req.runType}` !== 'live' && req.qc != null && req.qc !== '') {
|
||||
// Live가 아닌 환경에서 qc가 있으면 국가 코드로 설정하여 판별
|
||||
setCountryCode(req.qc || '')
|
||||
setIsKorea(arrKorea.includes(req.qc))
|
||||
setIsTaiwanHongKongMacau(arrTaiwanHongKongMacau.includes(req.qc))
|
||||
setIsNorthAmerica(arrNorthAmerica.includes(req.qc))
|
||||
|
||||
res = {
|
||||
code: 0,
|
||||
message: '',
|
||||
res_code: 0,
|
||||
res_data: {
|
||||
is_default: true,
|
||||
nation: req.qc,
|
||||
regulation: '',
|
||||
timezone: '',
|
||||
utc_offset: 0,
|
||||
lang: req.client_lang || DEFAULT_LOCALE_CODE,
|
||||
coverages: [],
|
||||
},
|
||||
} as ResGetGdsClientPolicyTotal
|
||||
} else {
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl
|
||||
|
||||
const url = `${stoveApiBaseUrl}/gds/v2/client/policy/total`
|
||||
const query = {
|
||||
policy_grp: req.policy_grp || 'onstove',
|
||||
device_nation: req.device_nation || 'KR',
|
||||
client_lang: req.client_lang || DEFAULT_LOCALE_CODE,
|
||||
include_coverages: req.include_coverages || false,
|
||||
}
|
||||
res = (await commonFetch('GET', url, {
|
||||
query,
|
||||
})) as ResGetGdsClientPolicyTotal
|
||||
|
||||
if (res != null && res.res_code === 0) {
|
||||
// is_default = false : 국가코드를 정상 처리했으나 타임존을 처리하지 못한 경우
|
||||
// is_default = true : device_nation 또는 policy_grp 별 디폴트 값으로 처리된 경우
|
||||
if (res.res_data != null && res.res_data.nation != null) {
|
||||
setCountryCode(res.res_data.nation || '')
|
||||
setIsKorea(arrKorea.includes(res.res_data.nation))
|
||||
setIsTaiwanHongKongMacau(
|
||||
arrTaiwanHongKongMacau.includes(res.res_data.nation)
|
||||
)
|
||||
setIsNorthAmerica(arrNorthAmerica.includes(res.res_data.nation))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`${logPrefix.exception}.checkCountryByIp: `, e)
|
||||
res = {
|
||||
code: -99999,
|
||||
message: `${e}`,
|
||||
}
|
||||
setCountryCode('')
|
||||
setIsKorea(false)
|
||||
setIsTaiwanHongKongMacau(false)
|
||||
setIsNorthAmerica(false)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return {
|
||||
isKorea,
|
||||
isTaiwanHongKongMacau,
|
||||
isNorthAmerica,
|
||||
countryCode,
|
||||
checkCountryByIp,
|
||||
}
|
||||
}
|
||||
|
||||
export { useGds }
|
||||
242
layers/composables/usePreregist.ts
Normal file
242
layers/composables/usePreregist.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import type {
|
||||
ReqPreorderSelectEvent,
|
||||
ResPreorderSelectEvent,
|
||||
ReqPreorderReserveDataUpdate,
|
||||
ResPreorderReserveDataUpdate,
|
||||
} from '#layers/types/PreregistType'
|
||||
|
||||
import { DEFAULT_LOCALE_CODE } from '@/i18n.config'
|
||||
import { countryDialingCodes } from '#layers/assets/data/countryData'
|
||||
|
||||
/**
|
||||
* 프로모션 - 사전등록
|
||||
*/
|
||||
const usePreregist = () => {
|
||||
const promotion = ref({
|
||||
id: 'preregist',
|
||||
title: '사전등록 프로모션',
|
||||
link: '/#preregist',
|
||||
startDate: '2025-07-09 10:00:00',
|
||||
endDate: '2025-12-31 23:59:59',
|
||||
}) // 사전 등록 프로모션 정보
|
||||
const preregistDate = ref(Date.now()) // 사전 등록일
|
||||
|
||||
// [Setter] 사전등록 - 사전 등록일 세팅
|
||||
const setPreregistDate = (newPreregistDate: number) => {
|
||||
preregistDate.value = newPreregistDate
|
||||
}
|
||||
|
||||
// 프로모션 기간 여부
|
||||
const isPromotionPeriod = computed(() => {
|
||||
if (
|
||||
!promotion.value ||
|
||||
!promotion.value.startDate ||
|
||||
!promotion.value.endDate
|
||||
) {
|
||||
return false
|
||||
}
|
||||
// return isBetweenPeriod(
|
||||
// kstStringToTimestamp(promotion.value.startDate),
|
||||
// kstStringToTimestamp(promotion.value.endDate)
|
||||
// )
|
||||
})
|
||||
// 국가 번호 조회
|
||||
const countryDialingCode = computed(() => {
|
||||
return countryDialingCodes[countryCode.value.toUpperCase()]
|
||||
})
|
||||
|
||||
// 사전 등록 - 특정 국가 여부 조회 (IP 기반)
|
||||
const {
|
||||
isKorea,
|
||||
isTaiwanHongKongMacau,
|
||||
isNorthAmerica,
|
||||
countryCode,
|
||||
checkCountryByIp,
|
||||
} = useGds()
|
||||
|
||||
// 사전 등록 - (등록 여부) 조회
|
||||
const getPreregist = async (req: ReqPreorderSelectEvent) => {
|
||||
let res: ResPreorderSelectEvent = {} as ResPreorderSelectEvent
|
||||
try {
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl
|
||||
|
||||
const url = `${stoveApiBaseUrl}/pub-comm/v1.0/Preorder/SelectEvent`
|
||||
const headers = {
|
||||
Authorization: `Bearer ${req.accessToken}`,
|
||||
}
|
||||
const body = {
|
||||
event_code: req.event_code,
|
||||
lang: req.lang || DEFAULT_LOCALE_CODE,
|
||||
terms_type: req.terms_type,
|
||||
}
|
||||
res = (await commonFetch('POST', url, {
|
||||
headers,
|
||||
body,
|
||||
})) as ResPreorderSelectEvent
|
||||
if (res != null) {
|
||||
if (res.code === 0) {
|
||||
setPreregistDate(res.value?.terms_time_long || Date.now())
|
||||
} else if (res.code === -1) {
|
||||
//= 조회 된 데이터가 없습니다. -> 최초
|
||||
} else if (res.code === -90002) {
|
||||
//= 사전 등록 기간이 아닙니다.
|
||||
} else if (`${res.code}`.startsWith('401')) {
|
||||
//= [01] 로그인 후 이용하실 수 있습니다.
|
||||
res = { code: -90028, message: res.message || '' } // 401은 -90028로 처리
|
||||
} else if (res.code === -90028) {
|
||||
//= [01] 로그인 후 이용하실 수 있습니다.
|
||||
} else {
|
||||
// [-90003] 점검 진행 중
|
||||
res = { code: res.code, message: res.message || '' }
|
||||
console.log(
|
||||
'[Failure] /composables/promotion/usePreregist.getPreregist: ',
|
||||
res
|
||||
)
|
||||
res.code = -99999 // else 알럿 띄우기 용 세팅
|
||||
}
|
||||
} else {
|
||||
res = { code: -99999, message: '' }
|
||||
console.log(
|
||||
'[Failure] /composables/promotion/usePreregist.getPreregist: ',
|
||||
res
|
||||
)
|
||||
res.code = -99999 // else 알럿 띄우기 용 세팅
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'[Exception] /composables/promotion/usePreregist.getPreregist: ',
|
||||
e
|
||||
)
|
||||
res = {
|
||||
code: -99999,
|
||||
message: `${e}`,
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 사전 등록 - 저장
|
||||
const setPreregist = async (req: ReqPreorderReserveDataUpdate) => {
|
||||
let res: ResPreorderReserveDataUpdate = {} as ResPreorderReserveDataUpdate
|
||||
try {
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl
|
||||
|
||||
const url = `${stoveApiBaseUrl}/pub-comm/v1.0/Preorder/ReserveDataUpdate`
|
||||
const headers = {
|
||||
Authorization: `Bearer ${req.accessToken}`,
|
||||
}
|
||||
const body = {
|
||||
necessary_consent1: req.necessary_consent1,
|
||||
necessary_consent2: req.necessary_consent2,
|
||||
necessary_consent3: req.necessary_consent3,
|
||||
event_code: req.event_code,
|
||||
terms_type: req.terms_type,
|
||||
c_num: req.c_num,
|
||||
lang_code: req.lang_code,
|
||||
hp: req.hp,
|
||||
email: req.email,
|
||||
metric_seq: req.metric_seq, //= mcode
|
||||
g_server: req.g_server,
|
||||
world_id: req.world_id,
|
||||
game_unique_num: req.game_unique_num,
|
||||
event_info1: req.event_info1, // 각 이벤트 필요할때 사용
|
||||
event_info2: req.event_info2, // 각 이벤트 필요할때 사용
|
||||
event_info3: req.event_info3, // 각 이벤트 필요할때 사용
|
||||
event_info4: req.event_info4, // 각 이벤트 필요할때 사용
|
||||
under14_terms: req.under14_terms, // 14세미만 진행 하는 구분값
|
||||
device_type: req.device_type, // 접속 기기 pc / mobile
|
||||
country_code: req.country_code,
|
||||
country_dialing_code: req.country_dialing_code, // 국가 번호
|
||||
birth_date: req.birth_date, // 생년월일 - 북미(미국, 캐나다)의 경우
|
||||
}
|
||||
|
||||
res = (await commonFetch('POST', url, {
|
||||
headers,
|
||||
body,
|
||||
})) as ResPreorderReserveDataUpdate
|
||||
|
||||
if (res != null) {
|
||||
if (res.code === 0) {
|
||||
//= [08] [사전 등록 완료 레이어] 노출
|
||||
if (
|
||||
res.message != null &&
|
||||
res.message !== '' &&
|
||||
isNumeric(res.message)
|
||||
) {
|
||||
// 사전 등록일이 숫자면 날짜로 변환
|
||||
setPreregistDate(Number(res.message))
|
||||
} else {
|
||||
setPreregistDate(Date.now())
|
||||
}
|
||||
} else if (`${res.code}`.startsWith('401')) {
|
||||
//= [01] 로그인 후 이용하실 수 있습니다.
|
||||
res = { code: -90028, message: res.message || '' } // 401은 -90028로 처리
|
||||
} else if (res.code === -90028) {
|
||||
//= [01] 로그인 후 이용하실 수 있습니다.
|
||||
} else if (res.code === -90002) {
|
||||
//= [02] 사전 등록 기간이 아닙니다.
|
||||
} else if (res.code === -90018) {
|
||||
//= [04] 생년 월일을 입력해 주세요.
|
||||
} else if (res.code === -90022) {
|
||||
//= [05] 사전 등록 가능한 연령이 아닙니다.
|
||||
} else if (res.code === -90000) {
|
||||
//= [06] 필수 약관을 모두 선택해 주세요.
|
||||
} else if (res.code === -90023) {
|
||||
//= [07] 이미 사전 등록이 완료된 계정입니다.
|
||||
if (
|
||||
res.message != null &&
|
||||
res.message !== '' &&
|
||||
isNumeric(res.message)
|
||||
) {
|
||||
// 사전 등록일이 숫자면 날짜로 변환
|
||||
setPreregistDate(Number(res.message))
|
||||
} else {
|
||||
setPreregistDate(Date.now())
|
||||
}
|
||||
} else {
|
||||
//= [03] 오류가 발생했습니다.\n계속 오류가 발생하면 고객센터로 문의해 주세요.
|
||||
res = { code: res.code, message: res.message || '' }
|
||||
console.log(
|
||||
'[Failure] /composables/promotion/usePreregist.setPreregist: ',
|
||||
res
|
||||
)
|
||||
res.code = -99999 // else 알럿 띄우기 용 세팅
|
||||
}
|
||||
} else {
|
||||
res = { code: -99999, message: '' }
|
||||
console.log(
|
||||
'[Failure] /composables/promotion/usePreregist.setPreregist: ',
|
||||
res
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'[Exception] /composables/promotion/usePreregist.setPreregist: ',
|
||||
e
|
||||
)
|
||||
res = {
|
||||
code: -99999,
|
||||
message: `${e}`,
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return {
|
||||
promotion,
|
||||
isKorea,
|
||||
isTaiwanHongKongMacau,
|
||||
isNorthAmerica,
|
||||
countryCode,
|
||||
isPromotionPeriod,
|
||||
countryDialingCode,
|
||||
preregistDate,
|
||||
checkCountryByIp,
|
||||
getPreregist,
|
||||
setPreregist,
|
||||
}
|
||||
}
|
||||
|
||||
export { usePreregist }
|
||||
@@ -2,6 +2,8 @@
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import { getComponentGroup, getComponentGroupAry } from '#layers/utils/dataUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
|
||||
import type { Platform } from '#layers/types/components/button'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
@@ -10,598 +12,357 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { tm } = useI18n()
|
||||
|
||||
const test = {
|
||||
subTitle: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
text: '사전 등록 기간 + 2024.06.04 ~ 정식 오픈 전까지',
|
||||
color_code: '',
|
||||
color_name: 'primary',
|
||||
},
|
||||
resource_type: 'TXT',
|
||||
},
|
||||
],
|
||||
},
|
||||
mainTitle: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
text: '로드나인 사전 등록',
|
||||
color_code: '',
|
||||
color_name: 'primary',
|
||||
},
|
||||
resource_type: 'TXT',
|
||||
},
|
||||
],
|
||||
},
|
||||
background: {
|
||||
groups: [
|
||||
{
|
||||
res_path: {
|
||||
path_mo: '/local/template/l9/16/1/1/FX_PREREGIST_01/common/bg_m.jpg',
|
||||
path_pc: '/local/template/l9/16/1/1/FX_PREREGIST_01/common/bg.jpg',
|
||||
},
|
||||
resource_type: 'IMG_COMM',
|
||||
},
|
||||
],
|
||||
},
|
||||
txtSnsLink: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
text: 'https://pf.kakao.com/_xmyxjpG',
|
||||
},
|
||||
resource_type: 'TXT',
|
||||
},
|
||||
{
|
||||
display: {
|
||||
text: 'https://www.youtube.com/@LORDNINE_KR',
|
||||
},
|
||||
resource_type: 'TXT',
|
||||
},
|
||||
],
|
||||
},
|
||||
description: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
text: '로드나인은 PC, GooglePlay, AppStore에서 즐기실 수 있습니다',
|
||||
color_code: '',
|
||||
color_name: 'text-primary',
|
||||
},
|
||||
resource_type: 'TXT',
|
||||
},
|
||||
],
|
||||
},
|
||||
imgAccReward: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
text: '골드 100,000',
|
||||
},
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward01_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward01.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
{
|
||||
display: {
|
||||
text: '중급 물약 x100',
|
||||
},
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward02_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward02.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
{
|
||||
display: {
|
||||
text: '방어구 강화석 x3',
|
||||
},
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward03_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward03.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
{
|
||||
display: {
|
||||
text: '무기 강화석 x3',
|
||||
},
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward04_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward04.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
// {
|
||||
// display: {
|
||||
// text: '순수한 무기 강화석 x1',
|
||||
// },
|
||||
// res_path: {
|
||||
// path_mo:
|
||||
// '/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward05_m.png',
|
||||
// path_pc:
|
||||
// '/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward05.png',
|
||||
// },
|
||||
// resource_type: 'IMG_LANG',
|
||||
// },
|
||||
],
|
||||
},
|
||||
imgSnsButton: {
|
||||
groups: [
|
||||
{
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/sns_button01_m.jpg',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/sns_button01.jpg',
|
||||
},
|
||||
tracking: {
|
||||
click_item: 'imgSnsButton_7. 사전등록-SNS버튼이미지(1)',
|
||||
action_type: 'click',
|
||||
click_sarea: 'promotionPreregist_tmpl_01__imgSnsButton',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
{
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/sns_button02_m.jpg',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/sns_button02.jpg',
|
||||
},
|
||||
tracking: {
|
||||
click_item: 'imgSnsButton_7. 사전등록-SNS버튼이미지(2)',
|
||||
action_type: 'click',
|
||||
click_sarea: 'promotionPreregist_tmpl_01__imgSnsButton',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
],
|
||||
},
|
||||
imgSnsReward: {
|
||||
groups: [
|
||||
{
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_preregist_reward_sns_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_preregist_reward_sns.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
],
|
||||
},
|
||||
subTitleAccReward: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
text: '이벤트 기간 + 2024.06.04 ~ 정식 오픈 전까지',
|
||||
color_code: '#c5902f',
|
||||
color_name: '',
|
||||
},
|
||||
resource_type: 'TXT',
|
||||
},
|
||||
],
|
||||
},
|
||||
imgPreregistReward: {
|
||||
groups: [
|
||||
{
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_preregist_reward_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_preregist_reward.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
],
|
||||
},
|
||||
mainTitleAccReward: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
text: '카카오톡 공식 채널 구독 이벤트',
|
||||
color_code: '#c5902f',
|
||||
color_name: '',
|
||||
},
|
||||
resource_type: 'TXT',
|
||||
},
|
||||
],
|
||||
},
|
||||
backgroundAccReward: {
|
||||
groups: [
|
||||
{
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/common/bg_acc_reward_m.jpg',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/common/bg_acc_reward.jpg',
|
||||
},
|
||||
resource_type: 'IMG_COMM',
|
||||
},
|
||||
],
|
||||
},
|
||||
descriptionAccReward: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
text: '※ 달성 여부는 실시간으로 반영되지 않을 수 있습니다.',
|
||||
color_code: '#737474',
|
||||
color_name: '',
|
||||
},
|
||||
resource_type: 'TXT',
|
||||
},
|
||||
],
|
||||
},
|
||||
isAccRewardCompleted: {},
|
||||
preregistButtonColor: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
color_code: '#cc0000',
|
||||
color_name: '',
|
||||
},
|
||||
resource_type: 'COLOR',
|
||||
},
|
||||
{
|
||||
display: {
|
||||
color_code: '#ccff00',
|
||||
color_name: '',
|
||||
},
|
||||
resource_type: 'COLOR',
|
||||
},
|
||||
],
|
||||
},
|
||||
rewardTitleAccReward: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
text: '누적 목표를 달성할수록 더 많은 보상을 드립니다!',
|
||||
color_code: '#33312e',
|
||||
color_name: '',
|
||||
},
|
||||
resource_type: 'TXT',
|
||||
},
|
||||
],
|
||||
},
|
||||
imgAccRewardIncomplete: {
|
||||
groups: [
|
||||
{
|
||||
display: {
|
||||
text: '골드 100,000',
|
||||
},
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward_incomplete01_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward_incomplete01.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
{
|
||||
display: {
|
||||
text: '중급 물약 x100',
|
||||
},
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward_incomplete02_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward_incomplete02.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
{
|
||||
display: {
|
||||
text: '방어구 강화석 x3',
|
||||
},
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward_incomplete03_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward_incomplete03.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
{
|
||||
display: {
|
||||
text: '무기 강화석 x3',
|
||||
},
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward_incomplete04_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward_incomplete04.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
{
|
||||
display: {
|
||||
text: '순수한 무기 강화석 x1',
|
||||
},
|
||||
res_path: {
|
||||
path_mo:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward_incomplete05_m.png',
|
||||
path_pc:
|
||||
'/local/template/l9/16/1/1/FX_PREREGIST_01/ko/img_acc_reward_incomplete05.png',
|
||||
},
|
||||
resource_type: 'IMG_LANG',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any
|
||||
const { locale } = useI18n()
|
||||
const { getOperateResourcesData } = useResourcesData()
|
||||
const { gameData } = storeToRefs(useGameDataStore())
|
||||
const { pageData } = storeToRefs(usePageDataStore())
|
||||
|
||||
// Constants
|
||||
const PREREGIST_PLATFORMS = [
|
||||
{ platform: 'stove', label: '사전 등록 하기' },
|
||||
{ platform: 'google_play', label: '사전 등록 하기' },
|
||||
{ platform: 'app_store', label: '사전 등록 하기' },
|
||||
] as const
|
||||
const COLOR_INDEX = { BACKGROUND: 0, TEXT: 1 } as const
|
||||
const OS_TYPE_MAP: Record<string, Platform[]> = {
|
||||
'1': ['google_play'],
|
||||
'2': ['app_store'],
|
||||
'3': ['google_play', 'app_store'],
|
||||
}
|
||||
|
||||
const COLOR_INDEX = {
|
||||
BACKGROUND: 0,
|
||||
TEXT: 1,
|
||||
} as const
|
||||
const preregistModalRef = ref<{ handlePreregist: () => void } | null>(null)
|
||||
|
||||
// Preregist Section
|
||||
const backgroundData = computed(() =>
|
||||
const preregistCode = computed(
|
||||
() => getComponentGroup(props.components, 'preregistCode')?.display?.text
|
||||
)
|
||||
const prdBackgroundData = computed(() =>
|
||||
getComponentGroup(props.components, 'background')
|
||||
)
|
||||
const mainTitleData = computed(() =>
|
||||
const preMainTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'mainTitle')
|
||||
)
|
||||
const subTitleData = computed(() =>
|
||||
const preSubTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'subTitle')
|
||||
)
|
||||
const imgPreregistRewardData = computed(() =>
|
||||
const preImgPreregistdData = computed(() =>
|
||||
getComponentGroup(props.components, 'imgPreregistReward')
|
||||
)
|
||||
const imgSnsRewardData = computed(() =>
|
||||
const preImgSnsData = computed(() =>
|
||||
getComponentGroup(props.components, 'imgSnsReward')
|
||||
)
|
||||
const preregistButtonColorData = computed(() =>
|
||||
getComponentGroupAry(props.components, 'preregistButtonColor')
|
||||
)
|
||||
const imgSnsButtonData = computed(() =>
|
||||
getComponentGroupAry(props.components, 'imgSnsButton')
|
||||
)
|
||||
const txtSnsLinkData = computed(() =>
|
||||
getComponentGroupAry(props.components, 'txtSnsLink')
|
||||
)
|
||||
const descriptionData = computed(() =>
|
||||
const preDescriptionData = computed(() =>
|
||||
getComponentGroup(props.components, 'description')
|
||||
)
|
||||
const snsButtons = computed(() => {
|
||||
const buttons = imgSnsButtonData.value
|
||||
const links = txtSnsLinkData.value
|
||||
// SNS Buttons
|
||||
const snsButtonsData = computed(() => {
|
||||
const buttons = getComponentGroupAry(props.components, 'imgSnsButton')
|
||||
const links = getComponentGroupAry(props.components, 'txtSnsLink')
|
||||
|
||||
if (!buttons?.length) return []
|
||||
|
||||
return buttons.map((button, index) => ({
|
||||
image: button,
|
||||
link: links?.[index]?.display?.text ?? '',
|
||||
id: button.id ?? `sns-${index}`,
|
||||
}))
|
||||
})
|
||||
const buttonColors = computed(() => {
|
||||
const colorData = preregistButtonColorData.value
|
||||
|
||||
if (!colorData?.length) {
|
||||
return { background: undefined, text: undefined }
|
||||
}
|
||||
// Button Colors
|
||||
const buttonColors = computed(() => {
|
||||
const colorData = getComponentGroupAry(
|
||||
props.components,
|
||||
'preregistButtonColor'
|
||||
)
|
||||
|
||||
if (!colorData?.length)
|
||||
return { backgroundColor: undefined, textColor: undefined }
|
||||
|
||||
return {
|
||||
background: getColorCode({
|
||||
backgroundColor: getColorCode({
|
||||
colorName: colorData[COLOR_INDEX.BACKGROUND]?.display?.color_name,
|
||||
colorCode: colorData[COLOR_INDEX.BACKGROUND]?.display?.color_code,
|
||||
}),
|
||||
text: getColorCode({
|
||||
textColor: getColorCode({
|
||||
colorName: colorData[COLOR_INDEX.TEXT]?.display?.color_name,
|
||||
colorCode: colorData[COLOR_INDEX.TEXT]?.display?.color_code,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
// Platform Buttons
|
||||
const platformButtons = computed<Platform[]>(() => {
|
||||
const osType = String(gameData.value?.os_type ?? '')
|
||||
return OS_TYPE_MAP[osType] ?? []
|
||||
})
|
||||
|
||||
// Reward Section
|
||||
const backgroundAccRewardData = computed(() =>
|
||||
const accBackgroundData = computed(() =>
|
||||
getComponentGroup(props.components, 'backgroundAccReward')
|
||||
)
|
||||
const mainTitleAccRewardData = computed(() =>
|
||||
const accMainTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'mainTitleAccReward')
|
||||
)
|
||||
const subTitleAccRewardData = computed(() =>
|
||||
const accSubTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'subTitleAccReward')
|
||||
)
|
||||
const rewardTitleAccRewardData = computed(() =>
|
||||
const accRewardTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'rewardTitleAccReward')
|
||||
)
|
||||
const imgAccRewardData = computed(() =>
|
||||
getComponentGroupAry(test, 'imgAccReward')
|
||||
)
|
||||
const imgAccRewardLength = computed(() => imgAccRewardData.value?.length ?? 0)
|
||||
const descriptionAccRewardData = computed(() =>
|
||||
const accDescriptionData = computed(() =>
|
||||
getComponentGroup(props.components, 'descriptionAccReward')
|
||||
)
|
||||
const accPaginationData = computed(() =>
|
||||
getComponentGroupAry(props.components, 'pagination')
|
||||
)
|
||||
|
||||
// Async Data - 리워드 완료 데이터
|
||||
const { data: rewardCompletedData } = await useAsyncData(
|
||||
`fx-preregist-resources-${pageData.value?.page_seq}-${pageData.value?.page_ver}-${props.pageVerTmplSeq}`,
|
||||
async () => {
|
||||
const { page_seq, page_ver } = pageData.value ?? {}
|
||||
|
||||
if (!page_seq || !page_ver) return []
|
||||
|
||||
try {
|
||||
const operateGroupList = await getOperateResourcesData({
|
||||
pageSeq: page_seq,
|
||||
pageVer: page_ver,
|
||||
pageVerTmplSeq: props.pageVerTmplSeq,
|
||||
langCode: locale.value,
|
||||
})
|
||||
|
||||
return getComponentContainer(operateGroupList, 'isAccRewardCompleted', {
|
||||
isGroup: true,
|
||||
}) as OperateGroupItem[]
|
||||
} catch (error) {
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[FxPreregist01] Failed to fetch reward data:', error)
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => [],
|
||||
server: false,
|
||||
}
|
||||
)
|
||||
|
||||
// Reward Images
|
||||
const rewardImages = computed(() => {
|
||||
const defaultList = getComponentGroupAry(props.components, 'imgAccReward')
|
||||
const incompleteList = getComponentGroupAry(
|
||||
props.components,
|
||||
'imgAccRewardIncomplete'
|
||||
)
|
||||
|
||||
if (!defaultList?.length) return []
|
||||
|
||||
return defaultList.map((defaultItem, index) => ({
|
||||
id: defaultItem.id ?? `reward-${index}`,
|
||||
default: defaultItem,
|
||||
incomplete: incompleteList?.[index] ?? null,
|
||||
flagType: rewardCompletedData.value?.[index]?.flag_type ?? 0,
|
||||
}))
|
||||
})
|
||||
|
||||
// Splide Options
|
||||
const splideOptions = computed(() => {
|
||||
const length = rewardImages.value.length
|
||||
|
||||
return {
|
||||
type: 'slide',
|
||||
type: 'slide' as const,
|
||||
gap: 16,
|
||||
arrows: false,
|
||||
pagination: false,
|
||||
destroy: true,
|
||||
breakpoints: {
|
||||
[BREAKPOINTS.md - 1]: {
|
||||
destroy: imgAccRewardLength.value <= 3,
|
||||
perPage: 'auto',
|
||||
focus: 'center',
|
||||
drag: imgAccRewardLength.value > 3,
|
||||
padding: {
|
||||
left: 40,
|
||||
right: 40,
|
||||
},
|
||||
937: {
|
||||
destroy: length < 5,
|
||||
gap: 12,
|
||||
padding: { left: 40, right: 40 },
|
||||
},
|
||||
[BREAKPOINTS.sm - 1]: {
|
||||
padding: { left: 20, right: 20 },
|
||||
},
|
||||
723: {
|
||||
destroy: length < 4,
|
||||
},
|
||||
561: {
|
||||
destroy: false,
|
||||
gap: 12,
|
||||
drag: true,
|
||||
padding: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Handler
|
||||
const handlePreregistClick = () => {
|
||||
preregistModalRef.value?.handlePreregist()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="relative py-[80px] md:py-[120px]">
|
||||
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
|
||||
<div class="content-standard">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
class="title-xlg"
|
||||
<div class="section-container">
|
||||
<!-- Preregist Section -->
|
||||
<section class="relative py-[80px] md:py-[120px]">
|
||||
<WidgetsBackground
|
||||
v-if="prdBackgroundData"
|
||||
:resources-data="prdBackgroundData"
|
||||
/>
|
||||
<WidgetsSubTitle
|
||||
v-if="subTitleData"
|
||||
:resources-data="subTitleData"
|
||||
class="title-sm mt-2"
|
||||
/>
|
||||
<div class="flex flex-col gap-4 mt-8 md:flex-row">
|
||||
<div v-if="imgPreregistRewardData" class="w-full max-w-[446px]">
|
||||
<AtomsImg
|
||||
:src="getImagePaths(imgPreregistRewardData)"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="imgSnsRewardData" class="relative w-full max-w-[446px]">
|
||||
<AtomsImg
|
||||
:src="getImagePaths(imgSnsRewardData)"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
<ul
|
||||
v-if="snsButtons.length"
|
||||
class="absolute bottom-[20px] left-0 w-full flex items-center justify-center gap-2 md:bottom-[24px] md:gap-3"
|
||||
>
|
||||
<li
|
||||
v-for="(snsButton, index) in snsButtons"
|
||||
:key="`sns-${snsButton.link}-${index}`"
|
||||
>
|
||||
<a
|
||||
:href="snsButton.link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<AtomsImg :src="getImagePaths(snsButton.image)" />
|
||||
<span class="sr-only">
|
||||
{{ snsButton.link }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="PREREGIST_PLATFORMS.length"
|
||||
class="flex gap-3 justify-center flex-wrap mt-8 md:gap-2.5"
|
||||
>
|
||||
<AtomsButtonLauncher
|
||||
v-for="item in PREREGIST_PLATFORMS"
|
||||
:key="`preregist-${item.platform}`"
|
||||
type="duplication"
|
||||
:platform="item.platform"
|
||||
:background-color="buttonColors.background"
|
||||
:text-color="buttonColors.text"
|
||||
>
|
||||
{{ item.label }}
|
||||
</AtomsButtonLauncher>
|
||||
</div>
|
||||
<WidgetsDescription
|
||||
v-if="descriptionData"
|
||||
:resources-data="descriptionData"
|
||||
class="mt-8"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section class="relative py-[80px] md:py-[120px]">
|
||||
<WidgetsBackground
|
||||
v-if="backgroundAccRewardData"
|
||||
:resources-data="backgroundAccRewardData"
|
||||
/>
|
||||
<div class="content-standard">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleAccRewardData"
|
||||
:resources-data="mainTitleAccRewardData"
|
||||
class="title-xlg"
|
||||
/>
|
||||
<WidgetsSubTitle
|
||||
v-if="subTitleAccRewardData"
|
||||
:resources-data="subTitleAccRewardData"
|
||||
class="title-sm mt-2"
|
||||
/>
|
||||
<WidgetsSubTitle
|
||||
v-if="rewardTitleAccRewardData"
|
||||
tag="h4"
|
||||
:resources-data="rewardTitleAccRewardData"
|
||||
class="mt-[48px] text-[18px] font-[700] leading-[26px] tracking-[-0.54px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:mt-[72px] md:text-[24px] md:leading-[34px] md:tracking-[0.72px]"
|
||||
/>
|
||||
<div v-if="imgAccRewardLength" class="mt-6 md:mt-8">
|
||||
<ul class="hidden md:flex justify-center md:mb-[20px]">
|
||||
<li
|
||||
v-for="index in imgAccRewardLength"
|
||||
:key="index"
|
||||
class="flex items-center"
|
||||
>
|
||||
<span class="progress-bullet"></span>
|
||||
<div v-if="index !== imgAccRewardLength" class="progress-bar">
|
||||
<span class="progress-fill"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<BlocksSlideDefault
|
||||
v-bind="splideOptions"
|
||||
class="w-[100vw] mx-[-20px] sm:mx-[-40px] md:w-auto md:mx-auto"
|
||||
>
|
||||
<SplideSlide
|
||||
v-for="(item, index) in imgAccRewardData"
|
||||
:key="`reward-${item.id ?? index}`"
|
||||
class="w-[162px] h-[228px] md:w-[176px] md:h-[249px]"
|
||||
>
|
||||
<div class="content-standard">
|
||||
<WidgetsMainTitle
|
||||
v-if="preMainTitleData"
|
||||
:resources-data="preMainTitleData"
|
||||
class="title-xlg"
|
||||
/>
|
||||
<WidgetsSubTitle
|
||||
v-if="preSubTitleData"
|
||||
:resources-data="preSubTitleData"
|
||||
class="title-sm mt-2"
|
||||
/>
|
||||
<div class="flex flex-col gap-4 mt-8 md:flex-row">
|
||||
<div v-if="preImgPreregistdData" class="w-full max-w-[446px]">
|
||||
<AtomsImg
|
||||
:src="getImagePaths(item)"
|
||||
:alt="item?.display?.text ?? `Reward ${index + 1}`"
|
||||
:src="getImagePaths(preImgPreregistdData)"
|
||||
:alt="preImgPreregistdData?.display?.text"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
</SplideSlide>
|
||||
</BlocksSlideDefault>
|
||||
</div>
|
||||
<div v-if="preImgSnsData" class="relative w-full max-w-[446px]">
|
||||
<AtomsImg
|
||||
:src="getImagePaths(preImgSnsData)"
|
||||
:alt="preImgSnsData?.display?.text"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
<ul
|
||||
v-if="snsButtonsData.length"
|
||||
class="absolute bottom-[20px] left-0 w-full flex items-center justify-center gap-2 md:bottom-[24px] md:gap-3"
|
||||
>
|
||||
<li
|
||||
v-for="btn in snsButtonsData"
|
||||
:key="btn.id"
|
||||
class="w-[48px] h-[40px] md:w-[72px] md:h-[56px]"
|
||||
>
|
||||
<a :href="btn.link" target="_blank" rel="noopener noreferrer">
|
||||
<AtomsImg
|
||||
:src="getImagePaths(btn.image)"
|
||||
:alt="btn.image?.display?.text"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 justify-center flex-wrap mt-8 md:gap-2.5">
|
||||
<AtomsButtonLauncher
|
||||
type="duplication"
|
||||
platform="stove"
|
||||
:background-color="buttonColors.backgroundColor"
|
||||
:text-color="buttonColors.textColor"
|
||||
@click="handlePreregistClick"
|
||||
>
|
||||
사전 등록 하기
|
||||
</AtomsButtonLauncher>
|
||||
<AtomsButtonLauncher
|
||||
v-for="platform in platformButtons"
|
||||
:key="`preregist-${platform}`"
|
||||
type="duplication"
|
||||
:platform="platform"
|
||||
:background-color="buttonColors.backgroundColor"
|
||||
:text-color="buttonColors.textColor"
|
||||
>
|
||||
사전 등록 하기
|
||||
</AtomsButtonLauncher>
|
||||
</div>
|
||||
<WidgetsDescription
|
||||
v-if="preDescriptionData"
|
||||
:resources-data="preDescriptionData"
|
||||
class="mt-8"
|
||||
/>
|
||||
</div>
|
||||
<WidgetsDescription
|
||||
v-if="descriptionAccRewardData"
|
||||
:resources-data="descriptionAccRewardData"
|
||||
class="mt-6 md:mt-8"
|
||||
</section>
|
||||
<!-- Reward Section -->
|
||||
<section class="relative py-[80px] md:py-[120px]">
|
||||
<WidgetsBackground
|
||||
v-if="accBackgroundData"
|
||||
:resources-data="accBackgroundData"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<div class="content-standard">
|
||||
<WidgetsMainTitle
|
||||
v-if="accMainTitleData"
|
||||
:resources-data="accMainTitleData"
|
||||
class="title-xlg"
|
||||
/>
|
||||
<WidgetsSubTitle
|
||||
v-if="accSubTitleData"
|
||||
:resources-data="accSubTitleData"
|
||||
class="title-sm mt-2"
|
||||
/>
|
||||
<WidgetsSubTitle
|
||||
v-if="accRewardTitleData"
|
||||
tag="h4"
|
||||
:resources-data="accRewardTitleData"
|
||||
class="mt-[48px] text-[18px] font-[700] leading-[26px] tracking-[-0.54px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:mt-[72px] md:text-[24px] md:leading-[34px] md:tracking-[0.72px]"
|
||||
/>
|
||||
<div
|
||||
v-if="rewardImages.length"
|
||||
class="overflow-hidden w-[calc(100%+40px)] min-h-[228px] mt-6 mx-[-20px] sm:w-[calc(100%+80px)] sm:mx-[-40px] md:w-full md:min-h-[281px] md:mt-8 md:mx-auto"
|
||||
>
|
||||
<ClientOnly>
|
||||
<ul
|
||||
class="hidden md:flex justify-center md:mb-[20px]"
|
||||
:style="getPaginationClass(accPaginationData)"
|
||||
>
|
||||
<li
|
||||
v-for="(item, index) in rewardImages"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'flex items-center',
|
||||
{ 'is-completed': item.flagType === 1 },
|
||||
]"
|
||||
>
|
||||
<span class="progress-bullet"></span>
|
||||
<div
|
||||
v-if="index < rewardImages.length - 1"
|
||||
class="progress-bar"
|
||||
></div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<BlocksSlideDefault v-bind="splideOptions">
|
||||
<SplideSlide
|
||||
v-for="item in rewardImages"
|
||||
:key="item.id"
|
||||
class="w-[162px] h-[228px] md:w-[176px] md:h-[249px]"
|
||||
>
|
||||
<AtomsImg
|
||||
:src="
|
||||
getImagePaths(
|
||||
item.flagType === 1 ? item.incomplete : item.default
|
||||
)
|
||||
"
|
||||
:alt="item.default?.display?.text"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
</SplideSlide>
|
||||
</BlocksSlideDefault>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<WidgetsDescription
|
||||
v-if="accDescriptionData"
|
||||
:resources-data="accDescriptionData"
|
||||
class="mt-6 md:mt-8"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<WidgetsModalPreregist
|
||||
ref="preregistModalRef"
|
||||
:preregist-code="preregistCode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -612,16 +373,14 @@ const splideOptions = computed(() => {
|
||||
|
||||
.progress-bullet {
|
||||
@apply block w-3 h-3 rounded-full transition-all duration-300;
|
||||
background-color: red;
|
||||
/* background-color: var(--pagination-disabled); */
|
||||
background-color: var(--pagination-disabled);
|
||||
}
|
||||
.progress-bar {
|
||||
@apply relative w-[180px] h-0.5 overflow-hidden;
|
||||
/* background-color: var(--pagination-disabled); */
|
||||
background-color: red;
|
||||
@apply w-[180px] h-0.5 overflow-hidden;
|
||||
background-color: var(--pagination-disabled);
|
||||
}
|
||||
.progress-fill {
|
||||
@apply absolute inset-y-0 left-0 w-[0];
|
||||
.is-completed .progress-bullet,
|
||||
.is-completed .progress-bar {
|
||||
background-color: var(--pagination-active);
|
||||
}
|
||||
</style>
|
||||
|
||||
34
layers/types/GdsType.ts
Normal file
34
layers/types/GdsType.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* GDS
|
||||
*/
|
||||
interface ReqGetGdsClientPolicyTotal {
|
||||
policy_grp: string
|
||||
device_nation?: string
|
||||
client_lang: string
|
||||
include_coverages?: boolean
|
||||
|
||||
// QA
|
||||
runType?: string
|
||||
qc?: string
|
||||
}
|
||||
|
||||
interface DtoGetGdsClientPolicyTotal {
|
||||
is_default: boolean
|
||||
nation: string
|
||||
regulation: string
|
||||
timezone: string
|
||||
utc_offset: number
|
||||
lang: string
|
||||
coverages: Array<string>
|
||||
}
|
||||
|
||||
interface ResGetGdsClientPolicyTotal {
|
||||
code?: number
|
||||
message?: string
|
||||
res_code?: number
|
||||
res_message?: string
|
||||
res_data?: DtoGetGdsClientPolicyTotal
|
||||
req_data?: ReqGetGdsClientPolicyTotal
|
||||
}
|
||||
|
||||
export type { ReqGetGdsClientPolicyTotal, ResGetGdsClientPolicyTotal }
|
||||
102
layers/types/PreregistType.ts
Normal file
102
layers/types/PreregistType.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
interface ReqPreorderSelectEvent {
|
||||
// Header
|
||||
accessToken?: string
|
||||
|
||||
// Body
|
||||
event_code: string
|
||||
lang: string
|
||||
terms_type: number // 1 : hp / 2 : email / 3 : 스토브 번호
|
||||
hp?: string
|
||||
email?: string
|
||||
}
|
||||
interface DtoResPreorderSelectEvent {
|
||||
game_code: number
|
||||
game_id: string
|
||||
game_name: string
|
||||
lang_code: string | null
|
||||
world_id: string | null
|
||||
stove_num: number
|
||||
game_unique_num: number
|
||||
event_code: string
|
||||
event_name: string
|
||||
event_info1: string | null
|
||||
event_info2: string | null
|
||||
event_info3: number | null
|
||||
event_info4: number | null
|
||||
terms_type: number
|
||||
event_info_set_terms_type: number | null
|
||||
hp: string | null
|
||||
email: string | null
|
||||
terms_yn: number
|
||||
terms_count: number
|
||||
terms_time: string | null
|
||||
cn_send_time: string | null
|
||||
cn_send_count: number
|
||||
under14_terms: number
|
||||
delete_date: string | null
|
||||
necessary_consent1: string | null
|
||||
necessary_consent2: string | null
|
||||
necessary_consent3: string | null
|
||||
choice_consent: string | null
|
||||
con_region: string | null
|
||||
device_type: string | null
|
||||
metric_seq: number
|
||||
email_auto_type: string | null
|
||||
option01: number | null
|
||||
option02: string | null
|
||||
option03: number | null
|
||||
caller_id: string | null
|
||||
caller_uuid: string | null
|
||||
user_ip: string | null
|
||||
chk_age: number
|
||||
country_code: string | null
|
||||
country_dialing_code: number | null
|
||||
birth_date: string | null
|
||||
terms_time_long: number
|
||||
g_server: string | null
|
||||
c_num: number
|
||||
}
|
||||
interface ResPreorderSelectEvent extends CommonResponseType {
|
||||
value?: DtoResPreorderSelectEvent
|
||||
}
|
||||
|
||||
interface ReqPreorderReserveDataUpdate {
|
||||
baseApiUrl?: string
|
||||
|
||||
// Header
|
||||
accessToken?: string
|
||||
|
||||
// Body
|
||||
necessary_consent1: string
|
||||
necessary_consent2: string
|
||||
necessary_consent3: string
|
||||
event_code: string
|
||||
terms_type: number // 1 : hp / 2 : email / 3 : 스토브 번호
|
||||
c_num?: number
|
||||
lang_code: string
|
||||
hp?: string
|
||||
email?: string
|
||||
metric_seq?: number //= mcode
|
||||
g_server?: string
|
||||
world_id?: string
|
||||
game_unique_num?: number
|
||||
event_info1?: string
|
||||
event_info2?: string
|
||||
event_info3?: number
|
||||
event_info4?: number
|
||||
under14_terms?: string
|
||||
device_type: string
|
||||
country_code: string
|
||||
country_dialing_code?: string
|
||||
birth_date?: string
|
||||
}
|
||||
interface ResPreorderReserveDataUpdate extends CommonResponseType {
|
||||
value?: number
|
||||
}
|
||||
|
||||
export type {
|
||||
ReqPreorderSelectEvent,
|
||||
ResPreorderSelectEvent,
|
||||
ReqPreorderReserveDataUpdate,
|
||||
ResPreorderReserveDataUpdate,
|
||||
}
|
||||
Reference in New Issue
Block a user