feat: 쿠폰등록

This commit is contained in:
최만억 (Jo)
2025-11-10 06:22:43 +00:00
committed by 김채린
parent ceee82268c
commit 77c760c022
33 changed files with 2681 additions and 16 deletions

View File

@@ -0,0 +1,524 @@
<script setup lang="ts">
import {
getYear,
getMonth,
startOfDay,
globalDateFormat,
} from '@seed-next/date'
import type { PublicMethods } from '@vuepic/vue-datepicker'
interface Props {
date: Date | string | null
minDate?: Date | null
maxDate?: Date | null
placeholder?: Date | string | null
}
const props = withDefaults(defineProps<Props>(), {
minDate: null,
maxDate: null,
placeholder: null,
})
const emits = defineEmits(['update:date'])
// Composabled
const breakpoints = useResponsiveBreakpoints()
// Refs -----
const { tm, locale } = useI18n()
const datePickerRef = ref<any>(null)
const resultDate = ref('')
const formatDate = ref('')
const currentYear = ref(new Date().getFullYear())
const currentMonth = ref(new Date().getMonth())
const currentDecade = ref<{ start: number; end: number } | null>(null)
// 상태값
const isMenuOpened = ref(false)
const isShowYearPicker = ref(false)
const isShowMonthPicker = ref(false)
const isShowDecadePicker = ref(false)
// Computed
const yearMonthText = computed(() => {
return `${currentYear.value}${currentMonth.value + 1}`
})
const formatMinDate = computed(() => {
if (props.minDate !== null) {
return globalDateFormat(props.minDate, locale.value)
} else {
return undefined
}
})
const formatMaxDate = computed(() => {
if (props.maxDate !== null) {
return globalDateFormat(props.maxDate, locale.value)
} else {
return undefined
}
})
const formatPlaceholder = computed(() => {
if (props.placeholder !== null) {
return props.placeholder instanceof Date
? (globalDateFormat(props.placeholder, locale.value) as string)
: props.placeholder
} else {
return ''
}
})
// Functions
const setFormatDate = () => {
const toDay = startOfDay(new Date())
resultDate.value =
props.date === null ? toDay.toString() : props.date.toString()
formatDate.value = globalDateFormat(resultDate.value, locale.value) as string
const dateObj = props.date === null ? toDay : new Date(props.date)
currentYear.value = dateObj.getFullYear()
currentMonth.value = dateObj.getMonth()
}
const updateDate = (_date: Date | null) => {
if (_date != null) {
formatDate.value = globalDateFormat(_date, locale.value) as string
currentYear.value = _date.getFullYear()
currentMonth.value = _date.getMonth()
emits('update:date', formatDate.value)
}
}
const format = (_date: Date) => {
return globalDateFormat(_date, locale.value)
}
const handleMenuOpen = () => {
isMenuOpened.value = true
}
const handleMenuClose = () => {
isMenuOpened.value = false
isShowMonthPicker.value = false
isShowYearPicker.value = false
}
const handleOpenMonthPicker = () => {
isShowMonthPicker.value = true
isShowYearPicker.value = false
}
const handleOpenYearPicker = () => {
isShowYearPicker.value = true
isShowMonthPicker.value = false
}
const handleMonthSelect = (month: number) => {
currentMonth.value = month
isShowMonthPicker.value = false
// 선택한 년도/월로 날짜 업데이트
const newDate = new Date(currentYear.value, currentMonth.value, 1)
if (props.minDate && newDate < props.minDate) {
newDate.setDate(props.minDate.getDate())
}
if (props.maxDate && newDate > props.maxDate) {
newDate.setDate(props.maxDate.getDate())
}
// DatePicker의 내부 상태 업데이트
if (datePickerRef.value) {
datePickerRef.value.selectDate(newDate)
}
updateDate(newDate)
}
const handleYearSelect = (year: number) => {
currentYear.value = year
isShowYearPicker.value = false
// 선택한 년도/월로 날짜 업데이트
const newDate = new Date(currentYear.value, currentMonth.value, 1)
if (props.minDate && newDate < props.minDate) {
newDate.setDate(props.minDate.getDate())
}
if (props.maxDate && newDate > props.maxDate) {
newDate.setDate(props.maxDate.getDate())
}
// DatePicker의 내부 상태 업데이트
if (datePickerRef.value) {
datePickerRef.value.selectDate(newDate)
}
updateDate(newDate)
}
const generateYearList = () => {
const years = []
const minYear = props.minDate ? props.minDate.getFullYear() : 1900
const maxYear = props.maxDate ? props.maxDate.getFullYear() : 2100
for (let year = minYear; year <= maxYear; year++) {
years.push(year)
}
return years
}
const generateMonthList = () => {
const months = []
const minMonth =
props.minDate && props.minDate.getFullYear() === currentYear.value
? props.minDate.getMonth()
: 0
const maxMonth =
props.maxDate && props.maxDate.getFullYear() === currentYear.value
? props.maxDate.getMonth()
: 11
for (let month = minMonth; month <= maxMonth; month++) {
months.push(month)
}
return months
}
watch(
() => props.date,
() => {
setFormatDate()
},
{ immediate: true }
)
onMounted(() => {
setFormatDate()
})
</script>
<template>
<VueDatePicker
ref="datePickerRef"
v-model="resultDate"
:locale="locale"
week-start="0"
:year-first="true"
:auto-apply="true"
:hide-offset-dates="true"
:enable-time-picker="false"
:preview-format="format"
:min-date="formatMinDate"
:max-date="formatMaxDate"
:prevent-min-max-navigation="true"
:month-change-on-scroll="false"
position="left"
:auto-position="true"
:offset="8"
:config="{
shadowDom: true,
}"
class="date-picker-wrap"
@update:model-value="updateDate"
@date-update="updateDate"
@open="handleMenuOpen"
@closed="handleMenuClose"
>
<template #trigger>
<div
class="relative flex items-center justify-between w-full h-[40px] px-[16px] border border-solid border-[1px] border-[#D9D9D9] rounded-[8px] bg-white cursor-pointer"
>
<AtomsInput
:model-value="formatDate"
:placeholder="formatPlaceholder"
:value="formatDate"
name="date-picker-text"
label
readonly
class="inline-flex items-center justify-start !w-full !h-auto !p-0 m-0 bg-transparent border-none outline-none cursor-pointer text-left !text-[14px] text-[#333333] font-[400] !leading-[20px] !tracking-[-0.42px]"
/>
<AtomsIconsCalendarLine
:size="breakpoints.isMobile ? 16 : 20"
color="#333333"
class="absolute top-[50%] right-[12px] translate-y-[-50%] md:right-[16px]"
/>
</div>
</template>
<template #calendar-header="{ index, day }">
<div :class="index === 0 ? 'sunday' : index === 6 ? 'saturday' : ''">
{{ day }}
</div>
</template>
<template #day="{ day, date }">
<span
:class="[
date.getDay() === 0 ? 'text-[#FC4420]' : 'text-[#333333]',
date > new Date(formatMaxDate) ? 'opacity-35' : 'opacity-100',
]"
>
{{ day }}
</span>
</template>
<template
#month-year="{
month,
year,
months,
years,
updateMonthYear,
handleMonthYearChange,
isDisabled,
}"
>
<div class="flex items-center justify-between w-full py-[16px]">
<button
type="button"
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
:disabled="isDisabled(false)"
@click="
updateMonthYear(
month === 0 ? 11 : month - 1,
month === 0 ? year - 1 : year,
false
)
"
>
<AtomsIconsArrowRightLine
:size="16"
:color="isDisabled(false) ? '#CCCCCC' : '#333333'"
class="relative rotate-180"
/>
</button>
<button
type="button"
class="relative inline-flex items-center justify-center gap-[8px] w-auto h-[32px]"
@click="handleOpenMonthPicker"
>
<span
class="inline-flex items-center justify-center text-[#1F1F1F] text-[18px] font-[500] leading-[26px] tracking-[-0.54px]"
>
{{ years.find(y => y.value === year)?.text }}.
{{ months.find(m => m.value === month)?.text }}
</span>
<AtomsIconsSelectArrowDownFill :size="10" color="#333333" />
</button>
<button
type="button"
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
:disabled="isDisabled(true)"
@click="
updateMonthYear(
month === 11 ? 0 : month + 1,
month === 11 ? year + 1 : year,
false
)
"
>
<AtomsIconsArrowRightLine
:size="16"
:color="isDisabled(true) ? '#CCCCCC' : '#333333'"
class="relative"
/>
</button>
</div>
<div
v-if="isShowMonthPicker && isMenuOpened"
class="absolute z-[2] top-0 left-0 flex flex-col items-center justify-start w-full h-full overflow-y-auto bg-white"
>
<div class="flex items-center justify-between w-full py-[16px]">
<button
type="button"
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
:disabled="isDisabled(false)"
@click="
updateMonthYear(
month,
year === getYear(minDate) ? year : year - 1,
false
)
"
>
<AtomsIconsArrowRightLine
:size="16"
:color="isDisabled(false) ? '#CCCCCC' : '#333333'"
class="relative rotate-180"
/>
</button>
<button
type="button"
class="relative inline-flex items-center justify-center gap-[8px] w-auto h-[32px]"
@click="handleOpenYearPicker"
>
<span
class="inline-flex items-center justify-center text-[#1F1F1F] text-[18px] font-[500] leading-[26px] tracking-[-0.54px]"
>
{{ years.find(y => y.value === year)?.text }}
</span>
</button>
<button
type="button"
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
:disabled="isDisabled(true)"
@click="
updateMonthYear(
month,
year === getYear(maxDate) ? year : year + 1,
false
)
"
>
<AtomsIconsArrowRightLine
:size="16"
:color="isDisabled(true) ? '#CCCCCC' : '#333333'"
class="relative"
/>
</button>
</div>
<div class="grid grid-cols-3 gap-[4px] w-full">
<button
v-for="month in generateMonthList()"
:key="month"
type="button"
:class="[
'inline-flex items-center justify-center w-full h-[52px] px-[12px] py-[8px] text-center text-[14px] font-[400] leading-[20px] tracking-[-0.42px] rounded-[4px] transition-colors',
year === currentYear && month === currentMonth
? 'bg-[#FC4420] text-white'
: 'text-[#333333] hover:bg-[#F5F5F5]',
]"
@click="handleMonthSelect(month)"
>
{{ months.find(m => m.value === month)?.text }}
</button>
</div>
</div>
<div
v-if="isShowYearPicker && isMenuOpened"
class="absolute z-[2] top-0 left-0 w-full h-full mt-[8px] overflow-y-auto bg-white"
>
<div class="grid grid-cols-4 gap-[4px]">
<button
v-for="year in generateYearList()"
:key="year"
type="button"
:class="[
'px-[12px] py-[8px] text-center text-[14px] font-[400] leading-[20px] tracking-[-0.42px] rounded-[4px] transition-colors',
year === currentYear
? 'bg-[#FC4420] text-white'
: 'text-[#333333] hover:bg-[#F5F5F5]',
]"
@click="handleYearSelect(year)"
>
{{ years.find(y => y.value === year)?.text }}
</button>
</div>
</div>
</template>
</VueDatePicker>
</template>
<style scoped>
.date-picker-wrap :deep(.dp__calendar_header_separator) {
display: none !important;
}
.date-picker-wrap :deep(.dp__menu) {
padding: 0 12px 8px !important;
}
.date-picker-wrap :deep(.dp__menu_inner) {
padding: 0 !important;
}
.date-picker-wrap :deep(.dp__calendar) {
padding: 0 !important;
}
.date-picker-wrap :deep(.dp__calendar_row) {
-webkit-gap: 4px !important;
-moz-gap: 4px !important;
-o-gap: 4px !important;
-ms-gap: 4px !important;
gap: 4px !important;
margin: 0 !important;
}
.date-picker-wrap :deep(.dp__calendar_item) {
padding: 2px 4px !important;
}
.date-picker-wrap :deep(.dp__cell_inner) {
padding: 8px !important;
border-radius: 100% !important;
outline: none !important;
border: none !important;
}
.date-picker-wrap :deep(.dp__cell_inner.dp__date_hover:hover) {
background-color: rgba(0, 0, 0, 0.04) !important;
}
.date-picker-wrap :deep(.dp__cell_inner.dp__today) {
border: 1px solid rgba(0, 0, 0, 0.15) !important;
}
.date-picker-wrap :deep(.dp__cell_inner.dp__active_date) {
background-color: #fc4420 !important;
color: #ffffff !important;
}
.date-picker-wrap :deep(.dp__cell_inner.dp__active_date span) {
color: #ffffff !important;
}
.date-picker-wrap :deep(.dp__cell_disabled:hover),
.date-picker-wrap :deep(.dp__cell_disabled) {
cursor: default !important;
background-color: transparent !important;
}
.date-picker-wrap :deep(.dp__menu),
.date-picker-wrap :deep(.dp__menu_wrap),
.date-picker-wrap :deep(.dp__overlay) {
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.1) !important;
}
.date-picker-wrap :deep(.dp__overlay_cell_pad) {
color: #333333 !important;
}
.date-picker-wrap :deep(.dp__overlay_cell_pad:hover) {
background-color: #f5f5f5 !important;
}
.date-picker-wrap :deep(.dp__overlay_cell_pad.dp__overlay_cell_active) {
background-color: #fc4420 !important;
color: #ffffff !important;
}
.date-picker-wrap :deep(.dp__overlay_cell_pad.dp__overlay_cell_disabled:hover),
.date-picker-wrap :deep(.dp__overlay_cell_pad.dp__overlay_cell_disabled) {
cursor: default !important;
background-color: transparent !important;
opacity: 0.35 !important;
}
.date-picker-wrap :deep(.dp__inner_nav) {
color: #333333 !important;
}
.date-picker-wrap :deep(.dp__inner_nav:hover) {
background-color: transparent !important;
}
.date-picker-wrap :deep(.dp__inner_nav.dp__inner_nav_disabled) {
background-color: transparent !important;
cursor: default !important;
opacity: 0.35 !important;
}
.date-picker-wrap :deep(.dp__month_year_select),
.date-picker-wrap :deep(.dp__month_year_select:hover) {
background-color: transparent !important;
}
.date-picker-wrap :deep(.dp__arrow_bottom),
.date-picker-wrap :deep(.dp__arrow_top),
.date-picker-wrap :deep(.dp__arrow_left),
.date-picker-wrap :deep(.dp__arrow_right) {
display: none !important;
}
.date-picker-wrap.date-picker-menu-absolute :deep(.dp__menu) {
position: absolute !important;
}
.date-picker-wrap.date-picker-menu-fixed :deep(.dp__menu) {
position: fixed !important;
}
.date-picker-wrap :deep(.dp__month_year_select) {
display: none !important;
}
</style>

