Merge branch 'feature/202501107-all' into feature/20251201-gil_qa
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { PlatformTransformType } from '#layers/types/api/gameData'
|
||||
import type {
|
||||
DownloadButtonType,
|
||||
ButtonVariant,
|
||||
@@ -21,8 +22,17 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const PLATFORM_LABEL_KEY: Record<PlatformTransformType, string> = {
|
||||
pc: 'PC',
|
||||
google_play: 'Google Play',
|
||||
app_store: 'App Store',
|
||||
} as const
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const { tm } = useI18n()
|
||||
const device = useDevice()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const modalStore = useModalStore()
|
||||
const { isProcessing, validateLauncher } = useCheckGameStart()
|
||||
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
@@ -42,15 +52,20 @@ const DUP_IMAGE_MAP: Record<Platform, string> = {
|
||||
} as const
|
||||
|
||||
const componentTag = computed(() => {
|
||||
if (props.type !== 'duplication' && props.platform === 'stove') {
|
||||
if (props.platform === 'stove' && props.type !== 'duplication') {
|
||||
return 'a'
|
||||
}
|
||||
return 'button'
|
||||
})
|
||||
const isDuplication = computed(() => props.type === 'duplication')
|
||||
const isSingle = computed(() => props.type === 'single')
|
||||
const supportedPlatforms = computed(
|
||||
() =>
|
||||
getSupportedPlatforms(
|
||||
gameData.value?.platform_type,
|
||||
gameData.value?.os_type
|
||||
) as PlatformTransformType[]
|
||||
)
|
||||
const platformIcon = computed(() => PLATFORM_ICON_MAP[props.platform])
|
||||
|
||||
const inlineStyle = computed<CSSProperties>(() => {
|
||||
const style: CSSProperties = {}
|
||||
|
||||
@@ -66,18 +81,61 @@ const inlineStyle = computed<CSSProperties>(() => {
|
||||
return style
|
||||
})
|
||||
|
||||
const highlight = (text: string) => `<span class="highlight">${text}</span>`
|
||||
|
||||
const tmWithGameName = (key: string): string => {
|
||||
const raw = tm(key)
|
||||
if (typeof raw !== 'string') return ''
|
||||
|
||||
const withName = raw.replace(
|
||||
/%게임명%/g,
|
||||
highlight(gameData.value?.game_name || '')
|
||||
)
|
||||
|
||||
const platformLines = supportedPlatforms.value
|
||||
.map(platform => highlight(PLATFORM_LABEL_KEY[platform] as string))
|
||||
.filter(Boolean)
|
||||
|
||||
return platformLines.length
|
||||
? `${withName}<br><br>${platformLines.join('<br>')}`
|
||||
: withName
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (props.platform === 'pc') {
|
||||
validateLauncher()
|
||||
return
|
||||
}
|
||||
if (props.platform === 'stove' && !isDuplication.value) {
|
||||
if (props.platform === 'stove') {
|
||||
if (props.type === 'duplication') return
|
||||
|
||||
const stoveClientDownloadUrl = runtimeConfig.public.stoveClientDownloadUrl
|
||||
location.href = stoveClientDownloadUrl
|
||||
return
|
||||
}
|
||||
|
||||
const url = gameData.value?.market_json[props.platform]?.url
|
||||
if (props.platform === 'pc') {
|
||||
if (device.isDesktop && gameData.value?.platform_type !== '2') {
|
||||
validateLauncher()
|
||||
return
|
||||
} else {
|
||||
const target = device.isAndroid
|
||||
? 'google_play'
|
||||
: device.isApple
|
||||
? 'app_store'
|
||||
: null
|
||||
|
||||
if (!target || !supportedPlatforms.value.includes(target)) {
|
||||
modalStore.handleOpenAlert({
|
||||
contentText: tmWithGameName('Alert_Not_SupportedOS'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const url = gameData.value?.market_json?.[target]?.url || ''
|
||||
|
||||
window.open(url, '_blank')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const url = gameData.value?.market_json[props.platform]?.url || ''
|
||||
if (url) window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
@@ -100,14 +158,14 @@ const handleClick = () => {
|
||||
<span class="btn-content">
|
||||
<component
|
||||
:is="platformIcon"
|
||||
v-if="!isDuplication"
|
||||
v-if="props.type !== 'duplication'"
|
||||
class="icon-platform"
|
||||
/>
|
||||
<span class="text">
|
||||
<slot />
|
||||
</span>
|
||||
<span
|
||||
v-if="type === 'default' && platform === 'pc'"
|
||||
v-if="props.platform === 'pc' && props.type === 'default'"
|
||||
class="icon-download"
|
||||
>
|
||||
<AtomsIconsDownloadLine />
|
||||
|
||||
@@ -6,26 +6,18 @@ import type {
|
||||
GameDataMenuChildren,
|
||||
GameDataResourceGroup,
|
||||
GameDataResourceGroupSet,
|
||||
PlatformTransformType,
|
||||
} from '#layers/types/api/gameData'
|
||||
|
||||
const MORE_WIDTH = 72
|
||||
const START_WIDTH_MARGIN = 40
|
||||
const PLATFORM_LABEL_KEY: Record<PlatformTransformType, string> = {
|
||||
pc: 'PC',
|
||||
google_play: 'Google Play',
|
||||
app_store: 'App Store',
|
||||
} as const
|
||||
|
||||
const route = useRoute()
|
||||
const { tm } = useI18n()
|
||||
const { width } = useWindowSize()
|
||||
const device = useDevice()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const pageDataStore = usePageDataStore()
|
||||
const scrollStore = useScrollStore()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
const { pageLayoutType } = storeToRefs(pageDataStore)
|
||||
@@ -55,16 +47,8 @@ const start1depthData = computed(
|
||||
const start2depthData = computed(
|
||||
() => gnbData.value?.buttons[1]?.button_json as GameDataResourceGroupSet
|
||||
)
|
||||
const supportedPlatforms = computed(
|
||||
() =>
|
||||
getSupportedPlatforms(
|
||||
gameData.value?.platform_type,
|
||||
gameData.value?.os_type
|
||||
) as PlatformTransformType[]
|
||||
)
|
||||
const isStartPCVisible = computed(() => {
|
||||
return device.isDesktop && gameData.value?.platform_type !== '2'
|
||||
})
|
||||
|
||||
console.log('start2depthData', start2depthData.value)
|
||||
|
||||
// 자식 중 활성 링크 존재 여부 확인
|
||||
const hasActiveChild = (children?: GameDataMenuChildren) => {
|
||||
@@ -158,52 +142,6 @@ const has2depthButton = (gnbItem: GameDataMenu) => {
|
||||
return gnbItem.children && Object.keys(gnbItem.children).length > 0
|
||||
}
|
||||
|
||||
const highlight = (text: string) => `<span class="highlight">${text}</span>`
|
||||
|
||||
const tmWithGameName = (key: string): string => {
|
||||
const raw = tm(key)
|
||||
if (typeof raw !== 'string') return ''
|
||||
|
||||
const withName = raw.replace(
|
||||
/%게임명%/g,
|
||||
highlight(gameData.value?.game_name || '')
|
||||
)
|
||||
|
||||
const platformLines = supportedPlatforms.value
|
||||
.map(platform => highlight(PLATFORM_LABEL_KEY[platform] as string))
|
||||
.filter(Boolean)
|
||||
|
||||
return platformLines.length
|
||||
? `${withName}<br><br>${platformLines.join('<br>')}`
|
||||
: withName
|
||||
}
|
||||
|
||||
const showNotSupportedOSAlert = () => {
|
||||
return modalStore.handleOpenAlert({
|
||||
contentText: tmWithGameName('Alert_Not_SupportedOS'),
|
||||
})
|
||||
}
|
||||
|
||||
const handleStartClick = () => {
|
||||
if (isStartPCVisible.value) return
|
||||
|
||||
const target = device.isAndroid
|
||||
? 'google_play'
|
||||
: device.isApple
|
||||
? 'app_store'
|
||||
: null
|
||||
|
||||
if (!target || !supportedPlatforms.value.includes(target)) {
|
||||
showNotSupportedOSAlert()
|
||||
return
|
||||
}
|
||||
|
||||
const url = gameData.value?.market_json?.[target]?.url || ''
|
||||
if (!url) return showNotSupportedOSAlert()
|
||||
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
overflowCount.value = 0
|
||||
isMounted.value = true
|
||||
@@ -263,14 +201,14 @@ onMounted(() => {
|
||||
<template v-if="hasGnbMenus">
|
||||
<div class="official custom-theme-scrollbar">
|
||||
<div
|
||||
v-for="(gnbItem, index) in gnbData?.menus"
|
||||
:key="index"
|
||||
v-for="(gnbItem, key) in gnbData?.menus"
|
||||
:key="key"
|
||||
class="nav-item group"
|
||||
:class="{
|
||||
'is-hidden':
|
||||
breakpoints.isDesktop &&
|
||||
overflowCount > 0 &&
|
||||
Number(index) >=
|
||||
Number(key) >=
|
||||
Object.keys(gnbData?.menus).length - overflowCount,
|
||||
}"
|
||||
>
|
||||
@@ -334,13 +272,13 @@ onMounted(() => {
|
||||
<div class="more-list">
|
||||
<div class="list-inner">
|
||||
<div
|
||||
v-for="(gnbItem, index) in gnbData?.menus"
|
||||
:key="index"
|
||||
v-for="(gnbItem, key) in gnbData?.menus"
|
||||
:key="key"
|
||||
:class="{
|
||||
'is-hidden':
|
||||
breakpoints.isDesktop &&
|
||||
overflowCount > 0 &&
|
||||
Number(index) >=
|
||||
Number(key) >=
|
||||
Object.keys(gnbData?.menus).length - overflowCount,
|
||||
}"
|
||||
>
|
||||
@@ -424,10 +362,7 @@ onMounted(() => {
|
||||
<ClientOnly>
|
||||
<div ref="startRef" class="btn-start">
|
||||
<template v-if="start1depthData">
|
||||
<component
|
||||
:is="
|
||||
isStartPCVisible ? 'BlocksButtonLauncher' : 'AtomsButton'
|
||||
"
|
||||
<BlocksButtonLauncher
|
||||
type="custom"
|
||||
platform="pc"
|
||||
:background-color="
|
||||
@@ -436,17 +371,16 @@ onMounted(() => {
|
||||
:text-color="
|
||||
getColorCodeFromData(start1depthData?.btn_info, 'txt')
|
||||
"
|
||||
@click="handleStartClick"
|
||||
>
|
||||
{{ start1depthData?.btn_info?.txt_btn_name }}
|
||||
</component>
|
||||
</BlocksButtonLauncher>
|
||||
<div
|
||||
v-if="breakpoints.isDesktop && start2depthData"
|
||||
class="nav-2depth"
|
||||
>
|
||||
<ul>
|
||||
<li v-for="(item, index) in start2depthData" :key="index">
|
||||
<BlocksButtonLauncher type="custom" :platform="index">
|
||||
<li v-for="(item, key) in start2depthData" :key="key">
|
||||
<BlocksButtonLauncher type="custom" :platform="key">
|
||||
{{ item.btn_info?.txt_btn_name }}
|
||||
</BlocksButtonLauncher>
|
||||
</li>
|
||||
|
||||
@@ -64,12 +64,10 @@ const useGameLinkedData = () => {
|
||||
setHasGuid(res.value?.guid > 0)
|
||||
} else {
|
||||
res = { code: res.code, message: res.message || '' }
|
||||
console.log(`${logPrefix.failure}.getGuid: `, res)
|
||||
res.code = -99999 // else 알럿 띄우기 용 세팅
|
||||
setHasGuid(false)
|
||||
}
|
||||
} else {
|
||||
console.log(`${logPrefix.failure}.getGuid - res is null: `, res)
|
||||
res = { code: -99999, message: '' }
|
||||
setHasGuid(false)
|
||||
}
|
||||
@@ -130,7 +128,6 @@ const useGameLinkedData = () => {
|
||||
} else if (res.code === 515) {
|
||||
// [515] AccessToken이 유효하지 않을 경우
|
||||
res = { code: res.code, message: res.message || '' }
|
||||
console.log(`${logPrefix.failure}.getCharacterList: `, res)
|
||||
setCharacterList([] as CharacterInfo[])
|
||||
setMainCharacter({} as CharacterInfo)
|
||||
} else {
|
||||
@@ -138,14 +135,12 @@ const useGameLinkedData = () => {
|
||||
// [502] 유효하지 않거나 잘못된 파라미터로 호출할 경우
|
||||
// [701] 존재하지 않은 게임일 경우(game_no 기준)
|
||||
res = { code: res.code, message: res.message || '' }
|
||||
console.log(`${logPrefix.failure}.getCharacterList: `, res)
|
||||
res.code = -99999 // else 알럿 띄우기 용 세팅
|
||||
setCharacterList([] as CharacterInfo[])
|
||||
setMainCharacter({} as CharacterInfo)
|
||||
}
|
||||
} else {
|
||||
res = { code: -99999, message: '' }
|
||||
console.log(`${logPrefix.failure}.getCharacterList: `, res)
|
||||
setCharacterList([] as CharacterInfo[])
|
||||
setMainCharacter({} as CharacterInfo)
|
||||
}
|
||||
|
||||
@@ -88,18 +88,15 @@ const usePreregist = () => {
|
||||
const headers = {
|
||||
Authorization: `Bearer ${req.accessToken}`,
|
||||
}
|
||||
console.log("🚀 ~ getPreregist ~ req.event_code:", req.event_code)
|
||||
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
|
||||
console.log("🚀 ~ getPreregist ~ res:", res.value)
|
||||
|
||||
// 응답 검증
|
||||
if (!res) {
|
||||
@@ -186,13 +183,11 @@ const usePreregist = () => {
|
||||
country_dialing_code: req.country_dialing_code,
|
||||
birth_date: req.birth_date,
|
||||
}
|
||||
console.log("🚀 ~ setPreregist ~ parema", body)
|
||||
|
||||
const res = (await commonFetch('POST', url, {
|
||||
headers,
|
||||
body,
|
||||
})) as ResPreorderReserveDataUpdate
|
||||
console.log("🚀 ~ ResPreorderReserveDataUpdate ~ res:", res.value)
|
||||
|
||||
// 응답 검증
|
||||
if (!res) {
|
||||
|
||||
@@ -15,7 +15,6 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
const gameDomain = useGetGameDomain()
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
console.log("🚀 ~ gameData:", gameData.value)
|
||||
const pageDataStore = usePageDataStore()
|
||||
const loadingStore = useLoadingStore()
|
||||
const { getPathAfterLanguage } = usePathResolver()
|
||||
@@ -31,25 +30,31 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
const pageUrl = getPathAfterLanguage(to.path)
|
||||
|
||||
// 루트 경로(언어 코드만 있는 경로)로 접근할 때만 intro.page_url로 리다이렉트
|
||||
const isRootPath = !pageUrl || pageUrl === '' || pageUrl === '/' || pageUrl === `/${langCode}/`
|
||||
console.log("🚀 ~ isRootPath:", isRootPath)
|
||||
|
||||
const isRootPath =
|
||||
!pageUrl ||
|
||||
pageUrl === '' ||
|
||||
pageUrl === '/' ||
|
||||
pageUrl === `/${langCode}/`
|
||||
console.log('🚀 ~ isRootPath:', isRootPath)
|
||||
|
||||
if (isRootPath) {
|
||||
// gameData.intro.page_url이 있으면 해당 URL로 리다이렉트
|
||||
const introPageUrl = gameData.value?.intro?.page_url
|
||||
if (introPageUrl && introPageUrl.trim() !== '') {
|
||||
// 외부 URL인지 확인
|
||||
const isExternalUrl = introPageUrl.startsWith('http://') || introPageUrl.startsWith('https://')
|
||||
|
||||
const isExternalUrl =
|
||||
introPageUrl.startsWith('http://') ||
|
||||
introPageUrl.startsWith('https://')
|
||||
|
||||
// 내부 경로인 경우 언어 코드 추가
|
||||
let finalIntroUrl = introPageUrl
|
||||
if (!isExternalUrl) {
|
||||
const normalizedIntroUrl = introPageUrl.split('?')[0] // 쿼리스트링 제외
|
||||
|
||||
|
||||
// 언어 코드 패턴 확인 (예: /ko, /en, /zh-tw 등)
|
||||
const languagePattern = /^\/[a-z]{2}(-[a-z]{2})?(\/|$)/
|
||||
const hasLanguageCode = languagePattern.test(normalizedIntroUrl)
|
||||
|
||||
|
||||
// 언어 코드가 없으면 추가
|
||||
if (!hasLanguageCode) {
|
||||
// 경로가 /로 시작하지 않으면 / 추가
|
||||
@@ -57,28 +62,29 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
? normalizedIntroUrl
|
||||
: `/${normalizedIntroUrl}`
|
||||
finalIntroUrl = `/${langCode}${pathWithSlash}`
|
||||
|
||||
|
||||
// 쿼리스트링이 있으면 다시 추가
|
||||
if (introPageUrl.includes('?')) {
|
||||
finalIntroUrl += '?' + introPageUrl.split('?')[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 무한 리다이렉트 방지: 현재 경로와 리다이렉트할 URL 비교
|
||||
const normalizedFinalUrl = finalIntroUrl.split('?')[0] // 쿼리스트링 제외
|
||||
const currentPath = to.path
|
||||
const isSamePath = !isExternalUrl && (currentPath === normalizedFinalUrl)
|
||||
|
||||
const isSamePath = !isExternalUrl && currentPath === normalizedFinalUrl
|
||||
|
||||
if (!isSamePath) {
|
||||
// 다른 경로에서 접근한 경우에만 리다이렉트
|
||||
const queryString = to.fullPath.includes('?')
|
||||
? '?' + to.fullPath.split('?')[1]
|
||||
: ''
|
||||
const redirectUrl = queryString && !finalIntroUrl.includes('?')
|
||||
? `${finalIntroUrl}${queryString}`
|
||||
: finalIntroUrl
|
||||
console.log("🚀 ~ pageData.global redirectUrl:", redirectUrl)
|
||||
const redirectUrl =
|
||||
queryString && !finalIntroUrl.includes('?')
|
||||
? `${finalIntroUrl}${queryString}`
|
||||
: finalIntroUrl
|
||||
console.log('🚀 ~ pageData.global redirectUrl:', redirectUrl)
|
||||
return navigateTo(redirectUrl, { external: isExternalUrl })
|
||||
}
|
||||
}
|
||||
@@ -91,7 +97,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
pageUrl === '/' ||
|
||||
pageUrl === `/${langCode}/`
|
||||
) {
|
||||
console.log("🚀 ~ pageData.global /home 리다이렉트")
|
||||
console.log('🚀 ~ pageData.global /home 리다이렉트')
|
||||
return navigateTo(`/${langCode}/home`, { external: false })
|
||||
}
|
||||
|
||||
@@ -106,16 +112,18 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
}
|
||||
|
||||
// 미리보기 쿼리스트링에서 파라미터 값 추출
|
||||
// 미리보기 쿼리스트링에서 파라미터 값 추출
|
||||
// preview?page_seq=1&page_ver=1&lang_code=ko
|
||||
const queryString = to.fullPath.includes('?') ? to.fullPath.split('?')[1] : ''
|
||||
const queryString = to.fullPath.includes('?')
|
||||
? to.fullPath.split('?')[1]
|
||||
: ''
|
||||
const urlParams = new URLSearchParams(queryString)
|
||||
const pageSeq = urlParams.get('page_seq') || ''
|
||||
const pageVer = urlParams.get('page_ver') || ''
|
||||
const queryLangCode = urlParams.get('lang_code') || langCode
|
||||
|
||||
let queryParams: Record<string, string>;
|
||||
let apiUrl: string;
|
||||
let queryParams: Record<string, string>
|
||||
let apiUrl: string
|
||||
if (pageUrl === '/preview') {
|
||||
apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/page/preview`
|
||||
queryParams = {
|
||||
@@ -124,7 +132,6 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
page_ver: pageVer,
|
||||
_t: Date.now().toString(), // 캐시 무효화를 위한 타임스탬프
|
||||
}
|
||||
|
||||
} else {
|
||||
apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
|
||||
queryParams = {
|
||||
@@ -158,7 +165,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
|
||||
}
|
||||
|
||||
// 404 에러 코드 체크
|
||||
const isNotFoundError =
|
||||
const isNotFoundError =
|
||||
(response?.code === 91002 && response?.message === 'Invalid LangCode') ||
|
||||
response?.code === 91003 ||
|
||||
response?.code === 90004
|
||||
|
||||
@@ -191,8 +191,8 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
|
||||
:key="dIndex"
|
||||
>
|
||||
<p
|
||||
class="relative flex items-center justify-start w-full text-left text-[#999999] text-[14px] font-[400] leading-[24px] tracking-[-0.42px] md:text-[15px] md:tracking-[-0.45px]"
|
||||
v-dompurify-html="tm(description as string)"
|
||||
class="relative flex items-center justify-start w-full text-left text-[#999999] text-[14px] font-[400] leading-[24px] tracking-[-0.42px] md:text-[15px] md:tracking-[-0.45px]"
|
||||
></p>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user