포포 일상 블로그

javascript 로 캘린더 + 투두리스트 만들기 본문

dev/JS

javascript 로 캘린더 + 투두리스트 만들기

dev포포 2021. 1. 10. 00:18

안녕하세요.  이번에는 전에 만들어뒀던 캘린더 (하단 url 참조)의 코드 내용을 확장해서

 

codepen.io/sinhooking/pen/xxbGevQ

 

virtual calendar javascript

virtual calendar...

codepen.io

아래 이미지와 같은 일정관리 어플을 만들어보도록 하겠습니다. (나중에 상단 주소 내용도 다시 다뤄보도록 하겠습니다.)

 

 

우선 필요한 내용을 먼저 정리해보면,

 

1. 자동으로 날짜를 계산해줘야 합니다. 

2. 날짜에 맞춰 데이터를 불러오거나 해야 합니다. ( 우선은 프론트 작업만 할 거기 때문에 이 부분은 나중에 서버 개발을 할 때 다시 다뤄보도록 하겠습니다. ) - 더미 데이터로 대체.

 

두 가지 정도면 우선 해당 웹 페이지를 만들 수 있을 거 같습니다.

 

상단 사진 같은 경우에는 5 * 5 로 이루어져 있는데 실제 만들게 되면 7 * 5로 제작하도록 하겠습니다.

 

오른쪽 사진은 날짜를 클릭하면 모달창이 뜨고 거기에 투두리스트를 작성할 수 있습니다.

 

시작해보겠습니다.

 

우선 html을 준비해주세요.

 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #pivot {
            text-align: center;
            margin: 0 auto;
            width: 34%;
            padding: 2%;
            box-shadow: 4px 1px 10px 6px #dcdcdc;
        }

        .s_calendar-month {
            height: 44px;
        }

        .s_calendar-days,
        .s_calendar-days-name,
        .s_calendar-day-space {
            vertical-align: top;
            border: 1px solid #333;
            width: 44px;
            height: 44px;
        }

        .s_calendar-day-space {
            background-color: #333;
        }

        .s_calendar-days,
        .s_calendar-days-name {
            line-height: 44px;
        }

        .s_calendar-days:nth-child(2n) {
            background-color: #33333329;
        }

        .s_calendar-days:hover {
            background-color: #33333382;
            cursor: pointer;
        }

        .s_calendar-days-name {
            background-color: #ff9595b5;
        }
    </style>
    <script src="S_Calendar.js"></script>
    <script src="app.js"></script>

</head>

<body>
    <!-- https://github.com/sinhooking -->
    <div id="pivot"></div>

    <button class="next">next</button>
    <button class="prev">prev</button>
    <input type="text" id="getDate">
</body>

</html>

 

기본 세팅입니다. 코드펜에 올라가 있는 웹과 동일한 모양이면 성공입니다.

날짜를 클릭하시면 하단 인풋창에 날짜가 넣어지고 next는 다음 달 prev는 저번 달 달력을 보여줍니다.

 

script는

S_Calendar.js

