Skip to content

[10기 soonysoo] TodoList with CRUD #211

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

Open
wants to merge 11 commits into
base: soonysoo
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}
37 changes: 7 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@

## 🎯 요구사항

- [ ] todo list에 todoItem을 키보드로 입력하여 추가하기
- [ ] todo list의 체크박스를 클릭하여 complete 상태로 변경 (li tag 에 completed class 추가, input 태그에 checked 속성 추가)
- [ ] todo list의 x버튼을 이용해서 해당 엘리먼트를 삭제
- [ ] todo list를 더블클릭했을 때 input 모드로 변경 (li tag 에 editing class 추가) 단 이때 수정을 완료하지 않은 상태에서 esc키를 누르면 수정되지 않은 채로 다시 view 모드로 복귀
- [ ] todo list의 item갯수를 count한 갯수를 리스트의 하단에 보여주기
- [ ] todo list의 상태값을 확인하여, 해야할 일과, 완료한 일을 클릭하면 해당 상태의 아이템만 보여주기
- [x] todo list에 todoItem을 키보드로 입력하여 추가하기
- [x] todo list의 체크박스를 클릭하여 complete 상태로 변경 (li tag 에 completed class 추가, input 태그에 checked 속성 추가)
- [x] todo list의 x버튼을 이용해서 해당 엘리먼트를 삭제
- [x] todo list를 더블클릭했을 때 input 모드로 변경 (li tag 에 editing class 추가) 단 이때 수정을 완료하지 않은 상태에서 esc키를 누르면 수정되지 않은 채로 다시 view 모드로 복귀
- [x] todo list의 item갯수를 count한 갯수를 리스트의 하단에 보여주기
- [x] todo list의 상태값을 확인하여, 해야할 일과, 완료한 일을 클릭하면 해당 상태의 아이템만 보여주기

## 🎯🎯 심화 요구사항
- [ ] localStorage에 데이터를 저장하여, TodoItem의 CRUD를 반영하기. 따라서 새로고침하여도 저장된 데이터를 확인할 수 있어야 함
- [x] localStorage에 데이터를 저장하여, TodoItem의 CRUD를 반영하기. 따라서 새로고침하여도 저장된 데이터를 확인할 수 있어야 함

<br/>

Expand Down Expand Up @@ -109,27 +109,4 @@ live-server 폴더명

<br/>

## 💬 1주차 미션 후기 블로그

