Merge branch 'feature/202501107-all' into feature/20251201-gil_qa

This commit is contained in:
“hyeonggkim”
2025-12-09 19:17:37 +09:00
6 changed files with 112 additions and 123 deletions

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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>