function S_Calendar(options) {
    this._CALENDAR_ELEMENT_NAME = options.targetName;
    this._CALENDAR_ELEMENT = document.querySelector(this._CALENDAR_ELEMENT_NAME);
  
    this._CALENDAR_DAYS_NAME = options.dayNames;
    this._CALENDAR_SPACE = options.space;
    this._TODAY = new Date();
    this._MONTH = this._TODAY.getMonth() + 1;
    this._FULL_YEAR = this._TODAY.getFullYear();
  
    return this.initializeCalendar();
  }
  
  S_Calendar.prototype.pivotCalendarName = function () {
    return this._CALENDAR_ELEMENT_NAME;
  }
  
  S_Calendar.prototype.getCalendarElement = function () {
    return this._CALENDAR_ELEMENT;
  }
  
  S_Calendar.prototype.getDaysName = function () {
    return this._CALENDAR_DAYS_NAME;
  }
  
  S_Calendar.prototype.getSpace = function() {
    return this._CALENDAR_SPACE;
  }
  
  S_Calendar.prototype.getDate = function() {
    return this._TODAY;
  }
  
  S_Calendar.prototype.nextMonth = function() {
    this._MONTH = this._MONTH + 1;
    this._CALENDAR_ELEMENT.innerHTML = null;
    return this.initializeCalendar()
  }
  
  S_Calendar.prototype.prevMonth = function() {
    this._MONTH = this._MONTH - 1;
    this._CALENDAR_ELEMENT.innerHTML = null;
    return this.initializeCalendar()
  }
  
  S_Calendar.prototype.setMonth = function(number){
    return this._MONTH = number;
  }
  
  S_Calendar.prototype.setFullYear = function(number){
    return  this._FULL_YEAR = number;
  }
  
  S_Calendar.prototype.getMonth = function() {
    var thisMonthOverFlow = this._MONTH > 12;
    
    if (this._MONTH <= 0 || thisMonthOverFlow) {
      var operator = thisMonthOverFlow ? this.getFullYear() + 1 :this.getFullYear() - 1;
      this._FULL_YEAR = this.setFullYear(operator);
      this._MONTH = this.setMonth(thisMonthOverFlow ? 1 : 12);
    }
    
    return this._MONTH;
  }
  
  S_Calendar.prototype.getFullYear = function() {
    return this._FULL_YEAR;
  }
  
  S_Calendar.prototype.getLastDate = function() {
    return new Date(this.getFullYear(), this.getMonth(), 0).getDate();
  }
  
  S_Calendar.prototype.getFirstDate = function() {
    return new Date(this.getFullYear(), this.getMonth() - 1, 1).getDay();
  }
  
  S_Calendar.prototype.initializeCalendar = function() {
    this.createMonth();
    this.createDaysName();
    this.createWeekOfMonth();
  }
  
  S_Calendar.prototype.createCalendarArray = function() {
    var calenderArr = [];
    var number = 1;
    var firstDate = this.getFirstDate();
    var lastDate = this.getLastDate();
    console.log(firstDate, lastDate)
    for(var i = 0; i < 35; i++){
      if (i >= firstDate && i < lastDate + firstDate) {
        calenderArr[i] = number;
        number += 1;
      } else {
        calenderArr[i] = 'none';
      }
    }
    return calenderArr;
  }
  
  S_Calendar.prototype.createMonth = function() {
    var month = this.getMonth();
  
    month = month > 12 ? month - 12 : month;
  
    var yearName = this.getFullYear() + '년'; // YYYY
    var monthName = month + '월'; // mm
  
    return this.getCalendarElement().prepend(this.createVirtualElements([ yearName + monthName ], 'month'));
  }
  
  S_Calendar.prototype.createWeekOfMonth = function () {
    var arr = this.createCalendarArray();
    var weekOfMonth = {};
  
    for(var i = 0; i < 5; i++){
      weekOfMonth[i] = arr.splice(0, 7);
      this.getCalendarElement().append(this.createVirtualElements(weekOfMonth[i]));
    }
  
    return weekOfMonth;
  }
  
  S_Calendar.prototype.createDaysName = function() {
    return this.getCalendarElement().append(this.createVirtualElements(this.getDaysName()));
  }
  
  S_Calendar.prototype.createVirtualElements = function(targetNames, month) {
    var docFrag = document.createDocumentFragment();
    var wrapper = document.createElement('div');
    wrapper.className = 's_calendar-row';
    for (var i = 0; i < targetNames.length;i++){
      var isNumber = typeof targetNames[i] === 'number';
      var isNone = targetNames[i] === 'none';
      var className = isNumber ? 's_calendar-days' : 's_calendar-days-name';
      var createElement = document.createElement('div');
  
      if (month) {
        createElement.className = 's_calendar-month';
      } else {
        createElement.className = className;
      }
  
      createElement.style.cssText = 'display: inline-block;';
      createElement.textContent = isNone ? '' : targetNames[i];
  
      if(isNone) {
        createElement.className = 's_calendar-day-space';
      } 
  
      if(isNumber) {
        createElement.dataset.key = targetNames[i];
      }
  
      if (this.getSpace() === false && targetNames[i] === 'none') {
        continue;
      } else {
        wrapper.append(createElement);
      }
    }
  
    docFrag.append(wrapper);
  
    return docFrag;
  }
  

