feat. GR_VISUAL_01 컴포넌트 제작
This commit is contained in:
@@ -1,37 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
interface ImageSource {
|
||||
mobileSrc?: string
|
||||
pcSrc?: string
|
||||
}
|
||||
import { getResponsiveSrc } from '#layers/utils/dataUtil'
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
|
||||
interface Props {
|
||||
text?: string
|
||||
imageSrc?: ImageSource
|
||||
resourcesData?: PageDataResourceGroup
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 텍스트 데이터 추출
|
||||
// [TODO] txt 대신 text 사용
|
||||
const displayText = computed(() => {
|
||||
return props.resourcesData?.display?.txt || ''
|
||||
})
|
||||
|
||||
// 이미지 소스 추출
|
||||
const imageSrc = computed(() => {
|
||||
return getResponsiveSrc(props.resourcesData?.res_path)
|
||||
})
|
||||
|
||||
// 색상 코드 추출 (우선순위: color_code_txt > color_code)
|
||||
const colorCode = computed(() => {
|
||||
return (
|
||||
props.resourcesData?.display?.color_code_txt ||
|
||||
props.resourcesData?.display?.color_code
|
||||
)
|
||||
})
|
||||
|
||||
// 색상 이름 추출 (우선순위: color_name_txt > color_name)
|
||||
const colorName = computed(() => {
|
||||
return (
|
||||
props.resourcesData?.display?.color_name_txt ||
|
||||
props.resourcesData?.display?.color_name
|
||||
)
|
||||
})
|
||||
|
||||
// 색상 스타일 계산
|
||||
const textStyles = computed(() => {
|
||||
const styles: Record<string, string> = {}
|
||||
|
||||
if (colorName.value) {
|
||||
styles.color = `var(--${colorName.value})`
|
||||
} else if (colorCode.value) {
|
||||
styles.color = colorCode.value
|
||||
}
|
||||
|
||||
return styles
|
||||
})
|
||||
|
||||
// HTML 콘텐츠 정리 (줄바꿈 처리)
|
||||
const sanitizedContent = computed(() => {
|
||||
return props.text?.replace(/\n/g, '<br/>') || ''
|
||||
return displayText.value?.replace(/\n/g, '<br/>') || ''
|
||||
})
|
||||
|
||||
// 이미지가 있는지 확인
|
||||
const hasImage = computed(() => {
|
||||
return imageSrc.value && (imageSrc.value.mobileSrc || imageSrc.value.pcSrc)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="imageSrc && 'mobileSrc' in imageSrc">
|
||||
<!-- 이미지가 있는 경우 -->
|
||||
<template v-if="hasImage">
|
||||
<!-- 모바일 이미지 (sm 미만) -->
|
||||
<img
|
||||
v-if="imageSrc.mobileSrc"
|
||||
:src="imageSrc.mobileSrc"
|
||||
:alt="text"
|
||||
:alt="displayText"
|
||||
class="sm:hidden w-full h-full object-contain"
|
||||
/>
|
||||
<!-- PC 이미지 (sm 이상) -->
|
||||
<img
|
||||
v-if="imageSrc.pcSrc"
|
||||
:src="imageSrc.pcSrc"
|
||||
:alt="text"
|
||||
:alt="displayText"
|
||||
class="hidden sm:block w-full h-full object-contain"
|
||||
/>
|
||||
</template>
|
||||
<span v-else-if="text" v-dompurify-html="sanitizedContent" />
|
||||
|
||||
<!-- 텍스트가 있는 경우 -->
|
||||
<span
|
||||
v-else-if="displayText"
|
||||
v-dompurify-html="sanitizedContent"
|
||||
:style="textStyles"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
import { getResponsiveClass, getResponsiveSrc } from '#layers/utils/dataUtil'
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
resourcesData: PageDataResourceGroup
|
||||
gradientClass?: string
|
||||
}>()
|
||||
gradient?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
gradient: false,
|
||||
})
|
||||
|
||||
const resPath = computed(() => {
|
||||
return props.resourcesData?.res_path
|
||||
@@ -37,10 +41,10 @@ const posterSrc = computed(() => {
|
||||
|
||||
<!-- 비디오 타입 -->
|
||||
<template v-else-if="resourcesData?.group_type === 'video'">
|
||||
<!-- 모바일 비디오 (sm 미만) -->
|
||||
<!-- 모바일 비디오 (md 미만) -->
|
||||
<video
|
||||
v-if="videoSrc?.mobileSrc"
|
||||
class="w-full h-full object-cover sm:hidden"
|
||||
class="w-full h-full object-cover md:hidden"
|
||||
:poster="posterSrc?.mobileSrc"
|
||||
autoplay
|
||||
muted
|
||||
@@ -50,10 +54,10 @@ const posterSrc = computed(() => {
|
||||
<source :src="videoSrc.mobileSrc" type="video/mp4" />
|
||||
<source :src="videoSrc.mobileSrc" type="video/webm" />
|
||||
</video>
|
||||
<!-- PC 비디오 (sm 이상) -->
|
||||
<!-- PC 비디오 (md 이상) -->
|
||||
<video
|
||||
v-if="videoSrc?.pcSrc"
|
||||
class="w-full h-full object-cover hidden sm:block"
|
||||
class="w-full h-full object-cover hidden md:block"
|
||||
:poster="posterSrc?.pcSrc"
|
||||
autoplay
|
||||
muted
|
||||
@@ -65,6 +69,17 @@ const posterSrc = computed(() => {
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<div class="absolute inset-0" :class="gradientClass" />
|
||||
<!-- 그라디언트 오버레이 (gradient가 true일 때만) -->
|
||||
<div
|
||||
v-if="props.gradient"
|
||||
class="absolute bottom-0 left-0 right-0 h-[342px] md:h-[720px] bg-gradient-to-b from-[#100d0f]/0 to-[#100d0f]"
|
||||
style="
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(16, 13, 15, 0) 0%,
|
||||
#100d0f 30%
|
||||
);
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
import type { ButtonSize } from '#layers/types/components/button'
|
||||
|
||||
const props = defineProps<{
|
||||
groupsData: PageDataResourceGroup[]
|
||||
}>()
|
||||
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
|
||||
const buttonSize = computed<ButtonSize>(() => {
|
||||
return breakpoints.md.value ? 'medium' : 'extra-small'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="props.groupsData">
|
||||
<div
|
||||
v-if="props.groupsData"
|
||||
class="flex flex-wrap justify-center gap-3 sm:gap-4"
|
||||
>
|
||||
<AtomsButton
|
||||
v-for="button in props.groupsData"
|
||||
:key="button.group_code"
|
||||
:size="buttonSize"
|
||||
:background-color="button.btn_info?.color_code_btn"
|
||||
:text-color="button.btn_info?.color_code_txt"
|
||||
:disabled="button.btn_info?.disabled"
|
||||
>
|
||||
{{ button.btn_info?.txt_btn_name }}
|
||||
</AtomsButton>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { getResponsiveSrc } from '#layers/utils/dataUtil'
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
|
||||
const props = defineProps<{
|
||||
resourcesData: PageDataResourceGroup
|
||||
}>()
|
||||
|
||||
const displayText = props.resourcesData?.display?.text
|
||||
const imageSrc = getResponsiveSrc(props.resourcesData?.res_path)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>
|
||||
<BlocksVisualContent :text="displayText" :image-src="imageSrc" />
|
||||
<BlocksVisualContent :resources-data="props.resourcesData" />
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { getResponsiveSrc } from '#layers/utils/dataUtil'
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
|
||||
const props = defineProps<{
|
||||
resourcesData: PageDataResourceGroup
|
||||
}>()
|
||||
|
||||
const displayText = props.resourcesData?.display?.text
|
||||
const imageSrc = getResponsiveSrc(props.resourcesData?.res_path)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>
|
||||
<BlocksVisualContent :text="displayText" :image-src="imageSrc" />
|
||||
<BlocksVisualContent :resources-data="props.resourcesData" />
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { getResponsiveSrc } from '#layers/utils/dataUtil'
|
||||
import type { PageDataResourceGroup } from '#layers/types/api/pageData'
|
||||
|
||||
const props = defineProps<{
|
||||
resourcesData: PageDataResourceGroup
|
||||
}>()
|
||||
|
||||
const displayText = props.resourcesData?.display?.text
|
||||
const imageSrc = getResponsiveSrc(props.resourcesData?.res_path)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3>
|
||||
<BlocksVisualContent :text="displayText" :image-src="imageSrc" />
|
||||
<BlocksVisualContent :resources-data="props.resourcesData" />
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
@@ -21,11 +21,14 @@ const handleVideoPlayClick = () => {
|
||||
<template>
|
||||
<button
|
||||
v-if="resourcesData && bgStyles"
|
||||
class="bg-cover bg-center bg-no-repeat w-[66px] h-[66px] lg:w-[100px] lg:h-[100px]"
|
||||
class="relative group bg-cover bg-center bg-no-repeat w-[66px] h-[66px] sm:w-[100px] sm:h-[100px]"
|
||||
:class="getResponsiveClass()"
|
||||
:style="bgStyles"
|
||||
@click="handleVideoPlayClick"
|
||||
>
|
||||
<span
|
||||
class="absolute inset-0 m-[10px] bg-white opacity-0 group-hover:opacity-10 transition-opacity duration-300 ease-in-out rounded-[50%]"
|
||||
/>
|
||||
<span class="sr-only">videoPlay</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
11
layers/composables/useBreakpoints.ts
Normal file
11
layers/composables/useBreakpoints.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 반응형 브레이크포인트 계산 헬퍼
|
||||
*/
|
||||
export const useResponsiveBreakpoints = () => {
|
||||
return useBreakpoints({
|
||||
xs: 360, // Mobile: 360px ~ 767px
|
||||
sm: 768, // Tablet: 768px ~ 1023px
|
||||
md: 1024, // PC: 1024px ~ 1439px
|
||||
lg: 1440, // Large PC: 1440px+
|
||||
})
|
||||
}
|
||||
@@ -1,78 +1,88 @@
|
||||
// server/routes/robots.txt.ts
|
||||
type RobotsConfig = {
|
||||
userAgent?: string | string[]
|
||||
allow?: string[]
|
||||
disallow?: string[]
|
||||
sitemap?: string | string[]
|
||||
host?: string
|
||||
cache?: { sMaxAge?: number; staleWhileRevalidate?: number }
|
||||
}
|
||||
userAgent?: string | string[]
|
||||
allow?: string[]
|
||||
disallow?: string[]
|
||||
sitemap?: string | string[]
|
||||
host?: string
|
||||
cache?: { sMaxAge?: number; staleWhileRevalidate?: number }
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const host =
|
||||
(getHeader(event, "host") || getRequestHost(event)).toString() || "";
|
||||
const baseDomain = process.env.BASE_DOMAIN || ".onstove.com";
|
||||
const isGameAliasExtractable = host.includes(baseDomain);
|
||||
export default defineEventHandler(async event => {
|
||||
const host =
|
||||
(getHeader(event, 'host') || getRequestHost(event)).toString() || ''
|
||||
const baseDomain = process.env.BASE_DOMAIN || '.onstove.com'
|
||||
const isGameAliasExtractable = host.includes(baseDomain)
|
||||
|
||||
let gameAlias = "";
|
||||
let gameAlias = ''
|
||||
if (isGameAliasExtractable) {
|
||||
gameAlias = host.split(".")[0];
|
||||
gameAlias = host.split('.')[0]
|
||||
}
|
||||
|
||||
// if (gameAlias && gameAlias !== "www") {
|
||||
// event.context.gameAlias = gameAlias;
|
||||
// }
|
||||
// }
|
||||
// robots 설정을 직접 가져오기 (미들웨어 context 사용)
|
||||
|
||||
let config: RobotsConfig;
|
||||
|
||||
try {
|
||||
|
||||
// robots 설정 추출
|
||||
config = {
|
||||
userAgent: "*",
|
||||
allow: ["/"],
|
||||
disallow: ["/error", "/inspection/", "/inspection/*", "/html/*"],
|
||||
sitemap: [`https://static-pubcomm.gate8.com/local/template/${gameAlias}/sitemap.xml`],
|
||||
host: `${gameAlias}.onstove.com`,
|
||||
cache: { sMaxAge: 300, staleWhileRevalidate: 600 }
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch robots config:', error);
|
||||
|
||||
// 에러 발생 시 기본값 반환
|
||||
config = {
|
||||
userAgent: "*",
|
||||
allow: ["/"],
|
||||
disallow: ["/error", "/inspection/", "/inspection/*", "/html/*"],
|
||||
cache: { sMaxAge: 300, staleWhileRevalidate: 600 }
|
||||
};
|
||||
// if (gameAlias && gameAlias !== "www") {
|
||||
// event.context.gameAlias = gameAlias;
|
||||
// }
|
||||
// }
|
||||
// robots 설정을 직접 가져오기 (미들웨어 context 사용)
|
||||
|
||||
let config: RobotsConfig
|
||||
|
||||
try {
|
||||
// robots 설정 추출
|
||||
config = {
|
||||
userAgent: '*',
|
||||
allow: ['/'],
|
||||
disallow: ['/error', '/inspection/', '/inspection/*', '/html/*'],
|
||||
sitemap: [
|
||||
`https://static-pubcomm.gate8.com/local/template/${gameAlias}/sitemap.xml`,
|
||||
],
|
||||
host: `${gameAlias}.onstove.com`,
|
||||
cache: { sMaxAge: 300, staleWhileRevalidate: 600 },
|
||||
}
|
||||
|
||||
setHeader(event, "Content-Type", "text/plain; charset=utf-8")
|
||||
|
||||
// 캐시 헤더 (CDN 친화)
|
||||
const sMax = config.cache?.sMaxAge ?? 300
|
||||
const swr = config.cache?.staleWhileRevalidate ?? 600
|
||||
setHeader(event, "Cache-Control", `public, s-maxage=${sMax}, stale-while-revalidate=${swr}`)
|
||||
|
||||
// 여러 user-agent 지원
|
||||
const agents = Array.isArray(config.userAgent) ? config.userAgent : [config.userAgent ?? "*"]
|
||||
|
||||
const lines: string[] = []
|
||||
for (const ua of agents) {
|
||||
lines.push(`User-agent: ${ua}`)
|
||||
for (const p of config.allow ?? []) lines.push(`Allow: ${p}`)
|
||||
for (const p of config.disallow ?? []) lines.push(`Disallow: ${p}`)
|
||||
lines.push("") // 블록 구분 공백
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch robots config:', error)
|
||||
|
||||
// 에러 발생 시 기본값 반환
|
||||
config = {
|
||||
userAgent: '*',
|
||||
allow: ['/'],
|
||||
disallow: ['/error', '/inspection/', '/inspection/*', '/html/*'],
|
||||
cache: { sMaxAge: 300, staleWhileRevalidate: 600 },
|
||||
}
|
||||
|
||||
const sitemaps = Array.isArray(config.sitemap) ? config.sitemap : (config.sitemap ? [config.sitemap] : [])
|
||||
for (const sm of sitemaps) lines.push(`Sitemap: ${sm}`)
|
||||
if (config.host) lines.push(`Host: ${config.host}`)
|
||||
|
||||
// 마지막 개행
|
||||
return lines.join("\n").trim() + "\n"
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
setHeader(event, 'Content-Type', 'text/plain; charset=utf-8')
|
||||
|
||||
// 캐시 헤더 (CDN 친화)
|
||||
const sMax = config.cache?.sMaxAge ?? 300
|
||||
const swr = config.cache?.staleWhileRevalidate ?? 600
|
||||
setHeader(
|
||||
event,
|
||||
'Cache-Control',
|
||||
`public, s-maxage=${sMax}, stale-while-revalidate=${swr}`
|
||||
)
|
||||
|
||||
// 여러 user-agent 지원
|
||||
const agents = Array.isArray(config.userAgent)
|
||||
? config.userAgent
|
||||
: [config.userAgent ?? '*']
|
||||
|
||||
const lines: string[] = []
|
||||
for (const ua of agents) {
|
||||
lines.push(`User-agent: ${ua}`)
|
||||
for (const p of config.allow ?? []) lines.push(`Allow: ${p}`)
|
||||
for (const p of config.disallow ?? []) lines.push(`Disallow: ${p}`)
|
||||
lines.push('') // 블록 구분 공백
|
||||
}
|
||||
|
||||
const sitemaps = Array.isArray(config.sitemap)
|
||||
? config.sitemap
|
||||
: config.sitemap
|
||||
? [config.sitemap]
|
||||
: []
|
||||
for (const sm of sitemaps) lines.push(`Sitemap: ${sm}`)
|
||||
if (config.host) lines.push(`Host: ${config.host}`)
|
||||
|
||||
// 마지막 개행
|
||||
return lines.join('\n').trim() + '\n'
|
||||
})
|
||||
|
||||
@@ -7,28 +7,36 @@ const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="relative h-[640px] lg:h-[1000px]">
|
||||
<section class="relative h-[640px] md:h-[1000px]">
|
||||
<WidgetsBackground
|
||||
v-if="props.components?.background"
|
||||
:resources-data="props.components?.background.groups[0]"
|
||||
gradient-class="bg-gradient-to-b from-[#100d0f]/0 to-[#100d0f]"
|
||||
:gradient="true"
|
||||
/>
|
||||
<div
|
||||
class="relative h-full flex flex-col items-center justify-center gap-4"
|
||||
class="relative h-full flex flex-col items-center justify-center gap-4 md:gap-5"
|
||||
>
|
||||
<WidgetsMainTitle
|
||||
v-if="props.components.mainTitle"
|
||||
v-if="props.components.mainTitle && props.components.mainTitle.groups"
|
||||
:resources-data="props.components.mainTitle.groups[0]"
|
||||
class="w-[355px] lg:w-[944px]"
|
||||
class="w-[355px] md:w-[944px]"
|
||||
/>
|
||||
<WidgetsDescription
|
||||
v-if="props.components.description"
|
||||
v-if="
|
||||
props.components.description && props.components.description.groups
|
||||
"
|
||||
:resources-data="props.components.description.groups[0]"
|
||||
class="w-[355px] md:w-[944px]"
|
||||
/>
|
||||
<WidgetsVideoPlay
|
||||
v-if="props.components.videoPlay"
|
||||
v-if="props.components.videoPlay && props.components.videoPlay.groups"
|
||||
:resources-data="props.components.videoPlay.groups[0]"
|
||||
/>
|
||||
<WidgetsButtonList
|
||||
v-if="props.components.buttonList && props.components.buttonList.groups"
|
||||
:groups-data="props.components.buttonList.groups"
|
||||
class="mt-[48px] md:mt-[72px]"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user