개발 동기
동아리에서 스터디나 행사 관련해서 약속을 잡을 때 매주 카톡으로 투표 및 조율을 하는 상황이 너무 피곤해서 사이트로 예약 어플을 만들면 편할 것 같아서 개발하게 되었다.
개발 순서
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]);
});
});
};
클릭 시 날짜가 선택되었다는 것은 명확히 표시되나 위의 코드의 경우 동시에 여러 날짜를 선택할 수 있으므로 한 번에 하나의 날짜만 선택이 가능하도록 조건문을 걸어 주어야 한다.
조건문의 경우 아래와 같은 논리로 작성될 것이다.
- 이전 달(달력에서 회색으로 표시된 날짜)의 날짜 또는(OR) 이미 선택된 날짜를 클릭 시 모든 날짜의 선택을 해제 및 selDate 리스트를 비운다.
- 한 날짜를 선택 후 다른 날짜를 선택하려 할때 이전에 선택한 날짜는 선택해제 이후 선택한 날짜를 선택과 함께 selDate에 정보 push와 정보창 띄우기를 하도록한다.
- 위의 상황에 해당되지 않는 경우(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"><</button>
<span class="year"></span>년
<span class="month"></span>월
<!-- <button class="nav-btn go-today">오늘로 가기</button> -->
<button class="nav-btn go-next">></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;
}
}
달력에서 달을 넘길때 이벤트리스너가 증발하는 버그가 있어 초기화 함수및 몇가지 함수를 추가함
다음 글에서는 데이터베이스 구성 및 예약기능을 구현 해보도록 하겠다.
'백엔드' 카테고리의 다른 글
[Django] 장고 캘린더 예약 사이트 만들기 (3 - 예약 구현) (0) | 2022.03.28 |
---|---|
[Django] 장고 캘린더 예약 사이트 만들기 (1 - 달력구현) (3) | 2021.06.08 |
장고 데이터베이스 모델링 맛보기 (2) | 2021.04.28 |
장고 MySQL 연동 방법 (1) | 2021.04.26 |