fix. 팝업 버튼 추가

This commit is contained in:
clkim
2025-11-13 10:59:21 +09:00
parent 2902fe0222
commit 6765ae905c
22 changed files with 338 additions and 223 deletions

View File

@@ -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 {

View File

@@ -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')];

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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,
})

View File

@@ -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,

View File

@@ -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]

View File

@@ -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,
})
}
}