fix. 템플릿 데이터 수정에 따른 fe 소스 수정
This commit is contained in:
@@ -27,6 +27,7 @@ const colorCode = computed(() => {
|
||||
})
|
||||
const currentImageSrc = computed(() => {
|
||||
if (!imageSrc.value) return ''
|
||||
|
||||
return breakpoints.value.isMobile
|
||||
? imageSrc.value.mobileSrc || ''
|
||||
: imageSrc.value.pcSrc || ''
|
||||
@@ -36,25 +37,20 @@ const currentImageSrc = computed(() => {
|
||||
const sanitizedContent = computed(() => {
|
||||
return displayText.value?.replace(/\n/g, '<br/>') || ''
|
||||
})
|
||||
// 이미지가 있는지 확인
|
||||
const hasImage = computed(() => {
|
||||
return imageSrc.value && (imageSrc.value.mobileSrc || imageSrc.value.pcSrc)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 이미지 -->
|
||||
<img
|
||||
v-if="hasImage && currentImageSrc"
|
||||
v-if="isTypeImage(resourcesData?.resource_type) && currentImageSrc"
|
||||
:src="currentImageSrc"
|
||||
:alt="alt || displayText"
|
||||
:class="`w-full h-full object-${objectFit}`"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<!-- 텍스트 -->
|
||||
<span
|
||||
v-else-if="displayText"
|
||||
v-else-if="isTypeText(resourcesData?.resource_type)"
|
||||
v-dompurify-html="sanitizedContent"
|
||||
:style="{ color: getColorCode({ colorName, colorCode }) }"
|
||||
class="block"
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { Splide, SplideSlide } from '@splidejs/vue-splide'
|
||||
import { getFirstGroup, isTypeVideo } from '#layers/utils/dataUtil'
|
||||
import { getMediaSrc, getYouTubeEmbedUrl } from '#layers/utils/youtube'
|
||||
import type { Splide as SplideType, Options } from '@splidejs/splide'
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
import type {
|
||||
PageDataResourceGroups,
|
||||
PageDataResourceGroup,
|
||||
} from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
slideData: { media: any; set_order: number }[]
|
||||
slideData: { media: PageDataResourceGroups; set_order: number }[]
|
||||
videoPlay?: PageDataResourceGroup
|
||||
arrows?: boolean
|
||||
pagination?: boolean
|
||||
@@ -46,10 +51,27 @@ const thumbOptions = computed<Options>(() => ({
|
||||
},
|
||||
}))
|
||||
|
||||
const isPassVideo = (groups: PageDataResourceGroups, index: number) => {
|
||||
const firstGroup = getFirstGroup(groups)
|
||||
return (
|
||||
firstGroup &&
|
||||
isTypeVideo(firstGroup?.resource_type) &&
|
||||
index !== playingSlideIndex.value
|
||||
)
|
||||
}
|
||||
|
||||
const handleVideoClick = (index: number) => {
|
||||
playingSlideIndex.value = index
|
||||
}
|
||||
|
||||
const getYouTubeEmbedUrlFromMedia = (
|
||||
resourceGroups: PageDataResourceGroup[]
|
||||
) => {
|
||||
const resourceGroup = getFirstGroup(resourceGroups)
|
||||
const mediaSrc = getMediaSrc(resourceGroup)
|
||||
return mediaSrc ? getYouTubeEmbedUrl(mediaSrc, true) : ''
|
||||
}
|
||||
|
||||
let mainInst: SplideType | null = null
|
||||
let thumbsInst: SplideType | null = null
|
||||
|
||||
@@ -87,16 +109,14 @@ onBeforeUnmount(() => {
|
||||
:class="{ 'opacity-0': playingSlideIndex === index }"
|
||||
/>
|
||||
<AtomsButtonPlay
|
||||
v-if="
|
||||
getMediaType(item.media) === 'video' && playingSlideIndex !== index
|
||||
"
|
||||
v-if="isPassVideo(item.media, index)"
|
||||
:resources-data="videoPlay"
|
||||
class="btn-play"
|
||||
@click="handleVideoClick(index)"
|
||||
/>
|
||||
<iframe
|
||||
v-if="playingSlideIndex === index"
|
||||
:src="getYouTubeEmbedUrl(getMediaText(item.media), true)"
|
||||
:src="getYouTubeEmbedUrlFromMedia(item.media)"
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
|
||||
@@ -46,7 +46,7 @@ const currentPosterSrc = computed(() => {
|
||||
<div class="absolute inset-0 w-full h-full">
|
||||
<!-- 이미지 타입-->
|
||||
<div
|
||||
v-if="resourcesData?.group_type === 'image'"
|
||||
v-if="isTypeImage(resourcesData?.resource_type)"
|
||||
class="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
:class="getResponsiveClass()"
|
||||
:style="bgStyles"
|
||||
@@ -54,7 +54,7 @@ const currentPosterSrc = computed(() => {
|
||||
|
||||
<!-- 비디오 타입 -->
|
||||
<video
|
||||
v-else-if="resourcesData?.group_type === 'video' && currentVideoSrc"
|
||||
v-else-if="isTypeVideo(resourcesData?.resource_type) && currentVideoSrc"
|
||||
class="w-full h-full object-cover"
|
||||
:poster="currentPosterSrc"
|
||||
autoplay
|
||||
|
||||
@@ -61,7 +61,7 @@ const getButtonProps = (button: PageDataResourceGroup) => ({
|
||||
>
|
||||
<AtomsButton
|
||||
v-for="(button, index) in props.resourcesData"
|
||||
:key="`${button.group_code}-${index}`"
|
||||
:key="index"
|
||||
v-bind="getButtonProps(button)"
|
||||
class="size-extra-small md:size-medium"
|
||||
>
|
||||
|
||||
@@ -36,12 +36,23 @@ export interface PageDataValue {
|
||||
lnb_text_color_code_active: string
|
||||
lnb_text_color_code_deactive: string
|
||||
lnb_menus: PageDataLnbMenu[]
|
||||
templates: Record<string, PageDataTemplate>
|
||||
meta_tag: PageDataMetaTag
|
||||
templates: Record<string, PageDataTemplate>
|
||||
}
|
||||
|
||||
// ===== 세부 데이터 타입들 =====
|
||||
|
||||
// LNB 메뉴 타입
|
||||
export interface PageDataLnbMenu {
|
||||
path_code: string
|
||||
depth: number
|
||||
sort_order: number
|
||||
menu_name: string
|
||||
target_type: number
|
||||
page_ver_tmpl_name_en: string
|
||||
tracking: string
|
||||
}
|
||||
|
||||
// 메타 태그 타입
|
||||
export interface PageDataMetaTag {
|
||||
x_desc: string
|
||||
@@ -54,7 +65,14 @@ export interface PageDataMetaTag {
|
||||
page_title: string
|
||||
}
|
||||
|
||||
// 리소스 그룹 타입
|
||||
export type PageDataResourceGroupType =
|
||||
| 'TXT'
|
||||
| 'BTN'
|
||||
| 'VID'
|
||||
| 'IMG_COMM'
|
||||
| 'IMG_LANG'
|
||||
| 'IMG_COMM_GLOBAL'
|
||||
|
||||
export interface PageDataResourceGroupResPath {
|
||||
path_mo: string
|
||||
path_pc?: string
|
||||
@@ -72,8 +90,7 @@ export interface PageDataResourceGroupBtnInfo {
|
||||
|
||||
// 리소스 그룹 타입
|
||||
export interface PageDataResourceGroup {
|
||||
group_type?: string
|
||||
group_code?: string
|
||||
resource_type?: PageDataResourceGroupType
|
||||
res_path?: PageDataResourceGroupResPath
|
||||
btn_info?: PageDataResourceGroupBtnInfo
|
||||
display?: {
|
||||
@@ -81,29 +98,27 @@ export interface PageDataResourceGroup {
|
||||
color_code?: string
|
||||
color_name?: string
|
||||
}
|
||||
tracking: string // JSON 문자열
|
||||
tracking: string
|
||||
}
|
||||
|
||||
export type PageDataResourceGroups = PageDataResourceGroup[]
|
||||
|
||||
// 컴포넌트 타입
|
||||
export interface PageDataComponent {
|
||||
groups: PageDataResourceGroup[]
|
||||
groups: PageDataResourceGroups
|
||||
}
|
||||
|
||||
// 템플릿 컴포넌트 타입 - 두 가지 패턴 지원
|
||||
export type PageDataTemplateComponent = Record<string, PageDataComponent>
|
||||
|
||||
// 그룹 세트 아이템 타입
|
||||
export type PageDataTemplateComponentSet = PageDataTemplateComponent & {
|
||||
set_order?: number
|
||||
}
|
||||
|
||||
// 템플릿 컴포넌트 타입 - 두 가지 패턴
|
||||
export type PageDataTemplateComponents =
|
||||
| Record<string, PageDataComponent> // 직접 PageDataComponent가 들어있는 패턴
|
||||
| { group_sets: Record<string, any> } // group_sets 안에 PageDataComponent가 들어있는 패턴
|
||||
|
||||
// LNB 메뉴 타입
|
||||
export interface PageDataLnbMenu {
|
||||
path_code: string
|
||||
depth: number
|
||||
sort_order: number
|
||||
menu_name: string
|
||||
target_type: number
|
||||
page_ver_tmpl_name_en: string
|
||||
tracking: string // JSON string
|
||||
}
|
||||
| PageDataTemplateComponent // 단일 컴포넌트 패턴
|
||||
| { group_sets: PageDataTemplateComponentSet[] } // 그룹 세트 패턴
|
||||
|
||||
// 템플릿 타입
|
||||
export interface PageDataTemplate {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import type {
|
||||
PageDataValue,
|
||||
PageDataComponent,
|
||||
PageDataResourceGroupType,
|
||||
} from '#layers/types/api/pageData'
|
||||
|
||||
/**
|
||||
@@ -19,6 +20,44 @@ export const getLayoutType = (
|
||||
return pageData?.page_type === 1 ? 'default' : 'promotion'
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 타입인지 확인합니다.
|
||||
* @param type 리소스 그룹 타입
|
||||
* @returns 이미지 타입 여부
|
||||
*/
|
||||
export const isTypeImage = (type: PageDataResourceGroupType): boolean => {
|
||||
return (
|
||||
type === 'IMG_COMM' || type === 'IMG_LANG' || type === 'IMG_COMM_GLOBAL'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 타입인지 확인합니다.
|
||||
* @param type 리소스 그룹 타입
|
||||
* @returns 비디오 타입 여부
|
||||
*/
|
||||
export const isTypeVideo = (type: PageDataResourceGroupType): boolean => {
|
||||
return type === 'VID'
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트 타입인지 확인합니다.
|
||||
* @param type 리소스 그룹 타입
|
||||
* @returns 텍스트 타입 여부
|
||||
*/
|
||||
export const isTypeText = (type: PageDataResourceGroupType): boolean => {
|
||||
return type === 'TXT'
|
||||
}
|
||||
|
||||
/**
|
||||
* 버튼 타입인지 확인합니다.
|
||||
* @param type 리소스 그룹 타입
|
||||
* @returns 버튼 타입 여부
|
||||
*/
|
||||
export const isTypeButton = (type: PageDataResourceGroupType): boolean => {
|
||||
return type === 'BTN'
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹의 첫 번째 데이터를 반환합니다.
|
||||
* @param source props.components 또는 group 객체
|
||||
|
||||
@@ -94,61 +94,6 @@ export const formatTimestamp = (
|
||||
.replace('ss', seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
* 타임스탬프를 상대적 시간으로 변환합니다 (예: "3일 전", "2시간 전")
|
||||
* @param timestamp 타임스탬프 (밀리초 또는 초)
|
||||
* @param locale 로케일 (기본값: 'ko-KR')
|
||||
* @returns 상대적 시간 문자열
|
||||
*/
|
||||
export const formatRelativeTime = (
|
||||
timestamp: number | string,
|
||||
locale: string = 'ko-KR'
|
||||
): string => {
|
||||
if (!timestamp) return ''
|
||||
|
||||
let ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
|
||||
if (ts < 10000000000) {
|
||||
ts = ts * 1000
|
||||
}
|
||||
|
||||
const date = new Date(ts)
|
||||
const now = new Date()
|
||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
|
||||
|
||||
if (diffInSeconds < 60) {
|
||||
return locale === 'ko-KR' ? '방금 전' : 'just now'
|
||||
}
|
||||
|
||||
const diffInMinutes = Math.floor(diffInSeconds / 60)
|
||||
if (diffInMinutes < 60) {
|
||||
return locale === 'ko-KR'
|
||||
? `${diffInMinutes}분 전`
|
||||
: `${diffInMinutes} minutes ago`
|
||||
}
|
||||
|
||||
const diffInHours = Math.floor(diffInMinutes / 60)
|
||||
if (diffInHours < 24) {
|
||||
return locale === 'ko-KR'
|
||||
? `${diffInHours}시간 전`
|
||||
: `${diffInHours} hours ago`
|
||||
}
|
||||
|
||||
const diffInDays = Math.floor(diffInHours / 24)
|
||||
if (diffInDays < 30) {
|
||||
return locale === 'ko-KR' ? `${diffInDays}일 전` : `${diffInDays} days ago`
|
||||
}
|
||||
|
||||
const diffInMonths = Math.floor(diffInDays / 30)
|
||||
if (diffInMonths < 12) {
|
||||
return locale === 'ko-KR'
|
||||
? `${diffInMonths}개월 전`
|
||||
: `${diffInMonths} months ago`
|
||||
}
|
||||
|
||||
const diffInYears = Math.floor(diffInMonths / 12)
|
||||
return locale === 'ko-KR' ? `${diffInYears}년 전` : `${diffInYears} years ago`
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열 또는 객체를 배열로 변환합니다.
|
||||
* @param value 변환할 값 (배열, 객체, 또는 undefined/null)
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
* @description 유튜브 관련 유틸리티 함수를 제공합니다.
|
||||
*/
|
||||
|
||||
import { getFirstGroup, isTypeVideo } from '#layers/utils/dataUtil'
|
||||
import type {
|
||||
PageDataResourceGroups,
|
||||
PageDataResourceGroup,
|
||||
} from '#layers/types/api/pageData'
|
||||
|
||||
/**
|
||||
* 유튜브 URL에서 비디오 ID를 추출합니다.
|
||||
* @param url - 유튜브 URL (watch, embed, youtu.be 등 다양한 형태 지원)
|
||||
@@ -95,11 +101,10 @@ export const getYouTubeThumbnailFromUrl = (
|
||||
* @param source - 미디어 소스 객체
|
||||
* @returns 미디어 text(src)
|
||||
*/
|
||||
export const getMediaText = (source: Record<string, any>): string => {
|
||||
if (!source) return ''
|
||||
const resource = source.groups?.[0]
|
||||
const mediaUrl = resource?.display?.text
|
||||
return mediaUrl || ''
|
||||
export const getMediaSrc = (resourceGroup: PageDataResourceGroup): string => {
|
||||
if (!resourceGroup) return ''
|
||||
const mediaSrc = resourceGroup?.display?.text
|
||||
return mediaSrc || ''
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,42 +114,20 @@ export const getMediaText = (source: Record<string, any>): string => {
|
||||
* @returns 미디어 이미지 소스
|
||||
*/
|
||||
export const getMediaImgSrc = (
|
||||
source: Record<string, any>,
|
||||
resourceGroups: PageDataResourceGroups,
|
||||
quality: 'default' | 'medium' | 'high' | 'standard' | 'maxres' = 'high'
|
||||
): string => {
|
||||
if (!source) return ''
|
||||
if (!resourceGroups) return ''
|
||||
|
||||
const resource = source.groups?.[0]
|
||||
const mediaType = resource?.group_type
|
||||
const mediaUrl = resource?.display?.text
|
||||
const resourceGroup = getFirstGroup(resourceGroups)
|
||||
const mediaSrc = getMediaSrc(resourceGroup)
|
||||
const mediaType = resourceGroup?.resource_type
|
||||
|
||||
if (mediaType === 'video' && mediaUrl) {
|
||||
const videoId = getYouTubeVideoId(mediaUrl)
|
||||
if (isTypeVideo(mediaType) && mediaSrc) {
|
||||
const videoId = getYouTubeVideoId(mediaSrc)
|
||||
const thumbnailUrl = getYouTubeThumbnail(videoId, quality)
|
||||
return thumbnailUrl
|
||||
}
|
||||
|
||||
return mediaUrl || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 타입을 확인합니다.
|
||||
* @param source - 미디어 소스 객체
|
||||
* @returns 미디어 타입 ('video' | 'image' | '')
|
||||
*/
|
||||
export const getMediaType = (source: Record<string, any>): string => {
|
||||
if (!source) return ''
|
||||
|
||||
const resource = source.groups?.[0]
|
||||
const mediaType = resource?.group_type
|
||||
return mediaType || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 비디오 아이템인지 확인합니다.
|
||||
* @param source - 미디어 소스 객체
|
||||
* @returns 비디오 여부
|
||||
*/
|
||||
export const isVideoItem = (source: Record<string, any>): boolean => {
|
||||
return getMediaType(source) === 'video'
|
||||
return mediaSrc || ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user