본문 바로가기

백엔드

[Django] 장고 캘린더 예약 사이트 만들기 (2 - 날짜 선택 구현)

개발 동기

동아리에서 스터디나 행사 관련해서 약속을 잡을 때 매주 카톡으로 투표 및 조율을 하는 상황이 너무 피곤해서 사이트로 예약 어플을 만들면 편할 것 같아서 개발하게 되었다.

 

개발 순서
1. 달력 구현 (HTML, CSS, JS)
2. 날짜 선택 구현 (CSS, JS)
3. 예약 구현 (JS, DATABASE, DJANGO)

[2. 날짜 선택 구현]

목표:

날짜 선택 기능을 구현 시 반드시 넣어야 했던 기능들은 아래와 같다.

1. 날짜 선택 시 그 날짜가 선택되었다는 것이 명확히 표시되어야 한다.

2. 클릭을 했을 때 예약을 위한 정보와 기능이 담긴 창을 띄워주어야 한다.

3. 한 번에 하나의 날짜만 선택이 되어야 한다.

위의 사항들을 지키며 하나씩 구현해 보았다.

 

개발:

처음으로 선택한 날짜의 정보를 담을 변수를 하나 선언, 추후 서버에 선택한 날짜의 정보를 쉽게 보낼 수 있도록 하기 위해 만들어 두었다.

const selDate = []

 

그 후 날짜 하나하나 EventListener를 걸어줄 함수를 생성

const dateFunc = ()=>{};

 

함수 내에 날짜 값을 가지고 있는 객체와 연도 값을 가지고 있는 객체 몇 월인지를 담고 있는 객체들을 querySelector로 지정후 변수로 선언

const dateFunc = ()=>{

  const dates = document.querySelectorAll('.date');
  const year = document.querySelector('.year');
  const month = document.querySelector('.month');
  
};

 

 

dates 변수에 forEach를 사용하여 날짜 값을 가지고 있는 모든. date(이하 데이트) 객체들에게 click 이벤트 리스너를 걸어준다.

const dateFunc = ()=>{

  const dates = document.querySelectorAll('.date');
  const year = document.querySelector('.year');
  const month = document.querySelector('.month');
  
  dates.forEach((i)=>{
  	i.addEventListener('click', ()=>{});
  });
};

 

 

데이트 객체를 클릭 시 클래스명을 추가 및 이전에 선언해둔 selDate 리스트에 정보들을 넣어주는 코드를 기입.

 

CSS-

.selected{
    background-color: black;
    color: #ffffff !important;
}

JS-

const selDate = []
const dateFunc = ()=>{

  const dates = document.querySelectorAll('.date');
  const year = document.querySelector('.year');
  const month = document.querySelector('.month');
  
  dates.forEach((i)=>{
  
  	i.addEventListener('click', ()=>{
		i.classList.add('selected');
        	selDate.push([year.innerHTML, month.innerHTML, i.innerHTML]);
	});
    
  });
  
};

 

클릭 시 날짜가 선택되었다는 것은 명확히 표시되나 위의 코드의 경우 동시에 여러 날짜를 선택할 수 있으므로 한 번에 하나의 날짜만 선택이 가능하도록 조건문을 걸어 주어야 한다.

 

조건문의 경우 아래와 같은 논리로 작성될 것이다.

  1. 이전 달(달력에서 회색으로 표시된 날짜)의 날짜 또는(OR) 이미 선택된 날짜를 클릭 시 모든 날짜의 선택을 해제 및  selDate 리스트를 비운다.
  2. 한 날짜를 선택 후 다른 날짜를 선택하려 할때 이전에 선택한 날짜는 선택해제 이후 선택한 날짜를 선택과 함께 selDate에 정보 push와 정보창 띄우기를 하도록한다.
  3. 위의 상황에 해당되지 않는 경우(else) 선택한 날짜에 선택과 함께 selDate에 정보 push와 정보창 띄우기를 하도록한다.

코드로 보는것이 더 쉬울것 같다..

// 정보창
const resvTab = document.querySelector('.resv-wrapper');

const selDate = []

