fix. 팝업 버튼 추가
This commit is contained in:
12
app/app.vue
12
app/app.vue
@@ -15,7 +15,7 @@ const scrollStore = useScrollStore()
|
||||
|
||||
const { setGameData } = gameDataStore
|
||||
const { gameData } = storeToRefs(gameDataStore)
|
||||
const { youtube, confirm, alert, toast, handleResetYoutube } = modalStore
|
||||
const { youtube, confirm, alert, content, handleResetYoutube } = modalStore
|
||||
const { scrollGnbPosition } = storeToRefs(scrollStore)
|
||||
|
||||
const metaData = ref<GameDataMetaTag | null>(null)
|
||||
@@ -154,8 +154,16 @@ onBeforeUnmount(() => {
|
||||
<h1 class="sr-only">{{ gameData?.game_name }}</h1>
|
||||
<NuxtPage />
|
||||
|
||||
<WidgetsModalClient />
|
||||
<!-- 공통 모달 컴포넌트 -->
|
||||
<WidgetsModalClient />
|
||||
<BlocksModalContent
|
||||
v-model:is-open="content.storeIsOpen"
|
||||
:is-outside-close="content.storeIsOutsideClose"
|
||||
:content-title="content.storeContentTitle"
|
||||
:tab-info="content.storeTabInfo"
|
||||
:tab-active-index="content.storeTabActiveIndex"
|
||||
:modal-name="content.storeModalName"
|
||||
/>
|
||||
<BlocksModalYouTube
|
||||
v-model:is-open="youtube.storeIsOpen"
|
||||
:youtube-url="youtube.storeYoutubeUrl"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@import './components/_layout.css';
|
||||
@import './components/_modal.css';
|
||||
@import './components/_splide.css';
|
||||
@import './components/_base-content.css';
|
||||
|
||||
@import '@splidejs/vue-splide/css';
|
||||
|
||||
|
||||
@@ -17,3 +17,18 @@
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition:
|
||||
transform 0.3s ease-out,
|
||||
opacity 0.3s ease-out;
|
||||
}
|
||||
.slide-up-enter-from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
.slide-up-leave-to {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
93
layers/assets/css/components/_base-content.css
Normal file
93
layers/assets/css/components/_base-content.css
Normal file
@@ -0,0 +1,93 @@
|
||||
/* 에디터 콘텐츠 기본 스타일 유틸리티 */
|
||||
/* use-base 클래스를 추가하면 하위 자식 요소들에 기본 스타일이 적용됩니다 */
|
||||
@layer components {
|
||||
.use-base ul,
|
||||
.use-base ol {
|
||||
@apply list-disc pl-6;
|
||||
}
|
||||
.use-base ol {
|
||||
@apply list-decimal;
|
||||
}
|
||||
.use-base ul ul,
|
||||
.use-base ol ol,
|
||||
.use-base ul ol,
|
||||
.use-base ol ul {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.use-base table {
|
||||
@apply w-full border-collapse;
|
||||
}
|
||||
.use-base thead {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
.use-base th,
|
||||
.use-base td {
|
||||
@apply border border-gray-300 px-4 py-2 text-left;
|
||||
}
|
||||
.use-base th {
|
||||
@apply font-semibold bg-gray-50;
|
||||
}
|
||||
.use-base tbody tr:nth-child(even) {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
|
||||
.use-base blockquote {
|
||||
@apply border-l-4 border-gray-300 pl-4 italic text-gray-700;
|
||||
}
|
||||
|
||||
.use-base h1 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
.use-base h2 {
|
||||
@apply text-xl;
|
||||
}
|
||||
.use-base h3 {
|
||||
@apply text-lg;
|
||||
}
|
||||
.use-base h4 {
|
||||
@apply text-base;
|
||||
}
|
||||
.use-base h5 {
|
||||
@apply text-sm;
|
||||
}
|
||||
.use-base h6 {
|
||||
@apply text-xs;
|
||||
}
|
||||
|
||||
.use-base strong,
|
||||
.use-base b {
|
||||
@apply font-bold;
|
||||
}
|
||||
.use-base em,
|
||||
.use-base i {
|
||||
@apply italic;
|
||||
}
|
||||
.use-base u {
|
||||
@apply underline;
|
||||
}
|
||||
.use-base s {
|
||||
@apply line-through;
|
||||
}
|
||||
|
||||
.use-base a {
|
||||
@apply text-blue-600 underline;
|
||||
}
|
||||
.use-base a:hover {
|
||||
@apply text-blue-800;
|
||||
}
|
||||
|
||||
.use-base img {
|
||||
@apply max-w-full h-auto my-4;
|
||||
}
|
||||
|
||||
.use-base pre {
|
||||
@apply bg-gray-100 p-4 rounded overflow-x-auto mb-4;
|
||||
}
|
||||
.use-base code {
|
||||
@apply bg-gray-100 px-1 py-0.5 rounded text-sm;
|
||||
}
|
||||
.use-base pre code {
|
||||
@apply bg-transparent p-0;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
/* Button Size Classes */
|
||||
@layer components {
|
||||
body.dimmed {
|
||||
@apply relative overflow-hidden
|
||||
after:content-[''] after:absolute after:inset-0 after:bg-black/60 after:z-[899];
|
||||
}
|
||||
|
||||
.modal-wrap {
|
||||
@apply overflow-auto fixed inset-0 flex p-5 z-[800];
|
||||
@apply overflow-auto fixed inset-0 flex p-5 z-[900];
|
||||
}
|
||||
|
||||
.modal-wrap.dimmed {
|
||||
|
||||
@@ -3,27 +3,26 @@ import { useLoadingStore } from '#layers/stores/useLoadingStore'
|
||||
|
||||
const loadingStore = useLoadingStore()
|
||||
const { fullLoading } = storeToRefs(loadingStore)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div v-if="fullLoading" class="spinner-container">
|
||||
<div v-if="fullLoading" class="spinner-wrap">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.spinner-container {
|
||||
@apply fixed inset-0 bg-black/90 flex items-center justify-center z-[900];
|
||||
.spinner-wrap {
|
||||
@apply fixed inset-0 bg-black/90 flex items-center justify-center z-[150];
|
||||
}
|
||||
.spinner {
|
||||
@apply w-[80px] h-[80px] bg-cover bg-center bg-no-repeat bg-[url('/images/common/publisning_template_loader_black.png')];
|
||||
}
|
||||
|
||||
[data-theme='light'] {
|
||||
.spinner-container {
|
||||
.spinner-wrap {
|
||||
@apply bg-white/90;
|
||||
}
|
||||
.spinner {
|
||||
|
||||
@@ -15,7 +15,7 @@ const canTeleport = (localId: string) => {
|
||||
<template v-for="[localId, loadingInfo] in localLoadings" :key="localId">
|
||||
<Teleport v-if="canTeleport(localId)" :to="`#${localId}`">
|
||||
<Transition name="fade">
|
||||
<div v-if="loadingInfo.active" class="spinner-container">
|
||||
<div v-if="loadingInfo.active" class="spinner-wrap">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -24,8 +24,8 @@ const canTeleport = (localId: string) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.spinner-container {
|
||||
@apply fixed inset-0 flex items-center justify-center z-[900];
|
||||
.spinner-wrap {
|
||||
@apply absolute inset-0 flex items-center justify-center z-[5];
|
||||
}
|
||||
.spinner {
|
||||
@apply w-[80px] h-[80px] bg-cover bg-center bg-no-repeat bg-[url('/images/common/publisning_template_loader_black.png')];
|
||||
|
||||
@@ -7,13 +7,13 @@ const { isLoading = false } = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isLoading" class="spinner-container">
|
||||
<div v-if="isLoading" class="spinner-wrap">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.spinner-container {
|
||||
.spinner-wrap {
|
||||
@apply absolute inset-0 flex items-center justify-center z-[90];
|
||||
}
|
||||
.spinner {
|
||||
@@ -26,4 +26,3 @@ const { isLoading = false } = defineProps<Props>()
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
interface props {
|
||||
isShowDimmed?: boolean
|
||||
contentText?: string
|
||||
confirmButtonText?: string
|
||||
isOutsideClose?: boolean
|
||||
modalName?: string
|
||||
}
|
||||
import type { DialogParams } from '#layers/types/components/modal'
|
||||
|
||||
const props = withDefaults(defineProps<props>(), {
|
||||
const props = withDefaults(defineProps<DialogParams>(), {
|
||||
isShowDimmed: true,
|
||||
isOutsideClose: false,
|
||||
})
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
interface props {
|
||||
isShowDimmed?: boolean
|
||||
contentText?: string
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string
|
||||
isOutsideClose?: boolean
|
||||
modalName?: string
|
||||
}
|
||||
import type { DialogParams } from '#layers/types/components/modal'
|
||||
|
||||
const props = withDefaults(defineProps<props>(), {
|
||||
const props = withDefaults(defineProps<DialogParams>(), {
|
||||
isShowDimmed: true,
|
||||
isOutsideClose: false,
|
||||
})
|
||||
|
||||
@@ -1,97 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
interface props {
|
||||
isShowDimmed?: boolean
|
||||
isOutsideClose?: boolean
|
||||
contentText?: string
|
||||
contentTitle: string
|
||||
modalName?: string
|
||||
areaClass?: string
|
||||
closeClass?: string
|
||||
tabLength?: number // 탭 총개수
|
||||
tabValue?: number // 현재 탭 번호 (1부터 시작)
|
||||
import type { ContentParams } from '#layers/types/components/modal'
|
||||
|
||||
interface TabItem {
|
||||
title: string
|
||||
desc: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<props>(), {
|
||||
isShowDimmed: true,
|
||||
const props = withDefaults(defineProps<ContentParams>(), {
|
||||
isOutsideClose: false,
|
||||
tabLength: 1,
|
||||
tabValue: 1,
|
||||
contentTitle: '',
|
||||
tabActiveIndex: 0,
|
||||
})
|
||||
|
||||
/**
|
||||
* @description Tab 형태의 모달 컴포넌트 props 전달 예시
|
||||
* const tabList = [{ key: 1, text: 'Trigger1' }, { key: 2, text: 'Trigger2' }, { key: 3, text: 'Trigger3' }]
|
||||
* <BlocksModalContent
|
||||
* :is-show-dimmed="true"
|
||||
* :is-outside-close="true"
|
||||
* :is-open="true"
|
||||
* content-title="타이틀"
|
||||
* :tab-length="tabList.length"
|
||||
* :tab-value="tabNumber"
|
||||
* @update-tab="탭 변경시 처리 함수"
|
||||
* >
|
||||
* <template v-for="trigger in tabList" :key="trigger" #[`trigger${trigger}`]>
|
||||
* <span>{{ trigger.text }}</span>
|
||||
* </template>
|
||||
*
|
||||
* <template #content1>
|
||||
* <p>콘텐츠 내용</p>
|
||||
* </template>
|
||||
* ...
|
||||
* </BlocksModalContent>
|
||||
*
|
||||
* @description 단일 콘텐츠 모달 컴포넌트 props 전달 예시
|
||||
* 위의 Tab 형태의 모달 컴포넌트에서 :tab-length와 :tab-value, @updata-tab 제거하고 사용하면 됩니다.
|
||||
* 내부에 template 태그를 사용하지 않고 콘텐츠 내용만 추가하면 됩니다.
|
||||
* <BlocksModalContent
|
||||
* :is-show-dimmed="true"
|
||||
* :is-outside-close="true"
|
||||
* :is-open="true"
|
||||
* content-title="타이틀"
|
||||
* >
|
||||
* <p>콘텐츠 내용</p>
|
||||
* </BlocksModalContent>
|
||||
*/
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
updateTab: [tabNumber: number]
|
||||
}>()
|
||||
const modalStore = useModalStore()
|
||||
const breakpoints = useResponsiveBreakpoints()
|
||||
|
||||
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
||||
const currentTab = ref(props.tabValue)
|
||||
const currentTab = ref<number>(props.tabActiveIndex)
|
||||
|
||||
const responsiveTransition = computed(() =>
|
||||
breakpoints.value.isXs ? 'slide-up' : 'fade'
|
||||
)
|
||||
const tabInfo = computed<TabItem[]>(() => props.tabInfo ?? [])
|
||||
const isTab = computed(() => tabInfo.value.length >= 2)
|
||||
|
||||
const isVisible = (index: number) => currentTab.value === index
|
||||
|
||||
const handleCloseModal = () => {
|
||||
emit('close')
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const handleOutsideClick = () => {
|
||||
if (props.isOutsideClose) {
|
||||
handleCloseModal()
|
||||
}
|
||||
if (props.isOutsideClose) handleCloseModal()
|
||||
}
|
||||
|
||||
const handleUpdateTab = (tabNumber: number) => {
|
||||
currentTab.value = tabNumber
|
||||
emit('updateTab', tabNumber)
|
||||
if (currentTab.value !== tabNumber) {
|
||||
currentTab.value = tabNumber
|
||||
}
|
||||
}
|
||||
|
||||
watch(isOpen, newVal => {
|
||||
if (newVal) {
|
||||
modalStore.handleControlDimmed(true)
|
||||
} else {
|
||||
modalStore.handleControlDimmed(false)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<Transition :name="responsiveTransition">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
:class="[
|
||||
'modal-wrap pt-[80px] pb-0',
|
||||
{ dimmed: props.isShowDimmed },
|
||||
props.modalName,
|
||||
]"
|
||||
:class="['modal-wrap', { 'is-open': isOpen }, props.modalName]"
|
||||
@click="handleOutsideClick"
|
||||
>
|
||||
<div class="modal-area" @click.stop>
|
||||
<div class="modal-title">
|
||||
<div class="modal-header">
|
||||
<strong class="title">{{ props.contentTitle }}</strong>
|
||||
|
||||
<button type="button" class="modal-close" @click="handleCloseModal">
|
||||
@@ -100,50 +66,45 @@ const handleUpdateTab = (tabNumber: number) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<div class="tab-wrap">
|
||||
<div v-if="$slots.trigger1" class="tab-trigger-wrap">
|
||||
<template v-for="trigger in tabLength" :key="trigger">
|
||||
<div class="modal-body">
|
||||
<template v-if="isTab">
|
||||
<div class="tab-trigger" role="tablist">
|
||||
<template
|
||||
v-for="(tab, index) in tabInfo"
|
||||
:key="tab.title + index"
|
||||
>
|
||||
<button
|
||||
v-if="$slots[`trigger${trigger}`]"
|
||||
type="button"
|
||||
:class="[
|
||||
'tab-trigger',
|
||||
{ 'tab-trigger-active': currentTab === trigger },
|
||||
]"
|
||||
@click="handleUpdateTab(trigger)"
|
||||
:class="['btn-trigger', { 'is-active': isVisible(index) }]"
|
||||
role="tab"
|
||||
@click="handleUpdateTab(index)"
|
||||
>
|
||||
<slot :name="`trigger${trigger}`"></slot>
|
||||
{{ tab.title }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="[
|
||||
'tab-panel-wrap',
|
||||
$slots.trigger1
|
||||
? 'h-[calc(100%-56px)] sm:max-h-[calc(680px-68px-60px)]'
|
||||
: 'sm:pt-[12px] sm:max-h-[calc(680px-60px)]',
|
||||
]"
|
||||
>
|
||||
<template v-for="content in tabLength" :key="content">
|
||||
<!-- 패널 겹침(컨테이너를 가장 긴 패널 높이에 맞춤) -->
|
||||
<div class="tab-panel grid">
|
||||
<template v-for="(tab, index) in tabInfo" :key="tab.desc + index">
|
||||
<div
|
||||
v-if="$slots[`content${content}`]"
|
||||
v-show="currentTab === content"
|
||||
class="tab-panel"
|
||||
>
|
||||
<div class="tab-panel-content">
|
||||
<slot :name="`content${content}`"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tab-panel">
|
||||
<div class="tab-panel-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
v-dompurify-html="tab.desc"
|
||||
:class="[
|
||||
'content-tex',
|
||||
'use-base',
|
||||
{ 'is-visible': isVisible(index) },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-dompurify-html="tabInfo[0].desc"
|
||||
class="content-tex use-base"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,53 +112,53 @@ const handleUpdateTab = (tabNumber: number) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-area {
|
||||
@apply flex flex-col items-center justify-start max-w-[375px] h-full mb-0 rounded-t-[20px] sm:max-w-[560px] sm:h-auto sm:min-h-0 sm:max-h-[680px] sm:mb-auto sm:rounded-[20px];
|
||||
.modal-wrap {
|
||||
@apply overflow-hidden flex-col p-0 pt-[80px] sm:p-5;
|
||||
}
|
||||
.modal-title {
|
||||
@apply relative flex items-center justify-between gap-[8px] w-full py-[16px] px-[20px]
|
||||
.modal-area {
|
||||
@apply overflow-hidden flex flex-col rounded-t-[20px] sm:w-[560px] sm:max-h-[680px] sm:rounded-b-[20px];
|
||||
}
|
||||
.modal-header {
|
||||
@apply flex items-center justify-between gap-[8px] w-full py-[16px] px-[20px]
|
||||
sm:pt-[20px] sm:px-[32px];
|
||||
}
|
||||
.modal-title .title {
|
||||
@apply relative overflow-hidden text-ellipsis line-clamp-2 w-full text-[#1F1F1F] text-[16px] font-[700] leading-[24px] tracking-[-0.48px];
|
||||
.modal-header .title {
|
||||
@apply line-clamp-2 w-full text-[#1F1F1F] text-[16px] font-[700] leading-[24px] tracking-[-0.48px];
|
||||
}
|
||||
.modal-body {
|
||||
@apply flex flex-col flex-1 min-h-0;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@apply relative flex flex-col items-center justify-start w-full h-full flex-1 min-h-0;
|
||||
}
|
||||
|
||||
.tab-wrap {
|
||||
@apply relative flex flex-col items-center justify-center w-full h-full min-h-0;
|
||||
}
|
||||
.tab-trigger-wrap {
|
||||
@apply relative flex items-center justify-center w-full mb-[12px] sm:mb-[24px];
|
||||
}
|
||||
.tab-trigger {
|
||||
@apply relative w-full h-[44px] before:content-[''] before:absolute before:bottom-0 before:left-0 before:w-full before:h-[1px] before:bg-[rgba(0,0,0,0.15)] text-[#1F1F1F] text-[14px] font-[500] leading-[24px] tracking-[-0.42px];
|
||||
@apply relative flex w-full mb-[12px] sm:mb-[24px];
|
||||
}
|
||||
.tab-trigger-active {
|
||||
@apply before:!bg-[#1F1F1F] before:!h-[2px];
|
||||
.btn-trigger {
|
||||
@apply relative w-full py-2.5 before:content-[''] before:absolute before:bottom-0 before:left-0 before:w-full before:h-[1px] before:bg-[rgba(0,0,0,0.15)] text-[#1F1F1F] text-[14px] font-[500] leading-[24px] tracking-[-0.42px];
|
||||
}
|
||||
.btn-trigger.is-active {
|
||||
@apply before:bg-[#1F1F1F] before:h-[2px];
|
||||
}
|
||||
|
||||
.tab-panel-wrap {
|
||||
@apply relative flex flex-col items-center justify-start overflow-hidden w-full pl-[20px] pr-[8px] sm:pl-[32px] flex-1 min-h-0;
|
||||
}
|
||||
.tab-panel {
|
||||
@apply relative flex flex-col items-start justify-start w-full h-full min-h-0 overflow-auto;
|
||||
@apply overflow-hidden grid w-full h-full;
|
||||
}
|
||||
.tab-panel-content {
|
||||
@apply w-full pb-[16px] pr-[12px] sm:pb-[24px] sm:pr-[24px] text-[#333333] text-[15px] font-[400] leading-[24px] tracking-[-0.45px];
|
||||
.tab-panel .content-tex {
|
||||
@apply col-start-1 row-start-1 transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
.tab-panel .content-tex.is-hidden {
|
||||
@apply opacity-0 invisible pointer-events-none;
|
||||
}
|
||||
|
||||
.tab-panel::-webkit-scrollbar {
|
||||
.content-tex {
|
||||
@apply overflow-y-auto mb-4 px-6 sm:mb-6 sm:px-8;
|
||||
}
|
||||
.content-tex::-webkit-scrollbar {
|
||||
@apply w-[4px];
|
||||
}
|
||||
|
||||
.tab-panel::-webkit-scrollbar-track {
|
||||
@apply bg-transparent mb-[16px] sm:mb-[24px];
|
||||
.content-tex::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
.tab-panel::-webkit-scrollbar-thumb {
|
||||
@apply bg-[#D9D9D9] rounded-full;
|
||||
.content-tex::-webkit-scrollbar-thumb {
|
||||
@apply bg-[#D9D9D9] rounded-full px-2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@ const { toast } = modalStore
|
||||
|
||||
<style scoped>
|
||||
.toast-container {
|
||||
@apply fixed left-1/2 max-w-[328px] py-3 px-6 rounded-[8px] bg-[rgba(85,85,85,0.4)] backdrop-blur-[25px] -translate-x-1/2 bottom-[20px] md:bottom-[40px] z-[800]
|
||||
@apply fixed left-1/2 max-w-[328px] py-3 px-6 rounded-[8px] bg-[rgba(85,85,85,0.4)] backdrop-blur-[25px] -translate-x-1/2 bottom-[20px] md:bottom-[40px] z-[900]
|
||||
before:content-[''] before:absolute before:inset-0 before:border before:border-white/10 before:rounded-[8px];
|
||||
}
|
||||
.toast-text {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { getYouTubeEmbedUrl } from '@/layers/utils/youtubeUtil'
|
||||
import type { YoutubeParams } from '#layers/types/components/modal'
|
||||
|
||||
interface Props {
|
||||
youtubeUrl: string
|
||||
isOutsideClose?: boolean
|
||||
modalName?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<YoutubeParams>(), {
|
||||
youtubeUrl: '',
|
||||
isOutsideClose: false,
|
||||
})
|
||||
|
||||
@@ -12,7 +12,6 @@ const { isPassedStoveGnb } = storeToRefs(scrollStore)
|
||||
|
||||
const isEventNavigationOpen = ref(true)
|
||||
const eventNavigationList = ref<Record<string, EventNavigation>>({})
|
||||
// const pinToMain = inject('pinToMain')
|
||||
|
||||
const getEventNavigation = async (): Promise<Record<
|
||||
string,
|
||||
|
||||
@@ -12,10 +12,17 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const mainContentRef = ref<HTMLElement>()
|
||||
|
||||
const { locale } = useI18n()
|
||||
const { getTemplateComponent } = useTemplateRegistry()
|
||||
const { height: viewportH } = useWindowSize()
|
||||
const { bottom: mainBottom } = useElementBounding(mainContentRef)
|
||||
|
||||
const pinToMain = inject('pinToMain')
|
||||
const pinToMain = computed(() => {
|
||||
if (!mainBottom.value) return false
|
||||
return mainBottom.value <= viewportH.value
|
||||
})
|
||||
|
||||
// 개별 메타 태그 표시 여부 확인
|
||||
const shouldShowMetaTag = computed(() => props.pageData?.meta_tag_type === 2)
|
||||
@@ -50,9 +57,11 @@ const setupSeoMeta = (metaTag: PageDataMetaTag) => {
|
||||
})
|
||||
}
|
||||
|
||||
provide('pinToMain', pinToMain)
|
||||
|
||||
onMounted(() => {
|
||||
const { sendLog } = useAnalytics()
|
||||
// sendLog(locale.value, useAnalyticsLogDataDirect('view', 1))
|
||||
sendLog(locale.value, useAnalyticsLogDataDirect('view', 1))
|
||||
})
|
||||
|
||||
// 메타 태그 설정 감시
|
||||
@@ -61,10 +70,13 @@ watchEffect(() => {
|
||||
setupSeoMeta(props.pageData?.meta_tag_json)
|
||||
}
|
||||
})
|
||||
|
||||
// const loadingStore = useLoadingStore()
|
||||
// loadingStore.startFullLoading()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main-content">
|
||||
<div ref="mainContentRef" class="main-content">
|
||||
<template
|
||||
v-for="(template, index) in visibleTemplates"
|
||||
:key="template.template_code ?? index"
|
||||
@@ -76,22 +88,22 @@ watchEffect(() => {
|
||||
:page-ver-tmpl-seq="template.page_ver_tmpl_seq"
|
||||
/>
|
||||
</template>
|
||||
<ClientOnly>
|
||||
<BlocksLnb v-if="isShowLnb" />
|
||||
<div
|
||||
v-if="isShowTopBtn || isShowSnsBtn"
|
||||
:class="['utile-wrap', { 'is-stop': pinToMain }]"
|
||||
>
|
||||
<BlocksButtonScrollTop v-if="isShowTopBtn" />
|
||||
<BlocksButtonSns v-if="isShowSnsBtn" />
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<ClientOnly>
|
||||
<BlocksLnb v-if="isShowLnb" />
|
||||
<div
|
||||
v-if="isShowTopBtn || isShowSnsBtn"
|
||||
:class="['utile-wrap', { 'is-stop': pinToMain }]"
|
||||
>
|
||||
<BlocksButtonScrollTop v-if="isShowTopBtn" />
|
||||
<BlocksButtonSns v-if="isShowSnsBtn" />
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-content {
|
||||
@apply relative min-h-[200px] pt-[48px] md:min-h-[800px] md:pt-[64px];
|
||||
@apply relative pt-[48px] md:pt-[64px];
|
||||
}
|
||||
.utile-wrap {
|
||||
@apply fixed flex flex-col z-[100]
|
||||
|
||||
@@ -27,11 +27,9 @@ const getBtnType = (item?: PageDataResourceGroupBtnInfo): ButtonType => {
|
||||
const handleLogClick = (button: PageDataResourceGroup) => {
|
||||
sendLog(locale.value, useAnalyticsLogDataDirect(button, props.pageVerTmplSeq))
|
||||
if (button.btn_info?.detail?.btn_type === 'POP') {
|
||||
const popupSize = button.btn_info?.detail?.size_info
|
||||
const popupTitle = button.btn_info?.detail?.title
|
||||
const popupContent = button.btn_info?.detail?.tab_info[0].title
|
||||
modalStore.handleOpenAlert({
|
||||
contentText: popupContent,
|
||||
modalStore.handleOpenContent({
|
||||
contentTitle: button.btn_info?.detail?.title,
|
||||
tabInfo: button.btn_info?.detail?.tab_info,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const isLoading = ref(true)
|
||||
const mainRef = ref<HTMLElement>()
|
||||
|
||||
const { height: viewportH } = useWindowSize()
|
||||
const { bottom: mainBottom } = useElementBounding(mainRef)
|
||||
|
||||
const pinToMain = computed(() => {
|
||||
if (!mainBottom.value) return false
|
||||
return mainBottom.value <= viewportH.value
|
||||
})
|
||||
|
||||
provide('pinToMain', pinToMain)
|
||||
|
||||
onMounted(() => {
|
||||
isLoading.value = false
|
||||
@@ -19,8 +8,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<LayoutsHeader />
|
||||
<AtomsLoadingSimple :is-loading="isLoading" />
|
||||
<main id="LayoutsMain" ref="mainRef" class="relative">
|
||||
<main class="relative">
|
||||
<slot />
|
||||
</main>
|
||||
<LayoutsFooter />
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
const mainRef = ref<HTMLElement>()
|
||||
|
||||
const { height: viewportH } = useWindowSize()
|
||||
const { bottom: mainBottom } = useElementBounding(mainRef)
|
||||
|
||||
const pinToMain = computed(() => {
|
||||
if (!mainBottom.value) return false
|
||||
return mainBottom.value <= viewportH.value
|
||||
})
|
||||
|
||||
provide('pinToMain', pinToMain)
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LayoutsHeader />
|
||||
<main id="LayoutsMain" ref="mainRef" class="relative">
|
||||
<main class="relative">
|
||||
<BlocksButtonHome />
|
||||
<LayoutsEventNavigation />
|
||||
<slot />
|
||||
|
||||
@@ -11,8 +11,10 @@ export default defineNuxtPlugin(nuxtApp => {
|
||||
'i',
|
||||
'em',
|
||||
'u',
|
||||
's',
|
||||
'a',
|
||||
'p',
|
||||
'ol',
|
||||
'ul',
|
||||
'li',
|
||||
'span',
|
||||
@@ -23,6 +25,11 @@ export default defineNuxtPlugin(nuxtApp => {
|
||||
'dl',
|
||||
'dt',
|
||||
'dd',
|
||||
'blockquote',
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'tfoot',
|
||||
'tr',
|
||||
'th',
|
||||
'td',
|
||||
@@ -39,6 +46,9 @@ export default defineNuxtPlugin(nuxtApp => {
|
||||
'height',
|
||||
'frameborder',
|
||||
'allowfullscreen',
|
||||
'colspan',
|
||||
'rowspan',
|
||||
'scope',
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,6 +2,8 @@ import { defineStore, skipHydrate } from 'pinia'
|
||||
import type {
|
||||
DialogParams,
|
||||
YoutubeParams,
|
||||
ToastParams,
|
||||
ContentParams,
|
||||
} from '#layers/types/components/modal'
|
||||
|
||||
const createModalState = () => ({
|
||||
@@ -17,6 +19,19 @@ const createModalState = () => ({
|
||||
export const useModalStore = defineStore('modalStore', () => {
|
||||
const scrollStore = useScrollStore()
|
||||
|
||||
// dimmed ------------------
|
||||
/**
|
||||
* @description 모달을 바디에서 컨트롤 필요 시 사용하는 함수입니다.
|
||||
* @param state - 모달 바디 상태
|
||||
*/
|
||||
const handleControlDimmed = (state: boolean) => {
|
||||
if (state) {
|
||||
document.body.classList.add('dimmed')
|
||||
} else {
|
||||
document.body.classList.remove('dimmed')
|
||||
}
|
||||
}
|
||||
|
||||
// alert ------------------
|
||||
const alert = {
|
||||
...createModalState(),
|
||||
@@ -101,7 +116,7 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
storeContentText: ref(''),
|
||||
}
|
||||
|
||||
const handleOpenToast = (contentText: string, duration: number = 2000) => {
|
||||
const handleOpenToast = ({ contentText, duration = 2000 }: ToastParams) => {
|
||||
toast.storeIsOpen.value = true
|
||||
toast.storeContentText.value = contentText
|
||||
|
||||
@@ -111,15 +126,43 @@ export const useModalStore = defineStore('modalStore', () => {
|
||||
}, duration)
|
||||
}
|
||||
|
||||
// content ------------------
|
||||
const content = {
|
||||
storeIsOpen: ref(false),
|
||||
storeModalName: ref(''),
|
||||
storeIsOutsideClose: ref(false),
|
||||
storeContentTitle: ref(''),
|
||||
storeTabInfo: ref(null),
|
||||
storeTabActiveIndex: ref(0),
|
||||
}
|
||||
|
||||
const handleOpenContent = ({
|
||||
isOutsideClose = false,
|
||||
modalName = '',
|
||||
contentTitle,
|
||||
tabInfo,
|
||||
tabActiveIndex,
|
||||
}: ContentParams) => {
|
||||
content.storeIsOpen.value = true
|
||||
content.storeModalName.value = modalName
|
||||
content.storeIsOutsideClose.value = isOutsideClose
|
||||
content.storeContentTitle.value = contentTitle
|
||||
content.storeTabInfo.value = tabInfo
|
||||
content.storeTabActiveIndex.value = tabActiveIndex
|
||||
}
|
||||
|
||||
return {
|
||||
alert,
|
||||
confirm,
|
||||
youtube,
|
||||
toast,
|
||||
content,
|
||||
handleOpenAlert,
|
||||
handleOpenConfirm,
|
||||
handleOpenYoutube,
|
||||
handleResetYoutube,
|
||||
handleOpenToast,
|
||||
handleOpenContent,
|
||||
handleControlDimmed,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -106,7 +106,6 @@ const sortedCharacterList = computed(() => {
|
||||
*/
|
||||
const openAlert = (text: string) => {
|
||||
handleOpenAlert({
|
||||
isShowDimmed: true,
|
||||
contentText: text,
|
||||
isOutsideClose: true,
|
||||
})
|
||||
|
||||
@@ -15,3 +15,19 @@ export interface YoutubeParams {
|
||||
isOutsideClose?: boolean
|
||||
modalName?: string
|
||||
}
|
||||
|
||||
export interface ToastParams {
|
||||
contentText: string
|
||||
duration: number
|
||||
}
|
||||
|
||||
export interface ContentParams {
|
||||
isOutsideClose?: boolean
|
||||
modalName?: string
|
||||
contentTitle: string
|
||||
tabInfo: {
|
||||
title: string
|
||||
desc: string
|
||||
}[]
|
||||
tabActiveIndex?: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user