아래 링크는 1주차 미션을 진행하면서 블로그를 작성해주신 분들의 글입니다. 미션을 진행하면서, 다른 분들의 문제 해결 과정이 궁금하다면 참고해주세요 😄
- [1주차 미션후기](https://www.notion.so/1-2-8b624729fbce4174b8b583efb10c3200)
- [블랙커피 프론트엔드 스터디 레벨1 후기](https://yujo11.github.io/%EB%B8%94%EB%9E%99%EC%BB%A4%ED%94%BC/%EB%B8%94%EB%9E%99%EC%BB%A4%ED%94%BC-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%8A%A4%ED%84%B0%EB%94%94-%EB%A0%88%EB%B2%A81-%ED%9B%84%EA%B8%B0/)
- [블랙커피 프론트엔드 스터디 회고](https://www.notion.so/bffb14daea984293a954ac7cdb4f7c1e)

<br/>

## 👏🏼 Contributing

만약 미션 수행 중에 개선사항이 필요하다면, 언제든 자유롭게 PR을 보내주세요.

<br/>

## 🐞 Bug Report

버그를 발견한다면, [Issues](https://github.com/next-step/js-todo-list-step1/issues)에 등록해주세요.

<br/>

## 📝 License

This project is [MIT](https://github.com/next-step/js-todo-list-step1/blob/main/LICENSE) licensed.
35 changes: 31 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>이벤트 - TODOS</title>
<title>TODOS</title>
<link rel="stylesheet" href="./src/css/style.css" />
</head>
<body>
<div class="todoapp">
<h1>TODOS</h1>
<!-- <h1>TODOS</h1>
<input
id="new-todo-title"
class="new-todo"
Expand All @@ -17,7 +17,32 @@ <h1>TODOS</h1>
/>
<main>
<input class="toggle-all" type="checkbox" />
<ul id="todo-list" class="todo-list"></ul>
<ul id="todo-list" class="todo-list">
<li>
<div class="view">
<input class="toggle" type="checkbox"/>
<label class="label">새로운 타이틀</label>
<button class="destroy"></button>
</div>
<input class="edit" value="새로운 타이틀" />
</li>
<li class="editing">
<div class="view">
<input class="toggle" type="checkbox" />
<label class="label">완료된 타이틀</label>
<button class="destroy"></button>
</div>
<input class="edit" value="완료된 타이틀" />
</li>
<li class="completed">
<div class="view">
<input class="toggle" type="checkbox" checked/>
<label class="label">완료된 타이틀</label>
<button class="destroy"></button>
</div>
<input class="edit" value="완료된 타이틀" />
</li>
</ul>
<div class="count-container">
<span class="todo-count">총 <strong>0</strong> 개</span>
<ul class="filters">
Expand All @@ -32,7 +57,9 @@ <h1>TODOS</h1>
</li>
</ul>
</div>
</main>
</main> -->
</div>
<script src="src/main.js" type="module"></script>

</body>
</html>
102 changes: 102 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Component from "./core/component.js";
import Filter from "./components/Filter.js"
import Input from "./components/Input.js"
import TodoList from "./components/TodoList.js"
Comment on lines +1 to +4

Choose a reason for hiding this comment

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

컴포넌트의 분리가 좋네요!

import {$, setLocalStorageItem, getLocalStorageItem} from './utils/util.js';



class App extends Component{
setup(){
const todo = getLocalStorageItem("todo")?
getLocalStorageItem("todo") : setLocalStorageItem("todo", {"count": 0,"Filtermode" : 0, "List" : []});
this.$state= todo;
}

template(){
return `
<h1>TODOS</h1>
<input id="new-todo-title" class="new-todo"
placeholder="할일을 추가해주세요"autofocus
/>
<main id="todos">
<input class="toggle-all" type="checkbox" />
<ul id="todo-list" class="todo-list"></ul>
<div id="todo-filter" class="count-container"></div>
</main>
`
}

mounted(){
const {$state ,onAddTodo, onToggleTodo, onDeleteTodo, onUpdateTodo, onFilterTodo} = this;
const _Input = $('#new-todo-title');
const _TodoList = $('#todo-list');
const _Filter = $('#todo-filter');

new Input(_Input, onAddTodo.bind(this));
new TodoList(_TodoList, {
$state,
onToggleTodo: onToggleTodo.bind(this),
onDeleteTodo : onDeleteTodo.bind(this),
onUpdateTodo : onUpdateTodo.bind(this)
});
new Filter(_Filter,{
$state,
onFilterTodo : onFilterTodo.bind(this)
});
}

onAddTodo(content){
const id = String(this.$state.count*1+1);
const Filtermode = this.$state.Filtermode;
const List = [...this.$state.List, {id ,content:content,activate:false}];

Choose a reason for hiding this comment

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

{ id, content: content, activate: false} -> { id, content, activate: false } 다음과 같이 content 속성을 단축 할수 있을 것 같아요. (property shorthand)

setLocalStorageItem("todo",{List,count : id*1,Filtermode});
this.setState(getLocalStorageItem("todo"));
}
onToggleTodo(id){
const List = [];
this.$state.List.map(todo => {
if(todo.id==id){
List.push({id:todo.id, content:todo.content, activate:!todo.activate})
}else{
List.push({id:todo.id,content:todo.content, activate:todo.activate})
}
Comment on lines +59 to +63

Choose a reason for hiding this comment

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

map을 사용하시면 결과값은 array입니다. 이 경우에는 forEach를 사용하여 리스트에 추가하거나

const List = this.$state.List.map(todo => {
  if(todo.id==id){
    return {id:todo.id, content:todo.content, activate:!todo.activate}
  }        
  return {id:todo.id,content:todo.content, activate:todo.activate}
})

이런식으로 작성해도 될 것 같아요! 그리고 if else보다는 if만 사용하면 가독성 측면에서 좋다고 하더라구요

});
const count = String(this.$state.count*1+1);
const Filtermode = this.$state.Filtermode;
setLocalStorageItem("todo",{List,count :count*1, Filtermode})
this.setState(getLocalStorageItem("todo"));

}
onDeleteTodo(id){
const List = this.$state.List.filter(todo => todo.id!==id);
const count = String(this.$state.count*1);

Choose a reason for hiding this comment

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

*1 은 형변환하려고 하실 때 말고 다른 용도로 사용한 것인지 궁금해요. 😀

const Filtermode = this.$state.Filtermode;
setLocalStorageItem("todo",{List,count :count*1, Filtermode})
this.setState(getLocalStorageItem("todo"));
}
onUpdateTodo(id, new_content){
const List = [];
this.$state.List.map(todo => {
if(todo.id==id){

Choose a reason for hiding this comment

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

이것도 스타일일 수 있는데요. 보통 if문과 (사이 는 공백 한칸 띄우는 스타일을 요즘 많이 사용하는 분위기 더라고요.
(https://naver.github.io/hackday-conventions-java/#braces-knr-style)

그래서
if(todo.id==id){

if (todo.id == id) {
로 쓰는 방법도 있답니다.

List.push({id:todo.id, content:new_content, activate:todo.activate})
}else{
List.push({id:todo.id,content:todo.content, activate:todo.activate})
}
});
const count = String(this.$state.count*1);
const Filtermode = this.$state.Filtermode;
setLocalStorageItem("todo",{List,count :count*1, Filtermode})
this.setState(getLocalStorageItem("todo"));
}
onFilterTodo(mode){
const List = this.$state.List;
const count = String(this.$state.count*1);
const Filtermode = mode;
setLocalStorageItem("todo",{List,count :count*1, Filtermode})
this.setState(getLocalStorageItem("todo"));
}
}


export default App;
45 changes: 45 additions & 0 deletions src/components/Filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Component from "../core/component.js";
import {$$} from '../utils/util.js';

class Filter extends Component{
setup(){
this.$state = this.$props.$state;
}

template(){

Choose a reason for hiding this comment

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

혹시, 전체를 템플릿으로 한 부분이 궁금해요. ^^
저렇게 하면, 데이터를 변하는 부분과 고정된 부분을 구별해서 가져오는 것이 쉽지 않을 것 같아서요.
제가 FE를 사용을 잘 몰라서 하는 이야기일 수도 있어요. 그냥 참고만 하셔도 됩니다.

const mode = this.$state.Filtermode;
const totalNum = ((mode)=>{
if(mode==0){
return this.$state.List.length;
}else if(mode==1){
return this.$state.List.filter(item => item.activate!=true).length;
}else{
return this.$state.List.filter(item => item.activate!=false).length;
}
})(mode)
return `
<span class="todo-count">총 <strong>${totalNum}</strong> 개</span>
<ul class="filters">
<li>
<a class="all ${mode==0?"selected":""}" id="0" href="#">전체보기</a>

Choose a reason for hiding this comment

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

  1. 이런 코드는 인라인보다는 따로 분리해서 작성하는게 보기 더 좋을 것 같아요!
const type = (mode) => {
  return mode === 0 ? "selected" : ""
}

이런식으로 뺀 다음에 해당 코드에서는 ${type(mode)} 이런식으로 작성해도 좋지 않을까요?

  1. == 동등연산자보다는 === 일치연산자를 사용하시는게 오류 발생시 유지보수 측면에서 좋을 것 같습니다.

</li>
<li>
<a class="active ${mode==1?"selected":""}" id="1" href="#active">해야할 일</a>
</li>
<li>
<a class="completed ${mode==2?"selected":""}" id="2" href="#completed">완료한 일</a>
</li>
</ul>
`
}
mounted(){
const filterBtn = $$('.filters > li > a');
filterBtn.forEach(element =>{
element.addEventListener('click',(e)=>{
this.$props.onFilterTodo(e.target.id);
})
});
}
}

export default Filter;
15 changes: 15 additions & 0 deletions src/components/Input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Component from "../core/component.js";

class Input extends Component{
setEvent(){
const onAddTodo = this.$props;
this.$target.addEventListener('keyup',(e)=>{
if(e.key=="Enter"){
const content = this.$target.value;
onAddTodo(content);
}
});
}
}

export default Input;
64 changes: 64 additions & 0 deletions src/components/TodoList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Component from "../core/component.js";
import {$$} from "../utils/util.js";

class TodoList extends Component{
setup(){
this.$state = this.$props.$state;
}

template(){
const todoList = this.$state;
return `
${todoList.List.map(item =>`

Choose a reason for hiding this comment

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

{ id, content, activate } 구조분해 할당을 이용해도 좋을 것 같아요!

<li data-id="${item.id}" class=${item.activate?"completed":"notcompleted"}>
<div class="view">
<input class="toggle" id=${item.id} type="checkbox" ${item.activate?"checked":""}/>
<label id=${item.id} class="label">${item.content}</label>
<button id=${item.id} class="destroy"></button>
</div>
<input id=${item.id} class="edit" value="${item.content}" />
</li>
`).join('')}
`
Comment on lines +12 to +22

Choose a reason for hiding this comment

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

분해할당을 적극적으로 사용해보세요!!

todoList.List.map(({ id, activate, content }) => {
  #code
})

이런식으로 작성하면 굳이 item. 을 안붙여도 되기 때문에 가독성 측면에서 좋을 것 같아요

}
mounted(){
const deleteBtns = $$('.destroy');
deleteBtns.forEach(element => {
element.addEventListener('click',(e)=>{
this.$props.onDeleteTodo(e.target.id);
})
});

const toggleBtn = $$('.toggle');
toggleBtn.forEach(element => {
element.addEventListener('click',(e)=>{
this.$props.onToggleTodo(e.target.id);
})
});

const editBtn = $$('.label');
editBtn.forEach(element =>{
element.addEventListener('dblclick', (e)=>{
this.editTodo(e.target);
})
});
}
editTodo(targetDom){
const edit_li = targetDom.parentNode.parentNode;
const edit_input = targetDom.parentNode.nextElementSibling;
Comment on lines +47 to +48

Choose a reason for hiding this comment

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

혹시 변수명을 스네이크 케이스로 작성하신 이유가 있으신가요?.?

edit_li.classList.add('editing');

if(edit_li.classList.contains("editing")){
edit_li.addEventListener('keyup',(e)=>{
if(e.key=="Enter"){
this.$props.onUpdateTodo(targetDom.id, edit_input.value);
}
if(e.key=="Escape"){
edit_li.classList.remove('editing');
}
})
}
}
}

export default TodoList;
24 changes: 24 additions & 0 deletions src/core/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default class Component {

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.

이렇게 하는 것이 모던한 JS 스타일인 것 같아요. 제가 이전에 본 장후님의 코드도 데이터 관리 부분, 그리는 부분, 컨트롤 하는 부분이 나누어 진 것을 봤어요. 마찬가지로 수은님도 그런 render(그리는 부분), setState(데이터 관리) 부분을 확인할 수 있어서 좋았습니다. 저도 앞으로 이렇게 해야 겠습니다. 😀

$target;
$props;
$state;
constructor ($target, $props) {
this.$target = $target;
this.$props = $props;
this.setup();
this.setEvent();
this.render();
}
setup () {};
template () { return ''; }
render () {
this.$target.innerHTML = this.template();
this.mounted();
}
setEvent () {}
mounted(){}
setState (List) {
this.$state = List;
this.render();
}
}
3 changes: 3 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import App from './App.js';

new App(document.querySelector('.todoapp'));

Choose a reason for hiding this comment

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

id에서 class에서 바꾼 의도가 있을 것 같은데요. 궁금해요.

9 changes: 9 additions & 0 deletions src/utils/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => document.querySelectorAll(selector);
const setLocalStorageItem = (key, value) => localStorage.setItem(key, JSON.stringify(value));
const getLocalStorageItem = (key) => {
const todoList = localStorage.getItem(key);
return JSON.parse(todoList);
};

export {$, $$, setLocalStorageItem, getLocalStorageItem};