const dateFunc = ()=>{

    const dates = document.querySelectorAll('.date');
    const year = document.querySelector('.year');
    const month = document.querySelector('.month');
    
    dates.forEach((i)=>{
    
        i.addEventListener('click', ()=>{
        	// 1
            if(i.classList.contains('other') || i.classList.contains('selected')){
            	// 모든 날짜 선택해제
                dates.forEach((ig)=>{ig.classList.remove('selected');});
                i.classList.remove('selected');
                
                // selDate 비우기
                selDate.length=0;
            //2
            }else if(selDate.length > 0){
                dates.forEach((ig)=>{ig.classList.remove('selected');});
                selDate.length=0;
                
                //날짜 선택
                i.classList.add('selected');
                
                //selDate에 정보넣기
                selDate.push([year.innerHTML, month.innerHTML, i.innerHTML]);
                
                // 정보창 띄우기
                resvTab.classList.add('open');
            //3
            }else{
                i.classList.add('selected');
                selDate.push([year.innerHTML, month.innerHTML, i.innerHTML]);
                resvTab.classList.add('open');
            }
            
        });
        
    });
    
};

 

시연 영상-

 

 

[최종 코드]

-HTML

{% extends 'index.html' %}
{% load static %}
{% block content %}
<link rel="stylesheet" href="{% static 'css/calendar/calendar.css' %}">
<div class="calendar">
    <div class="calendar_header">
        <div class="calendar_nav">
            <button class="nav-btn go-prev">&lt;</button>
            <span class="year"></span>년
            <span class="month"></span>월
            <!-- <button class="nav-btn go-today">오늘로 가기</button> -->
            <button class="nav-btn go-next">&gt;</button>
        </div>
    </div>
    <div class="calendar_main">
        <div class="days">
            <div class="day">일</div>
            <div class="day">월</div>
            <div class="day">화</div>
            <div class="day">수</div>
            <div class="day">목</div>
            <div class="day">금</div>
            <div class="day">토</div>
        </div>
        <div class="dates"></div>
    </div>

    <!-- 예약 창 -->
    <div class="resv-wrapper">
        <div class="resv-bg">
            <button class="resv-close">X</button>
            <div class="resv_info">
                <div class="resv_ym">
                    <span class="resv-year">2021</span>년
                    <span class="resv-month">05</span>월
                    <span class="resv-day">11</span>일
                </div>
                <div class="resv-remain">
                    <span>1</span>자리 남음
                </div>
                <h3>
                    예약자 목록
                </h3>
                <div class="resv-list">  
                    <p>Heo Sang <span>12:30</span></p>
                    <p>Sang Won <span>12:30</span></p>
                    <p>Heo Won <span>12:30</span></p>
                </div>
                <h3>
                    프로그램 목록
                </h3>
                <div class="resv-event">
                    <p>EVNET 1 <span>12:30</span></p>
                    <p>EVNET 2 <span>14:30</span></p>
                    <p>EVNET 3 <span>17:30</span></p>
                </div>
            </div>
            <div class="resv_set">
                <input class="resv_set time" type="time" name="" id="">
                <button class="resv_btn purchase">예약</button>
                <button class="resv_btn cancel">예약취소</button>
            </div>
        </div>
    </div>
    
</div>
<script src="{% static 'js/calendar/calendar.js' %}"></script>
<script src="{% static 'js/calendar/reserve.js' %}"></script>
{% endblock %}

 

-JS (reserve.js)

const resvTab = document.querySelector('.resv-wrapper');
const exitBtn = document.querySelector('.resv-close');
exitBtn.addEventListener('click', ()=>{resvTab.classList.remove('open');});


// 날짜별로 이벤트 등록용 함수 및 변수
const selDate = []
const dateFunc = ()=>{
    const dates = document.querySelectorAll('.date');
    const year = document.querySelector('.year');
    const month = document.querySelector('.month');
    dates.forEach((i)=>{
        i.addEventListener('click', ()=>{
            if(i.classList.contains('other') || i.classList.contains('selected')){
                dates.forEach((ig)=>{ig.classList.remove('selected');});
                i.classList.remove('selected');
                selDate.length=0;
            }else if(selDate.length > 0){
                dates.forEach((ig)=>{ig.classList.remove('selected');});
                selDate.length=0;
                i.classList.add('selected');
                selDate.push([year.innerHTML, month.innerHTML, i.innerHTML]);
                resvTab.classList.add('open');
            }else{
                i.classList.add('selected');
                selDate.push([year.innerHTML, month.innerHTML, i.innerHTML]);
                resvTab.classList.add('open');
            }
        });
    });
};

