숙소 예약 사이트 등에서 흔하게 볼 수 있는 달력을 구현해보았다.
좌, 우 버튼을 누를 때 마다 한 달 씩 넘어가는 달력을 만들고자 한다.
에어비앤비의 달력 예시
달력 한 면은 6행 7열의 행렬 구조로 되어있으므로, <table>
태그를 활용하여
HTML 구조를 짰다.
<div class = "calendar_container">
<button class = "left_button"> < </button>
<table class = "calendar">
<th class = "title">2월</th>
<tbody>
<tr class = "week">
<td class = "day">일</td>
<td class = "day">월</td>
<td class = "day">화</td>
<td class = "day">수</td>
<td class = "day">목</td>
<td class = "day">금</td>
<td class = "day">토</td>
</tr>
<tr> <!-- 달력 첫 번째 줄-->
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
</tr>
<tr> <!-- 달력 두 번째 줄-->
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
</tr>
<tr> <!-- 달력 세 번째 줄-->
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
</tr>
<tr> <!-- 달력 네 번째 줄-->
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
</tr>
<tr> <!-- 달력 다섯 번째 줄-->
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
</tr>
<tr> <!-- 달력 여섯 번째 줄-->
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
<td class = "calendar_days" roll = "button"> </td>
</tr>
</tbody>
</table>
<button class = "right_button"> > </button>
</div>
<script src = "calendar.js"></script>
달력의 연,월을 표시하는 부분과 요일, 일자를 표시하는 부분을 분리해서
각자 display: flex
로 레이아웃을 짰다.
.calendar_container {
position:absolute;
left:29%;
top:25%;
display: flex;
justify-content:space-evenly;
width: 42%;
background-color: white;
box-shadow: 1px 1px 25px rgb(0, 0, 0 / 17%);
border: 1px solid black;
border-radius: 50px;
}
.hide {
display:none;
}
.calendar_container > button {
margin-top: 4%;
font-weight: bold;
height:10%;
}
.left_button {
margin-left:17%;
border: transparent;
}
.right_button {
margin-right:17%;
border: transparent;
}
.calendar {
display:flex;
flex-direction: column;
justify-content:center;
align-items: center;
margin-top: 3%;
margin-right:2.5%;
}
.calendar_days > button {
width: 48px;
height: 47px;
border-radius: 100%;
background-color: transparent;
border: transparent;
}
table {
border-collapse: separate;
border-spacing: 0 10px;
}
.day {
text-align: center;
font-size:12px;
font-weight: bold;
color:rgb(189, 189, 189);
}
.day_hover {
border: 1px solid black !important;
}
.day_selected {
background-color: black !important;
color: #ffff;
}
.gray {
background-color: rgb(241, 241, 241) !important;
}
const calendarDays = document.querySelectorAll(".calendar_days"),
calendarTitle = document.querySelector(".title"),
leftButton = document.querySelector(".left_button"),
rightButton = document.querySelector(".right_button"),
calendar = document.querySelector(".calendar");
class Calendar {
constructor(year, month) {
this.today = new Date(year, month);
this.year = this.today.getFullYear(),
this.month = this.today.getMonth(),
this.date = this.today.getDate(),
this.day = this.today.getDay(),
this.handleEvents();
getFirstDay() {
const firstDate = new Date(this.year, this.month);
return firstDate.getDay();
}
getLastDay() {
let wholeDays = [];
if ((this.year % 4 === 0 && this.year % 100 !== 0) || (this.year % 400 === 0)) {
wholeDays = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
} else {
wholeDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
}
return wholeDays[this.month];
}
initCalendar() {
calendarDays.forEach((e) => {
e.innerHTML = "";
});
}
getFirstDay
로 구한 첫 요일부터 getLastDay
로 구한 마지막 요일까지 채워나간다. fillCalendar() {
this.initCalendar();
calendarTitle.innerHTML = `${this.year}년 ${this.month + 1}월`
const firstDay = this.getFirstDay();
const lastDay = this.getLastDay();
let day = 1;
for (let i = firstDay; i < calendarDays.length; i++) {
if (day <= lastDay) {
calendarDays[i].innerHTML = `<button class = "day_button">${day}</button>`;
day++;
}
}
}
updateCalendarStyle() {
const dayButtons = document.querySelectorAll(".day_button");
let firstSelectedDay = 0;
let lastSelectedDay = 0;
let clickCount = 0;
// 달력 스타일 초기화
dayButtons.forEach((element) => {
element.classList.remove("day_selected");
calendarDays.forEach((e) => e.classList.remove("gray"));
})
// 달력 날짜들에 클릭 이벤트 추가
dayButtons.forEach((element) => {
element.addEventListener("click", (event) => {
event.target.classList.toggle("day_selected");
clickCount++;
// 선택 일자 타입 변환
if (firstSelectedDay === 0) {
firstSelectedDay = Number(event.target.innerText);
} else {
lastSelectedDay = Number(event.target.innerText);
}
// 클릭 횟수 2회 넘어가면 달력 스타일 초기화
if (clickCount > 2) {
dayButtons.forEach((e) => {
e.parentNode.classList.remove("gray");
e.classList.remove("day_selected");
clickCount = 0;
firstSelectedDay = 0;
lastSelectedDay = 0;
});
}
// 선택 일자 사이에 회색 배경 적용
if (firstSelectedDay !== 0 && lastSelectedDay !== 0) {
dayButtons.forEach((e) => {
const day = Number(e.innerText);
if (day >= firstSelectedDay && day <= lastSelectedDay) {
e.parentNode.classList.toggle("gray");
}
});
}
// 선택 일자 중 왼쪽값이 오른쪽 값보다 크면 회색 배경 삭제
if (firstSelectedDay > lastSelectedDay) {
dayButtons.forEach((e) => {
e.parentNode.classList.remove("gray");
});
}
});
});
// 달력 날짜들에 호버링 이벤트 추가
dayButtons.forEach((element) => {
element.addEventListener("mouseenter", (event) => {
event.target.classList.add("day_hover")
});
});
dayButtons.forEach((element) => {
element.addEventListener("mouseleave", (event) => {
event.target.classList.remove("day_hover")
});
});
}
handleEvents() {
this.drawCalendar();
this.updateCalendarStyle();
}
const cal = new Calendar();
https://codepen.io/junzero741/pen/oNYmamj
코드스쿼드에서 줬던 미션을 수행하면서 구현한 기능인데,
글을 쓰면서 코드를 다시 보니 문제가 많은 것 같다.
전역에 DOM 선택자를 선언한 것이나, Date 객체를 제대로 활용하지 못한 점이 아쉽다.
updateCalendarStyle
메서드도 덩치가 너무 크고..
나중에 재활용하려면 리팩토링이 많이 필요할 듯 싶다.
그래도 처음으로 뭔가 복잡한 기능을 구현했던 거라 꽤 뿌듯했다.