꾸준한 개발자

계속적인 성장을 추구하는 개발자입니다. 꾸준함을 추구합니다.

계속 쓰는 개발 노트

JAVASCRIPT/자바스크립트 연습

비동기 이용해서 To-Do-List 만들기

gold_dragon 2020. 11. 9. 20:04

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Todos 4.0</title>
    <link href="css/style.css" rel="stylesheet" />
    <script defer src="js/app.js"></script>
  </head>
  <body>
    <div class="container">
      <h1 class="title">Todos</h1>
      <div class="ver">4.0</div>
      <input
        class="input-todo"
        placeholder="What needs to be done?"
        autofocus
      />
      <ul class="nav">
        <li id="all" class="active">All</li>
        <li id="active">Active</li>
        <li id="completed">Completed</li>
      </ul>
      <ul class="todos"></ul>
      <footer>
        <div class="complete-all">
          <input class="checkbox" type="checkbox" id="ck-complete-all" />
          <label for="ck-complete-all">Mark all as complete</label>
        </div>
        <div class="clear-completed">
          <button class="btn">
            Clear completed (<span class="completed-todos">0</span>)
          </button>
          <strong class="active-todos">0</strong> items left
        </div>
      </footer>
    </div>
  </body>
</html>

css/style.css

@import url('https://fonts.googleapis.com/css?family=Roboto:100,300,400,700|Noto+Sans+KR');
@import url('https://use.fontawesome.com/releases/v5.5.0/css/all.css');
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
body {
  font-family: 'Roboto', 'Noto Sans KR', sans-serif;
  font-size: 0.9em;
  color: #58666e;
  background-color: #f0f3f4;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.container {
  max-width: 750px;
  min-width: 450px;
  margin: 0 auto;
  padding: 15px;
}
.title {
  /* margin: 10px 0; */
  font-size: 4.5em;
  font-weight: 100;
  text-align: center;
  color: #23b7e5;
}
.ver {
  font-weight: 100;
  text-align: center;
  color: #23b7e5;
  margin-bottom: 30px;
} /* .input-todo */
.input-todo {
  display: block;
  width: 100%;
  height: 45px;
  padding: 10px 16px;
  font-size: 18px;
  line-height: 1.3333333;
  color: #555;
  border: 1px solid #ccc;
  border-color: #e7ecee;
  border-radius: 6px;
  outline: none;
  transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
}
.input-todo:focus {
  border-color: #23b7e5;
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
    0 0 8px rgba(102, 175, 233, 0.6);
}
.input-todo::-webkit-input-placeholder {
  color: #999;
} /* .nav */
.nav {
  display: flex;
  margin: 15px;
  list-style: none;
}
.nav > li {
  padding: 4px 10px;
  border-radius: 4px;
  cursor: pointer;
}
.nav > li.active {
  color: #fff;
  background-color: #23b7e5;
}
.todos {
  margin-top: 20px;
} /* .todo-item */
.todo-item {
  position: relative; /* display: block; */
  height: 50px;
  padding: 10px 15px;
  margin-bottom: -1px;
  background-color: #fff;
  border: 1px solid #ddd;
  border-color: #e7ecee;
  list-style: none;
}
.todo-item:first-child {
  border-top-left-radius: 4px;
  border-top-right-radius: 4px;
}
.todo-item:last-child {
  border-bottom-left-radius: 4px;
  border-bottom-right-radius: 4px;
} /* .checkbox .checkbox 바로 뒤에 위치한 label의 before와 after를 사용해 .checkbox의 외부 박스와 내부 박스를 생성한다. <input class="checkbox" type="checkbox" id="myId"> <label for="myId">Content</label> */
.checkbox {
  display: none;
}
.checkbox + label {
  position: absolute; /* 부모 위치를 기준으로 */
  top: 50%;
  left: 15px;
  transform: translate3d(0, -50%, 0);
  display: inline-block;
  width: 90%;
  line-height: 2em;
  padding-left: 35px;
  cursor: pointer;
  user-select: none;
}
.checkbox + label:before {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  transform: translate3d(0, -50%, 0);
  width: 20px;
  height: 20px;
  background-color: #fff;
  border: 1px solid #cfdadd;
}
.checkbox:checked + label:after {
  content: '';
  position: absolute;
  top: 50%;
  left: 6px;
  transform: translate3d(0, -50%, 0);
  width: 10px;
  height: 10px;
  background-color: #23b7e5;
} /* .remove-todo button */
.remove-todo {
  display: none;
  position: absolute;
  top: 50%;
  right: 10px;
  cursor: pointer;
  transform: translate3d(0, -50%, 0);
} /* todo-item이 호버 상태이면 삭제 버튼을 활성화 */
.todo-item:hover > .remove-todo {
  display: block;
}
footer {
  display: flex;
  justify-content: space-between;
  margin: 20px 0;
}
.complete-all,
.clear-completed {
  position: relative;
  flex-basis: 50%;
}
.clear-completed {
  text-align: right;
  padding-right: 15px;
}
.btn {
  padding: 1px 5px;
  font-size: 0.8em;
  line-height: 1.5;
  border-radius: 3px;
  outline: none;
  color: #333;
  background-color: #fff;
  border-color: #ccc;
  cursor: pointer;
}
.btn:hover {
  color: #333;
  background-color: #e6e6e6;
  border-color: #adadad;
}