// 초기화 함수 
const reset = ()=>{
    selDate.length=0;
    dateFunc();
}

// 로드시 Nav 버튼들 이벤트 등록 및 초기화
window.onload=()=>{
    const navBtn = document.querySelectorAll('.nav-btn');
    navBtn.forEach(inf=>{
        if(inf.classList.contains('go-prev')){
            inf.addEventListener('click', ()=>{prevMonth(); reset();});
        }else if(inf.classList.contains('go-today')){
            inf.addEventListener('click', ()=>{goToday(); reset();});
        }else if(inf.classList.contains('go-next')){
            inf.addEventListener('click', ()=>{nextMonth(); reset();});
        }
    });
    reset();
}

-JS (calendar.js)

let date = new Date();

const renderCalender = () => {
    const viewYear = date.getFullYear();
    const viewMonth = date.getMonth();

    document.querySelector('.year').textContent = `${viewYear}`;
    document.querySelector('.month').textContent = `${viewMonth + 1}`;

    const prevLast = new Date(viewYear, viewMonth, 0);
    const thisLast = new Date(viewYear, viewMonth + 1, 0);

    const PLDate = prevLast.getDate();
    const PLDay = prevLast.getDay();

    const TLDate = thisLast.getDate();
    const TLDay = thisLast.getDay();

    const prevDates = [];
    const thisDates = [...Array(TLDate + 1).keys()].slice(1);
    const nextDates = [];

    if (PLDay !== 6) {
        for (let i = 0; i < PLDay + 1; i++) {
            prevDates.unshift(PLDate - i);
        }
    }

    for (let i = 1; i < 7 - TLDay; i++) {
        nextDates.push(i);
    }

    const dates = prevDates.concat(thisDates, nextDates);
    const firstDateIndex = dates.indexOf(1);
    const lastDateIndex = dates.lastIndexOf(TLDate);

    dates.forEach((date, i) => {
        const condition = i >= firstDateIndex && i < lastDateIndex + 1 ?
            'this' :
            'other';
        dates[i] = `
            <div class="date ${condition}">

                <div class="date-itm">
                    ${date}
                </div>

                <div class="date_event">
                    <div class="event-itm">EVENT</div>
                </div>

            </div>
        `;
    });

    document.querySelector('.dates').innerHTML = dates.join('');

    const today = new Date();
    if (viewMonth === today.getMonth() && viewYear === today.getFullYear()) {
        for (let date of document.querySelectorAll('.date-itm')) {
            if (+date.innerText === today.getDate()) {
                date.parentNode.classList.add('today');
                break;

            }
        }
    }
};

renderCalender();

const prevMonth = () => {
    date.setMonth(date.getMonth() - 1);
    renderCalender();
};

const nextMonth = () => {
    date.setMonth(date.getMonth() + 1);
    renderCalender();
};

const goToday = () => {
    date = new Date();
    renderCalender();
};

 

 

-CSS

.calendar {
    width: 90%;
    margin: auto;
    padding-bottom: 50px;
}

.calendar_header {
    display: inline-block;
}

.calendar_nav {
    display: flex;
    flex-grow: 0;
    flex-shrink: 0;
    justify-content: center;
    align-items: center;
    font-size: 50px;
}

.nav-btn {
    width: 28px;
    height: 30px;
    border: none;
    font-size: 50px;
    line-height: 34px;
    margin: 0 20px;
    background-color: transparent;
    cursor: pointer;
}

.go-today {
    width: 80%;
}

.calendar_main{
    border-collapse: collapse;
    width: 100%;
}

.days {
    display: flex;
    margin: 25px 0 10px;
}

.day {
    width: calc(100% / 7);
    text-align: center;
    transition: all .3s;
}

