feat. videoPlay 컴포넌트 제작
This commit is contained in:
13
app/app.vue
13
app/app.vue
@@ -2,11 +2,14 @@
|
||||
import { useNuxtApp } from 'nuxt/app'
|
||||
import LoadingFull from '#layers/components/blocks/loading/Full.vue'
|
||||
import LoadingLocal from '#layers/components/blocks/loading/Local.vue'
|
||||
import BlocksModalYouTube from '#layers/components/blocks/modal/YouTube.vue'
|
||||
import type { GameDataMetaTag, GameDataValue } from '#layers/types/api/gameData'
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
const gameDataStore = useGameDataStore()
|
||||
const modalStore = useModalStore()
|
||||
const { youtube, handleResetYoutube } = modalStore
|
||||
const { setGameData } = gameDataStore
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
|
||||
@@ -114,7 +117,15 @@ if (serverGameData) {
|
||||
<h1 class="sr-only">{{ gameData?.game_name }}</h1>
|
||||
<NuxtPage />
|
||||
|
||||
<!-- 로딩 컴포넌트들 -->
|
||||
<!-- 공통 모달 컴포넌트 -->
|
||||
<BlocksModalYouTube
|
||||
:is-open="youtube.storeIsOpen"
|
||||
:youtube-id="youtube.storeYoutubeId"
|
||||
:class-name="youtube.storeClassName"
|
||||
@close-button-event="handleResetYoutube"
|
||||
/>
|
||||
|
||||
<!-- 로딩 컴포넌트 -->
|
||||
<LoadingFull />
|
||||
<LoadingLocal />
|
||||
</template>
|
||||
|
||||
@@ -3,4 +3,12 @@
|
||||
body {
|
||||
background-color: #000;
|
||||
}
|
||||
body.scroll-lock {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button,
|
||||
a {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { GameDataKeyCodeCodes } from '#layers/types/api/gameData'
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
size: 'medium',
|
||||
backgroundColor: 'var(--primary)',
|
||||
textColor: 'var(--text-primary)',
|
||||
textColor: 'var(--alternative-02)',
|
||||
icon: '',
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
@@ -19,6 +19,7 @@ withDefaults(defineProps<Props>(), {
|
||||
:height="size"
|
||||
viewBox="0 0 12 12"
|
||||
:fill="color"
|
||||
:class="className"
|
||||
>
|
||||
<path
|
||||
d="M5.29499 7.715L2.39999 4.875C2.07499 4.555 2.29999 4 2.75999 4L9.23499 4C9.69499 4 9.91999 4.555 9.59499 4.875L6.69999 7.715C6.30999 8.095 5.68999 8.095 5.29999 7.715H5.29499Z"
|
||||
|
||||
29
layers/components/atoms/icons/Close.vue
Normal file
29
layers/components/atoms/icons/Close.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
size: 32,
|
||||
color: '#EBEBEB',
|
||||
className: '',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 32 33"
|
||||
fill="none"
|
||||
:class="className"
|
||||
>
|
||||
<path
|
||||
d="M26.2768 8.10939C26.7975 7.5887 26.7975 6.74448 26.2768 6.22378C25.7561 5.70308 24.9119 5.70308 24.3912 6.22378L16.0007 14.6143L7.61013 6.22378C7.08943 5.70308 6.24521 5.70308 5.72451 6.22378C5.20381 6.74448 5.20381 7.5887 5.72451 8.10939L14.115 16.4999L5.72451 24.8904C5.20381 25.4111 5.20381 26.2554 5.72451 26.7761C6.24521 27.2968 7.08943 27.2968 7.61013 26.7761L16.0007 18.3855L24.3912 26.7761C24.9119 27.2968 25.7561 27.2968 26.2768 26.7761C26.7975 26.2554 26.7975 25.4111 26.2768 24.8904L17.8863 16.4999L26.2768 8.10939Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -19,6 +19,7 @@ withDefaults(defineProps<Props>(), {
|
||||
:height="size"
|
||||
viewBox="0 0 16 16"
|
||||
:fill="color"
|
||||
:class="className"
|
||||
>
|
||||
<path
|
||||
d="M3.63636 3.33333C3.469 3.33333 3.33333 3.469 3.33333 3.63636L3.33333 12.3636C3.33333 12.531 3.469 12.6667 3.63636 12.6667H12.3636C12.531 12.6667 12.6667 12.531 12.6667 12.3636V9.93939C12.6667 9.5712 12.9651 9.27273 13.3333 9.27273C13.7015 9.27273 14 9.5712 14 9.93939V12.3636C14 13.2674 13.2674 14 12.3636 14H3.63636C2.73262 14 2 13.2674 2 12.3636L2 3.63636C2 2.73263 2.73262 2 3.63636 2L6.06061 2C6.4288 2 6.72727 2.29848 6.72727 2.66667C6.72727 3.03486 6.4288 3.33333 6.06061 3.33333H3.63636Z"
|
||||
|
||||
@@ -1,67 +1,52 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="transition duration-300 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition duration-200 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
<Transition
|
||||
enter-active-class="transition duration-300 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition duration-200 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75"
|
||||
@click="handleBackdropClick"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75"
|
||||
@click="handleBackdropClick"
|
||||
class="relative mx-4 my-4"
|
||||
style="
|
||||
width: min(896px, 90vw, calc((90vh - 2rem) * 16 / 9));
|
||||
aspect-ratio: 16/9;
|
||||
"
|
||||
@click.stop
|
||||
>
|
||||
<Transition
|
||||
enter-active-class="transition duration-300 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition duration-200 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div v-if="isOpen" class="relative w-full max-w-4xl mx-4" @click.stop>
|
||||
<!-- 헤더 -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
class="p-1 text-white rounded-full transition-colors"
|
||||
aria-label="모달 닫기"
|
||||
@click="closeModal"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 헤더 -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
class="p-1 text-white rounded-full transition-colors"
|
||||
aria-label="모달 닫기"
|
||||
@click="closeModal"
|
||||
>
|
||||
<AtomsIconsClose />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 유튜브 영상 컨테이너 -->
|
||||
<div class="relative w-full" :style="{ paddingBottom: '56.25%' }">
|
||||
<iframe
|
||||
v-if="youtubeId"
|
||||
:src="`https://www.youtube.com/embed/${youtubeId}?autoplay=1&rel=0`"
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
title="YouTube video player"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- 유튜브 영상 컨테이너 -->
|
||||
<div class="relative w-full h-full">
|
||||
<iframe
|
||||
v-if="youtubeId"
|
||||
:src="`https://www.youtube.com/embed/${youtubeId}?autoplay=1&rel=0`"
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
title="YouTube video player"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -74,11 +59,12 @@ interface Props {
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'close'): void
|
||||
(e: 'update:isOpen', value: boolean): void
|
||||
(e: 'closeButtonEvent'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isOpen: false,
|
||||
youtubeId: '',
|
||||
title: '',
|
||||
description: '',
|
||||
closeOnBackdrop: true,
|
||||
@@ -102,8 +88,7 @@ const handleBackdropClick = () => {
|
||||
|
||||
// 모달 닫기 함수
|
||||
const closeModal = () => {
|
||||
emit('close')
|
||||
emit('update:isOpen', false)
|
||||
emit('closeButtonEvent')
|
||||
}
|
||||
|
||||
// 키보드 이벤트 리스너 등록/해제
|
||||
@@ -114,21 +99,4 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
// 모달이 열릴 때 body 스크롤 방지
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
isOpen => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 컴포넌트 언마운트 시 body 스크롤 복원
|
||||
onUnmounted(() => {
|
||||
document.body.style.overflow = ''
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -8,41 +8,24 @@ const bgStyles = getResponsiveSrc(props.resourcesData?.res_path, {
|
||||
resourcesType: 'bg',
|
||||
})
|
||||
|
||||
// YouTube 모달 상태 관리
|
||||
const isYouTubeModalOpen = ref(false)
|
||||
const youtubeVideoId = ref('')
|
||||
// YouTube 모달 스토어 사용
|
||||
const modalStore = useModalStore()
|
||||
|
||||
// 비디오 플레이 버튼 클릭 핸들러
|
||||
const handleVideoPlayClick = () => {
|
||||
// TODO: 실제 YouTube 비디오 ID를 설정해야 합니다
|
||||
// 예시: 'dQw4w9WgXcQ' (Rick Astley - Never Gonna Give You Up)
|
||||
youtubeVideoId.value = 'UKVsZYHxYTc' // 임시로 설정
|
||||
isYouTubeModalOpen.value = true
|
||||
}
|
||||
|
||||
// 모달 닫기 핸들러
|
||||
const handleCloseModal = () => {
|
||||
isYouTubeModalOpen.value = false
|
||||
youtubeVideoId.value = ''
|
||||
const youtubeId = props.resourcesData?.display?.text ?? ''
|
||||
modalStore.handleOpenYoutube({ youtubeId })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-if="resourcesData"
|
||||
v-if="resourcesData && bgStyles"
|
||||
class="bg-cover bg-center bg-no-repeat w-[66px] h-[66px] lg:w-[100px] lg:h-[100px]"
|
||||
:class="getResponsiveClass"
|
||||
:class="getResponsiveClass()"
|
||||
:style="bgStyles"
|
||||
@click="handleVideoPlayClick"
|
||||
>
|
||||
<span class="sr-only">videoPlay</span>
|
||||
</button>
|
||||
|
||||
<!-- YouTube 모달 -->
|
||||
<BlocksModalYouTube
|
||||
:is-open="isYouTubeModalOpen"
|
||||
:youtube-id="youtubeVideoId"
|
||||
@close="handleCloseModal"
|
||||
@update:is-open="(value: boolean) => (isYouTubeModalOpen = value)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
142
layers/stores/useModalStore.ts
Normal file
142
layers/stores/useModalStore.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface DialogParams {
|
||||
isShowDimmed?: boolean
|
||||
className?: string
|
||||
isOutsideClose?: boolean
|
||||
contentText: string
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string
|
||||
confirmButtonEvent?: () => void
|
||||
cancelButtonEvent?: () => void
|
||||
closeButtonEvent?: () => void
|
||||
}
|
||||
interface YoutubeParams {
|
||||
youtubeId: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const createModalState = () => ({
|
||||
storeIsOpen: ref(false),
|
||||
storeIsShowDimmed: ref(false),
|
||||
storeClassName: ref(''),
|
||||
storeIsOutsideClose: ref(true),
|
||||
storeContentText: ref(''),
|
||||
storeConfirmButtonText: ref(''),
|
||||
storeConfirmButtonEvent: ref(() => {}),
|
||||
storeCloseButtonEvent: ref(() => {}),
|
||||
})
|
||||
|
||||
const resetModalState = (type: ReturnType<typeof createModalState>) => {
|
||||
type.storeIsOpen.value = false
|
||||
type.storeIsShowDimmed.value = false
|
||||
type.storeClassName.value = ''
|
||||
type.storeIsOutsideClose.value = true
|
||||
type.storeContentText.value = ''
|
||||
type.storeConfirmButtonText.value = ''
|
||||
type.storeConfirmButtonEvent.value = () => {}
|
||||
type.storeCloseButtonEvent.value = () => {}
|
||||
}
|
||||
|
||||
export const useModalStore = defineStore('modalStore', () => {
|
||||
const scrollStore = useScrollStore()
|
||||
|
||||
// alert ------------------
|
||||
const alert = {
|
||||
...createModalState(),
|
||||
}
|
||||
|
||||
const handleOpenAlert = ({
|
||||
isShowDimmed = false,
|
||||
className = '',
|
||||
isOutsideClose = true,
|
||||
contentText,
|
||||
confirmButtonText = '',
|
||||
confirmButtonEvent,
|
||||
closeButtonEvent,
|
||||
}: DialogParams) => {
|
||||
alert.storeIsOpen.value = true
|
||||
alert.storeIsShowDimmed.value = isShowDimmed
|
||||
alert.storeClassName.value = className
|
||||
alert.storeContentText.value = contentText
|
||||
alert.storeConfirmButtonText.value = confirmButtonText
|
||||
alert.storeIsOutsideClose.value = isOutsideClose
|
||||
alert.storeConfirmButtonEvent.value = confirmButtonEvent ?? handleResetAlert
|
||||
alert.storeCloseButtonEvent.value = closeButtonEvent ?? handleResetAlert
|
||||
}
|
||||
|
||||
const handleResetAlert = () => {
|
||||
resetModalState(alert)
|
||||
}
|
||||
|
||||
// confirm ------------------
|
||||
const confirm = {
|
||||
...createModalState(),
|
||||
storeCancelButtonText: ref(''),
|
||||
storeCancelButtonEvent: ref(() => {}),
|
||||
}
|
||||
|
||||
const handleOpenConfirm = ({
|
||||
isShowDimmed = false,
|
||||
className = '',
|
||||
isOutsideClose = true,
|
||||
contentText,
|
||||
confirmButtonText = '',
|
||||
cancelButtonText = '',
|
||||
confirmButtonEvent,
|
||||
cancelButtonEvent,
|
||||
closeButtonEvent,
|
||||
}: DialogParams) => {
|
||||
confirm.storeIsOpen.value = true
|
||||
confirm.storeIsShowDimmed.value = isShowDimmed
|
||||
confirm.storeClassName.value = className
|
||||
confirm.storeContentText.value = contentText
|
||||
confirm.storeConfirmButtonText.value = confirmButtonText
|
||||
confirm.storeCancelButtonText.value = cancelButtonText
|
||||
confirm.storeIsOutsideClose.value = isOutsideClose
|
||||
confirm.storeConfirmButtonEvent.value =
|
||||
confirmButtonEvent ?? handleResetConfirm
|
||||
confirm.storeCancelButtonEvent.value =
|
||||
cancelButtonEvent ?? handleResetConfirm
|
||||
confirm.storeCloseButtonEvent.value = closeButtonEvent ?? handleResetConfirm
|
||||
}
|
||||
|
||||
const handleResetConfirm = () => {
|
||||
resetModalState(confirm)
|
||||
confirm.storeCancelButtonText.value = ''
|
||||
confirm.storeCancelButtonEvent.value = () => {}
|
||||
}
|
||||
|
||||
// youtube ------------------
|
||||
const youtube = {
|
||||
storeIsOpen: ref(false),
|
||||
storeYoutubeId: ref(''),
|
||||
storeClassName: ref(''),
|
||||
}
|
||||
|
||||
const handleOpenYoutube = ({ youtubeId, className = '' }: YoutubeParams) => {
|
||||
youtube.storeIsOpen.value = true
|
||||
youtube.storeYoutubeId.value = youtubeId
|
||||
youtube.storeClassName.value = className
|
||||
scrollStore.controlScrollLock(true)
|
||||
}
|
||||
|
||||
const handleResetYoutube = () => {
|
||||
youtube.storeIsOpen.value = false
|
||||
youtube.storeYoutubeId.value = ''
|
||||
youtube.storeClassName.value = ''
|
||||
scrollStore.controlScrollLock(false)
|
||||
}
|
||||
|
||||
return {
|
||||
alert,
|
||||
confirm,
|
||||
youtube,
|
||||
handleOpenAlert,
|
||||
handleOpenConfirm,
|
||||
handleResetAlert,
|
||||
handleResetConfirm,
|
||||
handleOpenYoutube,
|
||||
handleResetYoutube,
|
||||
}
|
||||
})
|
||||
36
layers/stores/useScrollStore.ts
Normal file
36
layers/stores/useScrollStore.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useWindowScroll } from '@vueuse/core'
|
||||
|
||||
export const useScrollStore = defineStore('scrollStore', () => {
|
||||
const { x: windowX, y: windowY } = useWindowScroll({ behavior: 'smooth' })
|
||||
|
||||
const stoveGnbHeight = 48
|
||||
const scrollXValue = ref('0px')
|
||||
const isPassedStoveGnb = ref(false)
|
||||
|
||||
const updateScrollValue = () => {
|
||||
if (stoveGnbHeight <= windowY.value) {
|
||||
isPassedStoveGnb.value = true
|
||||
scrollXValue.value = `-${windowX.value}px`
|
||||
} else {
|
||||
isPassedStoveGnb.value = false
|
||||
scrollXValue.value = '0px'
|
||||
}
|
||||
}
|
||||
|
||||
const controlScrollLock = (state: boolean) => {
|
||||
if (state) {
|
||||
document.body.classList.add('scroll-lock')
|
||||
} else {
|
||||
document.body.classList.remove('scroll-lock')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scrollXValue,
|
||||
isPassedStoveGnb,
|
||||
|
||||
updateScrollValue,
|
||||
controlScrollLock,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user