Skip to content

[9기 bomeeyoon] TodoList CRUD #208

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 18 commits into
base: bomeeyoon
Choose a base branch
from
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
</p>

# ☕️ 코드리뷰 모임 - Black Coffee

<br/>

> '훌륭한 의사소통은 블랙커피처럼 자극적이며, 후에 잠들기가 어렵다'. <br> A.M. 린드버그(미국의 작가, 수필가) -

<br/>

블랙커피처럼 서로를 자극해주고, 동기부여 해주며, 그 성장과정으로 인해 의미있는 가치를 만들어내고자 하는
블랙커피처럼 서로를 자극해주고, 동기부여 해주며, 그 성장과정으로 인해 의미있는 가치를 만들어내고자 하는
**프론트엔드 코드리뷰 모임** ☕️ **Black Coffee**입니다.

<br/>
Expand All @@ -39,25 +40,28 @@

## 🎯 요구사항

- [ ] 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를 반영하기. 따라서 새로고침하여도 저장된 데이터를 확인할 수 있어야 함

<br/>

## 🔔 참고사항

`TodoItem`을 추가할 시 아래 템플릿을 활용하면 됩니다.

```html
<ul id="todo-list" class="todo-list">
<li>
<div class="view">
<input class="toggle" type="checkbox"/>
<input class="toggle" type="checkbox" />
<label class="label">새로운 타이틀</label>
<button class="destroy"></button>
</div>
Expand All @@ -73,7 +77,7 @@
</li>
<li class="completed">
<div class="view">
<input class="toggle" type="checkbox" checked/>
<input class="toggle" type="checkbox" checked />
<label class="label">완료된 타이틀</label>
<button class="destroy"></button>
</div>
Expand Down Expand Up @@ -103,7 +107,9 @@ live-server 폴더명
<br/>

## 💻 Code Review
아래 링크들에 있는 리뷰 가이드를 보고, 좋은 코드 리뷰 문화를 만들어 나가려고 합니다.

아래 링크들에 있는 리뷰 가이드를 보고, 좋은 코드 리뷰 문화를 만들어 나가려고 합니다.

- [코드리뷰 가이드1](https://edykim.com/ko/post/code-review-guide/)
- [코드리뷰 가이드2](https://wiki.lucashan.space/code-review/01.intro.html#_1-code%EB%A5%BC-%EB%A6%AC%EB%B7%B0%ED%95%98%EB%8A%94-%EC%82%AC%EB%9E%8C%EB%93%A4%EC%9D%80-%EC%96%B4%EB%96%A4%EA%B2%83%EC%9D%84-%EC%A4%91%EC%A0%90%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%82%B4%ED%8E%B4%EC%95%BC%ED%95%98%EB%8A%94%EA%B0%80)

Expand All @@ -112,6 +118,7 @@ live-server 폴더명
## 💬 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)
Expand Down
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ <h1>TODOS</h1>
<input class="toggle-all" type="checkbox" />
<ul id="todo-list" class="todo-list"></ul>
<div class="count-container">
<span class="todo-count">총 <strong>0</strong> 개</span>
<span class="todo-count">총 <strong class="todo-counter">0</strong> 개</span>
<ul class="filters">
<li>
<a class="all selected" href="#">전체보기</a>
<a class="all selected" href="#all">전체보기</a>
</li>
<li>
<a class="active" href="#active">해야할 일</a>
Expand All @@ -34,5 +34,6 @@ <h1>TODOS</h1>
</div>
</main>
</div>
<script type="module" src="./index.js"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import app from './src/js/App.js';

document.addEventListener('DOMContentLoaded', app);
102 changes: 102 additions & 0 deletions src/js/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import TodoInput from "./components/TodoInput.js";
import TodoList from './components/TodoList.js';
import TodoCount from './components/TodoCount.js';
import FilterTodo from './components/FilterTodo.js';
import { FILTER } from "./CONST.js";

class NewTodoItem {
constructor(text) {
return {
completed: false,
text,
}
}
}

class App {
constructor(TodoList, TodoCount) {
this.TodoList = TodoList;
this.TodoCount = TodoCount;

this.items = [];
this.filteredItems = [];

this.init();
}

init() {
this.setState([]);
this.setEvent();
}

get count() {
return this.items.length
}

get filteredCount() {
return this.filteredItems.length;
}

setFilteredState(updatedItems) {
this.filteredItems = updatedItems;
this.TodoList.setState(this.filteredItems);
this.TodoCount.setState(this.filteredCount);
}

setState(updatedItems, isFiltered) {
const mapItems = updatedItems.map((item, id) => ({ ...item, id }));
if (isFiltered) {
this.setFilteredState(mapItems);
return;
}

this.items = mapItems;
this.TodoList.setState(this.items);
this.TodoCount.setState(this.count);
}

setEvent() {
this.addTodo();
this.updateTodo();
this.filterTodo();
}

addTodo() {
new TodoInput({
add: text => {
const newItem = new NewTodoItem(text);
this.items.push(newItem);
this.setState(this.items);
}
})
}

updateTodo() {
this.TodoList.setEvent({
update: (id, { key, value }) => {
this.items[id][key] = value;
this.setState(this.items);
},
delete: (id) => {
this.items.splice(id, 1);
this.setState(this.items);
}
})
}

filter = {
[FILTER.ACTIVE]: () => this.setState(this.items.filter(item => !item.completed), true),
[FILTER.COMPLETED]: () => this.setState(this.items.filter(item => item.completed), true),
[FILTER.ALL]: () => this.setState(this.items, false),
}

filterTodo() {
new FilterTodo({
filterBy: (type) => this.filter[type] && this.filter[type]()
})
}
};

export default function() {
return new App(new TodoList(), new TodoCount());
};
39 changes: 39 additions & 0 deletions src/js/CONST.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export const EVENT = {
HASH_CHANGE: 'hashchange',
KEY_UP: 'keyup',
CLICK: 'click',
CHANGE: 'change',
DBL_CLICK: 'dblclick',
}

export const KEY_UP_EVENT = {
ENTER: 'Enter',
ESCAPE: 'Escape',
}

export const CUSTOM_EVENT = {
DELETE: 'delete',
UPDATE_TEXT: 'update:text',
UPDATE_COMPLETED: 'update:complated',
ADD: 'add',
}

export const CLASS_NAME = {
TODO_APP: 'todo-app',
NEW_TODO: 'new-todo',
TODO_LIST: 'todo-list',
TODO_ITEM: 'todo-item',
TODO_COUNTER: 'todo-counter',
EDITING: 'editing',
}

export const FILTER = {
ALL: 'all',
ACTIVE: 'active',
COMPLETED: 'completed',
}

export const ITEM_KEY = {
COMPLETED: 'completed',
TEXT: 'text',
}
14 changes: 14 additions & 0 deletions src/js/components/FilterTodo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EVENT } from "../CONST.js";

export default class FilterTodo {
constructor({ filterBy }) {
this.setEvent(filterBy);
}

setEvent(filterBy) {
window.addEventListener(EVENT.HASH_CHANGE, ({ newURL }) => {
const type = newURL.split('#')[1];
type && filterBy(type);
})
}
}
18 changes: 18 additions & 0 deletions src/js/components/TodoCount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { CLASS_NAME } from "../CONST.js";
import { $ } from "../utils/element.js";

export default class TodoCount {
constructor() {
this.$todoCount = $(CLASS_NAME.TODO_COUNTER);
this.count = 0;
}

setState(updatedCount) {
this.count = updatedCount;
this.render();
}

render() {
this.$todoCount.textContent = this.count;
}
}
42 changes: 42 additions & 0 deletions src/js/components/TodoInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { CLASS_NAME, CUSTOM_EVENT, EVENT, KEY_UP_EVENT } from "../CONST.js";
import { $ } from "../utils/element.js";

export default class TodoInput {
constructor({add}) {
this._add = add;

this.$todoInput = $(CLASS_NAME.NEW_TODO);
this.setEvent();
}

isValid(value) {
if (!value) {
alert('할일을 입력하세요.')
}
return !!value;
}

reset() {
this.$todoInput.value = '';
}

keyUpEvent = {
[KEY_UP_EVENT.ENTER]: ({ value }) => {
if (this.isValid(value)) {
this._add(value);
}
this.reset()
},
[KEY_UP_EVENT.ESCAPE]: () => {
this.reset();
}
}

onKeyUp({ key, target }) {
this.keyUpEvent[key] && this.keyUpEvent[key](target);
}

setEvent() {
this.$todoInput.addEventListener(EVENT.KEY_UP, this.onKeyUp.bind(this))
}
}
59 changes: 59 additions & 0 deletions src/js/components/TodoList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { CLASS_NAME, CUSTOM_EVENT, EVENT, ITEM_KEY, KEY_UP_EVENT } from "../CONST.js";
import { $, addClass, closest, removeClass } from "../utils/element.js";
import TodoItemTemplate from "../views/TodoItemTemplate.js";

export default class TodoList {
constructor() {
this.$todoList = $(CLASS_NAME.TODO_LIST);
this.items = [];
}

setState(updatedItems) {
this.items = updatedItems;
this.render();
}

stopEditing(target) {
removeClass(closest(CLASS_NAME.TODO_ITEM, target), CLASS_NAME.EDITING)
}

startEditing(target) {
addClass(closest(CLASS_NAME.TODO_ITEM, target), CLASS_NAME.EDITING);
}

event = {
[KEY_UP_EVENT.ENTER]: ({ id, value }, update) => update(Number(id), { key: ITEM_KEY.TEXT, value }),
[KEY_UP_EVENT.ESCAPE]: target => this.stopEditing(target),
[CUSTOM_EVENT.UPDATE_COMPLETED]: ({ id, checked }, update) => update(Number(id), { key: ITEM_KEY.COMPLETED, value: checked }),
[CUSTOM_EVENT.UPDATE_TEXT]: ({ id, value }, update) => update(Number(id), { key: ITEM_KEY.TEXT, value })
}

onClick(target, deleteItem) {
const {event} = target.dataset;
event === CUSTOM_EVENT.DELETE && deleteItem(Number(target.id));
}

onDblclick(target) {
this.startEditing(target)
}

onChange(target, update) {
const { event } = target.dataset;
this.event[event] && this.event[event](target, update);
}

onKeyUp(key, target, update) {
this.event[key] && this.event[key](target, update);
}

setEvent({ update, delete: deleteItem }) {
this.$todoList.addEventListener(EVENT.CLICK, ({ target }) => this.onClick(target, deleteItem));
this.$todoList.addEventListener(EVENT.CHANGE, ({ target }) => this.onChange(target, update));
this.$todoList.addEventListener(EVENT.DBL_CLICK, ({ target }) => this.onDblclick(target));
this.$todoList.addEventListener(EVENT.KEY_UP, ({ key, target }) => this.onKeyUp(key, target, update));
}

render() {
this.$todoList.innerHTML = this.items.map(TodoItemTemplate).join('');
}
}
7 changes: 7 additions & 0 deletions src/js/utils/element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const $ = (className) => document.querySelector(`.${className}`);

export const addClass = (target, className) => target.classList.add(className);

export const removeClass = (target, className) => target.classList.remove(className);

export const closest = (target, className) => target.closest(`.${className}`);
Loading