app.js

document.addEventListener('DOMContentLoaded',domHandler)
//used only javascript 
function domHandler() {
 
  //example
  var calender = new S_Calendar({
    targetName: '#pivot', //select your pivot element
    dayNames: //['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], //en
               ['일', '월', '화', '수', '목', '금', '토'], //ko
    space: true //defalut false
  });

//   handling your virtual calendar
  var days = document.querySelectorAll('.s_calendar-days');

  document.querySelector('.next').addEventListener('click', function() {
    calender.nextMonth();

    //handling your virtual calendar
    days = document.querySelectorAll('.s_calendar-days');
  });

  document.querySelector('.prev').addEventListener('click', function() {
    calender.prevMonth();

    //handling your virtual calendar
    days = document.querySelectorAll('.s_calendar-days');
  });
  
  //get Date value
  document.querySelector('#pivot').addEventListener('click', function(e) {
    if(e.target.dataset.key){
      var yeerMonth = document.querySelector('.s_calendar-month').textContent;
      yeerMonth = yeerMonth.replace('년','-');
      yeerMonth = yeerMonth.replace('월','-');
      console.log(yeerMonth + e.target.dataset.key);
      document.querySelector('#getDate').value = yeerMonth + e.target.dataset.key;
    };
  })
}

 

전에 프로토타입 스터디하면서 짰던 건데 이렇게 다시 쓰게 되네요.

 

캘린더 js는 무시하셔도 되고 오늘은 app.js 만 사용해서 목표한 이미지를 구현하도록 하겠습니다.

 

조작해보시면 아시겠지만 이미 날짜를 클릭할 때 인풋 창에 날짜 값이 들어가도록 세팅이 되어 있습니다.

 

여기 걸려 있는 이벤트 리스너 내용을 수정하도록 하겠습니다. 

우선은 간단하게 alert이 뜨도록 하고 (이 부분은 추후에 모달 창을 뜨도록 하면 되겠습니다.)

document.querySelector('#pivot').addEventListener('click', function(e) {
    if(e.target.dataset.key){
      var yeerMonth = document.querySelector('.s_calendar-month').textContent;
      yeerMonth = yeerMonth.replace('년','-');
      yeerMonth = yeerMonth.replace('월','-');
      console.log(yeerMonth + e.target.dataset.key);
    
      alert(yeerMonth + e.target.dataset.key)

      //   document.querySelector('#getDate').value = yeerMonth + e.target.dataset.key;
    };
  })

이런 식으로 다 주석 처리하고 날짜 값을 받아옵니다.

 

잘 나오네요.

 

이제 모달 창을 만들어 보겠습니다.

 

우선 모달 뼈대와 콘텐츠 를 보여줄 inner를 만들도록 하겠습니다.

<div id="modal">
        <div id="modal-inner">
            modal
        </div>
    </div>
#modal {
            background-color: rgba(71, 71, 71, 0.253);
            position: fixed;
            top: 0;
            left: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            width: 100vw;
            height: 100vh;
        }

        #modal-inner {
            background-color: rgb(124, 69, 69);
            width: 50vh;
            height: 50vh;
        }

이런 식으로 만들어주면 

 

요런 모습을 보실 수 있습니다.

 

배경에 투명하게 회색 계열의 색상이 깔리고 그 위에 모달 창이 뜹니다.

이제 모달 창을 끄고 킬 수 있도록 js를 작성해보도록 하겠습니다.

 