js/app.js

let todos = [];
let currentNav = 'all';

const $nav = document.querySelector('.nav');
const $inputTodo = document.querySelector('.input-todo');
const $todos = document.querySelector('.todos');
const $completedAllBtn = document.querySelector('#ck-complete-all');
const $clearBtn = document.querySelector('.btn');
const $completedTodos = document.querySelector('.completed-todos');
const $activeTodos = document.querySelector('.active-todos');

const request = {
  get(url) {
    return fetch(url);
  },
  post(url, payload) {
    return fetch(url, {
      method: 'POST',
      headers: { 'content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
  },
  patch(url, payload) {
    return fetch(url, {
      method: 'PATCH',
      headers: { 'content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
  },
  delete(url) {
    return fetch(url, { method: 'DELETE' });
  },
};

const render = () => {
  let html = '';

  todos.forEach(({ id, content, completed }) => {
    html += `
      <li id="${id}" class="todo-item">
        <input id="ck-${id}"
          class="checkbox"
          type="checkbox" ${completed ? 'checked' : ''}
        >
        <label for="ck-${id}">${content}</label>
        <i class="remove-todo far fa-times-circle"></i>
      </li>
    `;
  });

  $todos.innerHTML = html;

  $completedTodos.textContent = todos.filter(todo => todo.completed).length;
  $activeTodos.textContent = todos.filter(todo => !todo.completed).length;
};

const addClassActive = $currentLi => {
  [...$nav.children].forEach(li =>
    li.classList.toggle('active', li === $currentLi)
  );

  currentNav = $currentLi.id;
};

const createMaxId = () =>
  todos.length ? Math.max(...todos.map(todo => todo.id)) + 1 : 1;

const clearInput = () => {
  $inputTodo.value = '';
  $inputTodo.focus();
};

const checkCompleted = () => {
  $completedTodos.textContent = todos.filter(todo => todo.completed).length;
};

window.onload = () => {
  fetch('/todos')
    .then(_todos => _todos.json())
    .then(_todos => (todos = _todos))
    .then(render);
};

$nav.onclick = e => {
  if (!e.target.matches('.nav > li')) return;

  addClassActive(e.target);

  request
    .get('/todos')
    .then(_todos => _todos.json())
    .then(
      _todos =>
        (todos =
          currentNav === 'all'
            ? _todos
            : currentNav === 'active'
            ? _todos.filter(todo => !todo.completed)
            : _todos.filter(todo => todo.completed))
    )
    .then(render);
};

$inputTodo.onkeyup = e => {
  if (!(e.key === 'Enter')) return;

  const newTodo = {
    id: createMaxId(),
    content: $inputTodo.value,
    completed: false,
  };

  request
    .post(`/todos`, newTodo)
    .then(_todos => _todos.json())
    .then(_todos => (todos = _todos))
    .then(render);

  clearInput();
};

$todos.onclick = e => {
  if (!e.target.matches('.todos > .todo-item > i.remove-todo')) return;

  request
    .delete(`/todos/${e.target.parentNode.id}`)
    .then(_todos => _todos.json())
    .then(_todos => (todos = _todos))
    .then(render);
};

$todos.onchange = e => {
  const { completed } = todos.find(todo => todo.id === +e.target.parentNode.id);

  request
    .patch(`/todos/${e.target.parentNode.id}`, { completed: !completed })
    .then(_todos => _todos.json())
    .then(_todos => (todos = _todos))
    .then(render);
};

$completedAllBtn.onchange = e => {
  request
    .patch(`/todos/completed`, { completed: e.target.checked })
    .then(_todos => _todos.json())
    .then(_todos => (todos = _todos))
    .then(render);
};

$clearBtn.onclick = () => {
  request
    .delete(`/todos/completed`)
    .then(_todos => _todos.json())
    .then(_todos => (todos = _todos))
    .then(render);
};

server.js

const express = require('express');
const cors = require('cors');

let { todos } = require('./data/todos');

const app = express();

app.use(cors());
app.use(express.static('public'));
app.use(express.json()); // for parsing application/json

app.get('/todos', (req, res) => {
  res.send(todos);
});

app.get('/todos/:id', (req, res) => {
  res.send(todos.filter(todo => todo.id === +req.params.id));
});

app.post('/todos', (req, res) => {
  const newTodo = req.body;

  if (!Object.keys(newTodo).length) {
    return res.send({
      error: true,
      reason: '페이로드가 없습니다. 새롭게 생성할 할일 데이터를 전달해 주세요.',
    });
  }

  if (todos.map(todo => todo.id).includes(newTodo.id)) {
    return res.send({
      error: true,
      reason: `${newTodo.id}는 이미 존재하는 id입니다.`,
    });
  }

  todos = [newTodo, ...todos];
  res.send(todos);
});

// 모든 할일의 completed를 일괄 변경
app.patch('/todos/completed', (req, res) => {
  const completed = req.body;

  todos = todos.map(todo => ({ ...todo, ...completed }));
  res.send(todos);
});

app.patch('/todos/:id', (req, res) => {
  const id = +req.params.id;
  const completed = req.body;

  if (!todos.map(todo => todo.id).includes(id)) {
    return res.send({
      error: true,
      reason: `id가 ${id}인 할일 데이터가 존재하지 않습니다.`,
    });
  }

  todos = todos.map(todo =>
    todo.id === id ? { ...todo, ...completed } : todo
  );
  res.send(todos);
});

// completed가 true인 모든 할일 데이터 삭제
app.delete('/todos/completed', (req, res) => {
  todos = todos.filter(todo => !todo.completed);
  res.send(todos);
});

// 아래 라우터를 DELETE '/todos/completed'보다 앞에 위치시키려면 url을 '/todos/:id([0-9]+)'로 변경한다.
app.delete('/todos/:id', (req, res) => {
  const id = +req.params.id;

  if (!todos.map(todo => todo.id).includes(id)) {
    return res.send({
      error: true,
      reason: `id가 ${id}인 할일 데이터가 존재하지 않습니다.`,
    });
  }

  todos = todos.filter(todo => todo.id !== id);
  res.send(todos);
});

app.listen('7000', () => {
  console.log('Server is listening on http://localhost:7000');
});

'JAVASCRIPT > 자바스크립트 연습' 카테고리의 다른 글

To-Do-List 만들기  (0) 2020.11.02
Array HOF 연습 문제  (0) 2020.10.23
다양한 패턴의 제어문 연습  (0) 2020.09.07
if문을 삼항 조건 연산자로 바꾸기  (0) 2020.09.07