Merge branch 'feature/202501107-all' into feature/20251001-gil
This commit is contained in:
@@ -1,20 +1,31 @@
|
||||
/* Layout Utility Classes */
|
||||
@layer components {
|
||||
.section-wrap {
|
||||
/* 표준형 */
|
||||
.section-standard {
|
||||
@apply relative h-[640px] md:h-[1000px];
|
||||
}
|
||||
|
||||
.section-content {
|
||||
.content-standard {
|
||||
@apply relative h-full flex flex-col items-center justify-center text-center px-[20px] sm:px-[40px];
|
||||
}
|
||||
|
||||
/* 고정형 */
|
||||
.section-container.static {
|
||||
@apply relative pt-[32px] pb-[80px] px-[20px] sm:px-[40px] md:pt-[64px] md:pb-[200px] bg-[#F0F0F0];
|
||||
}
|
||||
.section-static {
|
||||
@apply mx-auto max-w-[684px] md:max-w-[944px] lg:max-w-[1300px];
|
||||
}
|
||||
.section-static + .section-static {
|
||||
@apply mt-[80px] md:mt-[100px];
|
||||
}
|
||||
|
||||
.border-line {
|
||||
@apply overflow-hidden relative rounded-[4px] md:rounded-lg
|
||||
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full
|
||||
after:border after:border-white/10 after:rounded-[4px] after:md:rounded-lg;
|
||||
}
|
||||
|
||||
/* Title Utility Classes */
|
||||
/* 표준형 Title Classes */
|
||||
.title-xlg {
|
||||
@apply line-clamp-4 text-[24px] font-[700] leading-[34px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.6)] md:line-clamp-3 md:text-[50px] md:leading-[70px];
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ onBeforeUnmount(() => {
|
||||
<header class="header">
|
||||
<BlocksStoveGnbNew class="h-[48px]" />
|
||||
|
||||
<div class="game-wrapper" :class="{ 'is-fixed': isPassedStoveGnb }">
|
||||
<div class="game-wrap" :class="{ 'is-fixed': isPassedStoveGnb }">
|
||||
<AtomsLocaleLink to="/brand" class="mx-auto md:hidden">
|
||||
<img
|
||||
:src="getImageHost(gnbData?.bi_path)"
|
||||
@@ -361,11 +361,11 @@ onBeforeUnmount(() => {
|
||||
.header {
|
||||
@apply bg-theme-foreground text-theme-foreground-reversal relative z-[100];
|
||||
}
|
||||
.game-wrapper {
|
||||
.game-wrap {
|
||||
@apply absolute flex w-full h-[48px] items-center whitespace-nowrap px-[52px] bg-theme-foreground sm:px-[72px] md:h-16 md:pl-0 md:pr-[40px]
|
||||
before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-px before:bg-theme-foreground-reversal-6;
|
||||
}
|
||||
.game-wrapper.is-fixed {
|
||||
.game-wrap.is-fixed {
|
||||
@apply fixed top-0 left-0;
|
||||
}
|
||||
.game-logo {
|
||||
|
||||
32
layers/components/widgets/FixMainTitle.vue
Normal file
32
layers/components/widgets/FixMainTitle.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
resourcesData: PageDataResourceGroup
|
||||
}>()
|
||||
|
||||
const { getCurrentSrc } = useResponsiveSrc()
|
||||
|
||||
const resPath = computed(() => props.resourcesData?.res_path)
|
||||
const imageSrc = computed(() => {
|
||||
return getCurrentSrc(resPath.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-center items-center w-full h-[120px] bg-center px-[20px] bg-no-repeat bg-cover sm:px-[40px] md:h-[180px]"
|
||||
:style="{ backgroundImage: `url(${imageSrc})` }"
|
||||
>
|
||||
<h2
|
||||
class="flex justify-center items-center w-full max-w-full md:justify-start md:max-w-[1300px]"
|
||||
>
|
||||
<span
|
||||
class="flex justify-center items-center text-white text-[20px] font-bold tracking-[-0.6px] leading-[30px] md:text-[32px] md:leading-[44px] md:tracking-[-0.96px]"
|
||||
>
|
||||
{{ props.title }}
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
72
layers/components/widgets/FixSubTitle.vue
Normal file
72
layers/components/widgets/FixSubTitle.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
description?: string
|
||||
link?: string
|
||||
target?: '_self' | '_blank'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
target: '_blank',
|
||||
})
|
||||
|
||||
const componentTag = computed((): string => {
|
||||
switch (props.target) {
|
||||
case '_self':
|
||||
return 'AtomsLocaleLink'
|
||||
case '_blank':
|
||||
return 'a'
|
||||
default:
|
||||
return 'a'
|
||||
}
|
||||
})
|
||||
|
||||
const componentProps = computed(() => {
|
||||
if (props.target === '_self') {
|
||||
return {
|
||||
to: props.link,
|
||||
}
|
||||
}
|
||||
|
||||
if (props.target === '_blank') {
|
||||
return {
|
||||
href: props.link,
|
||||
target: props.target,
|
||||
rel: 'noopener noreferrer',
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-end justify-between">
|
||||
<h3
|
||||
class="text-[#1F1F1F] text-[18px] font-bold leading-[26px] tracking-[-0.54px] md:text-[24px] md:leading-[34px] md:tracking-[0.72px]"
|
||||
>
|
||||
<span>{{ props.title }}</span>
|
||||
</h3>
|
||||
<div class="flex items-center justify-between">
|
||||
<slot />
|
||||
<p
|
||||
v-if="props.description && !props.link"
|
||||
class="text-[#666666] text-[13px] font-[400] leading-[22px] tracking-[-0.325px] md:text-[14px] md:leading-[24px] md:tracking-[-0.421px]"
|
||||
>
|
||||
{{ props.description }}
|
||||
</p>
|
||||
<component
|
||||
:is="componentTag"
|
||||
v-else-if="props.description && props.link"
|
||||
v-bind="componentProps"
|
||||
class="relative flex items-center justify-center gap-[4px] w-auto h-auto text-[#3C75FF] text-[14px] font-[500] leading-[20px] tracking-[-0.42px] md:text-[16px] md:leading-[24px] md:tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-0 before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20"
|
||||
>
|
||||
<span>{{ props.description }}</span>
|
||||
<AtomsIconsWebLinkLine
|
||||
v-if="props.target === '_blank'"
|
||||
:size="20"
|
||||
color="#3C75FF"
|
||||
class="icon"
|
||||
/>
|
||||
</component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
189
layers/composables/useCheckPCSpec.ts
Normal file
189
layers/composables/useCheckPCSpec.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { useTokenValidation } from '#layers/composables/useTokenValidation'
|
||||
import { csrGoStoveLogin } from '#layers/utils/stoveUtil'
|
||||
|
||||
interface ReqCheckSpec {
|
||||
schemeFormat: string
|
||||
setupUrl: string
|
||||
gameNo: string
|
||||
locale: string
|
||||
}
|
||||
|
||||
export const useCheckPCSpec = (tm: Function) => {
|
||||
const { isTokenValid, validateToken } = useTokenValidation()
|
||||
|
||||
// Store
|
||||
const modalStore = useModalStore()
|
||||
const { handleOpenConfirm } = modalStore
|
||||
|
||||
const isButtonDisabled = ref(false) // 버튼 비활성화 여부
|
||||
|
||||
/**
|
||||
* STOVE 미로그인 상태일 경우 로그인 모달을 표시합니다.
|
||||
* 케이스에 따라 모달 내용을 변경할 수 있습니다.
|
||||
* @param content<string> 모달 내용(로그인 안내 메시지)
|
||||
*/
|
||||
const showLoginModal = (content: string) => {
|
||||
handleOpenConfirm({
|
||||
contentText: content,
|
||||
confirmButtonText: tm('Download_Text_StoveLogin'),
|
||||
modalName: 'modal-login',
|
||||
confirmButtonEvent: () => {
|
||||
csrGoStoveLogin()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PC사양 프로그램이 미설치일 경우 설치 안내 모달을 표시합니다.
|
||||
* @param {string} setupUrl 설치 프로그램 URL
|
||||
*/
|
||||
const showInstallGuideModal = (setupUrl: string) => {
|
||||
isButtonDisabled.value = false
|
||||
|
||||
handleOpenConfirm({
|
||||
contentText: tm('Download_Alert_InstallGuide'),
|
||||
confirmButtonText: tm('Download_Text_Download'),
|
||||
modalName: 'modal-download',
|
||||
isShowDimmed: true,
|
||||
confirmButtonEvent: () => {
|
||||
location.href = String(setupUrl)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* iframe을 생성합니다.
|
||||
* @returns {iframe<HTMLIFrameElement>} 생성된 iframe
|
||||
*/
|
||||
const createHiddenIframe = () => {
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.src = 'about:blank'
|
||||
iframe.id = 'hiddenIframe'
|
||||
iframe.style.display = 'none'
|
||||
document.body.appendChild(iframe)
|
||||
return iframe
|
||||
}
|
||||
|
||||
/**
|
||||
* Firefox 브라우저에서 URI를 열기 위한 함수입니다.
|
||||
* @param {string} uri URI
|
||||
* @param {Function} successCb 성공 콜백
|
||||
* @param {Function} failCb 실패 콜백
|
||||
*/
|
||||
const openUriUsingFirefox = (
|
||||
uri: string,
|
||||
successCb: Function,
|
||||
failCb: Function
|
||||
) => {
|
||||
let iframe = document.querySelector(
|
||||
'#hiddenIframe'
|
||||
) as HTMLIFrameElement | null
|
||||
if (!iframe) {
|
||||
iframe = createHiddenIframe() as HTMLIFrameElement
|
||||
}
|
||||
try {
|
||||
iframe.contentWindow!.location.href = uri
|
||||
successCb()
|
||||
} catch (e) {
|
||||
if ((e as any).name === 'NS_ERROR_UNKNOWN_PROTOCOL') {
|
||||
failCb()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chrome 브라우저에서 URI를 열기 위한 함수입니다.
|
||||
* @param {string} uri 사양 프로그램 URI
|
||||
* @param {Function} successCb 성공 콜백
|
||||
* @param {Function} failCb 실패 콜백
|
||||
*/
|
||||
const openUriWithTimeoutHack = (
|
||||
uri: string,
|
||||
successCb: Function,
|
||||
failCb: Function
|
||||
) => {
|
||||
let called = false
|
||||
const timeout = setTimeout(() => {
|
||||
if (!called) {
|
||||
called = true
|
||||
window.removeEventListener('blur', onBlur)
|
||||
failCb()
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
const onBlur = () => {
|
||||
if (!called) {
|
||||
called = true
|
||||
clearTimeout(timeout)
|
||||
window.removeEventListener('blur', onBlur)
|
||||
successCb()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('blur', onBlur)
|
||||
window.location.href = uri
|
||||
}
|
||||
|
||||
/**
|
||||
* PC사양 프로그램이 설치되어 있는지 여부를 확인합니다.
|
||||
* @param {string }uri 사양 프로그램 URI
|
||||
* @param {string} setupUrl 설치 프로그램 URL
|
||||
*/
|
||||
const checkPCSpecProgramInstalled = (uri: string, setupUrl: string) => {
|
||||
const device = useDevice()
|
||||
|
||||
const successCallback = () => {
|
||||
isButtonDisabled.value = false
|
||||
}
|
||||
|
||||
if (typeof (navigator as any).msLaunchUri === 'function') {
|
||||
;(navigator as any).msLaunchUri(
|
||||
uri,
|
||||
successCallback,
|
||||
showInstallGuideModal(setupUrl)
|
||||
)
|
||||
} else if (device.isFirefox) {
|
||||
// 파이어폭스 브라우저일 경우
|
||||
openUriUsingFirefox(uri, successCallback, () =>
|
||||
showInstallGuideModal(setupUrl)
|
||||
)
|
||||
} else if (device.isChrome || device.isEdge) {
|
||||
// 크롬 / 엣지 브라우저일 경우
|
||||
openUriWithTimeoutHack(uri, successCallback, () =>
|
||||
showInstallGuideModal(setupUrl)
|
||||
)
|
||||
} else {
|
||||
// 기타 미지원 브라우저일 경우
|
||||
showInstallGuideModal(setupUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 사양 체크를 시작합니다.
|
||||
* 최종 호출 함수입니다.
|
||||
* @param {ReqCheckSpec} req {schemeFormat:스킴 포맷, setupUrl:셋업 링크, gameNo:게임넘버, locale:언어}
|
||||
*/
|
||||
const checkPCSpec = async (req: ReqCheckSpec) => {
|
||||
const accessToken = useCookie('SUAT')
|
||||
await validateToken(accessToken.value || '')
|
||||
|
||||
// 로그인 상태 아닐 경우
|
||||
if (!isTokenValid.value) {
|
||||
showLoginModal(tm('Download_Alert_StoveLogin'))
|
||||
return
|
||||
}
|
||||
|
||||
const localeValue: string = req.locale === 'ja' ? 'jp' : req.locale
|
||||
const uri = `${req.schemeFormat}${accessToken.value}/${req.gameNo}/${localeValue}`
|
||||
|
||||
checkPCSpecProgramInstalled(uri, req.setupUrl)
|
||||
|
||||
isButtonDisabled.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
isButtonDisabled,
|
||||
|
||||
checkPCSpec,
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import GrDetail02 from '#layers/templates/GrDetail02/index.vue'
|
||||
import GrDetail03 from '#layers/templates/GrDetail03/index.vue'
|
||||
import GrBoard01 from '#layers/templates/GrBoard01/index.vue'
|
||||
import GrContents01 from '#layers/templates/GrContents01/index.vue'
|
||||
import FxDownload01 from '#layers/templates/FxDownload01/index.vue'
|
||||
|
||||
const templateRegistry = {
|
||||
GR_VISUAL_01: { component: GrVisual01 },
|
||||
@@ -22,6 +23,7 @@ const templateRegistry = {
|
||||
GR_DETAIL_02: { component: GrDetail02 },
|
||||
GR_DETAIL_03: { component: GrDetail03 },
|
||||
GR_CONTENTS_01: { component: GrContents01 },
|
||||
FX_DOWNLOAD_01: { component: FxDownload01 },
|
||||
} as const
|
||||
|
||||
type TemplateKey = keyof typeof templateRegistry
|
||||
|
||||
BIN
layers/public/images/common/grades_driver/Type-AMD.png
Normal file
BIN
layers/public/images/common/grades_driver/Type-AMD.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
layers/public/images/common/grades_driver/Type-DirectX.png
Normal file
BIN
layers/public/images/common/grades_driver/Type-DirectX.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
BIN
layers/public/images/common/grades_driver/Type-NVIDIA.png
Normal file
BIN
layers/public/images/common/grades_driver/Type-NVIDIA.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
BIN
layers/public/images/common/grades_driver/Type-Visual.png
Normal file
BIN
layers/public/images/common/grades_driver/Type-Visual.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
BIN
layers/public/images/common/grades_driver/Type-intel.png
Normal file
BIN
layers/public/images/common/grades_driver/Type-intel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -193,7 +193,8 @@ export default defineEventHandler(async event => {
|
||||
const config = useRuntimeConfig()
|
||||
const stoveApiUrlServer = config.public.stoveApiUrlServer
|
||||
const apiUrl = `${stoveApiUrlServer}/pub-comm/v1.0/template/game`
|
||||
|
||||
|
||||
console.log("🚀 ~ apiUrl:", apiUrl)
|
||||
|
||||
// 2. 언어 코드 추출
|
||||
const finalLocale = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
|
||||
@@ -203,7 +204,6 @@ export default defineEventHandler(async event => {
|
||||
game_domain: event.context.gameDomain || '',
|
||||
lang_code: finalLocale,
|
||||
}
|
||||
console.log("🚀 ~ apiUrl:", queryParams)
|
||||
const response = (await $fetch(apiUrl, {
|
||||
query: queryParams,
|
||||
})) as GameDataResponse | null
|
||||
@@ -224,12 +224,14 @@ export default defineEventHandler(async event => {
|
||||
let inspectionData
|
||||
if (cache.has(cacheKey)) {
|
||||
inspectionData = cache.get(cacheKey) as WebInspectionData
|
||||
// console.log("🚀 22222 ~ 캐시키 있어 inspectionData:", inspectionData)
|
||||
console.log("🚀 ~ response.value.game_id:", response.value.game_id)
|
||||
console.log("🚀 22222 ~ 캐시키 있어 inspectionData:", inspectionData)
|
||||
} else {
|
||||
// 점검 데이터 조회
|
||||
if (response.value.game_id) {
|
||||
const inspectionApiUrl = `${iBaseApiUrl}/pub-comm/v3.0/inspection/${response.value.game_id}`
|
||||
// console.log("🚀 ~ inspectionApiUrl:", inspectionApiUrl)
|
||||
console.log("🚀 ~ inspectionApiUrl:", inspectionApiUrl)
|
||||
console.log("🚀 33333 ~ 캐시키 없어 inspectionData:", inspectionData)
|
||||
|
||||
// 직접 $fetch 사용 (composable 사용하지 않음)
|
||||
const inspectionResponse = await $fetch<ResGetInspectionData>(inspectionApiUrl, {
|
||||
@@ -262,6 +264,8 @@ export default defineEventHandler(async event => {
|
||||
*/
|
||||
// 현재 경로가 점검 페이지가 아닐 경우 리다이렉트
|
||||
const inspectionPath = `/${finalLocale}/inspection`
|
||||
console.log("🚀 44444 ~ fullPath:", fullPath)
|
||||
console.log("🚀 44444 ~ inspectionPath:", inspectionPath)
|
||||
if (fullPath !== inspectionPath) {
|
||||
setCacheHeaders(event, 'no-cache')
|
||||
}
|
||||
|
||||
468
layers/templates/FxDownload01/index.vue
Normal file
468
layers/templates/FxDownload01/index.vue
Normal file
@@ -0,0 +1,468 @@
|
||||
<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'
|
||||
|
||||
// Types
|
||||
type Platform = 'google_play' | 'app_store' | 'pc' | 'stove'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
components: PageDataTemplateComponents
|
||||
pageVerTmplSeq: number
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Configuration
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const runType = runtimeConfig.public.runType
|
||||
const staticUrl = runtimeConfig.public.staticUrl
|
||||
const multilingualBaseApiUrl = `${staticUrl}/${runType}/test`
|
||||
const multilingualFileName = 'test_homepage_brand_download.json'
|
||||
|
||||
// Multilingual
|
||||
const resultGetMultilingual = await useGetMultilingual({
|
||||
baseApiUrl: multilingualBaseApiUrl,
|
||||
fileName: multilingualFileName,
|
||||
})
|
||||
const { tm, locale }: any = useI18n({
|
||||
useScope: 'local',
|
||||
messages: Object(resultGetMultilingual?.value?.multilingual),
|
||||
})
|
||||
|
||||
// Composables
|
||||
const device = useDevice()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
const { checkPCSpec } = useCheckPCSpec(tm)
|
||||
|
||||
// Store
|
||||
const gameDataStore = useGameDataStore()
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
// Data
|
||||
const backgroundData = computed(() =>
|
||||
getComponentGroup(props.components, 'background')
|
||||
)
|
||||
const specCheckData = computed(() =>
|
||||
getComponentGroupAry(props.components, 'tablePcSpecTool')
|
||||
)
|
||||
const schemeFormatData = computed(
|
||||
() => specCheckData.value[0]?.display?.text || ''
|
||||
)
|
||||
const setupUrlData = computed(() => specCheckData.value[1]?.display?.text || '')
|
||||
|
||||
// Refs
|
||||
const specPCRef = ref<HTMLElement | null>(null)
|
||||
const specMobileRef = ref<HTMLElement | null>(null)
|
||||
const driverArray = ref<Array<string>>([
|
||||
'DirectX',
|
||||
'NVIDIA',
|
||||
'AMD',
|
||||
'intel',
|
||||
'Visual',
|
||||
])
|
||||
const pcSpecArray = ref<Array<string>>([
|
||||
'Os',
|
||||
'Cpu',
|
||||
'Vga',
|
||||
'Ram',
|
||||
'Directx',
|
||||
'Hdd',
|
||||
])
|
||||
const mobileSpecArray = ref<Array<string>>(['Android', 'Ios'])
|
||||
const mobileOSArray = ref<Array<string>>(['AOS', 'iOS'])
|
||||
|
||||
// Computed
|
||||
const platformList = computed(() => {
|
||||
if (breakpoints.value.isMobile) {
|
||||
return ['MOBILE', 'PC', 'STOVE']
|
||||
} else {
|
||||
return ['PC', 'STOVE', 'MOBILE']
|
||||
}
|
||||
})
|
||||
const driverList = computed(() =>
|
||||
driverArray.value.map(driver => ({
|
||||
id: `DRIVER_${driver}`,
|
||||
driverCode: driver,
|
||||
driverText: tm(`Download_Driver_${driver}`),
|
||||
}))
|
||||
)
|
||||
const pcSpecList = computed(() =>
|
||||
pcSpecArray.value.map(data => ({
|
||||
id: `PC_TABLE_ITEM_${data}`,
|
||||
itemCode: data,
|
||||
itemText: tm(`Download_Table_${data}`),
|
||||
itemData: getComponentGroupAry(props.components, `tablePcSpec${data}`),
|
||||
}))
|
||||
)
|
||||
const mobileSpecList = computed(() =>
|
||||
mobileSpecArray.value.map(data => ({
|
||||
id: `MO_TABLE_ITEM_${data}`,
|
||||
itemCode: data,
|
||||
itemText: tm(`Download_Table_${data}`),
|
||||
itemData: getComponentGroupAry(props.components, `tableMoSpec${data}`),
|
||||
}))
|
||||
)
|
||||
const mobileOSList = computed(() =>
|
||||
mobileOSArray.value.map(os => ({
|
||||
id: `MO_OS_${os}`,
|
||||
osCode: os,
|
||||
osText: tm(`Download_${os}_OS`),
|
||||
platformCode: tm(`Download_${os}_Platform`),
|
||||
platformText: tm(`Download_${os}_Name`),
|
||||
isValue: device[tm(`Download_${os}_IsValue`) as keyof typeof device],
|
||||
}))
|
||||
)
|
||||
|
||||
// Functions
|
||||
/**
|
||||
* 입력한 pc, mobile 인자값에 따라 해당 최소사양&권장사양 테이블로 포커스 스크롤 이동합니다.
|
||||
* @param target<string> 'pc' | 'mobile'
|
||||
*/
|
||||
const handleMoveFocus = (target: 'pc' | 'mobile') => {
|
||||
const refs: Record<string, HTMLElement | null> = {
|
||||
pc: specPCRef.value,
|
||||
mobile: specMobileRef.value,
|
||||
}
|
||||
|
||||
const adjustedOffset: number = breakpoints.value.isMobile ? 80 : 100
|
||||
const adjustedOffsetTop: number =
|
||||
refs[target]?.getBoundingClientRect().top + window.scrollY - adjustedOffset
|
||||
|
||||
window.scrollTo({ top: adjustedOffsetTop, behavior: 'smooth' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WidgetsFixMainTitle
|
||||
:title="tm('Download_Page_Title')"
|
||||
:resourcesData="backgroundData"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="overflow-hidden h-auto pt-[32px] px-[20px] pb-[80px] bg-[#F0F0F0] sm:px-[40px] md:pt-[64px] md:pb-[200px]"
|
||||
>
|
||||
<section class="relative flex flex-col items-center justify-start w-full">
|
||||
<div class="w-full max-w-full md:max-w-[1300px]">
|
||||
<WidgetsFixSubTitle :title="tm('Download_Section_Platform_Title')" />
|
||||
|
||||
<BlocksSlideDefault
|
||||
:per-page="platformList.length"
|
||||
:gap="20"
|
||||
:arrows="false"
|
||||
:pagination="false"
|
||||
:drag="false"
|
||||
:breakpoints="{
|
||||
1023: {
|
||||
autoWidth: true,
|
||||
perPage: 'auto',
|
||||
gap: 12,
|
||||
focus: 0,
|
||||
drag: true,
|
||||
padding: { left: 0, right: 0 },
|
||||
},
|
||||
}"
|
||||
class="w-[calc(100%+40px)] px-[20px] ml-[-20px] mt-[16px] sm:w-[calc(100%+80px)] sm:px-[40px] sm:ml-[-40px] md:w-full md:px-0 md:mt-[24px] md:ml-0"
|
||||
>
|
||||
<SplideSlide
|
||||
v-for="platform in platformList"
|
||||
:key="platform"
|
||||
class="flex flex-col items-center justify-between shrink-0 whitespace-normal w-[295px] h-[280px] bg-[#FFFFFF] p-[20px] rounded-[12px] text-left md:w-[calc((100%-40px)/3)] md:h-[314px] md:p-[24px] md:rounded-[16px] lg:w-[420px] lg:h-[340px] lg:p-[32px]"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-start justify-start gap-[8px] w-full md:gap-[12px]"
|
||||
>
|
||||
<WidgetsFixSubTitle
|
||||
:title="tm(`Download_Box_${platform}_Title`)"
|
||||
/>
|
||||
|
||||
<p
|
||||
v-if="
|
||||
tm(`Download_Box_${platform}_Description_List`).length === 1
|
||||
"
|
||||
v-dompurify-html="tm(`Download_Box_${platform}_Description01`)"
|
||||
class="text-[#999999] text-[14px] font-[400] leading-[24px] tracking-[-0.42px] md:text-[15px] md:tracking-[-0.45px]"
|
||||
></p>
|
||||
<ul
|
||||
v-else-if="
|
||||
tm(`Download_Box_${platform}_Description_List`).length > 1
|
||||
"
|
||||
>
|
||||
<li
|
||||
v-for="description in tm(
|
||||
`Download_Box_${platform}_Description_List`
|
||||
)"
|
||||
:key="description"
|
||||
v-dompurify-html="tm(description)"
|
||||
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] md:text-[15px] md:tracking-[-0.45px]"
|
||||
></li>
|
||||
</ul>
|
||||
|
||||
<AtomsButton
|
||||
v-if="platform !== 'STOVE'"
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="transparent"
|
||||
text-color="#1F1F1F"
|
||||
class="relative w-auto h-auto px-0 text-[16px] font-[500] leading-[24px] tracking-[-0.48px] before:content-[''] before:absolute before:z-[2] before:top-p before:left-0 before:w-full before:h-full before:bg-[#FFFFFF] before:transition-opacity before:duration-300 before:ease-in-out before:opacity-0 hover:before:opacity-20"
|
||||
@click="
|
||||
handleMoveFocus(platform.toLowerCase() as 'pc' | 'mobile')
|
||||
"
|
||||
>
|
||||
<span>{{ tm(`Download_Box_${platform}_SpecCheck`) }}</span>
|
||||
<AtomsIconsLongArrowRightLine
|
||||
:size="20"
|
||||
color="#1F1F1F"
|
||||
class="relative rotate-90"
|
||||
/>
|
||||
</AtomsButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-center justify-center gap-[8px] w-full md:gap-[12px]"
|
||||
>
|
||||
<template v-if="platform === 'MOBILE'">
|
||||
<template v-for="os in mobileOSList" :key="os.id">
|
||||
<AtomsButtonLauncher
|
||||
v-if="device.isMobile ? os.isValue : true"
|
||||
:platform="`${os.platformCode as Platform}`"
|
||||
class="!w-full"
|
||||
>
|
||||
<span>{{ os.platformText }}</span>
|
||||
</AtomsButtonLauncher>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AtomsButton
|
||||
v-if="breakpoints.isMobile"
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="#EBEBEB"
|
||||
text-color="#999999"
|
||||
:disabled="true"
|
||||
class="w-full px-0 border border-solid border-[rgba(0,0,0,0.1)] cursor-default"
|
||||
>
|
||||
<span>{{ tm(`Download_Button_${platform}_Mobile`) }}</span>
|
||||
</AtomsButton>
|
||||
<AtomsButtonLauncher
|
||||
v-else-if="breakpoints.isMd || breakpoints.isDesktop"
|
||||
:platform="`${platform.toLowerCase() as Platform}`"
|
||||
class="!w-full"
|
||||
>
|
||||
<span>{{ tm(`Download_Button_${platform}`) }}</span>
|
||||
</AtomsButtonLauncher>
|
||||
</template>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
</BlocksSlideDefault>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
ref="specPCRef"
|
||||
class="relative flex flex-col items-center justify-start w-full mt-[80px] md:mt-[100px]"
|
||||
>
|
||||
<div class="w-full max-w-full md:max-w-[1300px]">
|
||||
<WidgetsFixSubTitle :title="tm('Download_Section_PC_Title')" />
|
||||
|
||||
<div class="mt-[16px] md:mt-[24px]">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-[80px] md:w-[172px]">
|
||||
{{ tm('Download_Table_Item') }}
|
||||
</th>
|
||||
<th>{{ tm('Download_Table_Min_Spec') }}</th>
|
||||
<th>{{ tm('Download_Table_Rec_Spec') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="tr in pcSpecList" :key="tr.id">
|
||||
<tr>
|
||||
<th class="w-[80px] md:w-[172px]">{{ tr.itemText }}</th>
|
||||
<template v-for="td in tr.itemData" :key="td.display.text">
|
||||
<td>{{ td.display.text }}</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!breakpoints.isMobile"
|
||||
class="relative flex items-end justify-between w-full mt-[24px] gap-[24px]"
|
||||
>
|
||||
<ul
|
||||
v-if="tm('Download_Section_PC_Notice_List').length > 0"
|
||||
class="relative flex flex-col items-center justify-start w-full"
|
||||
>
|
||||
<template
|
||||
v-for="notice in tm('Download_Section_PC_Notice_List')"
|
||||
:key="notice"
|
||||
>
|
||||
<li
|
||||
v-dompurify-html="tm(notice)"
|
||||
class="relative w-full pl-[26px] text-left text-[#666666] text-[16px] font-[400] leading-[26px] tracking-[-0.48px] before:content-[''] before:absolute before:top-[11px] before:left-[10px] before:w-[4px] before:h-[4px] before:bg-[#666666] before:rounded-full"
|
||||
></li>
|
||||
</template>
|
||||
</ul>
|
||||
<AtomsButton
|
||||
type="action"
|
||||
button-size="size-small"
|
||||
background-color="#383838"
|
||||
text-color="#FFFFFF"
|
||||
class="shrink-0 w-[206px] px-0 text-[16px]"
|
||||
@click="
|
||||
checkPCSpec({
|
||||
schemeFormat: schemeFormatData,
|
||||
setupUrl: setupUrlData,
|
||||
gameNo: gameData?.game_code?.toString(),
|
||||
locale: locale as string,
|
||||
})
|
||||
"
|
||||
>
|
||||
<em
|
||||
class="inline-flex items-center justify-center gap-[8px] not-italic"
|
||||
>
|
||||
<span
|
||||
class="relative inline-flex items-center justify-center w-[20px] h-[20px]"
|
||||
>
|
||||
<img
|
||||
:src="
|
||||
getImageHost(
|
||||
'/images/common/ic-v2-hardware-desktop-line.svg',
|
||||
{ imageType: 'common' }
|
||||
)
|
||||
"
|
||||
alt="Desktop-Icon"
|
||||
loading="lazy"
|
||||
draggable="false"
|
||||
class="w-full object-contain"
|
||||
/>
|
||||
</span>
|
||||
<span>{{ tm('Download_Button_SpecCheck') }}</span>
|
||||
</em>
|
||||
</AtomsButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="!breakpoints.isMobile"
|
||||
class="relative flex flex-col items-center justify-start w-full mt-[80px] md:mt-[100px]"
|
||||
>
|
||||
<div class="w-full max-w-full md:max-w-[1300px]">
|
||||
<WidgetsFixSubTitle :title="tm('Download_Section_Driver_Title')" />
|
||||
|
||||
<div class="mt-[16px] md:mt-[24px]">
|
||||
<ul class="flex items-center justify-between gap-[20px] w-full">
|
||||
<template v-for="driver in driverList" :key="driver.id">
|
||||
<li class="w-full h-[250px]">
|
||||
<div
|
||||
class="flex flex-col items-center justify-between gap-[16px] h-[250px] rounded-[16px] p-[20px] bg-[#FFFFFF]"
|
||||
>
|
||||
<div class="flex items-center justify-center w-full">
|
||||
<img
|
||||
:src="
|
||||
getImageHost(
|
||||
`/images/common/grades_driver/Type-${driver.driverCode}.svg`,
|
||||
{ imageType: 'common' }
|
||||
)
|
||||
"
|
||||
:alt="driver.driverText"
|
||||
loading="lazy"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-start justify-end gap-[16px] w-full"
|
||||
>
|
||||
<p
|
||||
class="text-left text-[#666666] text-[16px] font-[400] leading-[26px] tracking-[-0.48px]"
|
||||
>
|
||||
{{ driver.driverText }}
|
||||
</p>
|
||||
|
||||
<AtomsButton
|
||||
type="download"
|
||||
button-size="size-small"
|
||||
background-color="#383838"
|
||||
text-color="#FFFFFF"
|
||||
class="w-full px-0"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:href="tm(`Download_Driver_${driver.driverCode}_Url`)"
|
||||
>
|
||||
<span>{{ tm('Download_Button_Download') }}</span>
|
||||
</AtomsButton>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
ref="specMobileRef"
|
||||
class="relative flex flex-col items-center justify-start w-full mt-[80px] md:mt-[100px]"
|
||||
>
|
||||
<div class="w-full max-w-full md:max-w-[1300px]">
|
||||
<WidgetsFixSubTitle :title="tm('Download_Section_MOBILE_Title')" />
|
||||
|
||||
<div class="mt-[16px] md:mt-[24px]">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2" class="w-[80px] md:w-[172px]">
|
||||
{{ tm('Download_Table_Item') }}
|
||||
</th>
|
||||
<th colspan="2">{{ tm('Download_Table_Min_Spec') }}</th>
|
||||
<th colspan="2">{{ tm('Download_Table_Rec_Spec') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ tm('Download_Table_Device') }}</th>
|
||||
<th>{{ tm('Download_Table_Os') }}</th>
|
||||
<th>{{ tm('Download_Table_Device') }}</th>
|
||||
<th>{{ tm('Download_Table_Os') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="tr in mobileSpecList" :key="tr.id">
|
||||
<tr>
|
||||
<th class="w-[80px] md:w-[172px]">{{ tr.itemText }}</th>
|
||||
<template v-for="td in tr.itemData" :key="td.display.text">
|
||||
<td>{{ td.display.text }}</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 고정형 페이지 Table Style */
|
||||
table {
|
||||
@apply w-full h-auto border-collapse border-spacing-0 table-fixed;
|
||||
}
|
||||
table th {
|
||||
@apply py-[8px] px-[12px] border border-[#D9D9D9] bg-[#FAFAFA] text-[#1F1F1F] text-[14px] font-bold leading-[24px] tracking-[-0.42px]
|
||||
md:py-[11px] md:px-[20px] md:text-[16px] md:leading-[26px] md:tracking-[-0.48px];
|
||||
}
|
||||
table td {
|
||||
@apply h-[64px] py-[8px] px-[12px] border border-[#D9D9D9] bg-[#FFFFFF] text-[#666666] text-[14px] font-[400] leading-[24px] tracking-[-0.42px]
|
||||
md:h-[80px] md:py-[14px] md:px-[20px] md:text-[16px] md:leading-[26px] md:tracking-[-0.48px];
|
||||
}
|
||||
|
||||
/* 플랫폼별 다운로드 Mobile Overflow Visible 처리 */
|
||||
.splide :deep(.splide__track) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
</style>
|
||||
@@ -107,7 +107,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
:style="getPaginationClass(paginationData)"
|
||||
>
|
||||
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
|
||||
<div class="section-content px-0">
|
||||
<div class="content-standard px-0">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
|
||||
@@ -32,7 +32,7 @@ const buttonListData = computed(() => {
|
||||
<template>
|
||||
<section class="relative py-[80px] md:py-[120px]">
|
||||
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
|
||||
<div class="section-content">
|
||||
<div class="content-standard">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
|
||||
@@ -41,7 +41,7 @@ const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="section-wrap">
|
||||
<section class="section-standard">
|
||||
<BlocksSlideFade
|
||||
v-if="slideData"
|
||||
ref="splideRef"
|
||||
@@ -58,7 +58,7 @@ const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
v-if="hasComponentGroup(item, 'background')"
|
||||
:resources-data="getComponentGroup(item, 'background')"
|
||||
/>
|
||||
<div class="section-content">
|
||||
<div class="content-standard">
|
||||
<WidgetsMainTitle
|
||||
v-if="hasComponentGroup(item, 'mainTitle')"
|
||||
:resources-data="getComponentGroup(item, 'mainTitle')"
|
||||
|
||||
@@ -22,7 +22,7 @@ const paginationData = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="section-wrap">
|
||||
<section class="section-standard">
|
||||
<BlocksSlideThumbnail
|
||||
:slide-data="slideData"
|
||||
:pagination-data="paginationData"
|
||||
@@ -32,7 +32,7 @@ const paginationData = computed(() => {
|
||||
v-if="hasComponentGroup(item, 'background')"
|
||||
:resources-data="getComponentGroup(item, 'background')"
|
||||
/>
|
||||
<div class="section-content">
|
||||
<div class="content-standard">
|
||||
<WidgetsMainTitle
|
||||
v-if="hasComponentGroup(item, 'mainTitle')"
|
||||
:resources-data="getComponentGroup(item, 'mainTitle')"
|
||||
|
||||
@@ -43,7 +43,7 @@ const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="section-wrap">
|
||||
<section class="section-standard">
|
||||
<BlocksSlideThumbnail
|
||||
:slide-data="slideData"
|
||||
:pagination-data="paginationData"
|
||||
@@ -60,7 +60,7 @@ const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
:resources-data="getComponentGroup(item, 'foreground')"
|
||||
/>
|
||||
<div
|
||||
class="section-content max-w-[1024px] mx-auto items-start pt-[48px] md:pt-0"
|
||||
class="content-standard max-w-[1024px] mx-auto items-start pt-[48px] md:pt-0"
|
||||
>
|
||||
<WidgetsSubTitle
|
||||
v-if="hasComponentGroup(item, 'category')"
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
getComponentGroup,
|
||||
isTypeVideo,
|
||||
} from '#layers/utils/dataUtil'
|
||||
import { getMediaImgSrc, getMediaSrc } from '#layers/utils/youtubeUtil'
|
||||
import { getMediaImgSrc } from '#layers/utils/styleUtil'
|
||||
import type {
|
||||
PageDataTemplateComponents,
|
||||
PageDataTemplateComponentSet,
|
||||
@@ -50,7 +50,8 @@ const getMediaImgSrcFromItem = (item: PageDataTemplateComponentSet) => {
|
||||
const getYouTubeEmbedUrlFromMedia = (item: PageDataTemplateComponentSet) => {
|
||||
const mediaComponent = getMediaComponent(item)
|
||||
if (!mediaComponent) return ''
|
||||
const mediaSrc = getMediaSrc(mediaComponent)
|
||||
|
||||
const mediaSrc = mediaComponent.display?.text
|
||||
return mediaSrc ? getYouTubeEmbedUrl(mediaSrc, true) : ''
|
||||
}
|
||||
|
||||
@@ -102,9 +103,9 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="section-wrap min-h-[700px]">
|
||||
<section class="section-standard min-h-[700px]">
|
||||
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
|
||||
<div class="section-content">
|
||||
<div class="content-standard">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
|
||||
@@ -59,9 +59,9 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="section-wrap">
|
||||
<section class="section-standard">
|
||||
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
|
||||
<div class="section-content px-0">
|
||||
<div class="content-standard px-0">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
@@ -98,7 +98,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-wrap {
|
||||
.section-standard {
|
||||
@apply before:hidden md:before:block before:content-[''] before:absolute before:top-0 before:left-0 before:w-[104px] before:h-full before:bg-gradient-to-l from-transparent to-[rgba(0,0,0,0.7)] before:z-[5]
|
||||
after:hidden md:after:block after:content-[''] after:absolute after:top-0 after:right-0 after:w-[104px] after:h-full after:bg-gradient-to-r from-transparent to-[rgba(0,0,0,0.7)];
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ const onArrowClick = (direction, targetIndex) => {
|
||||
<template>
|
||||
<section class="pt-[80px] pb-[100px] md:pt-[120px] md:pb-[140px]">
|
||||
<WidgetsBackground v-if="backgroundData" :resources-data="backgroundData" />
|
||||
<div class="section-content px-0 max-w-[2043px] mx-auto">
|
||||
<div class="content-standard px-0 max-w-[2043px] mx-auto">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
|
||||
@@ -27,13 +27,13 @@ const buttonListData = computed(() =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="section-wrap">
|
||||
<section class="section-standard">
|
||||
<WidgetsBackground
|
||||
v-if="backgroundData"
|
||||
:resources-data="backgroundData"
|
||||
gradient="h-[342px] bg-[linear-gradient(180deg,rgba(16,13,15,0)_0%,#100D0F_90%)] md:h-[720px]"
|
||||
/>
|
||||
<div class="section-content gap-4 md:gap-5">
|
||||
<div class="content-standard gap-4 md:gap-5">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
|
||||
@@ -90,7 +90,7 @@ const onArrowClick = direction => {
|
||||
:resources-data="backgroundData"
|
||||
gradient="h-[440px] bg-[linear-gradient(180deg,rgba(16,13,15,0)_0%,#100D0F_40%)] md:h-[720px] md:bg-[linear-gradient(180deg,rgba(16,13,15,0)_0%,#100D0F_50%)]"
|
||||
/>
|
||||
<div class="section-content px-0 gap-5">
|
||||
<div class="content-standard px-0 gap-5">
|
||||
<WidgetsMainTitle
|
||||
v-if="mainTitleData"
|
||||
:resources-data="mainTitleData"
|
||||
|
||||
@@ -33,7 +33,7 @@ const onArrowClick = direction => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="section-wrap">
|
||||
<section class="section-standard">
|
||||
<BlocksSlideFade
|
||||
v-if="slideData"
|
||||
:arrows="true"
|
||||
@@ -47,7 +47,7 @@ const onArrowClick = direction => {
|
||||
v-if="hasComponentGroup(item, 'background')"
|
||||
:resources-data="getComponentGroup(item, 'background')"
|
||||
/>
|
||||
<div class="section-content gap-3 md:gap-5">
|
||||
<div class="content-standard gap-3 md:gap-5">
|
||||
<WidgetsSubTitle
|
||||
v-if="hasComponentGroup(item, 'subTitle')"
|
||||
:resources-data="getComponentGroup(item, 'subTitle')"
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
// OperateResources
|
||||
export interface OperateGroupItem {
|
||||
seq: number
|
||||
flag_type?: number
|
||||
sort_order: number
|
||||
title: string
|
||||
img_path: string
|
||||
url: string
|
||||
link_target: string
|
||||
display_status: number
|
||||
reg_dt?: number
|
||||
reg_dt: number
|
||||
option01: number
|
||||
option02: number
|
||||
option03: string
|
||||
|
||||
@@ -116,21 +116,6 @@ export const getComponentContainer = (
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 그룹의 첫 번째 데이터를 반환합니다.
|
||||
* @param components props.components 또는 group 객체
|
||||
* @param componentName 컴포넌트 이름
|
||||
* @returns 첫 번째 그룹 데이터 또는 null
|
||||
*/
|
||||
export const getComponentGroup = (
|
||||
components: PageDataTemplateComponents | OperateComponents,
|
||||
componentName: string
|
||||
) => {
|
||||
if (!components) return null
|
||||
|
||||
return components[componentName]?.groups?.[0] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 그룹의 모든 데이터를 반환합니다.
|
||||
* @param components props.components 또는 group 객체
|
||||
@@ -206,3 +191,17 @@ export const formatDateOffset = ({
|
||||
return `${format}`
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 컴포넌트 그룹의 첫 번째 데이터를 반환합니다.
|
||||
* @param components props.components 또는 group 객체
|
||||
* @param componentName 컴포넌트 이름
|
||||
* @returns 첫 번째 그룹 데이터 또는 null
|
||||
*/
|
||||
export const getComponentGroup = (
|
||||
components: PageDataTemplateComponents | OperateComponents,
|
||||
componentName: string
|
||||
) => {
|
||||
if (!components) return null
|
||||
|
||||
return components[componentName]?.groups?.[0] || null
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* 스타일 유틸리티 함수
|
||||
* @description 스타일 처리에 필요한 유틸리티 함수를 제공합니다.
|
||||
* @description ui 처리에 필요한 유틸리티 함수를 제공합니다.
|
||||
*/
|
||||
|
||||
import { isTypeVideo } from '#layers/utils/dataUtil'
|
||||
import type {
|
||||
PageDataResourceGroups,
|
||||
PageDataResourceGroup,
|
||||
PageDataResourceGroupResPath,
|
||||
} from '#layers/types/api/pageData'
|
||||
|
||||
@@ -128,3 +130,26 @@ export const getPaginationClass = (
|
||||
'--pagination-disabled': paginationDisabled,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 이미지를 반환합니다. (이미지 / 유튜브 썸네일)
|
||||
* @param resourceGroups - 미디어 리소스 그룹 객체
|
||||
* @param quality - 썸네일 품질
|
||||
* @returns 미디어 이미지 소스 (이미지 / 유튜브 썸네일)
|
||||
*/
|
||||
export const getMediaImgSrc = (
|
||||
resourceGroups: PageDataResourceGroup,
|
||||
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'high'
|
||||
): string => {
|
||||
if (!resourceGroups) return ''
|
||||
|
||||
const mediaSrc = resourceGroups?.display?.text
|
||||
const mediaType = resourceGroups?.resource_type
|
||||
|
||||
if (isTypeVideo(mediaType) && mediaSrc) {
|
||||
const thumbnailUrl = getYouTubeThumbnail(mediaSrc, quality)
|
||||
return thumbnailUrl
|
||||
}
|
||||
|
||||
return mediaSrc || ''
|
||||
}
|
||||
|
||||
@@ -3,15 +3,12 @@
|
||||
* @description 유튜브 관련 유틸리티 함수를 제공합니다.
|
||||
*/
|
||||
|
||||
import { isTypeVideo } from '#layers/utils/dataUtil'
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
|
||||
/**
|
||||
* 유튜브 URL에서 비디오 ID를 추출합니다.
|
||||
* @param url - 유튜브 URL (watch, embed, youtu.be 등 다양한 형태 지원)
|
||||
* @returns 비디오 ID 또는 빈 문자열
|
||||
*/
|
||||
export const getYouTubeVideoId = (url: string): string => {
|
||||
export const getYouTubeId = (url: string): string => {
|
||||
if (!url) return ''
|
||||
|
||||
// 다양한 유튜브 URL 패턴 지원
|
||||
@@ -45,7 +42,7 @@ export const getYouTubeEmbedUrl = (
|
||||
autoplay: boolean = false,
|
||||
rel: boolean = false
|
||||
): string => {
|
||||
const videoId = getYouTubeVideoId(url)
|
||||
const videoId = getYouTubeId(url)
|
||||
if (!videoId) return ''
|
||||
|
||||
const params = new URLSearchParams()
|
||||
@@ -57,15 +54,16 @@ export const getYouTubeEmbedUrl = (
|
||||
}
|
||||
|
||||
/**
|
||||
* 유튜브 비디오 ID로부터 썸네일 URL을 생성합니다.
|
||||
* @param videoId - 유튜브 비디오 ID
|
||||
* 유튜브 URL에서 비디오 ID를 추출하고, 비디오 ID로부터 썸네일 URL을 생성합니다.
|
||||
* @param url - 유튜브 URL
|
||||
* @param quality - 썸네일 품질 ('default', 'medium', 'high', 'standard', 'maxres')
|
||||
* @returns 썸네일 URL
|
||||
*/
|
||||
export const getYouTubeThumbnail = (
|
||||
videoId: string,
|
||||
url: string,
|
||||
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'high'
|
||||
): string => {
|
||||
const videoId = getYouTubeId(url)
|
||||
if (!videoId) return ''
|
||||
|
||||
const qualityMap = {
|
||||
@@ -78,52 +76,3 @@ export const getYouTubeThumbnail = (
|
||||
|
||||
return `https://img.youtube.com/vi/${videoId}/${qualityMap[quality]}.jpg`
|
||||
}
|
||||
|
||||
/**
|
||||
* 유튜브 URL에서 직접 썸네일 URL을 추출합니다.
|
||||
* @param url - 유튜브 URL
|
||||
* @param quality - 썸네일 품질
|
||||
* @returns 썸네일 URL
|
||||
*/
|
||||
export const getYouTubeThumbnailFromUrl = (
|
||||
url: string,
|
||||
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'high'
|
||||
): string => {
|
||||
const videoId = getYouTubeVideoId(url)
|
||||
return getYouTubeThumbnail(videoId, quality)
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 text(src)를 추출합니다.
|
||||
* @param source - 미디어 소스 객체
|
||||
* @returns 미디어 text(src)
|
||||
*/
|
||||
export const getMediaSrc = (resourceGroup: PageDataResourceGroup): string => {
|
||||
if (!resourceGroup) return ''
|
||||
const mediaSrc = resourceGroup?.display?.text
|
||||
return mediaSrc || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 이미지를 추출합니다. (유튜브인 경우 썸네일)
|
||||
* @param source - 미디어 소스 객체
|
||||
* @param quality - 썸네일 품질
|
||||
* @returns 미디어 이미지 소스
|
||||
*/
|
||||
export const getMediaImgSrc = (
|
||||
resourceGroups: PageDataResourceGroup,
|
||||
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'high'
|
||||
): string => {
|
||||
if (!resourceGroups) return ''
|
||||
|
||||
const mediaSrc = getMediaSrc(resourceGroups)
|
||||
const mediaType = resourceGroups?.resource_type
|
||||
|
||||
if (isTypeVideo(mediaType) && mediaSrc) {
|
||||
const videoId = getYouTubeVideoId(mediaSrc)
|
||||
const thumbnailUrl = getYouTubeThumbnail(videoId, quality)
|
||||
return thumbnailUrl
|
||||
}
|
||||
|
||||
return mediaSrc || ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user