.dates {
    display: flex;
    flex-flow: row wrap;
    border: 1px solid #33333341;
}

.date {
    cursor: pointer;
    width: calc(100% / 7);
    padding: 5% 1%;
    /* text-align: left; */
    text-align: center;
    border: 1px solid #33333341;
    transition: all .3s;
}

.date-itm{
    display: inline-block;
    vertical-align: middle;
}

.date_event{
    display: inline-block;
    vertical-align: middle;
}

.event-itm{
    display: block;
    font-size: 15px;
}

.day:nth-child(7n + 1),
.date:nth-child(7n + 1) {
    color: #D13E3E;
}

.day:nth-child(7n),
.date:nth-child(7n) {
    color: #396EE2;
}

.other {
    color: rgba(88, 88, 88, 0.315) !important;
}

.today {
    position: relative;
    background-color: #ff1c1c96;
    color: #ffffff !important;
}

.selected{
    background-color: black;
    color: #ffffff !important;
}


/* RESERVE TAB */
.resv-wrapper{
    z-index: 10;
    top: 0;
    left: 0;
    position: fixed;
    width: 100%;
    height: 100vh;
    background-color: #3333335b;
    display: none;
    justify-content: center;
}

.resv-wrapper.open{
    display: flex;
}

.resv-bg{
    width: 90%;
    position: relative;
    padding: 5% 5%;
    background-color: #ffffff;
    height: fit-content;
    top: 50%;
    transform: translateY(-50%);
    border-radius: 20px;
    font-size: 20px;
}

.resv-close{
    cursor: pointer;
    font-size: 20px;
    background-color: #ffffff;
    text-align: right;
    float: right;
}

/* 예약인 리스트 */
.resv-list{
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    width: 80%;
    margin: auto;
    font-size: 15px;
}

.resv-list > p{
    display: inline-block;
    padding: 5px;
    margin: 5px;
    border: 2px solid var(--BG);
}

.resv-event{
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    width: 80%;
    margin: auto;
    font-size: 15px;
}

.resv-event > p{
    display: inline-block;
    padding: 5px;
    margin: 5px;
    border: 2px solid var(--BG);
}

.resv_set{
    cursor: pointer;
    width: 80%;
    margin: auto;
    margin-top: 10px;
    font-size: 20px;
}

.resv_btn{
    cursor: pointer;
    font-size: 18px;
    width: 40%;
    height: 30px;
    margin-top: 20px;
    padding: 5px 0px;
}

@media screen and (max-width: 280px) {
 
    .calendar {
        width: 90%;
        margin: auto;
        padding-bottom: 50px;
    }

    
    .calendar_nav {
        font-size: 20px;
    }

    .nav-btn {
        font-size: 20px;
    }

    .date_event{
        display: block;
        width: 10px;
        height: 10px;
        background: rgba(51, 51, 51, 0.2);
        margin: auto;
        border-radius: 100%;
    }
    
    .event-itm{
        font-size: 0;
    }
}

@media screen and (min-width: 280px) {
 
    .calendar {
        width: 90%;
        margin: auto;
        padding-bottom: 50px;
    }

    .calendar_nav {
        font-size: 20px;
    }

    .nav-btn {
        font-size: 20px;
    }

    .date_event{
        display: block;
        width: 10px;
        height: 10px;
        background: rgba(51, 51, 51, 0.2);
        margin: auto;
        border-radius: 100%;
    }

    .event-itm{
        font-size: 0;
    }
}

@media screen and (min-width: 600px) {
 
    .calendar {
        width: 70%;
        margin: auto;
        padding-bottom: 50px;
    }

    .date_event{
        display: inline-block;
        width: auto;
        height: auto;
        background: #ff8f8f00;
        margin: auto;
        padding-bottom: 0%;
        border-radius: 0%;
    }

    .event-itm{
        font-size: 15px;
    }

}

 

달력에서 달을 넘길때 이벤트리스너가 증발하는 버그가 있어 초기화 함수및 몇가지 함수를 추가함

 

다음 글에서는 데이터베이스 구성 및 예약기능을 구현 해보도록 하겠다.