우선 modal이라는 이름으로 엘리먼트를 먼저 잡아줍니다.

 const modal = document.querySelector('#modal');

모달을 껐다 킬 수 있는 프로퍼티,

선택된 날짜의 데이터를 가지고 있는 프로퍼티

위 두 가지의 속성을 가진 오브젝트를 생성합니다.

 

 const state = {
      modalVisible: false,
      currentDate: '',
  }

 

이제 저 modalVisible에 값에 따라 모달 창이 최신화할 수 있는 함수를 하나 만듭니다.

 

function modalUpdate () {
      modal.style.display = state.modalVisible ? 'flex': 'none';
  }

그리고 모달 이너 창이 아닌 밖의 영역을 선택하면 모달 창이 닫힐 수 있도록 이벤트 리스너를 걸어줍니다.

 

 modal.addEventListener('click', function (e) {
      if (e.target.id === 'modal') {
        state.modalVisible = false;
        modalUpdate();
      }
  });

보시면 타깃의 아이디가 모달일 때만 상태 값을 false로 준 후 업데이트를 하고 있는 모습입니다.

 

자 그럼 캘린더 내부 날짜를 클릭했을 때 모달 창이 나올 수 있도록 해야겠죠?

document.querySelector('#pivot').addEventListener('click', function (e) {
        if (e.target.dataset.key) {
            var yeerMonth = document.querySelector('.s_calendar-month').textContent;
            yeerMonth = yeerMonth.replace('년', '-');
            yeerMonth = yeerMonth.replace('월', '-');

            state.currentDate = yeerMonth + e.target.dataset.key
            state.modalVisible = true;
            modalUpdate();
        };
    })

alert 창을 지우고 이런 식으로 코드를 바꿔줍니다.

모달 창을 키기 전에 currentDate 라고 하는 프로퍼티에 값을 할당한 후에 모달창을 켜주고 있는 모습입니다.

 

이렇게까지 하면 모달 창을 켜고 끌 수 있습니다. 

 

자 이번에는 모달 창에 있어야 하는 모달 제목과 각 버튼 등 html을 조금씩 수정해주도록 하겠습니다.

 

 

이런 이미지대로 만들어야 하기 때문에 필요한 버튼과 텍스트를 입력할 수 있는 칸 및 날짜 등을 넣을 수 있도록 우선 꾸미도록 하겠습니다.

 

<div id="modal">
        <div id="modal-inner">
            <div class="modal_header">
                <h4 class="current_date">20200109</h4>
                <div class="buttons">
                    <button>Save</button>
                    <button>Delete</button>
                </div>
            </div>
            <div class="modal_body"></div>
        </div>
    </div>

우선 날짜는 임시로 넣어주겠습니다. 추후에 날짜는 js를 통해 변경하겠습니다.

이런 형태로 나옵니다.

 

이제 스타일을 넣겠습니다.

 .modal_header{
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

 

디자인이 완벽하진 않지만 얼추 잡혔습니다.

 

이제 날짜가 클릭된 날짜로 보일 수 있도록 수정하겠습니다.

 

const currentDate = document.querySelector('.current_date');

    function modalUpdate() {
        modal.style.display = state.modalVisible ? 'flex' : 'none';
        currentDate.textContent = state.currentDate;
    }

날짜가 적혀있는 클래스 이름을 잡아 모달이 업데이트될 때마다 수정하도록 합니다.

 

 modal.addEventListener('click', function (e) {
        if (e.target.id === 'modal') {
            state.modalVisible = false;
            state.currentDate = '';
            modalUpdate();
        }
    });

모달 창이 꺼졌을 때 currentDate 또한 초기화합니다.

 

클릭해보시면 날짜가 계속해서 최신화되는 모습을 보실 수 있습니다.

 

모달 창 스타일을 조금 더 변경하겠습니다.

 

        #modal-inner {
            background-color: rgb(255 255 255);
            box-shadow: 3px 5px 5px 0px #8c8b8b;
            width: 70vh;
            height: 70vh;
            padding: 20px;
            border-radius: 4px;
        }

 

그림자 효과에 배경색을 변경했더니 이제 좀 볼만 한 거 같네요.

 

이제 투두 리스트를 만들어보겠습니다.

 

1. 투두 리스트가 리스트 형태로 랜더링 된다.

2. 리스트 중 하나를 더블 클릭하면 인풋 창으로 변경된다.

3. 리스트 마지막은 추가하기.

4. 리스트 끝에는 저장 삭제 버튼이 존재한다.

 

요 정도로 해서 작업을 해보도록 하겠습니다.

 

더미 데이터를 렌더링 할 태그를 만들어 줍니다.

<ul class="todo-list"></ul>

 

더미데이터를 만들고 렌더링 하도록 하겠습니다. 

 

const todoParent = document.querySelector('.todo-list');
    const dummyList = [
        { sort: 0, text: '밥 먹기'},
        { sort: 2, text: '코딩하기'},
        { sort: 1, text: '잠 자기'},
        { sort: 3, text: '영화보기'},
    ]

    const sortList = (a, b) => {
        if (a.sort < b.sort) return -1;
        if (a.sort > b.sort) return 1;
        return 0;
    }

    function modalListRender () {
        if (state.currentDate !== '') {
            const frag = document.createDocumentFragment();
            
            dummyList
                .sort(sortList)
                .map(item => {
                    const li = document.createElement('li');
                    li.textContent = item.text;
                    frag.appendChild(li);

                    return false;
                });

            return todoParent.appendChild(frag);
        }

        return todoParent.innerHTML = '';
    }

이렇게 작성하면 modalListRender함수를 통해 내용을 렌더링 시킬 수 있습니다.

 

modal.addEventListener('click', function (e) {
        if (e.target.id === 'modal') {
            state.modalVisible = false;
            state.currentDate = '';
            modalUpdate();
            modalListRender();
        }
    });

    //get Date value
    document.querySelector('#pivot').addEventListener('click', function (e) {
        if (e.target.dataset.key) {
            var yeerMonth = document.querySelector('.s_calendar-month').textContent;
            yeerMonth = yeerMonth.replace('년', '-');
            yeerMonth = yeerMonth.replace('월', '-');

            state.currentDate = yeerMonth + e.target.dataset.key
            state.modalVisible = true;
            modalUpdate();
            modalListRender();
        };
    })

 

잘 나오네요. 근데 문제는 하나의 더미 데이터를 모든 날짜가 공유를 하고 있기 때문에

 

날짜마다 다르게 렌더링 될 수 있도록 더미 데이터, 관련 로직을 조금 변경하겠습니다.

const dummyDates = [{
        date: '2021-1-7',
        list: [
            { sort: 0, text: '밥 먹기'},
            { sort: 2, text: '코딩하기'},
            { sort: 1, text: '잠 자기'},
            { sort: 3, text: '영화보기'},
        ]
    },
    {
        date: '2021-1-8',
        list: [
            { sort: 0, text: '밥 먹기'},
            { sort: 1, text: '잠 자기'},
            { sort: 3, text: '영화보기'},
        ]
    },
    {
        date: '2021-1-9',
        list: [
            { sort: 1, text: '잠 자기'},
        ]
    }]

더미 데이터를 이런 식으로 변경하였습니다.

 

이에 따라 변경된 로직 및 추가된 로직은

const createList = (frag, text) => {
        const li = document.createElement('li');
        li.textContent = text;
        frag.appendChild(li);
    }

    function modalListRender () {
        if (state.currentDate !== '') {
            const frag = document.createDocumentFragment();

            const foundList = dummyDates.find(dateItem => dateItem.date === state.currentDate);
            if (foundList) {
                foundList.list
                    .sort(sortList)
                    .map(item => createList(frag, item.text));
            } else {
                createList(frag, '오늘은 할 일이 없습니다 ~')
            }

            return todoParent.appendChild(frag);
        }

        return todoParent.innerHTML = '';
    }

