Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- @jsonproperty
- docker
- 쇼트유알엘
- 프로그래머스
- CentOS
- AWS
- Kafka
- Spring Security
- 데이터베이스
- 패스트캠퍼스
- 스프링의 정석
- 시큐리티
- DB
- WEB SOCKET
- 스파르타코딩클럽
- 카프카
- 생성자 주입
- emqx
- MYSQL
- 스웨거
- EC2
- visualvm
- Spring
- JWT
- 웹개발
- 항해99
- java
- 개인프로젝트
- JavaScript
- 남궁성과 끝까지 간다
Archives
- Today
- Total
Nellie's Blog
[Vue3] 커스텀 달력 구현 본문
728x90
아래와 같은 커스텀 달력 구현을 완료하였다.
props, emit 을 사용하여 약간 복잡해보이는 로직이어서 조금 어려웠다.
개발은 환자의 일정 리스트의 개수를 달력에 보여주고, 클릭을 하면 해당 일정의 리스트의 상세내역을 보여준다.
주요 컴포넌트는 단 두개이다.
list 컴포넌트(부모) / ExmpleFull(달력, 자식) 컴포넌트.
구현 과정
list 컴포넌트 :
리스트의 수를 가져와서 {날짜 : 상세데이터} 오브젝트로 만들어주고,
달력 컴포넌에 props로 해당 오브젝트를 넘겨주고,
ExampleFull 컴포넌트 :
달력 컴포넌트에 해당 날짜의 상세데이터.length를 읽어서 표시해준다.
클릭 시에는 부모 컴포넌트에 emit으로 날짜를 넘겨주고,
list 컴포넌트 :
부모 컴포넌트는 emit 으로 받은 날짜에 해당하는 예약상세데이터를 출력해준다.
전체 코드이다.
일정 > list 컴포넌트
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { RETURN_CODE } from '@/apis/common'
import { ReserveModel, getReserveScheduleList, getReserveList } from '@/apis/reserve'
import { useRouter } from 'vue-router'
/**
|--------------------------------------------------
| 😃 1. 변수
|--------------------------------------------------
*/
const isRefresh = ref(false)
const router = useRouter()
const reserveType = ref<any>('')
const reserveDate = ref<any>('')
const patientData = ref<any>('') // 환자 정보
const data = ref<ReserveModel>({} as ReserveModel) // 환자 예약 정보
const reservationCounts = ref<any>({}) // 환자 예약 정보 리스트로 저장
const bottom = ref(false) // 바텀 팝업 설정
const popupMsg = ref<any>([]) // 팝업 메시지
/**
|--------------------------------------------------
| 🔑 2. 일반 함수
|--------------------------------------------------
*/
/* 1. 토큰 확인 함수 */
function tokenError(code: string) {
if (/^30002\d{3}$/.test(code)) {
Snackbar.error('로그인 후 이용 부탁드립니다')
router.replace('/login')
}
}
/** 2. 새로고침 함수 */
function handleRefresh() {
isRefresh.value = false
}
/** 3. 팝업을 오픈하는 함수 */
function openPopup(date: any) {
console.log('날짜 클릭 이벤트를 수신했습니다. ::', date)
bottom.value = true
popupMsg.value = []
for (const item of reservationCounts.value[date]) {
popupMsg.value.push(item)
}
}
/** 4. 날짜 포매팅 함수 */
const formattedDateTime = formatDateTime('2024-03-26T07:07:00')
function formatDateTime(dateTimeString: string): string {
const dateTime = new Date(dateTimeString)
const year = dateTime.getFullYear()
const month = ('0' + (dateTime.getMonth() + 1)).slice(-2)
const day = ('0' + dateTime.getDate()).slice(-2)
const hour = ('0' + dateTime.getHours()).slice(-2)
const minute = ('0' + dateTime.getMinutes()).slice(-2)
return `${year}.${month}.${day} ${hour}:${minute}`
}
console.log(formattedDateTime) // 출력: "2024. 03. 26 07:07"
/**
|--------------------------------------------------
| 🎁 3. fetch 함수
|--------------------------------------------------
*/
/**------------------------
| 🎈 fetch 함수 1
|------------------------*/
/** 1. 예약 스케줄 리스트 조회 호출 */
async function fetchData() {
let response
const params = {
patientId: 3 // TODO 로그인 사용자로 수정해야 함
} as ReserveModel
if (history.state.type) params.reserveType = history.state.type
if (history.state.type) params.reserveDt = history.state.date
/** 환자 정보 조회 */
response = await getReserveScheduleList(params)
if (response.code !== RETURN_CODE.SUCCESS) {
console.log('예약 정보를 가져오는 중 오류가 발생했습니다.')
return
}
patientData.value = response.data
console.log('response- getReserveScheduleList .....::', response.data)
/** 환자 예약 리스트 조회 */
response = await getReserveList(params)
if (response.code !== RETURN_CODE.SUCCESS) {
console.log('예약 정보를 가져오는 중 오류가 발생했습니다.')
return
}
tokenError(response.code)
data.value = response.data
for (const item of data.value.dataList) {
let dateString = item.reserveDt?.toString()
reserveDate.value = dateString?.slice(8, 10)
// 날짜-예약정보 (키-밸류) 오브젝트 만들기
if (reserveDate.value) {
if (reservationCounts.value[reserveDate.value]) {
reservationCounts.value[reserveDate.value].push(item)
} else {
reservationCounts.value[reserveDate.value] = [item]
}
}
}
// 결과 출력
for (const date in reservationCounts) {
console.log(`{${date}: ${reservationCounts.value[date]}}`)
}
}
/**
|--------------------------------------------------
| 🎁 4. LifeCycle 함수
|--------------------------------------------------
*/
onMounted(() => {
fetchData()
})
</script>
<!-- /**
|--------------------------------------------------
| 🎨 화면
|--------------------------------------------------
*/ -->
<template>
<div class="message">
<var-pull-refresh v-model="isRefresh" @refresh="handleRefresh">
<app-header title="일정">
<template #left>
<app-back />
</template>
<template #right>
<app-side-menu />
</template>
</app-header>
<div class="message-list">
<div class="message-item">
<div class="message-item-detail">
<div class="message-item-detail-header-centered-box">
<var-paper>{{ patientData?.patientName }}님</var-paper>
<var-paper>환자 등록번호 : {{ patientData?.patientId }}</var-paper>
</div>
<div class="message-item-detail-header-centered-box">
<ExampleFull
:reserveDate="reserveDate"
:reserveType="reserveType"
:reservationCounts="reservationCounts"
:data="data"
@dateEmit="openPopup"
/>
</div>
<div class="message-item-detail-description"></div>
</div>
</div>
</div>
</var-pull-refresh>
</div>
<var-button type="primary" block @click="bottom = true" style="display: none"> Below Popup </var-button>
<router-stack-view />
<!-- 아래 팝업 -->
<var-popup position="bottom" v-model:show="bottom">
<div class="popup-example-block">
<div v-for="(item, i) in popupMsg" :key="i">
{{ formatDateTime(item.reserveDt) }} <br />
<div style="font-weight: bold">
{{ item.name }}<span style="display: block; text-align: right">{{ item.status }}</span>
</div>
{{ item.comment }}<br /><br />
<hr />
</div>
</div>
</var-popup>
</template>
<style lang="less" scoped>
.message {
padding: calc(var(--app-bar-height)) 0 0;
&-list {
padding: 3px 0;
}
&-item {
position: relative;
display: flex;
padding-top: 3px;
&-avatar {
flex-shrink: 0;
margin: 0 18px;
}
&-detail {
width: 100%;
border-bottom: thin solid var(--divider-color);
padding-bottom: 5px;
&-header {
display: flex;
justify-content: space-between;
align-items: center;
&-name {
color: var(--app-title-color);
font-size: 16px;
width: 190px;
}
&-date {
color: var(--app-subtitle-color);
font-size: 14px;
margin-bottom: 2px;
}
&-centered-box {
/* 위아래 여백 조절 */
margin: 10px;
padding: 17px;
border: 1.5px solid rgb(222, 218, 218);
border-radius: 10px;
color: rgb(49, 49, 165);
}
}
&-description {
color: var(--app-subtitle-color);
font-size: 15px;
margin-top: 6px;
}
}
}
.var-steps {
margin: 10px;
}
.var-step {
margin: 15px;
}
}
.var-step__vertical-content {
margin: 15px !important;
border: 1.5px solid rgb(222, 218, 218) !important;
border-radius: 10px !important;
}
.var-step__vertical-tag {
margin: 100px !important; /* 원하는 간격을 지정합니다. */
}
.popup-example-block {
padding: 24px;
width: 100%;
height: 40vh;
font-size: 12px;
}
</style>
달력 ExampleFull 컴포넌트
<script setup lang="ts">
// Using the publish version, you would do this instead:
// import { CalendarView, CalendarViewHeader, CalendarMath } from "vue-simple-calendar"
import CalendarView from '../vue-simple-calendar/CalendarView.vue'
import CalendarViewHeader from '../vue-simple-calendar/CalendarViewHeader.vue'
import CalendarMath from '../vue-simple-calendar/CalendarMath'
import { ICalendarItem, INormalizedCalendarItem } from './ICalendarItem'
import { onMounted, reactive, defineEmits } from 'vue'
/**
|--------------------------------------------------
| 😃 1. 변수
|--------------------------------------------------
*/
const date = ref('')
const type = ref('')
const count = ref({})
// 현재 달을 가져오는 함수 (선택적으로 시간 및 분도 포함)
const thisMonth = (d: number, h?: number, m?: number): Date => {
const t = new Date()
return new Date(t.getFullYear(), t.getMonth(), d, h || 0, m || 0)
}
// 부모 컴포넌트로부터 받은 props 정의
const props = defineProps<{ reserveDate: string; reserveType: string; data: any; reservationCounts: any }>() // 부모Component에서 받은값
// 상태 객체를 반응적으로 정의
interface IExampleState {
showDate: Date // 캘린더에 표시되는 날짜
message: string // 상태 메시지
startingDayOfWeek: number // 주의 시작 요일 (0: Sunday, 1: Monday, ..., 6: Saturday)
disablePast: boolean // 과거 날짜 사용 금지 여부
disableFuture: boolean // 미래 날짜 사용 금지 여부
displayPeriodUom: string // 표시 기간 단위 (예: 'month', 'year')
displayPeriodCount: number // 표시 기간 개수
displayWeekNumbers: boolean // 주 번호 표시 여부
showTimes: boolean // 시간 표시 여부
selectionStart?: Date // 선택 시작 날짜
selectionEnd?: Date // 선택 종료 날짜
newItemTitle: string // 새로운 아이템 제목
newItemStartDate: string // 새로운 아이템 시작 날짜
newItemEndDate: string // 새로운 아이템 종료 날짜
useDefaultTheme: boolean // 기본 테마 사용 여부
useHolidayTheme: boolean // 휴일 테마 사용 여부
useTodayIcons: boolean // 오늘 아이콘 사용 여부
items: ICalendarItem[] // 캘린더에 표시되는 아이템 배열
}
const state = reactive({
showDate: thisMonth(1), // 캘린더에 표시할 현재 달의 첫 날짜
message: '', // 상태 메시지
startingDayOfWeek: 0, // 주의 시작 요일 (0: Sunday, 1: Monday, ..., 6: Saturday)
disablePast: false, // 과거 날짜 사용 금지 여부
disableFuture: false, // 미래 날짜 사용 금지 여부
displayPeriodUom: 'month', // 표시 기간의 단위 (예: 'month', 'year')
displayPeriodCount: 1, // 표시할 기간의 개수
displayWeekNumbers: false, // 주 번호 표시 여부
// showTimes: true, // 시간 표시 여부
selectionStart: undefined, // 선택 시작 날짜
selectionEnd: undefined, // 선택 종료 날짜
newItemTitle: '', // 새로운 아이템 제목
newItemStartDate: '', // 새로운 아이템 시작 날짜
newItemEndDate: '', // 새로운 아이템 종료 날짜
useDefaultTheme: true, // 기본 테마 사용 여부
// useHolidayTheme: true, // 휴일 테마 사용 여부
useTodayIcons: true, // 오늘 아이콘 사용 여부
items: [] as ICalendarItem[]
} as IExampleState)
// 계산된 속성 정의
const themeClasses = computed(() => ({
'theme-default': state.useDefaultTheme
// "holiday-us-traditional": state.useHolidayTheme,
// "holiday-us-official": state.useHolidayTheme,
}))
// 날짜 클래스를 계산하는 메서드 정의
const myDateClasses = (): Record<string, string[]> => {
// This was added to demonstrate the dateClasses prop. Note in particular that the
// keys of the object are `yyyy-mm-dd` ISO date strings (not dates), and the values
// for those keys are strings or string arrays. Keep in mind that your CSS to style these
// may need to be fairly specific to make it override your theme's styles. See the
// CSS at the bottom of this component to see how these are styled.
const o = {} as Record<string, string[]>
const theFirst = thisMonth(1)
const ides = [2, 4, 6, 9].includes(theFirst.getMonth()) ? 15 : 13
const idesDate = thisMonth(ides)
o[CalendarMath.isoYearMonthDay(idesDate)] = ['ides']
o[CalendarMath.isoYearMonthDay(thisMonth(21))] = ['do-you-remember', 'the-21st']
return o
}
// 마운트 후 수행할 작업 정의
onMounted((): void => {
state.newItemStartDate = CalendarMath.isoYearMonthDay(CalendarMath.today())
state.newItemEndDate = CalendarMath.isoYearMonthDay(CalendarMath.today())
})
// props 로드 시 수행
watch(props, () => {
date.value = props.reserveDate
type.value = props.reserveType
count.value = props.reservationCounts
console.log('count.value :::::', count.value)
for (const date in count.value) {
state.items.push({
id: date,
startDate: thisMonth(Number(date)), // 아이템 시작 날짜 설정
// title: props.reserveType, // 아이템 제목 설정
title: count.value[date].length,
url: 'https://en.wikipedia.org/wiki/Birthday' // 아이템 URL 설정
})
}
})
/**
|--------------------------------------------------
| 🔑 2. 일반 함수
|--------------------------------------------------
*/
// 부모 컴포넌트에 메시지를 전달하기 위한 이벤트 정의
const emit = defineEmits(['dateEmit'])
// 날짜 클릭을 처리하는 함수
const onClickDay = (d: Date): void => {
state.selectionStart = undefined
state.selectionEnd = undefined
state.message = `You clicked: ${d.toLocaleDateString()}`
let dateString = d.toString().slice(8, 10) // 26
for (const date in props.reservationCounts) {
console.log('🍀 date ---- ', date)
if (date === dateString) {
emit('dateEmit', date)
}
}
}
// 항목 클릭을 처리하는 함수
const onClickItem = (item: INormalizedCalendarItem): void => {
state.message = `You clicked: ${item.title}`
let dateString = item.originalItem.startDate.toString().slice(8, 10) // 26
for (const date in props.reservationCounts) {
console.log('🍀 date ---- ', date)
if (date === dateString) {
emit('dateEmit', date)
}
}
}
// 표시할 날짜 설정을 처리하는 함수
const setShowDate = (d: Date): void => {
state.message = `Changing calendar view to ${d.toLocaleDateString()}`
state.showDate = d
}
// 선택 설정을 처리하는 함수
const setSelection = (dateRange: Date[]): void => {
state.selectionEnd = dateRange[1]
state.selectionStart = dateRange[0]
}
// 선택 완료를 처리하는 함수
const finishSelection = (dateRange: Date[]): void => {
setSelection(dateRange)
state.message = `You selected: ${state.selectionStart?.toLocaleDateString() ?? 'n/a'} - ${
state.selectionEnd?.toLocaleDateString() ?? 'n/a'
}`
}
// 드롭 이벤트를 처리하는 함수
const onDrop = (item: INormalizedCalendarItem, date: Date): void => {
state.message = `You dropped ${item.id} on ${date.toLocaleDateString()}`
// 이전 시작 날짜와 선택한 날짜 사이의 차이를 결정하고,
// 해당 차이를 시작 및 종료 날짜에 모두 적용하여 항목을 이동합니다.
const eLength = CalendarMath.dayDiff(item.startDate, date)
item.originalItem.startDate = CalendarMath.addDays(item.startDate, eLength)
item.originalItem.endDate = CalendarMath.addDays(item.endDate, eLength)
}
// 항목 추가를 처리하는 함수
// const clickTestAddItem = (): void => {
// state.items.push({
// startDate: CalendarMath.fromIsoStringToLocalDate(state.newItemStartDate),
// endDate: CalendarMath.fromIsoStringToLocalDate(state.newItemEndDate),
// title: state.newItemTitle,
// id: "e" + Math.random().toString(36).substring(2, 11),
// })
// state.message = "You added a calendar item!"
// }
</script>
<!-- /**
|--------------------------------------------------
| 🎨 화면
|--------------------------------------------------
*/ -->
<template>
<div id="example-full">
<div class="calendar-parent">
<CalendarView
:items="state.items"
:show-date="state.showDate"
:time-format-options="{ hour: 'numeric', minute: '2-digit' }"
:enable-drag-drop="true"
:disable-past="state.disablePast"
:disable-future="state.disableFuture"
:show-times="state.showTimes"
:date-classes="myDateClasses()"
:display-period-uom="state.displayPeriodUom"
:display-period-count="state.displayPeriodCount"
:starting-day-of-week="state.startingDayOfWeek"
:class="themeClasses"
:period-changed-callback="periodChanged"
:current-period-label="state.useTodayIcons ? 'icons' : ''"
:displayWeekNumbers="state.displayWeekNumbers"
:enable-date-selection="true"
:selection-start="state.selectionStart"
:selection-end="state.selectionEnd"
@date-selection-start="setSelection"
@date-selection="setSelection"
@date-selection-finish="finishSelection"
@drop-on-date="onDrop"
@click-date="onClickDay"
@click-item="onClickItem"
>
<!-- 달력 헤더 -->
<template #header="{ headerProps }">
<CalendarViewHeader :header-props="headerProps" @input="setShowDate" />
</template>
<template> </template>
</CalendarView>
</div>
</div>
</template>
<style>
@import 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css';
/* npm 패키지를 사용하는 앱의 경우 아래 URL은 /node_modules/vue-simple-calendar/dist/css/를 참조해야 합니다 */
@import '/css/gcal.css';
@import '/css/holidays-us.css';
@import '/css/holidays-ue.css';
/* 전체 예제 컨테이너 스타일 */
#example-full {
display: flex;
flex-direction: row;
font-family: Calibri, sans-serif;
width: 85vw; /* 전체 너비는 화면 너비의 86%로 설정 */
min-width: 10rem; /* 최소 너비는 30rem으로 설정 */
max-width: 30rem; /* 최대 너비는 90rem으로 설정 */
min-height: 75vh; /* 최소 높이는 55rem으로 설정!!!!!!!!!! 여기를 조절해야돼 !!!!*/
margin-left: auto; /* 왼쪽 여백을 자동으로 설정하여 가운데 정렬 */
margin-right: auto; /* 오른쪽 여백을 자동으로 설정하여 가운데 정렬 */
}
/* #example-full .calendar-controls {
margin-right: 1rem;
min-width: 14rem;
max-width: 14rem;
} */
/* 달력 부모 컨테이너 스타일 */
#example-full .calendar-parent {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-x: hidden;
overflow-y: hidden;
max-height: 98vh; /* 최대 높이는 화면 높이의 90%로 설정 */
background-color: white; /* 배경색은 흰색으로 설정 */
}
/* 달력의 각 주의 높이를 조정하는 스타일 */
#example-full .cv-wrapper.period-month.periodCount-2 .cv-week,
#example-full .cv-wrapper.period-month.periodCount-3 .cv-week,
#example-full .cv-wrapper.period-year .cv-week {
min-height: 7rem; /* 주당 최소 높이는 7rem으로 설정 */
}
/* 특정 날짜에 배경색을 변경하는 예제 - 분홍색 */
/* #example-full .theme-default .cv-day.ides {
background-color: #ffe0e0;
} */
/* 날짜 요소에 아이콘을 추가하는 예제 */
#example-full .ides .cv-day-number::before {
content: '\271D';
}
/* #example-full .cv-day.do-you-remember.the-21st .cv-day-number::after {
content: '\1F30D\1F32C\1F525';
} */
</style>
'Frond-end > Vue' 카테고리의 다른 글
[vue3] 글 수정 페이지 구현하기 (computed() 사용) (0) | 2024.04.01 |
---|---|
[Vue3] vue-simple-calendar 라이브러리 적용하기 (0) | 2024.03.25 |
[트러블슈팅/ Vue3] 컴포넌트에 데이터 바인딩이 되지 않는 문제(feat. ref, watch) (0) | 2024.03.25 |
[Vue3, SpringBoot, MariaDB] 이전글/ 다음글 구현하기 (0) | 2024.03.21 |
[Vue3] 의료 플랫폼 개발 기록 (0) | 2024.03.19 |