할 일 관리 프로젝트 (feat. TypeScript)

2023. 1. 6. 18:55기록/Projects

    목차

배포 링크 : todo-array
원본 저장소 : 깃허브

이번에 진행한 프로젝트는 간단한 할 일 관리 프로젝트이다!

 

어떻게 디자인을 하면 좋을 지 구상하다가,

아이패드에서 어플을 실행하는 것 처럼 보여지면 재미있을 것 같았다.

그래서 아이패드, 애플펜슬 이미지를 사용했다!

 

실제로 내 아이패드는 그저 강의용.....ㅎㅎㅎㅋ

 

처음에는 자바스크립트로 완성했었는데,

이번에 배운 타입스크립트를 프로젝트에 직접 적용해보고 싶어서

나중에 타입스크립트를 사용해 리팩토링 하게 되었다!

 

기능 별로 4개의 파일로 나누어 모듈화 하였다.

  1. main.ts : 페이지 접속 시 맨 처음 실행되어야 하는 로직들
  2. renderTodo.ts : 각 todo를 렌더링
  3. requests.ts : API 호출
  4. setElements.ts : 엘리먼트 조작/설정

할 일 추가

할 일을 입력하고 엔터키를 누르거나 플러스 아이콘을 클릭하면

insert 하는 API를 호출한 후, 데이터 추가가 완료된 목록을 새로 불러와 보여준다.

 

const addBtnEl = document.querySelector('.add-btn') as HTMLButtonElement;
addBtnEl.addEventListener('click', async () => {
  const title = getElementValue('.add-input').trim();
  if (title.length === 0) {
    alert('할 일을 입력해 주세요.');
    return;
  }
  showElement('.loading');
  await insertTodo(title, curOrder);
  renderTodoList();
  hideElement('.loading');
  setElementValue('.add-input');
  showToast('추가가 완료되었습니다.');
});

addInputEl.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' && !event.isComposing) {
    addBtnEl.click();
  }
});

할 일 수정 & 체크

할 일을 체크하거나 내용을 수정하면

update 하는 API를 호출하고, 데이터 수정이 완료된 목록을 새로 불러와 보여준다.

수정 후에 수정 일시를 바로 확인할 수 있다.

 

inputEl.addEventListener('change', () => {
  const id = data.id;
  const title = inputEl.value;
  const done = checkboxEl.checked;
  const order = todoItemEl.dataset.order as string;
  fnUpdateTodo({ id, title, done, order });
});

순서 변경

Sortable.js 라이브러리를 이용해 드래그&드롭 시 순서를 변경할 수 있도록 구현하였다!

보여지는 순서만 변경되는 것이 아니라, 드롭 시 해당 할 일 데이터의 순서가 실제로 변경된다.

(update 하는 API 호출)

 

const todosEl = document.querySelector('.todos') as HTMLElement;
const sortable = Sortable.create(todosEl, {
  group: 'todos',
  animation: 100,
  handle: '.order-handle',
  onStart: function (event) {
    ...
  },
  onEnd: async function () {
    if (sortFlag) {
      showElement('.loading');
      try {
        await fnReorderTodo();
        renderTodoList();
        showToast('순서 변경이 완료되었습니다.');
      } catch (error) {
        showToast();
      } finally {
        hideElement('.loading');
      }
    }
  },
});
export async function fnReorderTodo() {
  const ids: string[] = [];
  const todoItems = document.querySelectorAll('.todo-item') as NodeListOf<HTMLElement>;
  todoItems.forEach((item) => {
    if (item.dataset.id) ids.unshift(item.dataset.id);
  });
  await reorderTodo(ids);
}

출력 옵션 설정 (완료 여부, 정렬 순서)

모든 항목 / 완료 항목 / 미완료 항목별로 볼 수 있고

사용자 지정 순 / 최근 순 / 오래된 순으로 볼 수 있다.

 

할 일 목록을 가져오는 API 는 무조건 모든 데이터를 가져오게 되어있어서

모든 데이터를 가져온 후, filter 메서드를 이용해 데이터를 걸러 주었다. 

 

const typeEl = document.querySelector('.type') as HTMLSelectElement;
typeEl.addEventListener('change', () => {
  showElement('.loading');
  renderTodoList(getElementValue('.type'), getElementValue('.order'));
  hideElement('.loading');
});
export async function renderTodoList(done?: string, order?: string) {
  let res = Array.from(await getListTodo()).reverse() as dataType[];

  if (order === 'recent') res.sort((a, b) => +new Date(b.createdAt!) - +new Date(a.createdAt!));
  else if (order === 'old') res.sort((a, b) => +new Date(a.createdAt!) - +new Date(b.createdAt!));

  if (done === 'true') res = res.filter((item) => item.done === true);
  else if (done === 'false') res = res.filter((item) => item.done === false);

  curOrder = res.length;
  setElementHtml('.todo-length', String(curOrder));
  setElementHtml('.todos');
  if (res.length === 0) showToast('등록된 할 일이 없습니다.');
  else res.forEach((item) => renderTodo(item));
}

단일 항목 삭제 & 체크된 항목 삭제

각 할 일의 쓰레기통 아이콘을 클릭하면 단일 삭제가 가능하다.

 

오른쪽 상단의 체크 쓰레기통 아이콘을 클릭하면

체크된 할 일의 목록을 한 번에 삭제할 수 있다.

 

const checkedDeleteBtnEl = document.querySelector('.checked-delete-btn') as HTMLButtonElement;
checkedDeleteBtnEl.addEventListener('click', async () => {
  if (!confirm('완료된 할 일을 모두 삭제하시겠어요?')) return;
  const checkedsEl = document.querySelectorAll('.todo-done:checked');
  let checkedIds: string[] = [];
  if (checkedsEl) {
    checkedIds = Array.from(checkedsEl).map((item) => {
      if (item.parentElement && typeof item.parentElement.dataset.id === 'string') {
        return item.parentElement.dataset.id;
      } else {
        return '';
      }
    });
  }
  if (checkedsEl.length === 0) {
    alert('완료된 할 일이 없습니다.');
    return;
  }
  checkedIds.forEach((item) => {
    const todosEl = document.querySelector('.todos') as HTMLUListElement;
    const todoEl = document.querySelector(`[data-id="${item}"]`) as HTMLLIElement;
    todosEl.removeChild(todoEl);
  });
  showElement('.loading');
  await deleteListTodo(checkedIds);
  await fnReorderTodo();
  renderTodoList();
  hideElement('.loading');
  showToast('삭제가 완료되었습니다.');
});

메모 수정

새로고침 시에도 값을 유지하고 싶어서

localStorage 를 이용해 구현하였다!

 

const memoTextareaEl = document.querySelector('.memo-txa') as HTMLTextAreaElement;
memoTextareaEl.addEventListener('change', () => {
  showElement('.loading');
  localStorage.setItem('memo', getElementValue('.memo-txa'));
  hideElement('.loading');
  showToast('메모가 저장되었습니다.');
});

타입스크립트를 프로젝트에 처음 적용해 보았는데,

자바스크립트 문법만으로 부족했던 부분들을 타입스크립트가 채워준다는 것과

엄격하게 타입을 관리하면 에러를 미리 방지할 수 있다는 것을 느끼게 되었다!

 

앞으로 더 규모있고 복잡한 프로젝트에서도 타입스크립트를 써보고 싶다! 😻