캘린더

Goody·2021년 3월 13일
3

JS실습

목록 보기
2/2

소개

숙소 예약 사이트 등에서 흔하게 볼 수 있는 달력을 구현해보았다.
좌, 우 버튼을 누를 때 마다 한 달 씩 넘어가는 달력을 만들고자 한다.

에어비앤비의 달력 예시



HTML

달력 한 면은 6행 7열의 행렬 구조로 되어있으므로, <table> 태그를 활용하여
HTML 구조를 짰다.


    <div class = "calendar_container"> 
        
        <button class = "left_button"> &lt </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>


CSS

달력의 연,월을 표시하는 부분과 요일, 일자를 표시하는 부분을 분리해서
각자 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;
}



JS

  • DOM 셀렉터들을 생성한다.
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();
   

  • 현재 날짜의 1일의 요일을 반환하는 메소드이다.
    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 메서드도 덩치가 너무 크고..
나중에 재활용하려면 리팩토링이 많이 필요할 듯 싶다.
그래도 처음으로 뭔가 복잡한 기능을 구현했던 거라 꽤 뿌듯했다.

0개의 댓글