feat: 추가 정보 팝업 컴포넌트

This commit is contained in:
최만억 (Jo)
2025-11-12 01:58:05 +00:00
committed by 김채린
parent 24ca011399
commit 989589f23c

View File

@@ -0,0 +1,203 @@
<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부터 시작)
}
const props = withDefaults(defineProps<props>(), {
isShowDimmed: true,
isOutsideClose: false,
tabLength: 1,
tabValue: 1,
contentTitle: '',
})
/**
* @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 isOpen = defineModel<boolean>('isOpen', { default: false })
const currentTab = ref(props.tabValue)
const handleCloseModal = () => {
emit('close')
isOpen.value = false
}
const handleOutsideClick = () => {
if (props.isOutsideClose) {
handleCloseModal()
}
}
const handleUpdateTab = (tabNumber: number) => {
currentTab.value = tabNumber
emit('updateTab', tabNumber)
}
</script>
<template>
<Transition name="fade">
<div
v-if="isOpen"
:class="[
'modal-wrap pt-[80px] pb-0',
{ dimmed: props.isShowDimmed },
props.modalName,
]"
@click="handleOutsideClick"
>
<div class="modal-area" @click.stop>
<div class="modal-title">
<strong class="title">{{ props.contentTitle }}</strong>
<button type="button" class="modal-close" @click="handleCloseModal">
<span class="sr-only">close</span>
<AtomsIconsCloseLine size="24" color="#333333" />
</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">
<button
v-if="$slots[`trigger${trigger}`]"
type="button"
:class="[
'tab-trigger',
{ 'tab-trigger-active': currentTab === trigger },
]"
@click="handleUpdateTab(trigger)"
>
<slot :name="`trigger${trigger}`"></slot>
</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
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>
</template>
</div>
</div>
</div>
</div>
</div>
</Transition>
</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-title {
@apply relative 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-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];
}
.tab-trigger-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;
}
.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::-webkit-scrollbar {
@apply w-[4px];
}
.tab-panel::-webkit-scrollbar-track {
@apply bg-transparent mb-[16px] sm:mb-[24px];
}
.tab-panel::-webkit-scrollbar-thumb {
@apply bg-[#D9D9D9] rounded-full;
}
</style>