View File

@@ -0,0 +1,150 @@
<script setup lang="ts">
interface Props {
totalCount: number
currentPage: number
pageSize: number
pageBlock?: number
useMinMax?: boolean
}
const props = withDefaults(defineProps<Props>(), {
currentPage: 1,
pageBlock: 10,
useMinMax: true,
})
const emits = defineEmits(['update:page'])
const currentPage = ref(props.currentPage)
const firstPage = ref(1)
const lastPage = computed(() => Math.ceil(props.totalCount / props.pageSize))
const prevPage = computed(() => currentPage.value - 1)
const nextPage = computed(() => currentPage.value + 1)
const currentBlock = computed(() =>
isShortThanBlock.value ? 1 : Math.ceil(currentPage.value / props.pageBlock)
)
// 블럭 크기가 전체 페이지 수보다 작은지 여부
const isShortThanBlock = computed(() => lastPage.value < props.pageBlock)
// 현재 블럭이 첫 블럭인지 여부
const isFirstBlock = computed(() => currentBlock.value === 1)
// 현재 블럭이 마지막 블럭인지 여부
const isLastBlock = computed(
() => currentBlock.value === Math.ceil(lastPage.value / props.pageBlock)
)
const lastBlock = computed(() =>
isShortThanBlock.value ? 1 : Math.ceil(lastPage.value / props.pageBlock)
)
const currentBlockFirstPage = computed(() =>
isShortThanBlock.value ? 1 : (currentBlock.value - 1) * props.pageBlock + 1
)
const blocks = computed(() => {
if (props.totalCount === 0) {
return []
}
const blockSize =
currentBlock.value === lastBlock.value &&
lastPage.value % props.pageBlock !== 0
? lastPage.value - currentBlockFirstPage.value + 1
: props.pageBlock
return Array.from(
{ length: blockSize },
(_, i) => currentBlockFirstPage.value + i
)
})
const handlePagination = (page: number) => {
currentPage.value = page
emits('update:page', page)
}
onMounted(() => {
console.log(blocks.value)
})
</script>
<template>
<div
v-if="props.totalCount > 0"
class="relative flex items-center justify-center w-full py-[32px]"
>
<template v-if="totalCount > pageSize">
<template v-if="props.useMinMax">
<button
type="button"
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
:disabled="currentPage === firstPage"
@click="handlePagination(firstPage)"
>
<AtomsIconsArrowDoubleLeftLine
:size="11"
:color="currentPage === firstPage ? '#CCCCCC' : '#333333'"
/>
</button>
</template>
<button
type="button"
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
:disabled="currentPage === firstPage"
@click="handlePagination(prevPage)"
>
<AtomsIconsArrowRightLine
:size="16"
:color="currentPage === firstPage ? '#CCCCCC' : '#333333'"
class="rotate-180"
/>
</button>
</template>
<ol class="relative inline-flex items-center justify-center">
<template v-for="page in blocks" :key="page">
<li
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
>
<AtomsButton
type="action"
button-size="size-small"
background-color="transparent"
text-color="#333333"
:class="[
'!w-full !h-full p-0 rounded-full text-center text-[14px] font-[500] leading-[24px] tracking-[-0.42px]',
page === currentPage
? '!bg-[#C7AE8B] !text-white cursor-default'
: '',
]"
@click="handlePagination(page)"
>
<span>{{ page }}</span>
</AtomsButton>
</li>
</template>
</ol>
<template v-if="totalCount > pageSize">
<button
type="button"
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
:disabled="currentPage === lastPage"
@click="handlePagination(nextPage)"
>
<AtomsIconsArrowRightLine
:size="16"
:color="currentPage === lastPage ? '#CCCCCC' : '#333333'"
/>
</button>
<button
type="button"
class="relative inline-flex items-center justify-center w-[32px] h-[32px]"
:disabled="currentPage === lastPage"
@click="handlePagination(lastPage)"
>
<AtomsIconsArrowDoubleLeftLine
:size="11"
:color="currentPage === lastPage ? '#CCCCCC' : '#333333'"
class="rotate-180"
/>
</button>
</template>
</div>
</template>
<style scoped></style>