Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1주차] 오대균 미션 제출합니다. #1

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
63 changes: 8 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,11 @@
# 1주차 미션: Vanilla-Todo
# 배포

# 서론
[오대균의 Todo List](https://daegyuns-todo-list.vercel.app/)

안녕하세요 🙌🏻 18기 프론트엔드 운영진 **배성준**입니다.
# Key Feature

이번 미션은 개발 환경 구축과 스터디 진행 방식에 익숙해지실 수 있도록 간단한 **to-do list** 만들기를 진행합니다. 무작정 첫 스터디부터 React를 다루는 것보다는 왜 React가 필요한지, React가 없으면 무엇이 불편한지 느껴 보고 본격적인 스터디에 들어가는 것이 React를 이해하는 데 더 많은 도움이 될 것이라 생각합니다.

비교적 가벼운 미션인 만큼 코드를 짜는 데 있어 여러분의 **창의성**을 충분히 발휘해 보시기 바랍니다. 작동하기만 하면 되는 것보다 같은 코드를 짜는 여러가지 방식과 패턴에 대해 고민해 보시고, 본인이 생각한 가장 창의적인 방법으로 코드를 작성해 주세요. 여러분이 미션을 수행하는 과정에서 겪는 고민과 생각의 깊이만큼 스터디에서 더 많은 것을 얻어가실 수 있을 것입니다.

막히는 부분이 있더라도 우선은 스스로 공부하고 찾아보는 방법을 권고드리지만, 운영진의 도움이 필요하시다면 얼마든지 슬랙 Q&A 채널이나 프론트엔드 카톡방에 질문을 남겨 주세요!

# 미션

## 미션 목표

- VSCode, Prettier를 이용하여 개발 환경을 관리합니다.
- HTML/CSS의 기초를 이해합니다.
- JavaScript를 이용한 DOM 조작을 이해합니다.
- Vanilla Js를 이용한 어플리케이션 상태 관리 방법을 이해합니다.

## 기한

- 2023년 9월 15일 금요일

## Key Questions

- DOM은 무엇인가요?
- HTML (tag) Element를 JavaScript로 생성하는 방법은 어떤 것이 있고, 어떤 방법이 가장 적합할까요?
- Semantic tag에는 어떤 것이 있으며, 이를 사용하는 이유는 무엇일까요?
- Flexbox Layout은 무엇이며, 어떻게 사용하나요?
- JavaScript가 다른 언어들에 비해 주목할 만한 점에는 어떤 것들이 있나요?
- 코드에서 주석을 다는 바람직한 방법은 무엇일까요?

## 필수 요건

- [결과 화면](https://vanilla-todo-17th-qras.vercel.app/)의 기능을 그대로 구현합니다.
- 결과 링크의 화면 디자인 그대로 구현해도 좋고, 자신만의 디자인을 적용해도 좋습니다.
- CSS의 Flexbox를 이용하여 레이아웃을 구성합니다.
- JQuery, React, Bootstrap 등 외부 라이브러리를 사용하지 않습니다.
- 함수와 변수의 이름은 lowerCamelCase로 짓습니다.
- 코딩의 단위를 기능별로 나누어 Commit 메세지를 작성합니다.

## 선택 요건

- 외부 폰트([눈누 상업용 무료폰트](https://noonnu.cc/))로 입맛에 맞게 꾸밉니다.
- 브라우저의 `localStorage` 혹은 `sessionStorage`를 이용하여 다음 번 접속 시에 기존의 투두 데이터를 불러옵니다.
- 이 외에도 추가하고 싶은 기능이 있다면 마음껏 추가하셔도 됩니다.

# 링크 및 참고자료

- [HTML/CSS 기초](https://heropy.blog/2019/04/24/html-css-starter/)
- [HTML 태그](https://heropy.blog/2019/05/26/html-elements/)
- [FlexBox 가이드](https://heropy.blog/2018/11/24/css-flexible-box/)
- [JS를 통한 DOM 조작](https://velog.io/@bining/javascript-DOM-%EC%A1%B0%EC%9E%91%ED%95%98%EA%B8%B0#append)
- [localStorage, sessionStorage](https://www.daleseo.com/js-web-storage/)
- [git 사용법](https://wayhome25.github.io/git/2017/07/08/git-first-pull-request-story/)
- [좋은 코드리뷰 방법](https://tech.kakao.com/2022/03/17/2022-newkrew-onboarding-codereview/)
- todo 추가, 제거, 완료
- 중요도 및 기한 설정
- 중요도 및 기한(시작일)을 기반으로 정렬된 list
- 중요도에 따라 다른 글자색(중요도가 높을 수록 더 진하게)
- 완료된 todo는 list의 가장 아래에 배치, 글자색 매우 연하게
70 changes: 68 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,73 @@
</head>

<body>
<div class="container"></div>
<div class="wrapper">
<h1 class="title">나의 할 일</h1>
<div class="body">
<div class="inputAndAdd">
<div class="inputOuter">
<input
type="text"
class="contentInput"
placeholder="할 일을 입력해주세요."
/>

<div class="radioOuter">
<input
type="radio"
name="priority"
value="3"
class="priority"
checked
id="high"
/>

<label for="high">높음</label>

<input
type="radio"
name="priority"
value="2"
class="priority"
id="mid"
/>

<label for="mid">보통</label>

<input
type="radio"
name="priority"
value="1"
class="priority"
id="low"
/>

<label for="low">낮음</label>
</div>

<div class="dateOuter">
<input
type="date"
class="dateInput from"
data-placeholder="시작"
required
aria-required="true"
/>
<input
type="date"
class="dateInput to"
data-placeholder="종료"
required
aria-required="true"
/>
</div>
</div>
<button class="addBtn">+</button>
</div>

<ul class="todoList"></ul>
</div>
</div>
</body>
<script src="script.js"></script>
</html>
</html>
206 changes: 206 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// ############################## init ##############################
// localStorage에서 todo elements 가져오기
const unparsedTODOS = localStorage.getItem('todos');
const TODOS = unparsedTODOS
? JSON.parse(unparsedTODOS).map((todo, idx) => ({
...todo,
idx,
fromDate: new Date(todo.fromDate),
toDate: new Date(todo.toDate),
}))
: [];

let nextIdx = TODOS.length; // idx값을 중복되지 않게 설정하도록 초기값 지정
const ul = document.querySelector('.todoList'); // child node를 추가/제거 하기 위해 변수에 저장

// in memory에 저장된 todo list를 view에 display
for (const todo of TODOS) {
addNewTodoLi(todo);
}
Comment on lines +17 to +19
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미 알고 계시겠지만 이부분에 고차함수를 써보는 것도 괜찮을 것 같아요!

Suggested change
for (const todo of TODOS) {
addNewTodoLi(todo);
}
TODOS.forEach(todo => addNewTodoLi(todo));


// 추가 버튼 click event handler 등록
const addBtn = document.querySelector('.addBtn');
addBtn.addEventListener('click', handleClickAddBtn);

// 오늘 이전의 date은 선택하지 못하도록 제한
const utc = Date.now();
const timeOff = new Date().getTimezoneOffset() * 60000;
const today = new Date(utc - timeOff).toISOString().split('T')[0];
const dateInputs = document
.querySelectorAll('.dateInput')
.forEach((dateInput) => {
dateInput.setAttribute('min', today);
});
Comment on lines +29 to +33

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시작일 설정에 대한 제한도 걸어두신거 좋은 아이디어 인것 같아요! foreach 함수를 자유롭게 사용하시는 것 같아요 저도 배워야겠어요!


// 시작일 설정에 change event handler 등록
const fromDate = document.querySelector('.from');
fromDate.addEventListener('change', handleChangeFromDate);

// ############################## core functions ##############################
// view update
function updateView() {
while (ul.hasChildNodes()) {
// 기존에 추가됐든 li 삭제
ul.removeChild(ul.firstChild);
}
for (const todo of TODOS) {
// 업데이트된 todo list 추가
addNewTodoLi(todo);
}
}

// ul에 list들을 삽입
function addNewTodoLi(todo) {
// li에 필요한 element들 선언
const newLi = document.createElement('li');
const contentDiv = document.createElement('div');
const fromDateDiv = document.createElement('div');
const betweenDiv = document.createElement('div');
const toDateDiv = document.createElement('div');
const doneBtn = document.createElement('button');
const deleteBtn = document.createElement('button');

// elements의 내용 추가
contentDiv.innerHTML = todo.content;
contentDiv.className = 'content';
fromDateDiv.innerHTML = convertDate(todo.fromDate);
fromDateDiv.className = 'fromDate';
betweenDiv.innerHTML = '-';
betweenDiv.className = 'betweenDate';
toDateDiv.innerHTML = convertDate(todo.toDate);
toDateDiv.className = 'toDate';
doneBtn.innerHTML = todo.isDone ? '↪' : '✓';
doneBtn.className = 'doneBtn';
deleteBtn.innerHTML = '-';
deleteBtn.className = 'deleteBtn';

// event listener 추가
doneBtn.addEventListener('click', handleClickDoneBtn);
deleteBtn.addEventListener('click', handleClickDeleteBtn);

// li에 child node들 추가
newLi.appendChild(contentDiv);
newLi.appendChild(fromDateDiv);
newLi.appendChild(betweenDiv);
newLi.appendChild(toDateDiv);
newLi.appendChild(doneBtn);
newLi.appendChild(deleteBtn);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 여러 개 노드들 추가할 때 appendChild를 반복해서 썼었는데, 다른 방법이 없을까 하고 찾아보니까 append() 를 사용하면 문자열을 포함해서 여러 개 요소들을 추가할 수 있는 것 같아서 참고하시면 좋을 것 같습니다 😀
https://choijying21.tistory.com/132

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 새롭게 알게되었네요~~ 다음에 element 추가할 때에는 append를 사용해보겠습니다~~


newLi.id = todo.idx; // 정렬을 위한 id 추가
// 각종 className 추가
newLi.classList.add('todoLi');
if (todo.priority === 3) newLi.classList.add('high');
else if (todo.priority === 2) newLi.classList.add('mid');
else newLi.classList.add('low');
Comment on lines +92 to +94
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 이부분은 아직 작은 기능 구현이라 굳이 필요는 없을 것 같은데 이렇게 관리해도 괜찮을 것 같습니다!

Suggested change
if (todo.priority === 3) newLi.classList.add('high');
else if (todo.priority === 2) newLi.classList.add('mid');
else newLi.classList.add('low');
const priorityClassMap = {
3: 'high',
2: 'mid',
1: 'low'
};
const priorityClass = priorityClassMap[todo.priority] || 'low';
newLi.classList.add(priorityClass);


if (todo.isDone) newLi.classList.add('done');
ul.appendChild(newLi);
}

// 기준에 따라 정렬되어 memory에 저장되도록 push, push할 때만 정렬하면 따로 정렬과정 거치지 않아도 됨
function pushTodo(todo) {
if (todo.isDone) { // 완료한 todo는 맨 뒤에 위치
TODOS.splice(TODOS.length, 0, todo);
return;
}
let i = 0;
for (; i < TODOS.length; i++) {
if (TODOS[i].isDone) break; // 완료한 todo보다 무조건 앞에 위치
if (TODOS[i].priority < todo.priority) break; // priority가 더 높으면 그 앞에 위치
else if (TODOS[i].priority === todo.priority) { // priority가 같다면
if (TODOS[i].fromDate >= todo.fromDate) break; // 시작일이 빠른 todo가 더 낲에 위치
}
}
TODOS.splice(i, 0, todo);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선순위랑, 시작일에 따라 배치해서 보여주는 거 아이디어 좋은것 같아요!👏 그리고 따로 정렬조건 거치지 않게 push 할때 조건을 주니까 코드가 훨씬 효율적이네요!!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러니까요!!! 이런 추가적인 기능까지 짱이네요~!!👍

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

splice 메서드의 두번째 매개변수를 0으로 지정해서 todo 값을 추가한 거 좋은 것 같습니다 ㅎㅎ 기존 배열에서 내가 원하는 값을 특정위치에 삽입할 때 유용하게 쓸 수 있을 것 같아요!

}

// Date를 적절하게 display 하도록 convert
function convertDate(date) {
return `${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()}`;
}

// ############################## click handlers ##############################
// done button click event handler
function handleClickDoneBtn(e) {
const idx = e.currentTarget.parentNode.id;
const clickedLi = document.getElementById(idx);
// class 추가/제거
if (clickedLi.classList.contains('done')) clickedLi.classList.remove('done');
else clickedLi.classList.add('done');

// TODOS에 저장된 todo 객체 수정
let i;
for (i = 0; i < TODOS.length; i++) {
if (TODOS[i].idx == idx) {
TODOS[i].isDone = !TODOS[i].isDone;
break;
}
}
const todo = TODOS[i];

TODOS.splice(i, 1); // 해당 todo를 삭제한 후
Comment on lines +132 to +141
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 고차함수를 이용할 수 있을 것 같아요!

Suggested change
let i;
for (i = 0; i < TODOS.length; i++) {
if (TODOS[i].idx == idx) {
TODOS[i].isDone = !TODOS[i].isDone;
break;
}
}
const todo = TODOS[i];
TODOS.splice(i, 1); // 해당 todo를 삭제한 후
const todoIndex = TODOS.findIndex((todo) => todo.idx == idx);
if (todoIndex !== -1) {
const todo = TODOS[todoIndex];
todo.isDone = !todo.isDone;
TODOS.splice(todoIndex, 1);
}

pushTodo(todo); // 새롭게 추가(정렬을 위해)
updateView(); // 화면 업데이트
localStorage.setItem('todos', JSON.stringify(TODOS));
}

// delete button click event handler
function handleClickDeleteBtn(e) {
const idx = e.currentTarget.parentNode.id;
const clickedLi = document.getElementById(idx);
ul.removeChild(clickedLi); // 클릭한 todo 삭제

let i;
for (i = 0; i < TODOS.length; i++) {
if (TODOS[i].idx == idx) break;
}
TODOS.splice(i, 1); // TODOS 에서도 삭제

localStorage.setItem('todos', JSON.stringify(TODOS));
}

// add button click event handler
function handleClickAddBtn() {
const newTodo = {};
// 각종 input element들 참조
const content = document.querySelector('.contentInput');
const priorities = document.querySelectorAll("input[name='priority']");
const fromDate = document.querySelector('.from');
const toDate = document.querySelector('.to');

// input들의 value값을 newTodo에 저장
if (!content.value) return;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 저도 리뷰를 받아서 알게 된 부분인데, input 입력시 스페이스바를 눌렀을때도 리스트에 추가되는데, trim()을 통해 공백을 제거하시면 빈 스페이스바 todo 를 방지할수 있다고 합니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 그렇네요!! 고려하지 못한 디테일이었는데 덕분에 알게되었습니다 감사합니다~~

newTodo.idx = nextIdx++;
newTodo.content = content.value;
priorities.forEach((priority) => {
if (priority.checked) {
newTodo.priority = Number(priority.value); // radio box에서 체크된 priority를 선택
return;
}
});
// 에러가 발생하지 않도록 삼항연산자로 분기
newTodo.fromDate = fromDate.value ? new Date(fromDate.value) : new Date();
newTodo.toDate = toDate.value ? new Date(toDate.value) : new Date();
Comment on lines +182 to +183

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삼항 연산자를 통해 날짜를 등록하지 않았을 떄의 예외처리 까지 하신 것이 인상깊네요!

newTodo.isDone = false;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isDone으로 todo랑 done 구분하는 방법도 직관적이고 좋은 것 같네요~!


pushTodo(newTodo);
updateView();
localStorage.setItem('todos', JSON.stringify(TODOS));

// input value들을 초기화
content.value = null;
priorities[0].checked = true;
fromDate.value = null;
toDate.value = null;
}

// 종료일이 시작일보다 빠를 수 없도록한 change event handler
function handleChangeFromDate(e) {
if (e.currentTarget.value) {
let utc = new Date(e.currentTarget.value).getTime();
let timeOff = new Date().getTimezoneOffset() * 60000;
let today = new Date(utc - timeOff).toISOString().split('T')[0];
document.querySelector('.to').setAttribute('min', today);
document.querySelector('.to').setAttribute('value', e.currentTarget.value);
}
}
Loading