Merge branch 'feature/202501107-all' into feature/20251001-gil
4
.env.dev
@@ -20,6 +20,10 @@ STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/laun
|
||||
STOVE_CLIENT_DOWNLOAD_URL=https://sgs-gate8-dl.game.playstove.com/game/lcs/STOVESetup.exe
|
||||
STOVE_LOGIN_URL=https://accounts-dev.onstove.com/login
|
||||
|
||||
|
||||
# STOVE
|
||||
STOVE_CS=https://cs.onstove.com/service
|
||||
|
||||
# Log Tracking ######################################################################
|
||||
# 81plug
|
||||
STOVE_81PLUG=https://dvudc0gwzz5wc.cloudfront.net/v3.1/dev/svc_81plug.min.js
|
||||
@@ -20,6 +20,9 @@ STOVE_LAUNCHER_SCRIPT=https://js-cdn.onstove.com/libs/stove-js-service/latest/la
|
||||
STOVE_CLIENT_DOWNLOAD_URL=https://sgs-live-dl.game.playstove.com/game/lcs/STOVESetup.exe
|
||||
STOVE_LOGIN_URL=https://accounts.onstove.com/login
|
||||
|
||||
# STOVE
|
||||
STOVE_CS=https://cs.onstove.com/service
|
||||
|
||||
# Log Tracking ######################################################################
|
||||
# 81plug
|
||||
STOVE_81PLUG=https://dvudc0gwzz5wc.cloudfront.net/v3.1/live/svc_81plug.min.js
|
||||
3
.env.qa
@@ -20,6 +20,9 @@ STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/laun
|
||||
STOVE_CLIENT_DOWNLOAD_URL=https://sgs-gate8-dl.game.playstove.com/game/lcs/STOVESetup.exe
|
||||
STOVE_LOGIN_URL=https://accounts-qa.onstove.com/login
|
||||
|
||||
# STOVE
|
||||
STOVE_CS=https://cs.onstove.com/service
|
||||
|
||||
# Log Tracking ######################################################################
|
||||
# 81plug
|
||||
STOVE_81PLUG=https://dvudc0gwzz5wc.cloudfront.net/v3.1/qa/svc_81plug.min.js
|
||||
|
||||
@@ -20,6 +20,9 @@ STOVE_LAUNCHER_SCRIPT=https://js-cdn.gate8.com/libs/stove-js-service/latest/laun
|
||||
STOVE_CLIENT_DOWNLOAD_URL=https://sgs-gate8-dl.game.playstove.com/game/lcs/STOVESetup.exe
|
||||
STOVE_LOGIN_URL=https://accounts.gate8.com/login
|
||||
|
||||
# STOVE
|
||||
STOVE_CS=https://cs.onstove.com/service
|
||||
|
||||
# Log Tracking ######################################################################
|
||||
# 81plug
|
||||
STOVE_81PLUG=https://dvudc0gwzz5wc.cloudfront.net/v3.1/sandbox/svc_81plug.min.js
|
||||
@@ -30,7 +30,7 @@ const getGameDataFromServer = (): GameDataValue | null => {
|
||||
const setupAllMetaData = (data: GameDataValue) => {
|
||||
const meta = data.meta_tag_json ?? ({} as GameDataMetaTag)
|
||||
const faviconPath = data.favicon_json ?? ({} as GameDataFavicon)
|
||||
const theme = data.design_theme === 1 ? 'dark' : 'light'
|
||||
const theme = data.design_theme === 1 ? 'light' : 'dark'
|
||||
|
||||
// 파비콘 링크 생성
|
||||
const faviconLinks = [
|
||||
|
||||
@@ -6,22 +6,34 @@
|
||||
<clientOnly>
|
||||
<!-- 로고 -->
|
||||
<div class="inspection-logo">
|
||||
<img :src="logoImgUrl" alt="logo" class="w-full h-full object-contain" />
|
||||
<img
|
||||
:src="logoImgUrl"
|
||||
alt="logo"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="inspection-content">
|
||||
<!-- 점검 메시지 -->
|
||||
|
||||
|
||||
<h1 class="inspection-title text-lg md:text-2xl">
|
||||
{{ tm('Inspection_Now_Maintenance') }}
|
||||
</h1>
|
||||
|
||||
<div class="inspection-cards">
|
||||
<!-- 점검 시간 카드 -->
|
||||
<div v-if="webInspectionData" class="inspection-card inspection-time-card">
|
||||
<h2 class="card-title text-base text-md md:text-lg">{{ tm('Inspection_Maintenance_Time') }}</h2>
|
||||
<div
|
||||
v-if="webInspectionData"
|
||||
class="inspection-card inspection-time-card"
|
||||
>
|
||||
<h2 class="card-title text-base text-md md:text-lg">
|
||||
{{ tm('Inspection_Maintenance_Time') }}
|
||||
</h2>
|
||||
<div class="inspection-time text-sm md:text-base font-medium">
|
||||
<div v-dompurify-html="getLocaleTimezone('', '')" class="time-row"></div>
|
||||
<div
|
||||
v-dompurify-html="getLocaleTimezone('', '')"
|
||||
class="time-row"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +46,7 @@
|
||||
<span v-else v-dompurify-html="tm('org_Inspection_During_Maintenance')"></span>
|
||||
</h3>
|
||||
<div class="button-group justify-center">
|
||||
<!-- <a
|
||||
<!-- <a
|
||||
:href="communityUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -45,49 +57,63 @@
|
||||
<AtomsIconsLongArrowRightLine :size="16" color="#1F1F1F" />
|
||||
</a> -->
|
||||
|
||||
<AtomsButtonVariant
|
||||
type="custom"
|
||||
class="inspection-btn inspection-btn-community color-black text-sm md:text-base"
|
||||
:class="{ 'inspection-btn-outline flex-1' : launchingStatus }"
|
||||
@click="handleCommunityClick"
|
||||
<AtomsButtonVariant
|
||||
type="custom"
|
||||
class="inspection-btn inspection-btn-community color-black text-sm md:text-base"
|
||||
:class="{ 'inspection-btn-outline flex-1': launchingStatus }"
|
||||
@click="handleCommunityClick"
|
||||
>
|
||||
<span>{{ tm('Inspection_Community_Btn') }}</span>
|
||||
<AtomsIconsLongArrowRightLine :size="16" color="#1F1F1F" />
|
||||
</AtomsButtonVariant>
|
||||
|
||||
<AtomsButtonVariant
|
||||
v-if="launchingStatus"
|
||||
type="custom"
|
||||
platform="pc"
|
||||
class="inspection-btn inspection-btn-primary w-auto color-black text-sm md:text-base flex-1"
|
||||
@click="handleGameStart"
|
||||
>
|
||||
<span>게임 시작</span>
|
||||
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<span>{{ tm('Inspection_Community_Btn') }}</span>
|
||||
<AtomsIconsLongArrowRightLine :size="16" color="#1F1F1F" />
|
||||
|
||||
</AtomsButtonVariant>
|
||||
|
||||
<AtomsButtonVariant
|
||||
v-if="launchingStatus"
|
||||
type="custom"
|
||||
platform="pc"
|
||||
class="inspection-btn inspection-btn-primary w-auto color-black text-sm md:text-base flex-1"
|
||||
@click="handleGameStart"
|
||||
>
|
||||
<span>게임 시작</span>
|
||||
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.3098 1.49172C8.86574 1.28049 7.15098 1.28055 5.70176 1.49167C3.50821 1.81315 1.81986 3.50786 1.49213 5.69168L1.4918 5.69391C1.38564 6.41904 1.33331 7.19181 1.33331 7.99998C1.33331 8.80815 1.3857 9.58136 1.49187 10.3065C1.81362 12.4934 3.50335 14.1805 5.69381 14.5079L5.69577 14.5082C6.42109 14.6143 7.19417 14.6666 7.99717 14.6666C8.80032 14.6666 9.57311 14.6143 10.3035 14.5083L10.3046 14.5082C12.4928 14.1865 14.1802 12.4917 14.5078 10.3083L14.5082 10.3061C14.6143 9.58092 14.6666 8.80815 14.6666 7.99998C14.6666 7.19527 14.6203 6.42148 14.5137 5.69347C14.1921 3.50726 12.4966 1.81316 10.3098 1.49172ZM6.38756 8.95267C6.39301 9.15365 6.40004 9.35195 6.40866 9.54742L6.40968 9.57054C6.41945 9.78867 6.43118 10.0033 6.44489 10.2141C6.45959 10.4433 6.68743 10.5836 6.88293 10.4823C7.22837 10.3029 7.58116 10.1096 7.9413 9.90379C8.20002 9.7541 8.46167 9.6013 8.72479 9.43914C8.98791 9.27854 9.24516 9.11482 9.49505 8.95111C9.84196 8.72502 10.1771 8.49581 10.4961 8.26817C10.6769 8.14031 10.6769 7.85965 10.4961 7.7318C10.1771 7.50415 9.84343 7.2765 9.49505 7.04886C9.24516 6.88514 8.98791 6.72298 8.72479 6.56082C8.46167 6.39866 8.20002 6.2443 7.9413 6.09618C7.58263 5.89036 7.22837 5.69702 6.88293 5.5177C6.68743 5.41636 6.45959 5.55669 6.44489 5.78589C6.43118 5.99671 6.41945 6.21129 6.40968 6.42943L6.40866 6.45254C6.40004 6.64801 6.39301 6.84631 6.38756 7.0473C6.37874 7.3607 6.37433 7.67722 6.37433 7.99998C6.37433 8.32274 6.37874 8.64082 6.38756 8.95267Z" fill="#332C2A"/>
|
||||
</svg>
|
||||
</AtomsButtonVariant>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.3098 1.49172C8.86574 1.28049 7.15098 1.28055 5.70176 1.49167C3.50821 1.81315 1.81986 3.50786 1.49213 5.69168L1.4918 5.69391C1.38564 6.41904 1.33331 7.19181 1.33331 7.99998C1.33331 8.80815 1.3857 9.58136 1.49187 10.3065C1.81362 12.4934 3.50335 14.1805 5.69381 14.5079L5.69577 14.5082C6.42109 14.6143 7.19417 14.6666 7.99717 14.6666C8.80032 14.6666 9.57311 14.6143 10.3035 14.5083L10.3046 14.5082C12.4928 14.1865 14.1802 12.4917 14.5078 10.3083L14.5082 10.3061C14.6143 9.58092 14.6666 8.80815 14.6666 7.99998C14.6666 7.19527 14.6203 6.42148 14.5137 5.69347C14.1921 3.50726 12.4966 1.81316 10.3098 1.49172ZM6.38756 8.95267C6.39301 9.15365 6.40004 9.35195 6.40866 9.54742L6.40968 9.57054C6.41945 9.78867 6.43118 10.0033 6.44489 10.2141C6.45959 10.4433 6.68743 10.5836 6.88293 10.4823C7.22837 10.3029 7.58116 10.1096 7.9413 9.90379C8.20002 9.7541 8.46167 9.6013 8.72479 9.43914C8.98791 9.27854 9.24516 9.11482 9.49505 8.95111C9.84196 8.72502 10.1771 8.49581 10.4961 8.26817C10.6769 8.14031 10.6769 7.85965 10.4961 7.7318C10.1771 7.50415 9.84343 7.2765 9.49505 7.04886C9.24516 6.88514 8.98791 6.72298 8.72479 6.56082C8.46167 6.39866 8.20002 6.2443 7.9413 6.09618C7.58263 5.89036 7.22837 5.69702 6.88293 5.5177C6.68743 5.41636 6.45959 5.55669 6.44489 5.78589C6.43118 5.99671 6.41945 6.21129 6.40968 6.42943L6.40866 6.45254C6.40004 6.64801 6.39301 6.84631 6.38756 7.0473C6.37874 7.3607 6.37433 7.67722 6.37433 7.99998C6.37433 8.32274 6.37874 8.64082 6.38756 8.95267Z"
|
||||
fill="#332C2A"
|
||||
/>
|
||||
</svg>
|
||||
</AtomsButtonVariant>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 다운로드 카드 -->
|
||||
<div v-if="launchingStatus" class="inspection-card inspection-download-card">
|
||||
<div
|
||||
v-if="launchingStatus"
|
||||
class="inspection-card inspection-download-card"
|
||||
>
|
||||
<h3 class="card-title text-base md:text-lg">
|
||||
{{ tm('Inspection_Txt_Download') || '게임 다운로드' }}
|
||||
</h3>
|
||||
<div class="flex flex-row gap-3">
|
||||
<AtomsButtonLauncher
|
||||
v-for="(btn, index) in enabledMarkets" :key="index"
|
||||
:class="getButtonClass(btn.platform)"
|
||||
<AtomsButtonLauncher
|
||||
v-for="(btn, index) in enabledMarkets"
|
||||
:key="index"
|
||||
:class="getButtonClass(btn.platform)"
|
||||
class="h-[48px]"
|
||||
:platform="btn.platform as any"
|
||||
:url="btn.url"
|
||||
:type="btn.platform === 'pc' ? 'default' : 'single'"
|
||||
:platform="btn.platform as any"
|
||||
:url="btn.url"
|
||||
:type="btn.platform === 'pc' ? 'default' : 'single'"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ getButtonText(btn.platform) }}
|
||||
{{ getButtonText(btn.platform) }}
|
||||
</AtomsButtonLauncher>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,11 +122,10 @@
|
||||
</div>
|
||||
</clientOnly>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { globalDateFormat } from '@seed-next/date';
|
||||
import { globalDateFormat } from '@seed-next/date'
|
||||
import { useCheckGameStart } from '#layers/composables/useGameStart'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
@@ -157,7 +182,6 @@ const launchingStatus = computed(() => {
|
||||
return webInspectionData.value?.launching_status
|
||||
})
|
||||
|
||||
|
||||
// const market_json = {
|
||||
// pc: { url: 'https://apps.apple.com/app/id1234567890', use_yn: 0 },
|
||||
// app_store: { url: 'https://apps.apple.com/app/id1234567890', use_yn: 0 },
|
||||
@@ -166,21 +190,25 @@ const launchingStatus = computed(() => {
|
||||
|
||||
//gameData.value.market_json 값 중 use_yn === 1 인 항목만 배열로 변환
|
||||
const enabledMarkets = computed(() => {
|
||||
return Object.entries(gameData.value.market_json)
|
||||
// return Object.entries(market_json)
|
||||
.filter(([, info]: [string, any]) => info && info.use_yn === 1)
|
||||
.map(([platform, info]: [string, any]) => ({ platform, url: info.url as string }))
|
||||
return (
|
||||
Object.entries(gameData.value.market_json)
|
||||
// return Object.entries(market_json)
|
||||
.filter(([, info]: [string, any]) => info && info.use_yn === 1)
|
||||
.map(([platform, info]: [string, any]) => ({
|
||||
platform,
|
||||
url: info.url as string,
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
const logoImgUrl = computed(() => {
|
||||
const currentLocale = locale.value || 'ko'
|
||||
const currentLocale = locale.value || 'ko'
|
||||
const localeData = (webInspectionData.value as any)?.[currentLocale]
|
||||
return getImageHost(localeData.img_json.bi_large)
|
||||
})
|
||||
|
||||
const communityUrl = computed(() => {
|
||||
const currentLocale = locale.value || 'ko'
|
||||
const currentLocale = locale.value || 'ko'
|
||||
const localeData = (webInspectionData.value as any)?.[currentLocale]
|
||||
return localeData?.url_json?.community
|
||||
})
|
||||
@@ -192,7 +220,7 @@ const handleCommunityClick = () => {
|
||||
const getButtonClass = (platform: string) => {
|
||||
// pc가 있으면 pc만 flex-1, 나머지는 기본
|
||||
const hasPc = enabledMarkets.value.some(btn => btn.platform === 'pc')
|
||||
if(hasPc) {
|
||||
if (hasPc) {
|
||||
return platform === 'pc' ? `flex-1 btn-platform-pc` : ``
|
||||
}
|
||||
//pc가 없으면서 하나만 있으면 ''
|
||||
@@ -215,7 +243,7 @@ const getButtonText = (platform: string) => {
|
||||
if (platform === 'app_store') {
|
||||
return tm('platform_app_store')
|
||||
}
|
||||
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -228,15 +256,14 @@ const handleGameStart = () => {
|
||||
definePageMeta({
|
||||
middleware: ['inspection'],
|
||||
layout: 'only-stove',
|
||||
showLoading: false
|
||||
showLoading: false,
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.inspection-section {
|
||||
@apply flex flex-col items-center gap-10 px-10 py-[120px] pb-[200px] min-h-[calc(100vh-48px)];
|
||||
background-color: #F0F0F0;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.inspection-logo {
|
||||
@@ -265,7 +292,6 @@ definePageMeta({
|
||||
|
||||
.inspection-time-card {
|
||||
@apply flex flex-col items-center gap-4;
|
||||
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@@ -310,7 +336,6 @@ definePageMeta({
|
||||
@apply flex items-center justify-center gap-1 px-6 md:px-8 w-auto h-10 md:h-12 rounded-lg;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
}
|
||||
|
||||
.inspection-btn span {
|
||||
|
||||
@@ -16,4 +16,16 @@
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
.size-large {
|
||||
/* height: 64px */
|
||||
@apply px-10 h-16 text-lg rounded-lg
|
||||
before:rounded after:rounded;
|
||||
before:rounded-lg after:rounded-lg;
|
||||
}
|
||||
|
||||
.size-medium {
|
||||
/* height: 56px */
|
||||
@apply px-10 h-14 text-base rounded-lg
|
||||
before:rounded after:rounded;
|
||||
before:rounded-lg after:rounded-lg;
|
||||
}
|
||||
|
||||
.size-small {
|
||||
/* height: 48px */
|
||||
@apply px-10 h-12 text-sm rounded-lg
|
||||
before:rounded after:rounded;
|
||||
before:rounded-lg after:rounded-lg;
|
||||
}
|
||||
|
||||
.size-extra-small {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
@apply line-clamp-2 text-[16px] font-[500] leading-[24px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-1 md:text-[24px] md:leading-[34px];
|
||||
}
|
||||
.title-sm {
|
||||
@apply text-[15px] font-[500] leading-[24px] tracking-[-0.45px] md:text-[20px] md:leading-[30px] md:tracking-[-0.6px];
|
||||
@apply text-[15px] font-[500] leading-[24px] tracking-[-0.45px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:text-[20px] md:leading-[30px] md:tracking-[-0.6px];
|
||||
}
|
||||
.title-xs {
|
||||
@apply text-[14px] font-[500] leading-[20px] tracking-[-0.42px] md:text-[18px] md:leading-[26px] md:tracking-[-0.54px];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Button Size Classes */
|
||||
@layer components {
|
||||
.modal-wrap {
|
||||
@apply fixed inset-0 flex p-5 z-[800];
|
||||
@apply overflow-auto fixed inset-0 flex p-5 z-[800];
|
||||
}
|
||||
|
||||
.modal-wrap.dimmed {
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
@apply hidden md:block;
|
||||
}
|
||||
.splide-arrow {
|
||||
@apply absolute top-1/2 w-[48px] h-[48px] bg-cover bg-center bg-no-repeat -translate-y-1/2 cursor-pointer z-[5]
|
||||
@apply absolute top-1/2 w-[40px] h-[40px] bg-cover bg-center bg-no-repeat -translate-y-1/2 cursor-pointer z-[5]
|
||||
md:w-[40px] md:h-[40px]
|
||||
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:rounded-full after:bg-white after:transition-opacity after:duration-300 after:ease-in-out after:opacity-0
|
||||
hover:after:opacity-10;
|
||||
}
|
||||
|
||||
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 }
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
@@ -39,6 +39,7 @@ export const useResponsiveSrc = () => {
|
||||
path: PageDataResourceGroupResPath,
|
||||
options?: {
|
||||
resourcesType?: 'image' | 'video'
|
||||
imageType?: 'game' | 'common'
|
||||
}
|
||||
) => {
|
||||
const result = getDeviceSrc(path, options)
|
||||
|
||||
@@ -27,7 +27,6 @@ export const useCheckPCSpec = (tm: (key: string) => string) => {
|
||||
contentText: tm('Download_Alert_InstallGuide'),
|
||||
confirmButtonText: tm('Download_Text_Download'),
|
||||
modalName: 'modal-download',
|
||||
isShowDimmed: true,
|
||||
confirmButtonEvent: () => {
|
||||
location.href = String(setupUrl)
|
||||
},
|
||||
|
||||
@@ -4,14 +4,18 @@ import { csrGoStoveLogin } from '#layers/utils/stoveUtil'
|
||||
|
||||
export const useCheckGameStart = () => {
|
||||
const { tm } = useI18n()
|
||||
const modalStore = useModalStore()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const modalStore = useModalStore()
|
||||
const { handleTokenValidation } = useTokenValidation()
|
||||
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
const stoveCs = runtimeConfig.public.stoveCs
|
||||
const isProcessing = ref(false) // 연속 클릭 방지
|
||||
const isShowCheckLauncher = ref(false) // 런처 실행 로딩 표시
|
||||
const isShowDownloadLauncher = ref(false) // 런처 다운로드 표시
|
||||
const customerService = 'https://www.google.com' //[TODO] 고객센터 링크
|
||||
const customerServiceUrl = `${stoveCs}/${gameData.value?.game_id}`
|
||||
|
||||
// 에러 처리
|
||||
const errorHandler = (errorCode: number) => {
|
||||
@@ -48,9 +52,9 @@ export const useCheckGameStart = () => {
|
||||
// 일시적으로 오류가 발생했습니다. 잠시 후 다시 이용해 주세요. 동일한 현상이 계속 발생할 경우 고객센터로 문의해 주세요.
|
||||
modalStore.handleOpenConfirm({
|
||||
contentText: tm('Alert_Error'),
|
||||
confirmButtonText: tm('Text_StoveLogin'),
|
||||
confirmButtonText: tm('Text_Customer'),
|
||||
confirmButtonEvent: () => {
|
||||
window.open(customerService, '_blank')
|
||||
window.open(customerServiceUrl, '_blank')
|
||||
},
|
||||
})
|
||||
break
|
||||
@@ -87,7 +91,7 @@ export const useCheckGameStart = () => {
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
const accessTokenSub = useCookie('SUAT')
|
||||
const accessTokenSub = csrGetAccessToken()
|
||||
const stoveGameId = gameData.value?.game_id || ''
|
||||
const nationCookie = useCookie('NNTO').value
|
||||
const localeCookie = useCookie('LOCALE').value
|
||||
@@ -96,7 +100,7 @@ export const useCheckGameStart = () => {
|
||||
|
||||
// 토큰 유효성 체크
|
||||
const validateTokenResult = await handleTokenValidation(
|
||||
accessTokenSub.value || ''
|
||||
accessTokenSub || ''
|
||||
)
|
||||
|
||||
// 토큰 유효성 체크 실패 시
|
||||
|
||||
157
layers/composables/useGds.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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 COUNTRY_GROUPS = {
|
||||
KOREA: ['KR'] as const,
|
||||
NORTH_AMERICA: ['US', 'CA'] as const,
|
||||
TAIWAN_HONG_KONG_MACAU: ['TW', 'HK', 'MO'] as const,
|
||||
} as const
|
||||
const ERROR_CODE = {
|
||||
UNKNOWN: -99999,
|
||||
} as const
|
||||
|
||||
const useGds = () => {
|
||||
const countryCode = ref('')
|
||||
const isKorea = ref(false)
|
||||
const isTaiwanHongKongMacau = ref(false)
|
||||
const isNorthAmerica = ref(false)
|
||||
|
||||
/**
|
||||
* 국가별 플래그 업데이트
|
||||
*/
|
||||
const updateCountryFlags = (nation: string) => {
|
||||
if (!nation) {
|
||||
countryCode.value = ''
|
||||
isKorea.value = false
|
||||
isTaiwanHongKongMacau.value = false
|
||||
isNorthAmerica.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const upperNation = nation.toUpperCase()
|
||||
countryCode.value = upperNation
|
||||
isKorea.value = COUNTRY_GROUPS.KOREA.includes(upperNation as any)
|
||||
isTaiwanHongKongMacau.value =
|
||||
COUNTRY_GROUPS.TAIWAN_HONG_KONG_MACAU.includes(upperNation as any)
|
||||
isNorthAmerica.value = COUNTRY_GROUPS.NORTH_AMERICA.includes(
|
||||
upperNation as any
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 응답 생성
|
||||
*/
|
||||
const createErrorResponse = (message: string): ResGetGdsClientPolicyTotal => {
|
||||
return {
|
||||
code: ERROR_CODE.UNKNOWN,
|
||||
message,
|
||||
res_code: ERROR_CODE.UNKNOWN,
|
||||
res_data: undefined as any,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 응답 생성 (개발/QA 환경)
|
||||
*/
|
||||
const createMockResponse = (
|
||||
qc: string,
|
||||
clientLang: string
|
||||
): ResGetGdsClientPolicyTotal => {
|
||||
return {
|
||||
code: 0,
|
||||
message: '',
|
||||
res_code: 0,
|
||||
res_data: {
|
||||
is_default: true,
|
||||
nation: qc.toUpperCase(),
|
||||
regulation: '',
|
||||
timezone: '',
|
||||
utc_offset: 0,
|
||||
lang: clientLang || DEFAULT_LOCALE_CODE,
|
||||
coverages: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사전 등록 - 특정 국가 여부 조회 (IP 기반)
|
||||
*/
|
||||
const checkCountryByIp = async (
|
||||
req: ReqGetGdsClientPolicyTotal
|
||||
): Promise<ResGetGdsClientPolicyTotal> => {
|
||||
try {
|
||||
// runType 우선순위: req.runType > runtimeConfig.runType
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const runType = req.runType || runtimeConfig.public.runType
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
|
||||
// Mock 모드 (개발/QA 환경)
|
||||
const isMockMode = runType !== 'live' && req.qc && req.qc !== ''
|
||||
|
||||
if (isMockMode) {
|
||||
const mockQc = req.qc!.toUpperCase()
|
||||
updateCountryFlags(mockQc)
|
||||
return createMockResponse(mockQc, req.client_lang || '')
|
||||
}
|
||||
|
||||
// 실제 API 호출
|
||||
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,
|
||||
}
|
||||
|
||||
const res = (await commonFetch('GET', url, {
|
||||
query,
|
||||
})) as ResGetGdsClientPolicyTotal
|
||||
|
||||
// 성공 응답 처리
|
||||
if (res.res_code === 0 && res.res_data?.nation) {
|
||||
updateCountryFlags(res.res_data.nation)
|
||||
return res
|
||||
}
|
||||
|
||||
// 실패 응답 처리
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[useGds].checkCountryByIp: Invalid response', res)
|
||||
updateCountryFlags('')
|
||||
return res
|
||||
} catch (error) {
|
||||
// 에러 로깅
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[useGds].checkCountryByIp: Exception', error)
|
||||
|
||||
// 상태 초기화
|
||||
updateCountryFlags('')
|
||||
|
||||
// 에러 응답 반환
|
||||
return createErrorResponse(
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Reactive state
|
||||
isKorea: readonly(isKorea),
|
||||
isTaiwanHongKongMacau: readonly(isTaiwanHongKongMacau),
|
||||
isNorthAmerica: readonly(isNorthAmerica),
|
||||
countryCode: readonly(countryCode),
|
||||
|
||||
// Methods
|
||||
checkCountryByIp,
|
||||
}
|
||||
}
|
||||
|
||||
export { useGds }
|
||||
@@ -13,9 +13,8 @@ export const useGetGameDataExternal = () => {
|
||||
|
||||
const getGameDataExternal = async (req: GameDataRequest) => {
|
||||
console.log('🚀 ~ getGameDataExternal ~ req:', req)
|
||||
// const config = useRuntimeConfig()
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/game?game_domain=${req.gameDomain}&lang_code=${req.langCode}`
|
||||
|
||||
try {
|
||||
@@ -23,7 +22,7 @@ export const useGetGameDataExternal = () => {
|
||||
console.log('🚀 ~ getGameDataExternal ~ response:', response)
|
||||
|
||||
// FIXME: 테스트용 데이터 ---------------------------------------------------
|
||||
/* if (['local', 'local-gate8', 'dev'].includes(`${config.public.runType}`)) {
|
||||
/* if (['local', 'local-gate8', 'dev'].includes(`${runtimeConfig.public.runType}`)) {
|
||||
response.value = {
|
||||
inspection_status: 1,
|
||||
inspection: {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ReqGameMaintenance, ResGameMaintenance } from '#layers/types/GameMaintenanceType'
|
||||
import type {
|
||||
ReqGameMaintenance,
|
||||
ResGameMaintenance,
|
||||
} from '#layers/types/GameMaintenanceType'
|
||||
|
||||
/**
|
||||
* 게임 점검
|
||||
@@ -7,7 +10,7 @@ const useGetGameMaintenance = () => {
|
||||
const inspectionStore = useInspectionStore()
|
||||
const logPrefix = {
|
||||
exception: '[Exception] /composables/useGetGameMaintenance',
|
||||
failure: '[Failure] /composables/useGetGameMaintenance'
|
||||
failure: '[Failure] /composables/useGetGameMaintenance',
|
||||
}
|
||||
const isGameMaintenance = ref(false) // 게임 서버 점검 여부
|
||||
|
||||
@@ -20,7 +23,11 @@ const useGetGameMaintenance = () => {
|
||||
const setGameMaintenanceFalse = () => {
|
||||
setIsGameMaintenance(false)
|
||||
inspectionStore.setGameMaintenanceStatus(false)
|
||||
inspectionStore.setGameMaintenanceData({ ts_start_date: 0, ts_end_date: 0, detail_link: '' })
|
||||
inspectionStore.setGameMaintenanceData({
|
||||
ts_start_date: 0,
|
||||
ts_end_date: 0,
|
||||
detail_link: '',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,8 +52,8 @@ const useGetGameMaintenance = () => {
|
||||
|
||||
if (res != null && res.code === 0) {
|
||||
// FIXME: 테스트용 데이터 ---------------------------------------------------
|
||||
/* const config = useRuntimeConfig()
|
||||
if (['local', 'local-gate8', 'dev'].includes(`${config.public.runType}`)) {
|
||||
/* const runtimeConfig = useRuntimeConfig()
|
||||
if (['local', 'local-gate8', 'dev'].includes(`${runtimeConfig.public.runType}`)) {
|
||||
res.value = {
|
||||
total_count: 1,
|
||||
list: [
|
||||
@@ -65,12 +72,16 @@ const useGetGameMaintenance = () => {
|
||||
}
|
||||
} */
|
||||
// ------------------------------------------------------------------------
|
||||
if (Number(res.value?.total_count) > 0 && res.value?.list != null && res.value?.list.length > 0) {
|
||||
if (
|
||||
Number(res.value?.total_count) > 0 &&
|
||||
res.value?.list != null &&
|
||||
res.value?.list.length > 0
|
||||
) {
|
||||
setIsGameMaintenance(true) // 서버 1개 이상 점검일 경우 점검 중으로 간주
|
||||
inspectionStore.setGameMaintenanceData({
|
||||
ts_start_date: res.value?.list[0].start_at || 0,
|
||||
ts_end_date: res.value?.list[0].end_at || 0,
|
||||
detail_link: res.value?.list[0].languages[0].link || ''
|
||||
detail_link: res.value?.list[0].languages[0].link || '',
|
||||
})
|
||||
inspectionStore.setGameMaintenanceStatus(true)
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { WebInspectionData, ReqGetInspectionData, ResGetInspectionData } from '#layers/types/InspectionType'
|
||||
import type {
|
||||
WebInspectionData,
|
||||
ReqGetInspectionData,
|
||||
ResGetInspectionData,
|
||||
} from '#layers/types/InspectionType'
|
||||
|
||||
/**
|
||||
* 웹 점검
|
||||
@@ -7,7 +11,7 @@ export const useGetInspectionDataExternal = () => {
|
||||
const inspectionStore = useInspectionStore()
|
||||
const logPrefix = {
|
||||
exception: '[Exception] /composables/useGetInspectionDataExternal',
|
||||
failure: '[Failure] /composables/useGetInspectionDataExternal'
|
||||
failure: '[Failure] /composables/useGetInspectionDataExternal',
|
||||
}
|
||||
const webInspectionData = ref<WebInspectionData | null>(null)
|
||||
const isWebInspection = ref(false) // 웹 점검 여부
|
||||
@@ -24,15 +28,18 @@ export const useGetInspectionDataExternal = () => {
|
||||
* @description https://wiki.smilegate.net/pages/viewpage.action?pageId=563198067
|
||||
*/
|
||||
const getInspectionDataExternal = async (req: ReqGetInspectionData) => {
|
||||
// const config = useRuntimeConfig()
|
||||
// const runtimeConfig = useRuntimeConfig()
|
||||
const apiUrl = `${req.baseApiUrl}/pub-comm/v3.0/inspection/${req.gameId}`
|
||||
|
||||
try {
|
||||
const response = (await commonFetch('GET', apiUrl)) as ResGetInspectionData
|
||||
console.log("🚀 ~ getInspectionDataExternal ~ response:", response)
|
||||
const response = (await commonFetch(
|
||||
'GET',
|
||||
apiUrl
|
||||
)) as ResGetInspectionData
|
||||
console.log('🚀 ~ getInspectionDataExternal ~ response:', response)
|
||||
|
||||
// FIXME: 테스트용 데이터 ---------------------------------------------------
|
||||
/* if (['local', 'local-gate8', 'dev'].includes(`${config.public.runType}`)) {
|
||||
/* if (['local', 'local-gate8', 'dev'].includes(`${runtimeConfig.public.runType}`)) {
|
||||
response.value = {
|
||||
inspection_status: 1,
|
||||
inspection: {
|
||||
|
||||
262
layers/composables/usePreregist.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
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 PREREGIST_ERROR_CODE = {
|
||||
SUCCESS: 0,
|
||||
NO_DATA: -1, // 조회된 데이터가 없습니다 (최초)
|
||||
NOT_PERIOD: -90002, // 사전 등록 기간이 아닙니다
|
||||
REQUIRED_TERMS: -90000, // 필수 약관을 모두 선택해 주세요
|
||||
// BIRTH_DATE_REQUIRED: -90018, // 생년 월일을 입력해 주세요
|
||||
AGE_RESTRICTION: -90022, // 사전 등록 가능한 연령이 아닙니다
|
||||
ALREADY_REGISTERED: -90023, // 이미 사전 등록이 완료된 계정입니다
|
||||
LOGIN_REQUIRED: -90028, // 로그인 후 이용하실 수 있습니다
|
||||
// MAINTENANCE: -90003, // 점검 진행 중
|
||||
UNKNOWN: -99999, // 알 수 없는 오류
|
||||
} as const
|
||||
|
||||
const usePreregist = () => {
|
||||
const preregistDate = ref(Date.now())
|
||||
|
||||
// GDS composable
|
||||
const {
|
||||
isKorea,
|
||||
isTaiwanHongKongMacau,
|
||||
isNorthAmerica,
|
||||
countryCode,
|
||||
checkCountryByIp,
|
||||
} = useGds()
|
||||
|
||||
// 국가 번호 조회
|
||||
const countryDialingCode = computed(() => {
|
||||
const code = countryCode.value?.toUpperCase()
|
||||
return code ? countryDialingCodes[code] : undefined
|
||||
})
|
||||
|
||||
/**
|
||||
* 사전 등록일 세팅 (숫자 검증)
|
||||
*/
|
||||
const setPreregistDate = (dateValue: number | string | undefined) => {
|
||||
if (dateValue && isNumeric(String(dateValue))) {
|
||||
preregistDate.value = Number(dateValue)
|
||||
} else {
|
||||
preregistDate.value = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 응답 생성
|
||||
*/
|
||||
const createErrorResponse = <T extends { code: number; message: string }>(
|
||||
code: number,
|
||||
message: string = ''
|
||||
): T => {
|
||||
return { code, message } as T
|
||||
}
|
||||
|
||||
/**
|
||||
* 401 에러를 LOGIN_REQUIRED로 정규화
|
||||
*/
|
||||
const normalizeAuthError = (code: number): number => {
|
||||
return String(code).startsWith('401')
|
||||
? PREREGIST_ERROR_CODE.LOGIN_REQUIRED
|
||||
: code
|
||||
}
|
||||
|
||||
/**
|
||||
* 사전 등록 - 조회 (등록 여부)
|
||||
*/
|
||||
const getPreregist = async (
|
||||
req: ReqPreorderSelectEvent
|
||||
): Promise<ResPreorderSelectEvent> => {
|
||||
try {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
const stoveApiBaseUrl = runtimeConfig.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,
|
||||
}
|
||||
|
||||
const res = (await commonFetch('POST', url, {
|
||||
headers,
|
||||
body,
|
||||
})) as ResPreorderSelectEvent
|
||||
|
||||
// 응답 검증
|
||||
if (!res) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[usePreregist].getPreregist: Empty response')
|
||||
return createErrorResponse(PREREGIST_ERROR_CODE.UNKNOWN)
|
||||
}
|
||||
|
||||
// 정규화된 에러 코드
|
||||
const normalizedCode = normalizeAuthError(res.code)
|
||||
|
||||
// 성공 케이스
|
||||
if (normalizedCode === PREREGIST_ERROR_CODE.SUCCESS) {
|
||||
setPreregistDate(res.value?.terms_time_long ?? Date.now())
|
||||
return res
|
||||
}
|
||||
|
||||
// 예상된 에러 케이스 (NO_DATA, NOT_PERIOD, LOGIN_REQUIRED)
|
||||
const expectedErrors: number[] = [
|
||||
PREREGIST_ERROR_CODE.NO_DATA,
|
||||
PREREGIST_ERROR_CODE.NOT_PERIOD,
|
||||
PREREGIST_ERROR_CODE.LOGIN_REQUIRED,
|
||||
]
|
||||
|
||||
if (expectedErrors.includes(normalizedCode)) {
|
||||
return createErrorResponse(normalizedCode, res.message)
|
||||
}
|
||||
|
||||
// 예상치 못한 에러
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[usePreregist].getPreregist: Unexpected error', res)
|
||||
}
|
||||
return createErrorResponse(PREREGIST_ERROR_CODE.UNKNOWN, res.message)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[usePreregist].getPreregist: Exception', error)
|
||||
return createErrorResponse(
|
||||
PREREGIST_ERROR_CODE.UNKNOWN,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사전 등록 - 저장
|
||||
*/
|
||||
const setPreregist = async (
|
||||
req: ReqPreorderReserveDataUpdate
|
||||
): Promise<ResPreorderReserveDataUpdate> => {
|
||||
try {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
|
||||
if (!stoveApiBaseUrl) {
|
||||
throw new Error('stoveApiUrl is not configured')
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
device_type: req.device_type,
|
||||
country_code: req.country_code,
|
||||
country_dialing_code: req.country_dialing_code,
|
||||
birth_date: req.birth_date,
|
||||
}
|
||||
|
||||
const res = (await commonFetch('POST', url, {
|
||||
headers,
|
||||
body,
|
||||
})) as ResPreorderReserveDataUpdate
|
||||
|
||||
// 응답 검증
|
||||
if (!res) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[usePreregist].setPreregist: Empty response')
|
||||
return createErrorResponse(PREREGIST_ERROR_CODE.UNKNOWN)
|
||||
}
|
||||
|
||||
// 정규화된 에러 코드
|
||||
const normalizedCode = normalizeAuthError(res.code)
|
||||
|
||||
// 성공 케이스
|
||||
if (normalizedCode === PREREGIST_ERROR_CODE.SUCCESS) {
|
||||
setPreregistDate(res.message ? Number(res.message) : Date.now())
|
||||
return res
|
||||
}
|
||||
|
||||
// 이미 등록된 경우 (날짜 업데이트)
|
||||
if (normalizedCode === PREREGIST_ERROR_CODE.ALREADY_REGISTERED) {
|
||||
setPreregistDate(res.message ? Number(res.message) : Date.now())
|
||||
return createErrorResponse(normalizedCode, res.message)
|
||||
}
|
||||
|
||||
// 예상된 에러 케이스
|
||||
const expectedErrors: number[] = [
|
||||
PREREGIST_ERROR_CODE.LOGIN_REQUIRED,
|
||||
PREREGIST_ERROR_CODE.NOT_PERIOD,
|
||||
// PREREGIST_ERROR_CODE.BIRTH_DATE_REQUIRED,
|
||||
PREREGIST_ERROR_CODE.AGE_RESTRICTION,
|
||||
PREREGIST_ERROR_CODE.REQUIRED_TERMS,
|
||||
]
|
||||
|
||||
if (expectedErrors.includes(normalizedCode)) {
|
||||
return createErrorResponse(normalizedCode, res.message)
|
||||
}
|
||||
|
||||
// 예상치 못한 에러
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[usePreregist].setPreregist: Unexpected error', res)
|
||||
}
|
||||
return createErrorResponse(PREREGIST_ERROR_CODE.UNKNOWN, res.message)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[usePreregist].setPreregist: Exception', error)
|
||||
return createErrorResponse(
|
||||
PREREGIST_ERROR_CODE.UNKNOWN,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// GDS state & methods
|
||||
isKorea,
|
||||
isTaiwanHongKongMacau,
|
||||
isNorthAmerica,
|
||||
countryCode,
|
||||
checkCountryByIp,
|
||||
|
||||
// Preregist state & computed
|
||||
countryDialingCode,
|
||||
preregistDate: readonly(preregistDate),
|
||||
|
||||
// Preregist methods
|
||||
getPreregist,
|
||||
setPreregist,
|
||||
}
|
||||
}
|
||||
|
||||
export { usePreregist, PREREGIST_ERROR_CODE }
|
||||
@@ -19,8 +19,8 @@ export const useResourcesData = () => {
|
||||
): Promise<OperateComponents | null> => {
|
||||
const { pageSeq, pageVer, pageVerTmplSeq, langCode, q, qc } = params
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/operateResources`
|
||||
|
||||
const queryParams: Record<string, string | number> = {
|
||||
@@ -68,8 +68,8 @@ export const useResourcesData = () => {
|
||||
size,
|
||||
} = params
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const apiUrl = `${stoveApiBaseUrl}/cwms/v3.0/article_group/${articleGroupCode}/${articleGroupSeq}/article/list`
|
||||
|
||||
const queryParams: Record<string, string | number | boolean> = {
|
||||
|
||||
@@ -12,6 +12,7 @@ import GrContents01 from '#layers/templates/GrContents01/index.vue'
|
||||
import FxVideo01 from '#layers/templates/FxVideo01/index.vue'
|
||||
import FxDownload01 from '#layers/templates/FxDownload01/index.vue'
|
||||
import FxSecure01 from '#layers/templates/FxSecure01/index.vue'
|
||||
import FxPreregist01 from '#layers/templates/FxPreregist01/index.vue'
|
||||
|
||||
const templateRegistry = {
|
||||
GR_VISUAL_01: { component: GrVisual01 },
|
||||
@@ -28,6 +29,7 @@ const templateRegistry = {
|
||||
FX_VIDEO_01: { component: FxVideo01 },
|
||||
FX_DOWNLOAD_01: { component: FxDownload01 },
|
||||
FX_SECURE_01: { component: FxSecure01 },
|
||||
FX_PREREGIST_01: { component: FxPreregist01 },
|
||||
} as const
|
||||
|
||||
type TemplateKey = keyof typeof templateRegistry
|
||||
|
||||
@@ -14,21 +14,23 @@ interface TokenValidationResponse {
|
||||
}
|
||||
|
||||
export const useTokenValidation = () => {
|
||||
const config = useRuntimeConfig()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const modalStore = useModalStore()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { tm } = useI18n()
|
||||
|
||||
const apiBaseUrl = config.public.stoveApiUrl
|
||||
const customerServiceUrl = 'https://www.google.com' // TODO: 고객센터 링크로 변경
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
const apiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const stoveCs = runtimeConfig.public.stoveCs
|
||||
const customerServiceUrl = `${stoveCs}/${gameData.value?.game_id}`
|
||||
const isTokenValid = ref(false)
|
||||
|
||||
// 로그인 모달 표시
|
||||
const showLoginModal = (alertKey: string) => {
|
||||
modalStore.handleOpenConfirm({
|
||||
contentText: tm(alertKey),
|
||||
isShowDimmed: true,
|
||||
confirmButtonText: tm('Text_StoveLogin'),
|
||||
modalName: 'modal-login',
|
||||
confirmButtonEvent: () => {
|
||||
csrGoStoveLogin()
|
||||
},
|
||||
@@ -39,7 +41,7 @@ export const useTokenValidation = () => {
|
||||
const showErrorModal = () => {
|
||||
modalStore.handleOpenConfirm({
|
||||
contentText: tm('Alert_Error'),
|
||||
confirmButtonText: tm('Text_StoveLogin'),
|
||||
confirmButtonText: tm('Text_Customer'),
|
||||
confirmButtonEvent: () => {
|
||||
window.open(customerServiceUrl, '_blank')
|
||||
},
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
console.log('🚀 ~ promotion')
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="promotion-wrap">
|
||||
<slot />
|
||||
</div>
|
||||
<LayoutsHeader />
|
||||
<slot />
|
||||
<LayoutsFooter />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.promo-wrap {
|
||||
background-color: tan;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,14 +14,14 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
const currentLangCode = match ? match[1] : null
|
||||
|
||||
//현재 url에서 게임 도메인만 추출
|
||||
const currentDomain = window.location.hostname;
|
||||
const config = useRuntimeConfig()
|
||||
const currentDomain = window.location.hostname
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const req: GameDataRequest = {
|
||||
gameDomain: `${currentDomain}`,
|
||||
langCode: `${currentLangCode}`,
|
||||
game_alias: '',
|
||||
lang_code: `${currentLangCode}`,
|
||||
baseApiUrl: `${config.public.stoveApiUrl}`,
|
||||
baseApiUrl: `${runtimeConfig.public.stoveApiUrl}`,
|
||||
gameId: '',
|
||||
}
|
||||
const { getGameDataExternal } = useGetGameDataExternal()
|
||||
@@ -30,7 +30,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
const gameDataStore = useGameDataStore()
|
||||
const gameData = gameDataStore.gameData as GameDataValue
|
||||
const langCodes = gameData?.lang_codes
|
||||
|
||||
|
||||
// 허용된 언어 코드 목록
|
||||
const allowedLangCodes = langCodes || []
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
export default defineNuxtRouteMiddleware(async to => {
|
||||
try {
|
||||
if (import.meta.client) {
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
console.log("🚀 ~ 00000 gameData:", gameData.value)
|
||||
console.log('🚀 ~ 00000 gameData:', gameData.value)
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
// const baseDomain = `${config.public.baseDomain}`
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
// const baseDomain = `${runtimeConfig.public.baseDomain}`
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const stoveGameId = gameData.value.game_id
|
||||
// const stoveMaintenanceApiUrl = `${config.public.stoveMaintenanceApiUrl}`
|
||||
// const stoveMaintenanceApiUrl = `${runtimeConfig.public.stoveMaintenanceApiUrl}`
|
||||
|
||||
// const localeCookie = useCookie('LOCALE', {
|
||||
// domain: baseDomain
|
||||
@@ -19,9 +19,12 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
// localeCookie.value = finalLocale.toUpperCase()
|
||||
|
||||
// 웹 점검 -----
|
||||
const { isWebInspection, getInspectionDataExternal } = useGetInspectionDataExternal()
|
||||
await getInspectionDataExternal({ baseApiUrl: stoveApiBaseUrl, gameId: stoveGameId })
|
||||
|
||||
const { isWebInspection, getInspectionDataExternal } =
|
||||
useGetInspectionDataExternal()
|
||||
await getInspectionDataExternal({
|
||||
baseApiUrl: stoveApiBaseUrl,
|
||||
gameId: stoveGameId,
|
||||
})
|
||||
|
||||
// 게임 점검 -----
|
||||
// const { checkGameMaintenance } = useGetGameMaintenance()
|
||||
@@ -32,10 +35,17 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
// lang: `${finalLocale}`.toLowerCase()
|
||||
// })
|
||||
|
||||
if (isWebInspection.value && !to.path.includes('inspection') && !to.path.includes('api')) {
|
||||
if (
|
||||
isWebInspection.value &&
|
||||
!to.path.includes('inspection') &&
|
||||
!to.path.includes('api')
|
||||
) {
|
||||
// 점검 중인 경우
|
||||
return navigateTo(`/${finalLocale}/inspection`, { external: true })
|
||||
} else if (!isWebInspection.value && to.path?.indexOf('inspection') !== -1) {
|
||||
} else if (
|
||||
!isWebInspection.value &&
|
||||
to.path?.indexOf('inspection') !== -1
|
||||
) {
|
||||
// 점검이 종료된 후 점검 페이지 접근시 메인으로 리다이렉트
|
||||
return navigateTo(`/${finalLocale}`, { external: true })
|
||||
}
|
||||
|
||||
@@ -3,15 +3,14 @@ import { usePageDataStore } from '#layers/stores/usePageDataStore'
|
||||
import { useGetGameDomain } from '#layers/composables/useGetGameDomain'
|
||||
import { usePathResolver } from '#layers/composables/usePathResolver'
|
||||
import type { PageDataResponse } from '#layers/types/api/pageData'
|
||||
import type {
|
||||
GameDataValue,
|
||||
} from '#layers/types/api/gameData'
|
||||
import type { GameDataValue } from '#layers/types/api/gameData'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
// [TODO] 하이드레이션 에러 처리
|
||||
if (!import.meta.client) return
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
|
||||
|
||||
const store = usePageDataStore()
|
||||
@@ -21,7 +20,12 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
const gameDataStore = useGameDataStore()
|
||||
const gameData = gameDataStore.gameData as GameDataValue
|
||||
|
||||
const langCode = ssrGetFinalLocale(to.path, headers, gameData?.lang_codes, gameData?.default_lang_code)
|
||||
const langCode = ssrGetFinalLocale(
|
||||
to.path,
|
||||
headers,
|
||||
gameData?.lang_codes,
|
||||
gameData?.default_lang_code
|
||||
)
|
||||
|
||||
try {
|
||||
// 서버 사이드에서는 스킵
|
||||
@@ -51,8 +55,8 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
query: queryParams,
|
||||
loading: true,
|
||||
})) as PageDataResponse | null
|
||||
|
||||
console.log('🚀 ~ response?.code:', response?.code)
|
||||
|
||||
// if(response?.code === 91003) {
|
||||
// throw createError({
|
||||
// statusCode: 404,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* i18n 다국어 로더 플러그인
|
||||
* S3에서 공통 다국어 파일을 로드하여 i18n 메시지에 주입합니다.
|
||||
*/
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
export default defineNuxtPlugin(async nuxtApp => {
|
||||
const $i18n = nuxtApp.$i18n as any
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
@@ -31,7 +31,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
'zh-cn': 'zh-CN',
|
||||
}
|
||||
|
||||
langCodes.forEach((langCode) => {
|
||||
langCodes.forEach(langCode => {
|
||||
// 로케일 코드 변환 (필요한 경우)
|
||||
const normalizedLangCode = localeMap[langCode] || langCode
|
||||
|
||||
@@ -47,6 +47,9 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Exception] i18n-loader: Failed to load translations:', error)
|
||||
console.error(
|
||||
'[Exception] i18n-loader: Failed to load translations:',
|
||||
error
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import {
|
||||
getHeader,
|
||||
getRequestHost,
|
||||
defineEventHandler,
|
||||
} from 'h3'
|
||||
import { getHeader, getRequestHost, defineEventHandler } from 'h3'
|
||||
import { ssrGetFinalLocale } from '../../utils/localeUtil'
|
||||
import type { GameDataResponse } from '../../types/api/gameData'
|
||||
import type { ResGetInspectionData } from '../../types/InspectionType'
|
||||
@@ -17,7 +13,9 @@ import { isStaticFile } from '#layers/utils/commonUtil'
|
||||
* @param customMaxAge - 커스텀 max-age 값 (초 단위)
|
||||
*/
|
||||
function setCacheHeaders(
|
||||
event: { node: { res: { setHeader: (name: string, value: string) => void } } },
|
||||
event: {
|
||||
node: { res: { setHeader: (name: string, value: string) => void } }
|
||||
},
|
||||
cacheMode: 'no-cache' | 'short' | 'medium' | 'default',
|
||||
customMaxAge?: number
|
||||
): void {
|
||||
@@ -59,7 +57,7 @@ function setCacheHeaders(
|
||||
|
||||
const cache = new LRUCache({
|
||||
max: 100, // 캐시에 저장할 최대 항목 수
|
||||
ttl: 1000 * 30 // 30초 동안 캐시 유지
|
||||
ttl: 1000 * 30, // 30초 동안 캐시 유지
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -74,7 +72,7 @@ function setFinalLocaleCookie(event: any, finalLocale: string, baseDomain: strin
|
||||
setCookie(event, 'LOCALE', finalLocale.toUpperCase(), {
|
||||
domain: baseDomain,
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 365 // 1년 (초 단위)
|
||||
maxAge: 60 * 60 * 24 * 365, // 1년 (초 단위)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -142,26 +140,26 @@ export default defineEventHandler(async event => {
|
||||
// const url = getRequestURL(event)
|
||||
|
||||
// if (['local', 'local-gate8', 'dev'].includes(runType)) {
|
||||
// Sandbox 이상 환경에서만 동작 및 확인 가능 (local, dev는 통과 처리)
|
||||
// try {
|
||||
// 언어 코드 추출
|
||||
// const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
|
||||
// console.log("🚀 ~ finalLocale:", finalLocale)
|
||||
// setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
// Sandbox 이상 환경에서만 동작 및 확인 가능 (local, dev는 통과 처리)
|
||||
// try {
|
||||
// 언어 코드 추출
|
||||
// const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
|
||||
// console.log("🚀 ~ finalLocale:", finalLocale)
|
||||
// setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// [Locale Middleware]
|
||||
// -------------------------------------------------------------------------------
|
||||
// fnLocaleMiddleware(event, finalLocale)
|
||||
// } catch (e) {
|
||||
// console.error('[Exception] /server/middleware/middleware-02-global: ', e)
|
||||
// }
|
||||
// -------------------------------------------------------------------------------
|
||||
// [Locale Middleware]
|
||||
// -------------------------------------------------------------------------------
|
||||
// fnLocaleMiddleware(event, finalLocale)
|
||||
// } catch (e) {
|
||||
// console.error('[Exception] /server/middleware/middleware-02-global: ', e)
|
||||
// }
|
||||
// }
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const iBaseApiUrl = `${config.public.stoveApiUrlServer}`
|
||||
const baseDomain = `${config.public.baseDomain}`
|
||||
const stoveApiUrlBaseServer = config.public.stoveApiUrlServer
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const iBaseApiUrl = `${runtimeConfig.public.stoveApiUrlServer}`
|
||||
const baseDomain = `${runtimeConfig.public.baseDomain}`
|
||||
const stoveApiUrlBaseServer = runtimeConfig.public.stoveApiUrlServer
|
||||
const apiUrl = `${stoveApiUrlBaseServer}/pub-comm/v1.0/template/game`
|
||||
|
||||
let initGameData: GameDataResponse | null = null
|
||||
@@ -180,7 +178,6 @@ export default defineEventHandler(async event => {
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const queryParams: Record<string, string> = {
|
||||
game_domain: cleanHost || '',
|
||||
lang_code: '',
|
||||
@@ -193,13 +190,13 @@ export default defineEventHandler(async event => {
|
||||
|
||||
initLangCodes = initResponse?.value?.lang_codes || null
|
||||
initDefaultLocale = initResponse?.value?.default_lang_code || null
|
||||
console.log("🚀 ~ 000111 initLangCodes:", initLangCodes)
|
||||
console.log('🚀 ~ 000111 initLangCodes:', initLangCodes)
|
||||
} catch (error) {
|
||||
console.error('init gameData load error:', error)
|
||||
}
|
||||
|
||||
const fullPath = event.path
|
||||
|
||||
|
||||
// 1-1. 정적 파일 패스
|
||||
if (isStaticFile(event.path)) {
|
||||
return
|
||||
@@ -208,7 +205,12 @@ export default defineEventHandler(async event => {
|
||||
// 1-2. /inspection 패스
|
||||
if (fullPath.includes('/inspection')) {
|
||||
// 리턴 되기 전 언어 쿠키 세팅
|
||||
finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers, initLangCodes, initDefaultLocale)
|
||||
finalLocale = ssrGetFinalLocale(
|
||||
event?.node.req.url,
|
||||
event.node.req.headers,
|
||||
initLangCodes,
|
||||
initDefaultLocale
|
||||
)
|
||||
setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
return
|
||||
}
|
||||
@@ -246,18 +248,16 @@ export default defineEventHandler(async event => {
|
||||
})) as GameDataResponse | null
|
||||
|
||||
// 언어패스 쿠키 굽기 - 장기방안에서는 굽지않음
|
||||
if(initLangCodes?.includes(finalLocale)) {
|
||||
if (initLangCodes?.includes(finalLocale)) {
|
||||
setFinalLocaleCookie(event, finalLocale, baseDomain)
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (response?.code === 0 && 'value' in response) {
|
||||
event.context.gameData = response.value
|
||||
event.context.googleAnalyticsId = response.value?.ga_code
|
||||
|
||||
// console.log('🚀 ~ gameData:', response.value)
|
||||
|
||||
|
||||
// 점검 데이터 조회
|
||||
let inspectionData
|
||||
if (cache.has(cacheKey)) {
|
||||
@@ -267,39 +267,50 @@ export default defineEventHandler(async event => {
|
||||
if (response?.value?.game_id) {
|
||||
const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response?.value?.game_id}`
|
||||
// 직접 $fetch 사용 (composable 사용하지 않음)
|
||||
const inspectionResponse = await $fetch<ResGetInspectionData>(inspectionApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
const inspectionResponse = await $fetch<ResGetInspectionData>(
|
||||
inspectionApiUrl,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
inspectionData = inspectionResponse?.value?.inspection
|
||||
cache.set(cacheKey, inspectionData) // 캐시에 저장
|
||||
// console.log("🚀 ~ inspectionData:", inspectionData)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 현재 시간과 점검 기간 비교
|
||||
const currentTime = Date.now()
|
||||
const tsStartDate = inspectionData?.ts_start_date || 0
|
||||
const tsEndDate = inspectionData?.ts_end_date || 0
|
||||
const timeUntilInspectionSeconds = Math.floor(
|
||||
(tsStartDate - currentTime) / 1000
|
||||
)
|
||||
|
||||
// 4. 현재 시간과 점검 기간 비교
|
||||
const currentTime = Date.now()
|
||||
const tsStartDate = inspectionData?.ts_start_date || 0
|
||||
const tsEndDate = inspectionData?.ts_end_date || 0
|
||||
const timeUntilInspectionSeconds = Math.floor((tsStartDate - currentTime) / 1000)
|
||||
// 5. 점검 상태별 캐시 설정
|
||||
if (
|
||||
inspectionData?.inspection_status === 1 &&
|
||||
currentTime >= tsStartDate &&
|
||||
currentTime <= tsEndDate
|
||||
) {
|
||||
/**
|
||||
* 점검 중인 경우
|
||||
* - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ
|
||||
* - 점검 URL 경로가 아닐 경우 no-cache 설정
|
||||
* - 화이트 리스트 체크
|
||||
*/
|
||||
// 현재 경로가 점검 페이지가 아닐 경우 리다이렉트
|
||||
const inspectionPath = `/${finalLocale}/inspection`
|
||||
if (fullPath !== inspectionPath) {
|
||||
setCacheHeaders(event, 'no-cache')
|
||||
}
|
||||
|
||||
// 5. 점검 상태별 캐시 설정
|
||||
if (inspectionData?.inspection_status === 1 && currentTime >= tsStartDate && currentTime <= tsEndDate) {
|
||||
/**
|
||||
* 점검 중인 경우
|
||||
* - 점검 상태가 1이고 현재 시간이 점검 시작과 종료 사이에 있는지 확인ㄹ
|
||||
* - 점검 URL 경로가 아닐 경우 no-cache 설정
|
||||
* - 화이트 리스트 체크
|
||||
*/
|
||||
// 현재 경로가 점검 페이지가 아닐 경우 리다이렉트
|
||||
const inspectionPath = `/${finalLocale}/inspection`
|
||||
if (fullPath !== inspectionPath) {
|
||||
setCacheHeaders(event, 'no-cache')
|
||||
}
|
||||
// 점검 중일 때 IP 필터링 활성화 여부 확인
|
||||
if (inspectionData?.ip_filter_use_yn === 'Y') {
|
||||
const clientIP = getTrueClientIp(event.node.req as any)
|
||||
|
||||
// 점검 중일 때 IP 필터링 활성화 여부 확인
|
||||
if (inspectionData?.ip_filter_use_yn === 'Y') {
|
||||
@@ -328,47 +339,50 @@ export default defineEventHandler(async event => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* 점검이 아닌 경우
|
||||
* - 홈 경로는 no-cache
|
||||
* - 점검 예정 시간에 따른 캐시 설정
|
||||
* - 점검 5분 전: 짧은 캐시 (10초)
|
||||
* - 점검 30분 전: 중간 캐시 (15초)
|
||||
* - 점검 30분 이후: 기본 캐시 (60초)
|
||||
*/
|
||||
// 홈 경로: 캐시 없음
|
||||
const isHomePath = [
|
||||
'',
|
||||
'/'
|
||||
//, ...Object.values(DEFAULT_LOCALE_COVERAGES).flatMap((locale) => [`/${locale}`, `/${locale}/`])
|
||||
].includes(fullPath)
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', inspectionPath)
|
||||
event.node.res.end()
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* 점검이 아닌 경우
|
||||
* - 홈 경로는 no-cache
|
||||
* - 점검 예정 시간에 따른 캐시 설정
|
||||
* - 점검 5분 전: 짧은 캐시 (10초)
|
||||
* - 점검 30분 전: 중간 캐시 (15초)
|
||||
* - 점검 30분 이후: 기본 캐시 (60초)
|
||||
*/
|
||||
// 홈 경로: 캐시 없음
|
||||
const isHomePath = [
|
||||
'',
|
||||
'/',
|
||||
//, ...Object.values(DEFAULT_LOCALE_COVERAGES).flatMap((locale) => [`/${locale}`, `/${locale}/`])
|
||||
].includes(fullPath)
|
||||
|
||||
if (isHomePath) {
|
||||
setCacheHeaders(event, 'no-cache')
|
||||
} else {
|
||||
// 점검 예정 시간에 따른 캐시 설정
|
||||
|
||||
if (tsStartDate > 0 && timeUntilInspectionSeconds > 0) {
|
||||
if (timeUntilInspectionSeconds < 300) {
|
||||
// 점검 5분 전: 짧은 캐시 (10초)
|
||||
setCacheHeaders(event, 'short', 10)
|
||||
} else if (timeUntilInspectionSeconds < 1800) {
|
||||
// 점검 30분 전: 중간 캐시 (15초)
|
||||
setCacheHeaders(event, 'medium', 15)
|
||||
} else {
|
||||
// 점검 30분 이후: 기본 캐시 (60초)
|
||||
setCacheHeaders(event, 'default')
|
||||
}
|
||||
if (isHomePath) {
|
||||
setCacheHeaders(event, 'no-cache')
|
||||
} else {
|
||||
// 점검 예정 시간에 따른 캐시 설정
|
||||
|
||||
if (tsStartDate > 0 && timeUntilInspectionSeconds > 0) {
|
||||
if (timeUntilInspectionSeconds < 300) {
|
||||
// 점검 5분 전: 짧은 캐시 (10초)
|
||||
setCacheHeaders(event, 'short', 10)
|
||||
} else if (timeUntilInspectionSeconds < 1800) {
|
||||
// 점검 30분 전: 중간 캐시 (15초)
|
||||
setCacheHeaders(event, 'medium', 15)
|
||||
} else {
|
||||
// 점검 30분 이후: 기본 캐시 (60초)
|
||||
setCacheHeaders(event, 'default')
|
||||
}
|
||||
}
|
||||
// -------------------------------------------------------------------------------
|
||||
// [Locale Middleware]
|
||||
// -------------------------------------------------------------------------------
|
||||
fnLocaleMiddleware(event, finalLocale)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// [Locale Middleware]
|
||||
// -------------------------------------------------------------------------------
|
||||
fnLocaleMiddleware(event, finalLocale)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('gameData load error:', error)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,6 @@ export const useGameDataStore = defineStore('gameData', () => {
|
||||
gameData.value = data
|
||||
}
|
||||
|
||||
const setLangCode = (data: string) => {
|
||||
langCode.value = data
|
||||
}
|
||||
|
||||
const clearGameData = () => {
|
||||
gameData.value = null
|
||||
}
|
||||
@@ -20,7 +16,6 @@ export const useGameDataStore = defineStore('gameData', () => {
|
||||
langCode,
|
||||
gameData,
|
||||
setGameData,
|
||||
setLangCode,
|
||||
clearGameData,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
}
|
||||
|
||||
const handleOpenAlert = ({
|
||||
isShowDimmed = false,
|
||||
isShowDimmed = true,
|
||||
modalName = '',
|
||||
isOutsideClose = false,
|
||||
contentText,
|
||||
@@ -47,7 +47,7 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
}
|
||||
|
||||
const handleOpenConfirm = ({
|
||||
isShowDimmed = false,
|
||||
isShowDimmed = true,
|
||||
modalName = '',
|
||||
isOutsideClose = false,
|
||||
contentText,
|
||||
|
||||
402
layers/templates/FxPreregist01/index.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import { getComponentGroup, getComponentGroupAry } from '#layers/utils/dataUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
|
||||
import type { Platform } from '#layers/types/components/button'
|
||||
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Configuration
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
|
||||
const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_preregist.json'
|
||||
|
||||
// Multilingual
|
||||
const resultGetMultilingual = await useGetMultilingual({
|
||||
baseApiUrl: dataResourcesUrl,
|
||||
fileName: multilingualFileName,
|
||||
})
|
||||
const { tm, locale }: any = useI18n({
|
||||
useScope: 'local',
|
||||
messages: Object(resultGetMultilingual?.value?.multilingual),
|
||||
})
|
||||
|
||||
const { getOperateResourcesData } = useResourcesData()
|
||||
const { gameData } = storeToRefs(useGameDataStore())
|
||||
const { pageData } = storeToRefs(usePageDataStore())
|
||||
|
||||
// Constants
|
||||
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 preregistModalRef = ref<{
|
||||
handleOpenPreregist: () => Promise<void>
|
||||
} | null>(null)
|
||||
|
||||
// Preregist Section
|
||||
const preregistCode = computed(
|
||||
() => getComponentGroup(props.components, 'eventKey')?.display?.text
|
||||
)
|
||||
const prdBackgroundData = computed(() =>
|
||||
getComponentGroup(props.components, 'background')
|
||||
)
|
||||
const preMainTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'mainTitle')
|
||||
)
|
||||
const preSubTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'subTitle')
|
||||
)
|
||||
const preImgPreregistdData = computed(() =>
|
||||
getComponentGroup(props.components, 'imgPreregistReward')
|
||||
)
|
||||
const preImgSnsData = computed(() =>
|
||||
getComponentGroup(props.components, 'imgSnsReward')
|
||||
)
|
||||
const preDescriptionData = computed(() =>
|
||||
getComponentGroup(props.components, 'description')
|
||||
)
|
||||
// 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}`,
|
||||
}))
|
||||
})
|
||||
|
||||
// Button Colors
|
||||
const buttonColors = computed(() => {
|
||||
const colorData = getComponentGroupAry(
|
||||
props.components,
|
||||
'preregistButtonColor'
|
||||
)
|
||||
|
||||
if (!colorData?.length)
|
||||
return { backgroundColor: undefined, textColor: undefined }
|
||||
|
||||
return {
|
||||
backgroundColor: getColorCode({
|
||||
colorName: colorData[COLOR_INDEX.BACKGROUND]?.display?.color_name,
|
||||
colorCode: colorData[COLOR_INDEX.BACKGROUND]?.display?.color_code,
|
||||
}),
|
||||
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 accBackgroundData = computed(() =>
|
||||
getComponentGroup(props.components, 'backgroundAccReward')
|
||||
)
|
||||
const accMainTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'mainTitleAccReward')
|
||||
)
|
||||
const accSubTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'subTitleAccReward')
|
||||
)
|
||||
const accRewardTitleData = computed(() =>
|
||||
getComponentGroup(props.components, 'rewardTitleAccReward')
|
||||
)
|
||||
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' as const,
|
||||
gap: 16,
|
||||
arrows: false,
|
||||
pagination: false,
|
||||
destroy: true,
|
||||
breakpoints: {
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Handler
|
||||
const handlePreregistClick = () => {
|
||||
preregistModalRef.value?.handleOpenPreregist()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container">
|
||||
<!-- Preregist Section -->
|
||||
<section class="relative py-[80px] md:py-[120px]">
|
||||
<WidgetsBackground
|
||||
v-if="prdBackgroundData"
|
||||
:resources-data="prdBackgroundData"
|
||||
/>
|
||||
<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(preImgPreregistdData)"
|
||||
:alt="preImgPreregistdData?.display?.text"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
{{ tm('Preregist_Btn_Preegist') }}
|
||||
</AtomsButtonLauncher>
|
||||
<AtomsButtonLauncher
|
||||
v-for="platform in platformButtons"
|
||||
:key="`preregist-${platform}`"
|
||||
type="duplication"
|
||||
:platform="platform"
|
||||
:background-color="buttonColors.backgroundColor"
|
||||
:text-color="buttonColors.textColor"
|
||||
>
|
||||
{{ tm('Preregist_Btn_Preegist') }}
|
||||
</AtomsButtonLauncher>
|
||||
</div>
|
||||
<WidgetsDescription
|
||||
v-if="preDescriptionData"
|
||||
:resources-data="preDescriptionData"
|
||||
class="mt-8"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Reward Section -->
|
||||
<section class="relative py-[80px] md:py-[120px]">
|
||||
<WidgetsBackground
|
||||
v-if="accBackgroundData"
|
||||
:resources-data="accBackgroundData"
|
||||
/>
|
||||
<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"
|
||||
: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-[172px] md:h-[245px]"
|
||||
>
|
||||
<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"
|
||||
:tm="tm"
|
||||
:preregist-code="preregistCode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* destroy되었을 때 (슬라이드 비활성화) 중앙 정렬 */
|
||||
.splide:not(.is-active):deep(.splide__list) {
|
||||
@apply flex justify-center gap-3 md:gap-4;
|
||||
}
|
||||
|
||||
.progress-bullet {
|
||||
@apply block w-3 h-3 rounded-full transition-all duration-300;
|
||||
background-color: var(--pagination-disabled);
|
||||
}
|
||||
.progress-bar {
|
||||
@apply w-[180px] h-0.5 overflow-hidden;
|
||||
background-color: var(--pagination-disabled);
|
||||
}
|
||||
.is-completed .progress-bullet,
|
||||
.is-completed .progress-bar {
|
||||
background-color: var(--pagination-active);
|
||||
}
|
||||
</style>
|
||||
@@ -13,10 +13,10 @@ const props = defineProps<Props>()
|
||||
const { handleTokenValidation } = useTokenValidation()
|
||||
|
||||
// Configuration
|
||||
const config = useRuntimeConfig()
|
||||
const dataResourcesUrl = config.public.dataResourcesUrl as string
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const dataResourcesUrl = runtimeConfig.public.dataResourcesUrl as string
|
||||
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
|
||||
const multilingualFileName = 'STOVE_PUBTEMPLATE_homepage_brand_secure.json'
|
||||
const stoveApiBaseUrl = config.public.stoveApiUrl
|
||||
|
||||
// Multilingual
|
||||
const resultGetMultilingual = await useGetMultilingual({
|
||||
@@ -33,7 +33,7 @@ const isLogin = ref(false)
|
||||
const secureSetting = ref({
|
||||
otpLoginYn: 'N',
|
||||
abroadLoginBlockYn: 'N',
|
||||
pcRegisterYn: 'N'
|
||||
pcRegisterYn: 'N',
|
||||
})
|
||||
|
||||
// 회원 보안 설정 설정
|
||||
@@ -52,26 +52,28 @@ const checkLoginValidation = async () => {
|
||||
|
||||
// 회원 보안 설정 조회
|
||||
const fnGetSecuritySetting = async () => {
|
||||
|
||||
const accessToken = useCookie('SUAT')
|
||||
checkLoginValidation()
|
||||
|
||||
const apiBase = `${stoveApiBaseUrl}/auth-secure/v1.0`
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken.value}`,
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
}
|
||||
try {
|
||||
const result = await commonFetch('GET', `${apiBase}/security/setting`, { headers })
|
||||
const result = await commonFetch('GET', `${apiBase}/security/setting`, {
|
||||
headers,
|
||||
})
|
||||
|
||||
if (result?.code === 0 && Array.isArray(result.value)) {
|
||||
const arrSecure = result.value
|
||||
const getValue = (key: string) => arrSecure.find((f: any) => f.key === key)?.value ?? 'N'
|
||||
const getValue = (key: string) =>
|
||||
arrSecure.find((f: any) => f.key === key)?.value ?? 'N'
|
||||
|
||||
secureSetting.value = {
|
||||
otpLoginYn: getValue('OTP_LOGIN_YN'),
|
||||
abroadLoginBlockYn: getValue('ABROAD_LOGIN_BLOCK_YN'),
|
||||
pcRegisterYn: getValue('PC_REGISTER_YN')
|
||||
pcRegisterYn: getValue('PC_REGISTER_YN'),
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -89,7 +91,9 @@ const secureCards = computed(() => {
|
||||
{
|
||||
id: 'SECURE_CARD_0',
|
||||
title: tm('Secure_Stove_otp') || '스토브 인증기 (OTP)',
|
||||
description: tm('Secure_Stove_otp_desc') || '스토브 앱으로 인증 후 안전하게 로그인하세요.',
|
||||
description:
|
||||
tm('Secure_Stove_otp_desc') ||
|
||||
'스토브 앱으로 인증 후 안전하게 로그인하세요.',
|
||||
status: secureSetting.value.otpLoginYn,
|
||||
benefitTitle: tm('Secure_Stove_otp_benefits') || '스토브 OTP 혜택',
|
||||
benefitDesc: tm('Secure_Defense_bonus_10') || '방어력 +10',
|
||||
@@ -100,7 +104,9 @@ const secureCards = computed(() => {
|
||||
{
|
||||
id: 'SECURE_CARD_1',
|
||||
title: tm('Secure_Block_foreign_login') || '해외 로그인 차단',
|
||||
description: tm('Secure_Block_foreign_login_desc') || '접속 국가를 제한하여 의심 로그인을 차단해요.',
|
||||
description:
|
||||
tm('Secure_Block_foreign_login_desc') ||
|
||||
'접속 국가를 제한하여 의심 로그인을 차단해요.',
|
||||
status: secureSetting.value.abroadLoginBlockYn,
|
||||
benefitTitle: '',
|
||||
benefitDesc: '',
|
||||
@@ -111,7 +117,9 @@ const secureCards = computed(() => {
|
||||
{
|
||||
id: 'SECURE_CARD_2',
|
||||
title: tm('Secure_Trusted_pc_management') || '지정 PC 관리',
|
||||
description: tm('Secure_Trusted_pc_desc') || '지정 PC에서만 로그인할 수 있게 설정해 보세요.',
|
||||
description:
|
||||
tm('Secure_Trusted_pc_desc') ||
|
||||
'지정 PC에서만 로그인할 수 있게 설정해 보세요.',
|
||||
status: secureSetting.value.pcRegisterYn,
|
||||
benefitTitle: '',
|
||||
benefitDesc: '',
|
||||
@@ -126,7 +134,6 @@ const cautionText = computed(() => {
|
||||
return tm('Secure_Notice_Content') || []
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
fnGetSecuritySetting()
|
||||
})
|
||||
@@ -143,12 +150,21 @@ onMounted(() => {
|
||||
<section class="section-secure bg-[#F0F0F0] pb-50">
|
||||
<div class="section-static content-standa md:max-w-[1300px] mx-auto">
|
||||
<!-- Title Section -->
|
||||
<div class="flex flex-col md:flex-row w-full md:items-end justify-between gap-5 mb-6">
|
||||
<h3 class="text-[#1F1F1F] text-2xl font-bold leading-8 tracking-[-0.72px]">
|
||||
<div
|
||||
class="flex flex-col md:flex-row w-full md:items-end justify-between gap-5 mb-6"
|
||||
>
|
||||
<h3
|
||||
class="text-[#1F1F1F] text-2xl font-bold leading-8 tracking-[-0.72px]"
|
||||
>
|
||||
{{ tm('Secure_Section_Title') || '보안 서비스' }}
|
||||
</h3>
|
||||
<p class="text-gray-500 text-[14px] font-[400] leading-[24px] tracking-[-0.42px] text-left md:text-right">
|
||||
{{ tm('Secure_Section_Description') || '*OTP / 해외 로그인 차단 / 지정 PC 관리 설정하고, 로드나인 계정을 보다 안전하게 보호하세요.' }}
|
||||
<p
|
||||
class="text-gray-500 text-[14px] font-[400] leading-[24px] tracking-[-0.42px] text-left md:text-right"
|
||||
>
|
||||
{{
|
||||
tm('Secure_Section_Description') ||
|
||||
'*OTP / 해외 로그인 차단 / 지정 PC 관리 설정하고, 로드나인 계정을 보다 안전하게 보호하세요.'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +176,9 @@ onMounted(() => {
|
||||
class="flex-1 min-h-[308px] md:min-h-[384px] p-[10px] md:p-4 bg-[#FFFFFF] rounded-2xl flex flex-col gap-3 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<!-- Card Content -->
|
||||
<div class="flex-1 p-[10px] md:p-8 flex flex-col gap-[8px] md:gap-3 text-left">
|
||||
<div
|
||||
class="flex-1 p-[10px] md:p-8 flex flex-col gap-[8px] md:gap-3 text-left"
|
||||
>
|
||||
<!-- Badge -->
|
||||
<div class="inline-flex">
|
||||
<span
|
||||
@@ -171,17 +189,25 @@ onMounted(() => {
|
||||
: 'bg-[#EBEBEB] text-[#999999]',
|
||||
]"
|
||||
>
|
||||
{{ card.status === 'Y' ? tm('Secure_Enabled') : tm('Secure_Disabled') }}
|
||||
{{
|
||||
card.status === 'Y'
|
||||
? tm('Secure_Enabled')
|
||||
: tm('Secure_Disabled')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h4 class="text-[#1F1F1F] text-[18px] md:text-[24px] font-bold leading-[26px] md:leading-[34px] tracking-[-0.54px] md:tracking-[-0.72px]">
|
||||
<h4
|
||||
class="text-[#1F1F1F] text-[18px] md:text-[24px] font-bold leading-[26px] md:leading-[34px] tracking-[-0.54px] md:tracking-[-0.72px]"
|
||||
>
|
||||
{{ card.title }}
|
||||
</h4>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="flex-1 text-[#999999] text-[14px] md:text-base font-[400] leading-[22px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.48px]">
|
||||
<p
|
||||
class="flex-1 text-[#999999] text-[14px] md:text-base font-[400] leading-[22px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.48px]"
|
||||
>
|
||||
{{ card.description }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -203,7 +229,9 @@ onMounted(() => {
|
||||
class="w-[48px] h-[48px] bg-[#3C75FF] rounded-[8px] flex items-center justify-center"
|
||||
>
|
||||
<img
|
||||
:src="getImageHost(card.benefitIcon, { imageType: 'common' })"
|
||||
:src="
|
||||
getImageHost(card.benefitIcon, { imageType: 'common' })
|
||||
"
|
||||
:alt="card.benefitTitle"
|
||||
class="w-[48px] h-[48px] object-contain rounded-2xl"
|
||||
loading="lazy"
|
||||
@@ -211,7 +239,9 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col text-left">
|
||||
<div class="text-[#3C75FF] text-[14px] md:text-[18px] font-bold leading-[22px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.54px]">
|
||||
<div
|
||||
class="text-[#3C75FF] text-[14px] md:text-[18px] font-bold leading-[22px] md:leading-[26px] tracking-[-0.42px] md:tracking-[-0.54px]"
|
||||
>
|
||||
{{ card.benefitTitle }}
|
||||
</div>
|
||||
<div
|
||||
@@ -230,7 +260,11 @@ onMounted(() => {
|
||||
button-size="size-small md:size-large"
|
||||
background-color="#000000"
|
||||
text-color="#FFFFFF"
|
||||
@click="isLogin ? handleSecureSetting(card.url) : checkLoginValidation()"
|
||||
@click="
|
||||
isLogin
|
||||
? handleSecureSetting(card.url)
|
||||
: checkLoginValidation()
|
||||
"
|
||||
>
|
||||
<span>{{ tm('Secure_Action_setup') }}</span>
|
||||
</AtomsButton>
|
||||
@@ -243,8 +277,19 @@ onMounted(() => {
|
||||
disabled
|
||||
>
|
||||
<span>{{ tm('Secure_Action_complete') }}</span>
|
||||
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.4298 2.80644L6.84645 0.240655C7.52385 -0.0802185 8.30948 -0.0802184 8.98688 0.240655L14.4035 2.80645C15.2767 3.22003 15.8333 4.09952 15.8333 5.06564V7.65038C15.8333 13.399 10.6191 16.1288 8.65401 16.9535C8.18024 17.1523 7.6531 17.1523 7.17932 16.9535C5.21423 16.1288 -0.000131724 13.399 2.49573e-09 7.65038L1.11287e-05 5.06566C6.95637e-06 4.09953 0.556675 3.22002 1.4298 2.80644ZM11.4226 7.4063C11.748 7.08086 11.748 6.55323 11.4226 6.22779C11.0972 5.90235 10.5695 5.90235 10.2441 6.22779L7.5 8.97187L6.00592 7.47779C5.68049 7.15235 5.15285 7.15235 4.82741 7.47779C4.50197 7.80323 4.50197 8.33086 4.82741 8.6563L6.91074 10.7396C7.23618 11.0651 7.76382 11.0651 8.08926 10.7396L11.4226 7.4063Z" fill="#999999"/>
|
||||
<svg
|
||||
width="16"
|
||||
height="18"
|
||||
viewBox="0 0 16 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M1.4298 2.80644L6.84645 0.240655C7.52385 -0.0802185 8.30948 -0.0802184 8.98688 0.240655L14.4035 2.80645C15.2767 3.22003 15.8333 4.09952 15.8333 5.06564V7.65038C15.8333 13.399 10.6191 16.1288 8.65401 16.9535C8.18024 17.1523 7.6531 17.1523 7.17932 16.9535C5.21423 16.1288 -0.000131724 13.399 2.49573e-09 7.65038L1.11287e-05 5.06566C6.95637e-06 4.09953 0.556675 3.22002 1.4298 2.80644ZM11.4226 7.4063C11.748 7.08086 11.748 6.55323 11.4226 6.22779C11.0972 5.90235 10.5695 5.90235 10.2441 6.22779L7.5 8.97187L6.00592 7.47779C5.68049 7.15235 5.15285 7.15235 4.82741 7.47779C4.50197 7.80323 4.50197 8.33086 4.82741 8.6563L6.91074 10.7396C7.23618 11.0651 7.76382 11.0651 8.08926 10.7396L11.4226 7.4063Z"
|
||||
fill="#999999"
|
||||
/>
|
||||
</svg>
|
||||
</AtomsButton>
|
||||
</div>
|
||||
@@ -252,8 +297,12 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Caution Section -->
|
||||
<div class="self-stretch p-8 bg-[#FAFAFA] rounded-2xl flex flex-col gap-3 text-left">
|
||||
<h5 class="text-[#333333] text-[20px] font-bold leading-[30px] tracking-[-0.6px]">
|
||||
<div
|
||||
class="self-stretch p-8 bg-[#FAFAFA] rounded-2xl flex flex-col gap-3 text-left"
|
||||
>
|
||||
<h5
|
||||
class="text-[#333333] text-[20px] font-bold leading-[30px] tracking-[-0.6px]"
|
||||
>
|
||||
{{ tm('Secure_Notice') }}
|
||||
</h5>
|
||||
<ul class="relative flex flex-col items-start justify-start w-full">
|
||||
@@ -262,11 +311,10 @@ onMounted(() => {
|
||||
:key="caution"
|
||||
v-dompurify-html="caution"
|
||||
class="relative pl-[22px] before:content-[''] before:absolute before:top-[10px] before:left-[9px] before:w-[3px] before:h-[3px] before:rounded-full before:bg-[#999999] text-[#999999] text-[14px] font-[400] leading-[24px] tracking-[-0.42px]"
|
||||
>
|
||||
</li>
|
||||
></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import { globalDateFormat } from '@seed-next/date'
|
||||
import {
|
||||
getComponentGroup,
|
||||
getComponentContainer,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import { getYouTubeThumbnail } from '#layers/utils/youtubeUtil'
|
||||
import { formatTimestamp } from '#layers/utils/formatUtil'
|
||||
import type { Splide as SplideType } from '@splidejs/splide'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
|
||||
@@ -61,7 +61,7 @@ const { data: slideData } = await useAsyncData(
|
||||
})
|
||||
|
||||
const mediaList = getComponentContainer(operateGroupList, 'mediaList', {
|
||||
hasGroup: true,
|
||||
isGroup: true,
|
||||
}) as OperateGroupItem[]
|
||||
|
||||
return mediaList ?? []
|
||||
@@ -165,7 +165,7 @@ const handleLoadMoreRecent = () => {
|
||||
<p
|
||||
class="mt-2 text-[14px] leading-[24px] tracking-[-0.42px] text-[#999] sm:mt-3.5 md:mt-4 md:text-[18px] md:font-[500] md:leading-[26px] md:tracking-[-0.54px] lg:mt-5 lg:text-[20px] lg:leading-[30px] lg:tracking-[-0.6px]"
|
||||
>
|
||||
{{ formatTimestamp(item.reg_dt, 'YYYY.MM.DD') }}
|
||||
{{ globalDateFormat(item.reg_dt, locale) }}
|
||||
</p>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
@@ -209,7 +209,7 @@ const handleLoadMoreRecent = () => {
|
||||
<p
|
||||
class="mt-2 text-[14px] leading-[24px] tracking-[-0.42px] text-[#999] md:text-[16px] md:leading-[26px] md:tracking-[-0.48px]"
|
||||
>
|
||||
{{ formatTimestamp(item.reg_dt, 'YYYY.MM.DD') }}
|
||||
{{ globalDateFormat(item.reg_dt, locale) }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
@@ -238,11 +238,11 @@ const handleLoadMoreRecent = () => {
|
||||
@apply block;
|
||||
}
|
||||
.splide:deep(.splide-arrows) .splide-arrow {
|
||||
@apply block top-[unset] bottom-[20px] translate-y-0 w-[40px] h-[40px] bg-cover bg-center bg-no-repeat
|
||||
sm:bottom-[24px] md:bottom-[36px] md:w-[48px] md:h-[48px] lg:bottom-[60px];
|
||||
@apply block top-[unset] bottom-[20px] translate-y-0 bg-cover bg-center bg-no-repeat
|
||||
sm:bottom-[24px] md:bottom-[36px] lg:bottom-[60px];
|
||||
}
|
||||
.splide:deep(.splide-arrows) .arrow-prev {
|
||||
@apply left-[20px] bg-[image:url('/images/common/btn_system_arrow_rev.png')]
|
||||
@apply left-[20px] bg-[image:url('/images/common/btn_system_arrow_prev.png')]
|
||||
sm:left-[calc(60.3%+21px)]
|
||||
md:left-[calc(56%+39px)]
|
||||
lg:left-[790px];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import { globalDateFormat } from '@seed-next/date'
|
||||
import { getComponentGroup } from '#layers/utils/dataUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { CwmsArticleItem } from '#layers/types/api/resourcesData'
|
||||
@@ -58,33 +59,42 @@ const { data: slideData } = await useAsyncData(
|
||||
server: false,
|
||||
}
|
||||
)
|
||||
const slideLength = computed(() => slideData.value.length)
|
||||
const slideClass = computed(() => ({
|
||||
'is-one-item': slideLength.value === 1,
|
||||
'is-two-items': slideLength.value === 2,
|
||||
'is-three-items': slideLength.value === 3,
|
||||
'is-four-items': slideLength.value === 4,
|
||||
}))
|
||||
const splideOptions = computed(() => ({
|
||||
gap: 20,
|
||||
perPage: 4,
|
||||
drag: false,
|
||||
arrows: slideLength.value > 4,
|
||||
pagination: slideLength.value > 4,
|
||||
breakpoints: {
|
||||
[BREAKPOINTS.lg - 1]: {
|
||||
perPage: 2,
|
||||
arrows: slideLength.value > 2,
|
||||
pagination: slideLength.value > 2,
|
||||
const slideLength = computed(() => slideData.value.length ?? 0)
|
||||
const splideOptions = computed(() => {
|
||||
return {
|
||||
gap: 20,
|
||||
perPage: 4,
|
||||
drag: false,
|
||||
arrows: slideLength.value > 4,
|
||||
pagination: slideLength.value > 4,
|
||||
destroy: slideLength.value <= 4,
|
||||
breakpoints: {
|
||||
[BREAKPOINTS.lg - 1]: {
|
||||
perPage: 2,
|
||||
arrows: slideLength.value > 2,
|
||||
pagination: slideLength.value > 2,
|
||||
destroy: slideLength.value < 3,
|
||||
},
|
||||
[BREAKPOINTS.md - 1]: {
|
||||
drag: true,
|
||||
perPage: 1,
|
||||
arrows: false,
|
||||
pagination: false,
|
||||
destroy: slideLength.value < 2,
|
||||
padding: {
|
||||
left: 40,
|
||||
right: 40,
|
||||
},
|
||||
},
|
||||
[BREAKPOINTS.sm - 1]: {
|
||||
padding: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
[BREAKPOINTS.md - 1]: {
|
||||
drag: true,
|
||||
perPage: 1,
|
||||
arrows: false,
|
||||
pagination: false,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
const getArticleUrl = (articleId: string) => {
|
||||
const communityUrl = gameData.value?.url_json?.community
|
||||
@@ -118,7 +128,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
v-if="slideLength > 0"
|
||||
:slide-item-length="slideLength"
|
||||
v-bind="splideOptions"
|
||||
:class="`${slideClass} w-full`"
|
||||
class="w-full"
|
||||
@arrow-click="onArrowClick"
|
||||
>
|
||||
<SplideSlide
|
||||
@@ -128,9 +138,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
<div class="slide-inner">
|
||||
<BlocksCardNews
|
||||
:title="item.title"
|
||||
:description="
|
||||
formatTimestamp(item.create_datetime, 'YYYY.MM.DD')
|
||||
"
|
||||
:description="globalDateFormat(item.create_datetime, locale)"
|
||||
:img-path="getImageHost(item.media_thumbnail_url)"
|
||||
:url="getArticleUrl(item.article_id)"
|
||||
link-target="_blank"
|
||||
@@ -158,9 +166,6 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
.splide {
|
||||
@apply mt-[24px] md:max-w-[776px] md:mt-[48px] md:mx-auto md:px-[72px] lg:max-w-[1428px];
|
||||
}
|
||||
.splide:deep(.splide__track) {
|
||||
@apply !px-[20px] md:max-w-[632px] lg:max-w-[1284px] md:mx-auto md:!px-[0] sm:!px-[40px];
|
||||
}
|
||||
.splide:deep(.arrow-prev) {
|
||||
@apply top-[calc(50%-28px)] left-[0];
|
||||
}
|
||||
@@ -168,23 +173,11 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
@apply top-[calc(50%-28px)] right-[0];
|
||||
}
|
||||
.slide-inner {
|
||||
@apply w-[275px] aspect-[1/1] md:w-[306px] md:box-border;
|
||||
@apply w-[275px] aspect-[1/1] md:w-[306px];
|
||||
}
|
||||
|
||||
.splide.is-one-item:deep(.splide__track) {
|
||||
@apply max-w-[315px] sm:max-w-[355px] mx-auto md:max-w-[306px];
|
||||
}
|
||||
.splide.is-two-items:deep(.splide__track) {
|
||||
@apply lg:max-w-[632px];
|
||||
}
|
||||
.splide.is-two-items:deep(.splide__list) {
|
||||
@apply md:!translate-x-0;
|
||||
}
|
||||
.splide.is-three-items:deep(.splide__track) {
|
||||
@apply lg:max-w-[958px];
|
||||
}
|
||||
.splide.is-three-items:deep(.splide__list),
|
||||
.splide.is-four-items:deep(.splide__list) {
|
||||
@apply lg:!translate-x-0;
|
||||
/* destroy되었을 때 (슬라이드 비활성화) 중앙 정렬 */
|
||||
.splide:not(.is-active):deep(.splide__list) {
|
||||
@apply flex justify-center gap-5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,7 +48,8 @@ const buttonListData = computed(() => {
|
||||
<AtomsImg
|
||||
v-if="getImagePaths(item)"
|
||||
:src="getImagePaths(item)"
|
||||
:alt="item?.group_label"
|
||||
:alt="item?.display?.text"
|
||||
class="w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,6 +166,9 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
.thumbnail-carousel:deep(.thumbnail-slide.is-active) {
|
||||
@apply opacity-100;
|
||||
}
|
||||
.thumbnail-carousel:deep(.thumbnail-splide .splide__track) {
|
||||
@apply md:max-w-[720px];
|
||||
}
|
||||
|
||||
.main-slide {
|
||||
@apply relative aspect-[16/9];
|
||||
|
||||
@@ -80,9 +80,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
<div class="slide-inner border-line mt-auto">
|
||||
<BlocksVisualContent
|
||||
:resources-data="getComponentGroup(item, 'imgList')"
|
||||
:page-ver-tmpl-seq="props.pageVerTmplSeq"
|
||||
object-fit="cover"
|
||||
:alt="getComponentGroup(item, 'subTitle')?.display?.text"
|
||||
/>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
|
||||
@@ -92,7 +92,6 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
<BlocksVisualContent
|
||||
:resources-data="getComponentGroup(item, 'imgList')"
|
||||
object-fit="cover"
|
||||
:alt="getComponentGroup(item, 'imgTitle')?.display?.text"
|
||||
/>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { SplideSlide } from '@splidejs/vue-splide'
|
||||
import { globalDateFormat } from '@seed-next/date'
|
||||
import {
|
||||
getComponentGroup,
|
||||
getComponentContainer,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import { formatTimestamp } from '#layers/utils/formatUtil'
|
||||
import { getImageHost } from '#layers/utils/styleUtil'
|
||||
import type { PageDataTemplateComponents } from '#layers/types/api/pageData'
|
||||
import type { OperateGroupItem } from '#layers/types/api/resourcesData'
|
||||
@@ -51,7 +51,7 @@ const { data: slideData } = await useAsyncData(
|
||||
})
|
||||
|
||||
const bannerList = getComponentContainer(operateGroupList, 'bannerList', {
|
||||
hasGroup: true,
|
||||
isGroup: true,
|
||||
minLength: 4,
|
||||
}) as OperateGroupItem[]
|
||||
|
||||
@@ -117,7 +117,7 @@ const onArrowClick = direction => {
|
||||
<SplideSlide v-for="(item, index) in slideData" :key="index">
|
||||
<BlocksCardNews
|
||||
:title="item.title"
|
||||
:description="formatTimestamp(item.reg_dt, 'YYYY.MM.DD')"
|
||||
:description="globalDateFormat(item.reg_dt, locale)"
|
||||
:img-path="getImageHost(item.img_path)"
|
||||
:url="item.url"
|
||||
:link-target="item.link_target"
|
||||
|
||||
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
@@ -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,
|
||||
}
|
||||
@@ -52,6 +52,8 @@ export interface GameDataValue {
|
||||
comm_img_json: GameDataCommImg
|
||||
market_json: Record<string, { url: string }>
|
||||
event_banner: GameDataEventBanner
|
||||
os_type: string // 1:AOS, 2:IOS, 3:둘다
|
||||
platform_type: string // 1:PC, 2:MOBILE, 3:둘다
|
||||
}
|
||||
|
||||
// ===== 세부 데이터 타입들 =====
|
||||
|
||||
@@ -118,7 +118,7 @@ export type PageDataTemplateComponentSet = PageDataTemplateComponent & {
|
||||
set_order?: number
|
||||
}
|
||||
|
||||
// 템플릿 컴포넌트 타입 - 세 가지 패턴
|
||||
// 템플릿 컴포넌트 타입 - 두가지 패턴
|
||||
export type PageDataTemplateComponents =
|
||||
| PageDataTemplateComponent // 단일 컴포넌트 패턴
|
||||
| {
|
||||
|
||||
@@ -44,7 +44,9 @@ const csrDownloadFile = (fileUrl: string, fileName?: string) => {
|
||||
*/
|
||||
const csrGetMarketingCode = () => {
|
||||
const route = useRoute()
|
||||
const mcode = Number(`${route.query.mcode != null && route.query.mcode !== '' ? route.query.mcode : ''}`)
|
||||
const mcode = Number(
|
||||
`${route.query.mcode != null && route.query.mcode !== '' ? route.query.mcode : ''}`
|
||||
)
|
||||
return isNaN(mcode) ? undefined : mcode
|
||||
}
|
||||
|
||||
@@ -82,15 +84,20 @@ const isNumeric = (str: string): boolean => {
|
||||
* @param {Function} tm - i18n의 tm 함수 (예: (key) => ({ txt: string }))
|
||||
* @param {any} query - 추가 쿼리 파라미터
|
||||
*/
|
||||
const getParsedCustomLink = (link: string, { tm, query = {} }: ParsedCustomLinkOptions) => {
|
||||
const config = useRuntimeConfig()
|
||||
const getParsedCustomLink = (
|
||||
link: string,
|
||||
{ tm, query = {} }: ParsedCustomLinkOptions
|
||||
) => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
let result = `${link || ''}`
|
||||
|
||||
// @c{key} 패턴 치환 (예: @c{stoveCommunityUrl})
|
||||
if (link.includes('@c')) {
|
||||
result = result.replace(/@c\{(.*?)\}/g, (_, key) => {
|
||||
// config.public에서 해당 key 값을 찾아 치환
|
||||
return typeof config.public[key] === 'string' ? config.public[key] : ''
|
||||
// runtimeConfig.public에서 해당 key 값을 찾아 치환
|
||||
return typeof runtimeConfig.public[key] === 'string'
|
||||
? runtimeConfig.public[key]
|
||||
: ''
|
||||
})
|
||||
}
|
||||
|
||||
@@ -132,7 +139,7 @@ const setCookieForDay = (name: string, value: string, exp?: number) => {
|
||||
|
||||
const setCookie = useCookie(name, {
|
||||
expires: new Date(date),
|
||||
path: '/'
|
||||
path: '/',
|
||||
})
|
||||
|
||||
setCookie.value = value
|
||||
@@ -140,7 +147,9 @@ const setCookieForDay = (name: string, value: string, exp?: number) => {
|
||||
|
||||
// 정적 파일인지 확인하는 함수
|
||||
const isStaticFile = (path: string): boolean => {
|
||||
return /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|scss)$/i.test(path)
|
||||
return /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|scss)$/i.test(
|
||||
path
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,5 +173,5 @@ export {
|
||||
getParsedCustomLink,
|
||||
setCookieForDay,
|
||||
isStaticFile,
|
||||
isInRange
|
||||
isInRange,
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export const hasComponentGroup = (
|
||||
* @param components props.components
|
||||
* @param componentName 컴포넌트 이름
|
||||
* @param options 옵션
|
||||
* - hasGroup: groups 속성에서 데이터 가져오기 (기본값: false)
|
||||
* - isGroup: groups 속성에서 데이터 가져오기 (기본값: false)
|
||||
* - maxLength: 최대 길이 제한
|
||||
* - minLength: 최소 길이 보장 (데이터 반복)
|
||||
* @returns 컴포넌트 컨테이너 배열
|
||||
@@ -90,17 +90,17 @@ export const hasComponentGroup = (
|
||||
export const getComponentContainer = (
|
||||
components: PageDataTemplateComponents | OperateComponents,
|
||||
componentName: string,
|
||||
options: { hasGroup?: boolean; maxLength?: number; minLength?: number } = {}
|
||||
options: { isGroup?: boolean; maxLength?: number; minLength?: number } = {}
|
||||
) => {
|
||||
if (!components) return []
|
||||
|
||||
const { hasGroup = false, maxLength, minLength } = options
|
||||
const { isGroup = false, maxLength, minLength } = options
|
||||
|
||||
// 1. 초기 컨테이너 가져오기
|
||||
const component = components[componentName]
|
||||
if (!component) return []
|
||||
|
||||
let result = hasGroup && 'groups' in component ? component.groups : component
|
||||
let result = isGroup && 'groups' in component ? component.groups : component
|
||||
|
||||
// 2. 최소 길이 보장 (데이터 복제)
|
||||
if (minLength && result.length > 1 && result.length < minLength) {
|
||||
|
||||
@@ -24,76 +24,6 @@ export const csrFormatJWT = (base64EncodeVal: string) => {
|
||||
return decodeVal
|
||||
}
|
||||
|
||||
/**
|
||||
* 타임스탬프를 다양한 날짜 형식으로 변환합니다.
|
||||
* @param timestamp 타임스탬프 (밀리초 또는 초)
|
||||
* @param format 날짜 형식 ('YYYY-MM-DD', 'YYYY-MM-DD HH:mm:ss', 'MM/DD/YYYY', 'YYYY년 MM월 DD일' 등)
|
||||
* @param locale 로케일 (기본값: 'ko-KR')
|
||||
* @returns 포맷된 날짜 문자열
|
||||
*/
|
||||
export const formatTimestamp = (
|
||||
timestamp: number | string,
|
||||
format: string = 'YYYY.MM.DD',
|
||||
_locale: string = 'ko-KR'
|
||||
): string => {
|
||||
if (!timestamp) return ''
|
||||
|
||||
// 타임스탬프를 숫자로 변환
|
||||
let ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
|
||||
|
||||
// 초 단위인 경우 밀리초로 변환
|
||||
if (ts < 10000000000) {
|
||||
ts = ts * 1000
|
||||
}
|
||||
|
||||
const date = new Date(ts)
|
||||
|
||||
// 유효하지 않은 날짜인 경우 빈 문자열 반환
|
||||
if (isNaN(date.getTime())) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 미리 정의된 형식들
|
||||
const predefinedFormats: Record<string, string> = {
|
||||
'YYYY.MM.DD': date.toISOString().split('T')[0].replace(/-/g, '.'),
|
||||
'YYYY-MM-DD': date.toISOString().split('T')[0],
|
||||
'YYYY-MM-DD HH:mm': date.toISOString().replace('T', ' ').substring(0, 16),
|
||||
'YYYY-MM-DD HH:mm:ss': date.toISOString().replace('T', ' ').split('.')[0],
|
||||
'MM/DD/YYYY': date.toLocaleDateString('en-US'),
|
||||
'DD/MM/YYYY': date.toLocaleDateString('en-GB'),
|
||||
'YYYY년 MM월 DD일': date.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}),
|
||||
'MM월 DD일': date.toLocaleDateString('ko-KR', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}),
|
||||
}
|
||||
|
||||
// 미리 정의된 형식이 있으면 사용
|
||||
if (predefinedFormats[format]) {
|
||||
return predefinedFormats[format]
|
||||
}
|
||||
|
||||
// 커스텀 형식 처리
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return format
|
||||
.replace('YYYY', String(year))
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열 또는 객체를 배열로 변환합니다.
|
||||
* @param value 변환할 값 (배열, 객체, 또는 undefined/null)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DEFAULT_LOCALE_CODE } from '../../i18n.config'
|
||||
export const getPreferredLanguage = (acceptLanguageHeader = '') => {
|
||||
const languages = acceptLanguageHeader
|
||||
.split(',')
|
||||
.map((lang) => {
|
||||
.map(lang => {
|
||||
const [code, priority = 'q=1'] = lang.trim().split(';q=')
|
||||
return { code, priority: parseFloat(priority) }
|
||||
})
|
||||
@@ -17,7 +17,7 @@ export const getPreferredLanguage = (acceptLanguageHeader = '') => {
|
||||
const parseCookies = (cookieHeader: string) => {
|
||||
const cookies: Record<string, string> = {}
|
||||
if (cookieHeader) {
|
||||
cookieHeader.split(';').forEach((cookie) => {
|
||||
cookieHeader.split(';').forEach(cookie => {
|
||||
const [name, value] = cookie.trim().split('=')
|
||||
if (name && value) {
|
||||
cookies[name] = decodeURIComponent(value)
|
||||
@@ -33,13 +33,17 @@ const parseCookies = (cookieHeader: string) => {
|
||||
* @param {string} path - 현재 URL 경로
|
||||
*/
|
||||
export const csrGetFinalLocale = (path = '', coveragesLocales: string[]) => {
|
||||
const config = useRuntimeConfig()
|
||||
const baseDomain = `${config.public.baseDomain}`
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const baseDomain = `${runtimeConfig.public.baseDomain}`
|
||||
|
||||
let finalLocale = DEFAULT_LOCALE_CODE // 기본값 설정
|
||||
|
||||
// coveragesLocales가 빈 배열이거나 유효하지 않은 경우 기본 언어 반환
|
||||
if (!coveragesLocales || !Array.isArray(coveragesLocales) || coveragesLocales.length === 0) {
|
||||
if (
|
||||
!coveragesLocales ||
|
||||
!Array.isArray(coveragesLocales) ||
|
||||
coveragesLocales.length === 0
|
||||
) {
|
||||
return finalLocale
|
||||
}
|
||||
|
||||
@@ -60,15 +64,21 @@ export const csrGetFinalLocale = (path = '', coveragesLocales: string[]) => {
|
||||
}
|
||||
|
||||
// 2. LOCALE 쿠키 언어
|
||||
const cookieLanguage = `${useCookie('LOCALE', { domain: baseDomain }).value}`.toLowerCase()
|
||||
const cookieLanguage =
|
||||
`${useCookie('LOCALE', { domain: baseDomain }).value}`.toLowerCase()
|
||||
if (cookieLanguage && cookieLanguage !== '') {
|
||||
finalLocale = cookieLanguage
|
||||
return finalLocale
|
||||
}
|
||||
|
||||
// 3. 브라우저 언어
|
||||
const browserLanguage = `${navigator.language || navigator.languages[0]}`.toLowerCase()
|
||||
if (browserLanguage && browserLanguage !== '' && coveragesLocales.includes(browserLanguage)) {
|
||||
const browserLanguage =
|
||||
`${navigator.language || navigator.languages[0]}`.toLowerCase()
|
||||
if (
|
||||
browserLanguage &&
|
||||
browserLanguage !== '' &&
|
||||
coveragesLocales.includes(browserLanguage)
|
||||
) {
|
||||
finalLocale = browserLanguage
|
||||
return finalLocale
|
||||
}
|
||||
@@ -89,7 +99,11 @@ export const ssrGetFinalLocale = (path = '', headers: any, coveragesLocales: str
|
||||
let finalLocale // 기본값 설정
|
||||
try {
|
||||
// coveragesLocales가 빈 배열이거나 유효하지 않은 경우 기본 언어 반환
|
||||
if (!coveragesLocales || !Array.isArray(coveragesLocales) || coveragesLocales.length === 0) {
|
||||
if (
|
||||
!coveragesLocales ||
|
||||
!Array.isArray(coveragesLocales) ||
|
||||
coveragesLocales.length === 0
|
||||
) {
|
||||
return finalLocale
|
||||
}
|
||||
|
||||
@@ -101,7 +115,11 @@ export const ssrGetFinalLocale = (path = '', headers: any, coveragesLocales: str
|
||||
}
|
||||
const pathLocalee = `${path.split('/')[1]}`.toLowerCase()
|
||||
// URL path에 포함된 언어 정보가 지원하는 언어인지 체크
|
||||
if (pathLocalee && pathLocalee !== '' && coveragesLocales.includes(pathLocalee)) {
|
||||
if (
|
||||
pathLocalee &&
|
||||
pathLocalee !== '' &&
|
||||
coveragesLocales.includes(pathLocalee)
|
||||
) {
|
||||
finalLocale = pathLocalee
|
||||
return finalLocale
|
||||
}
|
||||
@@ -110,8 +128,14 @@ export const ssrGetFinalLocale = (path = '', headers: any, coveragesLocales: str
|
||||
// 2. LOCALE 쿠키 언어 (SSR에서는 headers에서 직접 파싱)
|
||||
const cookieHeader = headers.cookie || ''
|
||||
const cookies = parseCookies(cookieHeader)
|
||||
const cookieLanguage = cookies.LOCALE ? `${cookies.LOCALE}`.toLowerCase() : ''
|
||||
if (cookieLanguage && cookieLanguage !== '' && coveragesLocales.includes(cookieLanguage)) {
|
||||
const cookieLanguage = cookies.LOCALE
|
||||
? `${cookies.LOCALE}`.toLowerCase()
|
||||
: ''
|
||||
if (
|
||||
cookieLanguage &&
|
||||
cookieLanguage !== '' &&
|
||||
coveragesLocales.includes(cookieLanguage)
|
||||
) {
|
||||
finalLocale = cookieLanguage
|
||||
return finalLocale
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import { csrFormatJWT } from '#layers/utils/formatUtil'
|
||||
* Stove 로그인
|
||||
*/
|
||||
export const csrGoStoveLogin = () => {
|
||||
const config = useRuntimeConfig()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const gameDataStore = useGameDataStore()
|
||||
|
||||
const loginUrl = config.public.stoveLoginUrl
|
||||
const loginUrl = runtimeConfig.public.stoveLoginUrl
|
||||
const stoveGameId = gameDataStore.gameData?.game_id
|
||||
const stoveGameNo = gameDataStore.gameData?.game_code
|
||||
const redirectUrl = encodeURIComponent(location.href)
|
||||
|
||||
@@ -23,17 +23,15 @@ export const getImageHost = (
|
||||
|
||||
if (/^(https?:\/\/|www\.)/.test(path)) return path
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const { staticUrl, assetsUrl } = config.public
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const { staticUrl, assetsUrl } = runtimeConfig.public
|
||||
const { imageType = 'game' } = options
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isDevelopment = import.meta.dev
|
||||
const isTypeGame = imageType === 'game'
|
||||
|
||||
// * [TODO] 수정 필요 : 게임별 이미지 로컬에 추가 필요.
|
||||
if (isTypeGame) {
|
||||
return `${staticUrl}${path}`
|
||||
}
|
||||
if (isTypeGame) return `${staticUrl}${path}`
|
||||
|
||||
// 개발 환경일 때는 루트 경로 생략
|
||||
if (isDevelopment) return path
|
||||
@@ -54,12 +52,13 @@ export const getDeviceSrc = (
|
||||
pathArray: PageDataResourceGroupResPath,
|
||||
options?: {
|
||||
resourcesType?: 'image' | 'video'
|
||||
imageType?: 'common' | 'game'
|
||||
}
|
||||
) => {
|
||||
// pathArray가 없으면 null 반환
|
||||
if (!pathArray) return null
|
||||
|
||||
const { resourcesType = 'image' } = options ?? {}
|
||||
const { resourcesType = 'image', imageType = 'game' } = options ?? {}
|
||||
const pcField = resourcesType === 'video' ? 'path_vid_pc' : 'path_pc'
|
||||
const mobileField = resourcesType === 'video' ? 'path_vid_mo' : 'path_mo'
|
||||
|
||||
@@ -70,8 +69,8 @@ export const getDeviceSrc = (
|
||||
if (!pcPath && !mobilePath) return null
|
||||
|
||||
const resolvedImages = {
|
||||
pc: pcPath ? getImageHost(pcPath) : '',
|
||||
mobile: mobilePath ? getImageHost(mobilePath) : '',
|
||||
pc: pcPath ? getImageHost(pcPath, { imageType }) : '',
|
||||
mobile: mobilePath ? getImageHost(mobilePath, { imageType }) : '',
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -88,6 +88,8 @@ export default defineNuxtConfig({
|
||||
|
||||
stoveGnb: process.env.STOVE_GNB,
|
||||
|
||||
stoveCs: process.env.STOVE_CS,
|
||||
|
||||
stoveLauncherScript: process.env.STOVE_LAUNCHER_SCRIPT,
|
||||
stoveClientDownloadUrl: process.env.STOVE_CLIENT_DOWNLOAD_URL,
|
||||
stoveLoginUrl: process.env.STOVE_LOGIN_URL,
|
||||
|
||||
BIN
public/images/common/btn_system_arrow_next.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/common/btn_system_arrow_prev.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/common/stove_app_qr.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
public/images/sample/FX_PREREGIST_01/common/bg_acc_reward.jpg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/images/sample/FX_PREREGIST_01/common/bg_acc_reward_m.jpg
Normal file
|
After Width: | Height: | Size: 887 KiB |
BIN
public/images/sample/FX_PREREGIST_01/ko/img_acc_reward01.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
public/images/sample/FX_PREREGIST_01/ko/img_acc_reward01_m.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
public/images/sample/FX_PREREGIST_01/ko/img_acc_reward02.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
public/images/sample/FX_PREREGIST_01/ko/img_acc_reward02_m.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
public/images/sample/FX_PREREGIST_01/ko/img_acc_reward03.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
public/images/sample/FX_PREREGIST_01/ko/img_acc_reward03_m.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
public/images/sample/FX_PREREGIST_01/ko/img_acc_reward04.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
public/images/sample/FX_PREREGIST_01/ko/img_acc_reward04_m.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
public/images/sample/FX_PREREGIST_01/ko/img_acc_reward05.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
public/images/sample/FX_PREREGIST_01/ko/img_acc_reward05_m.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 351 KiB After Width: | Height: | Size: 351 KiB |