feat.[SWV-812] 공통 리소스 이미지 관리 방식 변경(videoPlay, Arrow)
@@ -96,7 +96,7 @@ const setupGameHead = (data: GameDataValue) => {
|
||||
const designTheme = data.design_theme === 1 ? 'light' : 'dark'
|
||||
const styleLinks = createStyleLinks(
|
||||
data.favicon_json,
|
||||
data?.game_font?.font_path
|
||||
data?.game_font_key_json?.font_path
|
||||
)
|
||||
const styleCss = createStyleCss(data.key_color_json)
|
||||
|
||||
|
||||
@@ -38,8 +38,4 @@
|
||||
.type-full .splide__arrow--next {
|
||||
@apply right-10;
|
||||
}
|
||||
|
||||
.splide-arrow svg {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { TrackingObject } from '#layers/types/api/common'
|
||||
|
||||
interface Props {
|
||||
bgColor?: string
|
||||
tracking: TrackingObject
|
||||
}
|
||||
|
||||
@@ -16,14 +17,20 @@ const handlePlayClick = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="btn-play" @click="handlePlayClick">
|
||||
<button
|
||||
class="btn-play"
|
||||
:style="{ backgroundColor: props.bgColor }"
|
||||
@click="handlePlayClick"
|
||||
>
|
||||
<AtomsIconsArrowRightFill />
|
||||
<span class="sr-only">Play</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.btn-play {
|
||||
@apply relative w-[60px] h-[60px] bg-[image:var(--video-play)] bg-cover bg-center bg-no-repeat md:w-[80px] md:h-[80px]
|
||||
@apply relative flex items-center justify-center rounded-full w-[60px] h-[60px] md:w-[80px] md:h-[80px]
|
||||
before:content-[''] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border before:border-[rgba(255,255,255,0.5)] before:rounded-full
|
||||
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:bg-white after:rounded-[50%] after:opacity-0 after:transition-opacity after:duration-300 after:ease-in-out
|
||||
hover:after:opacity-10;
|
||||
}
|
||||
|
||||
@@ -50,11 +50,9 @@ const onSelectOption = (option: { [key: string | number]: any }): void => {
|
||||
>
|
||||
{{ selectedOption }}
|
||||
</span>
|
||||
<i
|
||||
class="inline-flex items-center justify-center w-[14px] h-[14px] shrink-0"
|
||||
>
|
||||
<AtomsIconsSelectArrowDownFill
|
||||
:size="12"
|
||||
<i class="inline-flex items-center justify-center shrink-0">
|
||||
<AtomsIconsArrowDownFill
|
||||
:size="14"
|
||||
color="#333333"
|
||||
:class="isActive ? 'rotate-180' : ''"
|
||||
/>
|
||||
|
||||
31
layers/components/atoms/icons/ArrowControlTopLine.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
fillOpacity?: number
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 24,
|
||||
color: '#ffffff',
|
||||
fillOpacity: 0.5,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 24 24"
|
||||
:fill="color"
|
||||
:fill-opacity="fillOpacity"
|
||||
>
|
||||
<path
|
||||
d="M13 9.41422L17.2929 13.7071C17.6834 14.0976 18.3166 14.0976 18.7071 13.7071C19.0976 13.3166 19.0976 12.6834 18.7071 12.2929L12.7078 6.29361C12.5289 6.1143 12.2822 6.00257 12.0094 6.00005C12.0063 6.00002 12.0032 6.00001 12 6.00001C11.9968 6.00001 11.9937 6.00002 11.9906 6.00005C11.7269 6.00249 11.4877 6.10694 11.3104 6.27585C11.3045 6.28145 11.2987 6.28714 11.2929 6.2929L5.29289 12.2929C4.90237 12.6834 4.90237 13.3166 5.29289 13.7071C5.68341 14.0976 6.31658 14.0976 6.7071 13.7071L11 9.41422L11 20C11 20.5523 11.4477 21 12 21C12.5523 21 13 20.5523 13 20L13 9.41422Z"
|
||||
/>
|
||||
<path
|
||||
d="M19.5 4.00001C19.5 4.55229 19.0523 5 18.5 5L5.5 5C4.94771 5 4.5 4.55228 4.5 4C4.5 3.44772 4.94771 3 5.5 3L18.5 3C19.0523 3 19.5 3.44772 19.5 4.00001Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
30
layers/components/atoms/icons/ArrowRightFill.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
fillOpacity?: number
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: '#ffffff',
|
||||
fillOpacity: 0.5,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 32 32"
|
||||
:fill="color"
|
||||
:fill-opacity="fillOpacity"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.4839 25.0812C12.7639 26.0629 11.079 26.9852 9.42912 27.8405C8.49538 28.3239 7.40719 27.6545 7.33699 26.5612C7.21062 24.62 7.11935 22.6119 7.06318 20.5443C7.02106 19.0568 7 17.5396 7 16C7 14.4604 7.02106 12.9506 7.06318 11.4557C7.11935 9.38809 7.21062 7.37997 7.33699 5.43879C7.40719 4.34548 8.49538 3.6761 9.42912 4.15954C11.079 5.01485 12.7709 5.9371 14.4839 6.91884C15.7196 7.6254 16.9692 8.36171 18.2259 9.13521C19.4826 9.90871 20.7112 10.6822 21.9047 11.4631C23.5686 12.549 25.1622 13.6349 26.6857 14.7208C27.5492 15.3306 27.5492 16.6694 26.6857 17.2793C25.1622 18.3651 23.5615 19.4584 21.9047 20.5369C20.7112 21.3178 19.4826 22.0987 18.2259 22.8648C16.9692 23.6383 15.7196 24.3672 14.4839 25.0812Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,26 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: '#EBEBEB',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 10 6"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.06454 4.95333L0.204544 1.16667C-0.228789 0.74 0.0712106 0 0.684544 0L9.31787 0C9.9312 0 10.2312 0.74 9.79787 1.16667L5.93787 4.95333C5.41787 5.46 4.59121 5.46 4.07121 4.95333H4.06454Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -42,10 +42,10 @@ const PLATFORM_ICON_MAP: Record<Platform, string> = {
|
||||
} as const
|
||||
|
||||
const DUP_IMAGE_MAP: Record<Platform, string> = {
|
||||
google_play: '/images/common/btn_logo-google.svg',
|
||||
app_store: '/images/common/btn_logo-app.svg',
|
||||
pc: '/images/common/btn_logo-pc.svg',
|
||||
stove: '/images/common/btn_logo-stove.svg',
|
||||
google_play: '/images/common/btn_launcher/btn_logo-google.svg',
|
||||
app_store: '/images/common/btn_launcher/btn_logo-app.svg',
|
||||
pc: '/images/common/btn_launcher/btn_logo-pc.svg',
|
||||
stove: '/images/common/btn_launcher/btn_logo-stove.svg',
|
||||
} as const
|
||||
|
||||
const componentTag = computed(() => {
|
||||
|
||||
@@ -26,7 +26,9 @@ const handleScrollToTop = () => {
|
||||
class="btn-top"
|
||||
sr-only="top"
|
||||
@click="handleScrollToTop"
|
||||
/>
|
||||
>
|
||||
<AtomsIconsArrowControlTopLine />
|
||||
</AtomsButtonCircle>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -10,6 +10,13 @@ const props = defineProps<Props>()
|
||||
const { locale } = useI18n()
|
||||
const { sendLog } = useAnalytics()
|
||||
|
||||
const getArrowBgColor = (direction: 'prev' | 'next') => {
|
||||
return getColorCodeFromData(
|
||||
props.arrowsData?.[direction === 'prev' ? 0 : 1]?.display,
|
||||
'none'
|
||||
)
|
||||
}
|
||||
|
||||
const handleArrowClick = (direction: 'prev' | 'next') => {
|
||||
if (props.arrowsData) {
|
||||
const arrowIndex = direction === 'prev' ? 0 : 1
|
||||
@@ -23,12 +30,18 @@ const handleArrowClick = (direction: 'prev' | 'next') => {
|
||||
<AtomsButtonCircle
|
||||
sr-only="Previous"
|
||||
class="splide-arrow splide__arrow--prev"
|
||||
:style="{ backgroundColor: getArrowBgColor('prev') }"
|
||||
@click="handleArrowClick('prev')"
|
||||
/>
|
||||
>
|
||||
<AtomsIconsArrowRightLine color="#ffffff" />
|
||||
</AtomsButtonCircle>
|
||||
<AtomsButtonCircle
|
||||
sr-only="Next"
|
||||
class="splide-arrow splide__arrow--next"
|
||||
:style="{ backgroundColor: getArrowBgColor('next') }"
|
||||
@click="handleArrowClick('next')"
|
||||
/>
|
||||
>
|
||||
<AtomsIconsArrowRightLine color="#ffffff" />
|
||||
</AtomsButtonCircle>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -436,7 +436,7 @@ onMounted(() => {
|
||||
>
|
||||
{{ t('Text_MonthYear', { month: month + 1, year: year }) }}
|
||||
</span>
|
||||
<AtomsIconsSelectArrowDownFill :size="10" color="#333333" />
|
||||
<AtomsIconsArrowDownFill :size="16" color="#333333" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -105,7 +105,7 @@ const handlePagination = (page: number) => {
|
||||
:class="[
|
||||
'!w-full !h-full p-0 rounded-full text-center text-[14px] font-[500] leading-[24px] tracking-[-0.42px]',
|
||||
page === currentPage
|
||||
? '!bg-[#C7AE8B] !text-white cursor-default'
|
||||
? '!bg-[var(--primary)] !text-white cursor-default'
|
||||
: '',
|
||||
]"
|
||||
@click="handlePagination(page)"
|
||||
|
||||
@@ -7,6 +7,10 @@ const props = defineProps<{
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const bgColor = computed(() => {
|
||||
return getColorCodeFromData(props.resourcesData.display, 'none')
|
||||
})
|
||||
|
||||
// 비디오 플레이 버튼 클릭 핸들러
|
||||
const handleVideoPlayClick = () => {
|
||||
const youtubeUrl = props.resourcesData?.display?.text ?? ''
|
||||
@@ -18,6 +22,7 @@ const handleVideoPlayClick = () => {
|
||||
|
||||
<template>
|
||||
<AtomsButtonPlay
|
||||
:bg-color="bgColor"
|
||||
:tracking="props.resourcesData.tracking"
|
||||
@click="handleVideoPlayClick"
|
||||
/>
|
||||
|
||||
@@ -163,7 +163,7 @@ export default defineEventHandler(async event => {
|
||||
event.context.gameDomain = gameDomain
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('gameData load error:', error)
|
||||
console.error('[API Error] gameData load error:', error)
|
||||
}
|
||||
|
||||
if (gameDataResponse?.code === 0 && 'value' in gameDataResponse) {
|
||||
@@ -344,6 +344,10 @@ export default defineEventHandler(async event => {
|
||||
}
|
||||
} else {
|
||||
// ### 에러 응답 처리 -------------------------------------------------------------
|
||||
// API 에러 코드를 명확하게 로깅하여 타입 에러와 구분
|
||||
const apiErrorCode = gameDataResponse?.code
|
||||
const apiErrorMessage = gameDataResponse?.message
|
||||
|
||||
// 언어 코드 추출 시도
|
||||
let errorLocale = 'ko' // 기본값
|
||||
try {
|
||||
@@ -359,7 +363,7 @@ export default defineEventHandler(async event => {
|
||||
}
|
||||
|
||||
// 91001 에러인 경우 바로 리다이렉트
|
||||
if (gameDataResponse?.code === 91001) {
|
||||
if (apiErrorCode === 91001) {
|
||||
const errorPath = `/${errorLocale}/error`
|
||||
event.node.res.statusCode = 302
|
||||
event.node.res.setHeader('Location', errorPath)
|
||||
@@ -369,8 +373,8 @@ export default defineEventHandler(async event => {
|
||||
|
||||
// 다른 에러는 기존대로 throw
|
||||
throw createError({
|
||||
statusCode: gameDataResponse?.code || 500,
|
||||
statusMessage: gameDataResponse?.message,
|
||||
statusCode: apiErrorCode || 500,
|
||||
statusMessage: apiErrorMessage,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,7 +18,9 @@ export const useGameDataStore = defineStore('gameData', () => {
|
||||
snsJson: null as GameDataValue['sns_json'] | null,
|
||||
urlJson: null as GameDataValue['url_json'] | null,
|
||||
marketJson: null as GameDataValue['market_json'] | null,
|
||||
fontFamily: null as GameDataValue['game_font']['font_family'] | null,
|
||||
fontFamily: null as
|
||||
| GameDataValue['game_font_key_json']['font_family']
|
||||
| null,
|
||||
gnb: null as GameDataValue['gnb'] | null,
|
||||
eventBanner: null as GameDataValue['event_banner'] | null,
|
||||
})
|
||||
@@ -42,7 +44,7 @@ export const useGameDataStore = defineStore('gameData', () => {
|
||||
state.snsJson = data?.sns_json
|
||||
state.urlJson = data?.url_json
|
||||
state.marketJson = data?.market_json
|
||||
state.fontFamily = data?.game_font?.font_family
|
||||
state.fontFamily = data?.game_font_key_json?.font_family
|
||||
state.gnb = data?.gnb
|
||||
state.eventBanner = data?.event_banner
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ const handleMoveFocus = (target: 'pc' | 'mobile') => {
|
||||
<img
|
||||
:src="
|
||||
formatPathHost(
|
||||
`/images/common/grades_driver/Type-${driver.driverCode}.svg`,
|
||||
`/images/common/download_driver/Type-${driver.driverCode}.svg`,
|
||||
{ imageType: 'common' }
|
||||
)
|
||||
"
|
||||
|
||||
@@ -50,20 +50,15 @@ const analytics = {
|
||||
action_type: 'click',
|
||||
click_sarea: props.pageVerTmplNameEn,
|
||||
} as TrackingObject
|
||||
const arrowsData: PageDataResourceGroups = [
|
||||
{
|
||||
tracking: {
|
||||
...analytics,
|
||||
click_item: '1. 컨텐츠 리스트 - 네비게이션(좌)',
|
||||
},
|
||||
const arrowsData: PageDataResourceGroups = ['좌', '우'].map(direction => ({
|
||||
display: {
|
||||
color_code: '#383838',
|
||||
},
|
||||
{
|
||||
tracking: {
|
||||
...analytics,
|
||||
click_item: '1. 컨텐츠 리스트 - 네비게이션(우)',
|
||||
},
|
||||
tracking: {
|
||||
...analytics,
|
||||
click_item: `1. 컨텐츠 리스트 - 네비게이션(${direction})`,
|
||||
},
|
||||
]
|
||||
}))
|
||||
|
||||
const recommendHover = ref(false)
|
||||
|
||||
@@ -291,21 +286,19 @@ const handleLoadMoreRecent = () => {
|
||||
.splide {
|
||||
@apply pb-[68px] sm:pb-[0];
|
||||
}
|
||||
.splide:deep(.splide__arrows) {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.splide:deep(.splide__arrows) .splide-arrow {
|
||||
@apply block top-[unset] bottom-[20px] translate-y-0 bg-cover bg-center bg-no-repeat
|
||||
@apply 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) .splide__arrow--prev {
|
||||
@apply left-[20px] bg-[image:url('/images/common/btn_system_arrow_prev.png')]
|
||||
@apply left-[20px]
|
||||
sm:left-[calc(60.3%+21px)]
|
||||
md:left-[calc(56%+39px)]
|
||||
lg:left-[790px];
|
||||
}
|
||||
.splide:deep(.splide__arrows) .splide__arrow--next {
|
||||
@apply right-[20px] bg-[image:url('/images/common/btn_system_arrow_next.png')]
|
||||
@apply right-[20px]
|
||||
sm:right-[28px]
|
||||
md:right-[unset] md:left-[calc(56%+99px)]
|
||||
lg:left-[850px];
|
||||
|
||||
@@ -40,6 +40,9 @@ const arrowsData = computed(() => {
|
||||
const paginationData = computed(() => {
|
||||
return getComponentGroupAry(props.components, 'pagination')
|
||||
})
|
||||
const videoPlayData = computed(() => {
|
||||
return getComponentGroup(props.components, 'videoPlay')
|
||||
})
|
||||
|
||||
const getVideoPlayTracking = (item: string) => {
|
||||
return {
|
||||
@@ -115,6 +118,7 @@ onBeforeUnmount(() => {
|
||||
<AtomsButtonPlay
|
||||
v-if="playingSlideIndex !== index"
|
||||
class="btn-play"
|
||||
:bg-color="getColorCodeFromData(videoPlayData?.display, 'none')"
|
||||
:tracking="getVideoPlayTracking(item?.group_label)"
|
||||
@click="handleVideoClick(index)"
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface GameDataValue {
|
||||
footer_dev_ci_img_yn: boolean
|
||||
footer_dev_ci_img_path: string
|
||||
default_lang_code?: string
|
||||
game_font: GameDataGameFont
|
||||
game_font_key_json?: GameDataGameFont
|
||||
globals: GameDataGlobal[]
|
||||
gnb: GameDataGnb
|
||||
intro: GameDataIntro
|
||||
|
||||
@@ -84,16 +84,18 @@ export interface PageDataResourceGroupResPath {
|
||||
}
|
||||
|
||||
export interface PageDataResourceGroupBtnInfo extends ColorObject {
|
||||
disabled: boolean
|
||||
txt_btn_name: string
|
||||
detail: Record<string, any>
|
||||
use_game_font: 0 | 1
|
||||
disabled?: boolean
|
||||
use_game_font?: 0 | 1
|
||||
}
|
||||
|
||||
// 리소스 그룹 타입
|
||||
export interface PageDataResourceGroupDisplay extends ColorObject {
|
||||
text: string
|
||||
use_game_font: 0 | 1
|
||||
text?: string
|
||||
color_code?: string
|
||||
color_name?: string
|
||||
use_game_font?: 0 | 1
|
||||
}
|
||||
|
||||
export interface PageDataResourceGroup {
|
||||
@@ -127,7 +129,6 @@ export type PageDataTemplateComponents =
|
||||
| PageDataTemplateComponent // 단일 컴포넌트 패턴
|
||||
| {
|
||||
group_sets: PageDataTemplateComponentSet[]
|
||||
arrow: PageDataArrowComponent
|
||||
} // 그룹 세트 패턴
|
||||
|
||||
// 템플릿 타입
|
||||
@@ -172,8 +173,3 @@ export interface PageDataApiResult {
|
||||
data: PageDataResponse | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
// Arrow 컴포넌트 타입
|
||||
export type PageDataArrowComponent = PageDataTemplateComponent & {
|
||||
groups: PageDataResourceGroups
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const getColorCode = ({
|
||||
* @returns 색상 값
|
||||
*/
|
||||
export const getColorCodeFromData = (
|
||||
data: ColorObject,
|
||||
data: ColorObject | undefined,
|
||||
type: 'btn' | 'txt' | 'none' = 'txt'
|
||||
) => {
|
||||
const suffixMap: Record<'btn' | 'txt' | 'none', string> = {
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |