상세 컨텐츠

본문 제목

[Javascript] Calendar & To-do List ④ Complete &Refactoring

Knowledge/Javascript

by winCow 2021. 6. 7. 23:48

본문

1. 개요

다음과 같은 기능을 완성했다.

 

달력

 - 현재 페이지에 이번 달의 날짜 및 요일 표시하기

 - 현재 페이지를 넘겨 다음 달, 이전 달의 날짜 표시하기

 - 휴일 및 토요일 표시하기

 - 오늘 날짜에 회색 원 표시하기

 - 클릭한 날짜에 파란색 원 표시하기

 

To-do List

 - 제출된 값을 로컬 저장소에 저장하기

 - 로컬 저장소에 있는 값 표시하기

 - 체크박스를 클릭하여 취소선 긋기

 - 표시된 값을 화면과 로컬 저장소에서 제거하기

 

 

2. 현재 페이지에 표시될 날짜 구하기

const now = new Date();
let year = now.getFullYear();
let month = now.getMonth();

Date 클래스로부터 now 객체를 생성해 현재 날짜와 시간 정보를 받아오고, year, month 정보를 각각의 변수에 할당한다. 달력의 페이지를 넘기면서 year, month 값이 바뀔 것이기 때문에 let으로 선언한다. 올해, 이번 달의 정보를 얻었다면, 달력에 일자들을 표시하기 위해 일자 정보를 받아야 한다. 현재 페이지에 나타나는 일자는, 이번 달의 모든 일자뿐 아니라, 이전 달의 마지막 주의 일자들, 다음 달의 첫째 주의 일자들도 포함된다. 

 

function getPrevDates() {
  const prevLast = new Date(year, month, 0);
  const prevLastDate = prevLast.getDate();
  const prevLastDay = prevLast.getDay();
  const prevDates = [];
  if (prevLastDay != 6) {
    for (i = 0; i < prevLastDay + 1; i++) {
      prevDates.unshift(prevLastDate - i);
    }
  }
  return prevDates;
}
getPrevDates();

먼저 이전 달의 마지막 주의 일자들을 구한다. prevLast는 지난 달의 마지막 일자이다. 매월 마지막 날이 며칠인지는 규칙이 없기 때문에, 이번 달의 인스턴스를 만들어 0번째 날을 지정하면 지난 달의 마지막 일자에 대한 시간 정보가 나온다. 여기서 getDate()를 통해 날짜를, getDay()를 통해 요일을 받아온다. 지난 달의 마지막 주가 총 며칠인지도 역시 불규칙적이므로, 요일 정보를 이용한다. 지난 달의 마지막날이 토요일이라면, 지난 달의 날짜들은 달력에 표시될 필요가 없으므로, prevLastDay가 6이 아닌 경우에만 반복문을 통하여 일자를 구한다. 반복문은 마지막 날짜로부터 1씩 감소시키는 형태로, prevLastDay는 0~6이 반복되므로, 지난 달의 마지막 날과 요일이 같지 않은 한 반복문을 실행시키도록 한다.

 

function getThisDates() {
  const thisLast = new Date(year, month + 1, 0);
  const thisLastDate = thisLast.getDate();
  const thisDates = [...Array(thisLastDate + 1).keys()].slice(1);
  return thisDates;
}
getThisDates();

이번 달의 날짜는 간단하게 받아올 수 있다. 이전 달의 마지막 날을 구할 때와 같은 요령으로 이번 달의 마지막 날의 날짜 정보를 구한 뒤, 1부터 해당 날짜까지를 배열로 만들면 된다. Array.keys()는 배열의 각 인덱스를 키 값으로 가지는 Array Iterator 객체를 반환하므로, Array(thisLastDate+1).keys()는 0부터 이번 달의 마지막 날짜까지를 키 값으로 가지는 객체를 반환한다. 전개 구문으로 이를 배열로 만들고, 0은 필요하지 않으므로 slice를 통해 제거한다.

 

function getNextDates() {
  const thisLast = new Date(year, month + 1, 0);
  const thisLastDay = thisLast.getDay();
  const nextDates = [];
  if (thisLastDay != 6) {
    for (i = 0; i < 6 - thisLastDay; i++) {
      nextDates.push(i + 1);
    }
  }
  return nextDates;
}
getNextDates();

이후, 현재 페이지에 표시될 다음 달의 날짜를 받아온다. 이를 위해 이번 달의 마지막 날의 요일 정보를 이용한다. 이번 달의 마지막 날의 요일이 토요일이라면 다음 달의 날짜는 표시될 필요가 없으므로, 이를 제외하고 for 반복문을 적용한다. 1부터 시작하여 이번 달의 마지막 날의 요일과 같은 요일이 되기 전까지를 배열에 push해 준다.

 

 

3. 현재 페이지 표시하기

function displayPage() {
  const prevDates = getPrevDates();
  const thisDates = getThisDates();
  const nextDates = getNextDates();
  const dateContainer = document.querySelector('.date_container');
  const modifiedPrevDates = prevDates.map(prevdate => `<div class="dates transparent prev">${prevdate}</div>`).join('');
  const modifiedThisDates = thisDates.map(thisdate => `<div class="dates these">${thisdate}</div>`).join('');
  const modifiedNextDates = nextDates.map(nextdate => `<div class="dates transparent next">${nextdate}</div>`).join('');
  dateContainer.innerHTML = modifiedPrevDates + modifiedThisDates + modifiedNextDates;

  const thisMonth = document.querySelector('.this_month');
  thisMonth.innerText = `${year}. ${month + 1}`;
}
displayPage();

현재 페이지를 표시하기 위해, 2에서 만든 일자들을 각각 가져온 뒤, map을 이용해 HTML의 <div> 태그 형태로 모양을 바꿨다. 이 때, 지난 달과 다음 달의 일자는 투명하게 표시하기 위해서 transparent 클래스를, 전체 일자들을 선택하기 위해 dates 클래스를, 그 밖에도 지난 달, 이번 달, 다음 달을 구분하기 위해 각각 prev, these, next 클래스를 주었다. 이렇게 만든 <div> 태그들을 innerHTML을 통해 HTML 요소로 삽입하면 해당 일자들이 HTML에 표시된다.

또, 이번 달의 연, 월 정보를 표시하기 위해 해당 태그를 잡아 innerText를 설정해 준다.

 

.day_container :nth-child(7n+1) {
  color: #eb3b5a;
}
.day_container :nth-child(7n+7) {
  color: #3867d6;
}

CSS에서, 토요일과 일요일을 각각 파란색, 빨간색으로 표시하기 위해, n번째 자식 요소에 스타일을 적용하는 nth-child를 이용해 색깔을 바꿔 줌으로써 달력의 형태가 얼추 갖춰진다.

 

const holidays = [
  {
    'name': '신정',
    'month': 1,
    'date': 1
  },
  {
    'name': '삼일절',
    'month': 3,
    'date': 1
  },
  {
    'name': '어린이날',
    'month': 5,
    'date': 5
  },
  {
    'name': '현충일',
    'month': 6,
    'date': 6
  },
  {
    'name': '광복절',
    'month': 8,
    'date': 15
  },
  {
    'name': '개천절',
    'month': 10,
    'date': 3
  },
  {
    'name': '한글날',
    'month': 10,
    'date': 9
  },
  {
    'name': '크리스마스',
    'month': 12,
    'date': 25
  }
];

다음은 이번 달의 공휴일 정보를 표시하기 위해 휴일 정보를 객체들의 배열로 작성했다. 이는 추후 JSON으로 변환하는 것이 좋아 보인다.

 

function expressHolidays() {
  const thesedates = Array.from(document.querySelectorAll('.these'));
  const theseHolidays = holidays
    .filter(holiday => holiday['month'] === month + 1)
    .map(holiday => holiday.date);
  thesedates
    .filter(data => theseHolidays.includes(parseInt(data.innerText)))
    .map(data => data.id = 'holiday');

  const prevDays = Array.from(document.querySelectorAll('.prev'));
  const prevHolidays = holidays
    .filter(holiday => holiday['month'] === month)
    .map(holiday => holiday.date);
  prevDays
    .filter(prevDay => prevHolidays.includes(parseInt(prevDay.innerText)))
    .map(prevDay => prevDay.id = 'holiday');

  const nextDays = Array.from(document.querySelectorAll('.next'));
  const nextHolidays = holidays
    .filter(holiday => holiday['month'] === month + 2)
    .map(holiday => holiday.date);
  nextDays
    .filter(nextDay => nextHolidays.includes(parseInt(nextDay.innerText)))
    .map(nextDay => nextDay.id = 'holiday');
}
expressHolidays();

다음으로, 현재 페이지의 휴일을 빨간색으로 표시하기 위해 expressHolidays 함수를 만든다. 먼저, 이번 달의 날짜들의 div 태그들을 모두 선택하여 thesedates에 할당한다. 또, theseHolidays 변수를 만들고, 위의 공휴일 정보에서 'month' 키에 대한 값이 현재 페이지의 월(month+1)과 일치하는 것만을 필터링한 뒤, 그 날짜만을 가져와 배열에 담는다. 이 두 값을 비교하여 일치하는 날짜에 'holiday'라는 id를 부여하고, 해당 id를 가진 태그들만 CSS에서 색상을 변경할 것이다.

theseHolidays에는, 이번 달의 공휴일 날짜들이 배열로 들어가 있는데, thesedates에서 선택된 태그들 각각에 대하여, Text 값이 theseHolidays에 포함된 값인지를 체크하도록 filter API를 사용하고, 반환된 값들에 map을 통해 id를 추가해 준다. 투명한 부분 역시 같은 로직으로 공휴일을 표시할 수 있다.

 

const date = now.getDate();
const day = now.getDay();
const days = ['일', '월', '화', '수', '목', '금', '토'];
const thisLast = new Date(year, month + 1, 0);
const displayedDate = document.querySelector('.todo_date');
function expressToday() {
  const allTheseDates = Array.from(document.querySelectorAll('.these'));
  const today = allTheseDates.find(data => data.innerText == date);
  if ((year === thisLast.getFullYear()) && (month === thisLast.getMonth())) {
    today.id = 'today';
  }
  displayedDate.innerText = `${year}. ${month + 1}. ${date} ${days[day]}요일`;
}
expressToday();

다음은 오늘 날짜를 찾아 회색 원으로 강조하고, To-do list 상단에도 오늘 날짜를 표시해 주는 기능이다. 먼저, date는 현재 날짜와 시간 정보를 가진 객체로부터 getDate를 통해 가져온 오늘의 날짜이다. 함수를 만들고 현재 페이지에 표시된 모든 날짜를 캐치한 뒤, find API를 이용해 innerText가 date와 일치하는 div를 찾아 today 변수에 할당한다.

다른 월과 다른 년도의 데이터를 캐치하지 않기 위해 조건문을 만드는데, thisLast 객체를 생성해 이번 달의 마지막 날에 대한 정보를 얻는다. 여기서 getFullYear, getMonth로 이번 달의 마지막 날의 연도, 월 정보를, 코드의 가장 앞에서 구했던 year, month와 비교하여 같은 값만을 골라 'today'라는 id를 부여하고 스타일을 설정한다.

To-do list의 상단에도 날짜를 표시하기 위해 이를 캐치한 뒤, innerText를 year, month, date, day 정보를 삽입한다. 

 

 

#today {
  background-color: #d1ccc0;
  border-radius: 100%;
  height: 20px;
  width: 20px;
  margin: 10px calc(100%/7/2 - 10px - 10px);
  padding: 10px;
}

 today id은 위와 같이 스타일을 설정했다.

 

let allDivDates = Array.from(document.querySelectorAll('.dates'));
function changeDate() {
  allDivDates = getDivDates();
  const these = Array.from(document.querySelectorAll('.these'));
  const prev = Array.from(document.querySelectorAll('.prev'));
  const prevSelected = allDivDates.find(date => date.id === 'selected');
  if (prevSelected) {
    prevSelected.removeAttribute('id');
  }
  expressToday();
  expressHolidays();
  this.id = 'selected';
  if (these.includes(this)) {
    displayedDate.innerText = `${year}. ${month + 1}. ${this.innerText} ${days[(parseInt(this.innerText) + 5) % 7]}요일`;
  } else if (prev.includes(this)) {
    displayedDate.innerText = `${year}. ${month}. ${this.innerText} ${days[(parseInt(this.innerText) + 5) % 7]}요일`;
  } else {
    displayedDate.innerText = `${year}. ${month + 2}. ${this.innerText} ${days[(parseInt(this.innerText) + 5) % 7]}요일`;
  }
}
allDivDates.forEach((date) => date.addEventListener('click', changeDate));

먼저 현재 달력에 표시된 날짜들을 캐치한다. 이 날짜들은 페이지의 이동에 따라 변하는 값이므로 let을 사용한다. 이 모든 날짜들에 대해, 클릭될 때 changeDate 함수가 실행되도록 이벤트 리스너를 건다. 그런데, 이렇게 캐치된 날짜들을 담은 div 태그들은, 페이지가 넘겨질 때마다 새로고침되므로, 그 때마다 새롭게 갱신해야 하는데, getDivDates를 이용해 이를 갱신해 준다. 

 

function getDivDates() {
  let changedDivDates = Array.from(document.querySelectorAll('.dates'));
  changedDivDates.forEach((date) => date.addEventListener('click', changeDate));
  return changedDivDates;
}

getDivDates는 위와 같이, 날짜들의 div를 캐치하고 각각 이벤트 리스너를 건 뒤, 캐치된 날짜들을 반환하는 함수이다.

 

changeDate에서 가장 먼저 getDivDates를 실행하여 allDivDates를 갱신한 뒤에 코드를 실행하도록 한다. 먼저, 이전 달의 날짜, 이번 달의 날짜, 다음 달의 날짜로 나누어 각각 할당한다. 이 중, 선택된 날짜에 'selected'라는 id를 부여할 것인데, 클릭이 여러 번 이루어지는 경우, id가 중복해서 사용될 수 있으므로, 먼저 find API를 이용해 id가 이미 존재하는지를 확인한 뒤, 존재한다면 removeAttribute를 이용해 id를 지워 준다. 그리고 selected를 부여하기 전에, 위에서 만들었던 expressToday, expressHolidays를 실행하지 않으면, 클릭된 후 today나 holiday id가 사라지기 때문에 다음 클릭이 일어날 때 갱신되도록 다시 한 번 changeDate 내부에서 실행되도록 한다. 선택된 태그인 this에 selected id를 부여하고, 각각 to-do list의 상단에 표시될 값인 displayedDate의 내용도 변경해 준다.

 

 

4. 페이지 이동하기

const arrowToPrevMonth = document.querySelector('.prev_month');
const arrowToNextMonth = document.querySelector('.next_month');
const changeMonthToPrev = () => {
  toPrevMonth()
    .then(displayPage)
    .then(expressHolidays)
    .then(expressToday)
    .then(getDivDates);
  }
  const changeMonthToNext = () => {
    toNextMonth()
    .then(displayPage)
    .then(expressHolidays)
    .then(expressToday)
    .then(getDivDates);
}
arrowToPrevMonth.addEventListener('click', changeMonthToPrev);
arrowToNextMonth.addEventListener('click', changeMonthToNext);

좌측, 우측 화살표를 각각 선택해서 클릭에 반응하는 이벤트 리스너를 건 뒤, 월 정보를 바꾸도록 한다. 월 정보를 바꾸는 함수는 프로미스를 반환하는 toPrevMonth(), toNextMonth() 함수를 시작으로, 이를 받아서 displayPage, expressHolidays, expressToday, getDivDates를 순차적으로 실행하도록 한다. 순서대로 월 정보를 받고, 페이지를 표시한 뒤, 휴일 및 오늘의 스타일을 변화시키고, 날짜 데이터를 전부 선택하는 함수이다. 날짜 데이터를 선택하고 나면 다시 클릭에 반응하여 위에서 만든 다른 이벤트들이 작동할 수 있게 된다.

 

async function toPrevMonth() {
  if (month > 0) {
    month = month - 1;
  } else {
    month = month + 11;
    year = year - 1;
  }
  return { 'year': year, 'month': month };
};
async function toNextMonth() {
  if (month < 11) {
    month = month + 1;
  } else {
    month = month - 11;
    year = year + 1;
  }
  return { 'year': year, 'month': month };
};

 

연/월 정보를 변경하는 함수이다. 왼쪽 방향 화살표는 이전 달로 이동하는 트리거이므로, month가 0보다 클 때는 1씩 빼고, 0이 되면 year에서 1을 뺀 뒤 month는 다시 11로 바꾼다. 오른쪽 방향 화살표는 마찬가지 로직으로 11보다 작을 때는 1씩 더하고, 11이 되면 year에 1을 더한 뒤 0부터 다시 시작한다. async를 이용하여 결과 year, month를 프로미스로 전달한다.

 

 

5. 로컬 저장소에 아이템 추가하기

const plusBtn = document.querySelector('.icon_box');
const textBox = document.querySelector('.todo_add_box');
const items = JSON.parse(localStorage.getItem('items')) || [];

to-do list는 입력해둔 값을 이용하기 위해 로컬 저장소를 이용한다.

items는 로컬 저장소에서 아이템을 받아 오도록 JSON.parse 함수를 이용하고, 만약 undefined인 경우 빈 배열을 만들어 주도록 || 연산자를 이용한다.

 

function addItem(e) {
  e.preventDefault();
  const item = document.querySelector('.textbox').value;
  if (!item) {return;}
  items.push(item);
  localStorage.setItem('items', JSON.stringify(items));
  displayItems(items);
  getTrashBoxes();
  getCheckBoxes();
  this.reset();
}
textBox.addEventListener('submit', addItem);
plusBtn.addEventListener('submit', addItem);

아이템의 추가는 textbox나 plusBtn의 값이 제출될 때 실행되도록 이벤트 리스너를 건다. 함수가 실행될 때 새로고침되는 것을 막기 위하여, e.preventDefault를 걸고, textbox의 아이템이 없다면 제출 이벤트가 발생해도 아무것도 반환하지 않도록 조건문을 건다. textbox의 아이템이 있다면, items 배열에 해당 item을 push하고, 이를 로컬 저장소에 items라는 이름으로 저장한다. 이를 이용해 아이템을 표시하는 displayItems, 삭제 버튼과 체크 버튼을 활성화하는 getTrashBoxes, getCheckBoxes를 실행하고, 마지막으로 textbox의 값이 제출되면 this.reset() 메소드로 텍스트 상자를 비우는 작업을 한다.

 

 

6. To-do list에 아이템 표시하기

function displayItems(items) {
  let i = 0;
  const itemList = document.querySelector('.todo_list');
  const itemsDiv = items.map(item => `<li class="list" data-num="${i++}"><button class="checkbox"><i class="far fa-square"></i></button><p>${item}</p><button class="trash"><i class="fas fa-trash-alt"></i></button></li>`);
  itemList.innerHTML = itemsDiv.join('');
}
displayItems(items);

displayItems 함수는, 로컬 저장소에 저장된 데이터를 배열의 형태로 가지고 있는 items를 매개변수로 받아 실행된다.

여기에 map API로 HTML의 li 태그 형식으로 값을 저장하는데, 이 때 체크 및 삭제에 이용하기 위해 배열에서의 index와 일치하는 값을 dataset-num 속성으로 부여한다. 체크 및 삭제 버튼도 이 때 입력한다.

 

 

7. 체크 버튼 활성화하기

function checkItem() {
  const lists = new Array(...document.querySelectorAll('.list'));
  const match = lists.find(item => item.dataset.num === this.parentNode.dataset.num);
  const matchedBtn = match.childNodes[0];
  const matchedIcon = matchedBtn.childNodes[0];
  matchedIcon.classList.toggle('fa-square');
  matchedIcon.classList.toggle('fa-grin-tongue-wink');
  match.classList.toggle('complete');
}

checkItem 함수는 체크 버튼을 클릭하면 체크 박스의 형태가 변하고, 텍스트에도 취소선이 표시되도록 한다. 

현재 표시되어 있는 to-do list를 모두 선택하고, 이 중, 각각의 dataset-num의 값이, this의 부모 노드의 dataset-num과 일치하는 값을 것을 찾아 match에 입력한다. 체크박스는 클릭에 반응하게 될 것이므로, 체크박스의 부모인 li 태그가 될 것이다. matchedBtn과 matchedIcon을 각각 선택하고, 클래스를 토글하도록 한다.

 

 

8. 삭제 버튼 활성화하기

function deleteItem() {
  const ul = document.querySelector('.todo_list');
  const lists = new Array(...document.querySelectorAll('.list'));
  const match = lists.find(item => item.dataset.num === this.parentNode.dataset.num);
  const item = items[lists.indexOf(match)];
  items.splice(items.indexOf(item), 1);
  localStorage.setItem('items', JSON.stringify(items));
  ul.removeChild(match);
}

deleteItem은 체크 버튼 활성화 때와 마찬가지의 요령으로 dataset-num 속성을 비교하여 match를 구한다. 로컬 저장소에서 불러온 값인 items 중, index가 match와 일치하는 값을 골라 item 변수에 할당하고, splice API를 이용해 해당 부분을 제외한 나머지 값들만을 items의 값으로 변경한다. 이렇게 변경된 값을 다시 로컬 저장소에 입력함으로써 로컬 저장소가 갱신되고, removeChild를 이용해 match를 제거함으로써 리스트에 표시된 값이 제거된다.

 

 

9. 버튼 갱신하기

function getTrashBoxes() {
  let trashBoxes = document.querySelectorAll('.trash');
  trashBoxes.forEach(trashBox => trashBox.addEventListener('click', deleteItem));
  return trashBoxes;
}
getTrashBoxes();

function getCheckBoxes() {
  let checkBoxes = document.querySelectorAll('.checkbox');
  checkBoxes.forEach(checkBox => checkBox.addEventListener('click', checkItem));
  return checkBoxes;
}
getCheckBoxes();

아이템이 추가될 때마다 체크박스와 트래쉬 박스도 갱신되어야 하므로 각각 getTrashBoxes, getCheckBoxes를 만들어 선택하고 이벤트 리스너를 걸어준다.

'Knowledge > Javascript' 카테고리의 다른 글

Intersection Observer  (0) 2021.11.05

관련글 더보기

댓글 영역