원래 사용했던 dummyList.map에 들어가던 함수를 외부 createList로 빼서 재활용하는 모습을 보실 수 있습니다.

그리고 리스트가 없는 부분에는 할 일이 없습니다~ 가 들어가고요.

 

 

이런 식입니다.

 

1. 투두 리스트가 리스트 형태로 랜더링 된다.

2. 리스트 중 하나를 더블 클릭하면 인풋 창으로 변경된다.

3. 리스트 마지막은 추가하기.

4. 리스트 끝에는 저장 삭제 버튼이 존재한다.

 

이제 더블 클릭하면 인풋창으로 변경시키는 부분이 추가가 되야겠네요.

 const createList = (frag, text, index) => {
        const li = document.createElement('li');
        li.textContent = text;
        li.dataset.key = index;
        
        li.ondblclick = () => {
            const input = document.createElement('input');
            const saveButton = document.createElement('button');
            const removeButton = document.createElement('button');
            
            const saveInputContent = () => li.textContent = input.value;
            saveButton.textContent = 'Save';
            saveButton.onclick = () => {
                if (text !== '오늘은 할 일이 없습니다 ~' && '추가하기' !== text) {
                    const targetArr = dummyDates.find(dateItem => dateItem.date === state.currentDate).list
                    targetArr.find(item => item.text === text).text = input.value;
                    saveInputContent();
                } else {
                    dummyDates.push({
                        date: state.currentDate,
                        list:  [{ sort: 0, text: input.value}]
                    });
                    
                    modalListRender();
                }
            }
            removeButton.textContent = 'Delete';
            removeButton.onclick = () => {
                if (text !== '오늘은 할 일이 없습니다 ~' && '추가하기' !== text) {
                    const targetArr = dummyDates.find(dateItem => dateItem.date === state.currentDate).list;
                    
                    targetArr.splice(targetArr.indexOf(targetArr.find(item => item.text === text)), 1);

                    modalListRender();
                }
                else saveInputContent();
            }

            input.value = li.textContent;
            
            li.textContent = '';
            li.appendChild(input);
            li.appendChild(saveButton);
            li.appendChild(removeButton);
        }

        frag.appendChild(li);
    }

    function modalListRender () {
        todoParent.innerHTML = '';

        if (state.currentDate !== '') {
            const frag = document.createDocumentFragment();
            let index = 0;
            const foundList = dummyDates.find(dateItem => dateItem.date === state.currentDate);

            if (foundList) {
                foundList.list
                    .sort(sortList)
                    .map(item => createList(frag, item.text, index++));
            } else {
                createList(frag, '오늘은 할 일이 없습니다 ~', index++)
            }

            createList(frag, '추가하기', index++);

            return todoParent.appendChild(frag);
        }
    }

더블 클릭 시 인풋,

리스트 마지막은 추가하기,

 데이터가 없는 경우에는 오늘은 할 일이 없습니다 ~ 까지 완료됐습니다.

 

그리고 추가로 save, delete 기능까지 추가된 화면입니다

 

 

막상 만들어보니 "추가하기"만 있어도 될 거 같아서 뺐습니다.

 

디자인은 다음에 수정하는 걸로 하고 이 글은 여기까지 작성하도록 하겠습니다. 감사합니다.

 

완성 코드는 여기에서 확인하실 수 있습니다.

 

codepen.io/sinhooking/pen/VwKBVWQ

 

virtual calendar + todolist javasciprt

...

codepen.io

 

'dev > JS' 카테고리의 다른 글

javascript + canvas 마우스 효과  (0) 2021.01.12
javascript + canvas 우주 만들기.  (0) 2021.01.11
typescript 스터디 3. 사용해서 아코디언 메뉴 만들기  (0) 2021.01.08
typescript 스터디 2.  (0) 2021.01.07
typescript 스터디 1.  (0) 2021.01.07