diff --git a/README.md b/README.md
index 502436de..0e28256a 100644
--- a/README.md
+++ b/README.md
@@ -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๋ฅผ ๋ฐ์ํ๊ธฐ. ๋ฐ๋ผ์ ์๋ก๊ณ ์นจํ์ฌ๋ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ํ์ธํ ์ ์์ด์ผ ํจ
diff --git a/index.html b/index.html
index 13a02fdb..18d411b0 100644
--- a/index.html
+++ b/index.html
@@ -5,6 +5,7 @@
diff --git a/src/app.js b/src/app.js
new file mode 100644
index 00000000..15afe7b6
--- /dev/null
+++ b/src/app.js
@@ -0,0 +1,11 @@
+import TodoApp from './components/TodoApp.js';
+import LocalStorage from './components/LocalStorage.js';
+
+const LS_KEY = 'TODOS';
+window.addEventListener('DOMContentLoaded', () => {
+ const storage = new LocalStorage(LS_KEY);
+ const savedData = storage.getItems();
+
+ const todoApp = new TodoApp(storage);
+ todoApp.init(savedData);
+});
diff --git a/src/components/LocalStorage.js b/src/components/LocalStorage.js
new file mode 100644
index 00000000..1732f9d9
--- /dev/null
+++ b/src/components/LocalStorage.js
@@ -0,0 +1,13 @@
+export default class LocalStorage {
+ constructor(key) {
+ this.key = key;
+ }
+ saveItems = (todoItems) => {
+ localStorage.setItem(this.key, JSON.stringify(todoItems));
+ };
+
+ getItems = () => {
+ const parsedData = localStorage.getItem(this.key);
+ return parsedData && JSON.parse(parsedData);
+ };
+}
diff --git a/src/components/TodoApp.js b/src/components/TodoApp.js
new file mode 100644
index 00000000..205d7670
--- /dev/null
+++ b/src/components/TodoApp.js
@@ -0,0 +1,111 @@
+import TodoItem from './TodoItem.js';
+import TodoInput from './TodoInput.js';
+import TodoList from './TodoList.js';
+import TodoCount from './TodoCount.js';
+import { TodoFilter, FilterType } from './TodoFilter.js';
+
+export default function TodoApp(storage) {
+ this.storage = storage;
+ this.todoItems = [];
+ let id = 0;
+
+ this.init = (savedData) => {
+ this.todoItems = savedData ?? [];
+ id = savedData.length ?? 0;
+
+ savedData && this.setState(savedData);
+ };
+
+ this.setState = (updatedItems) => {
+ todoList.setState(updatedItems);
+ todoCount.setState(updatedItems);
+ };
+
+ const onAdd = (contents) => {
+ const newTodoItem = new TodoItem(contents, ++id);
+ this.todoItems.push(newTodoItem);
+ this.setState(this.todoItems);
+ this.storage.saveItems(this.todoItems);
+ };
+
+ const onComplete = (id) => {
+ this.todoItems = this.todoItems.map((item) => {
+ if (item.id === id) {
+ item.completed = !item.completed;
+ }
+ return item;
+ });
+ this.setState(this.todoItems);
+ this.storage.saveItems(this.todoItems);
+ };
+
+ const onDelete = (id) => {
+ this.todoItems = this.todoItems.filter((item) => {
+ return item.id !== id;
+ });
+ this.setState(this.todoItems);
+ this.storage.saveItems(this.todoItems);
+ };
+
+ const onEdit = (id) => {
+ this.todoItems = this.todoItems.map((item) => {
+ if (item.id === id) {
+ item.editing = !item.editing;
+ }
+ return item;
+ });
+ this.setState(this.todoItems);
+ this.storage.saveItems(this.todoItems);
+ };
+
+ const onUpdate = (e, id) => {
+ if (e.key === 'Enter') {
+ this.todoItems = this.todoItems.map((item) => {
+ if (item.id === id) {
+ item.contents = e.target.value;
+ item.editing = false;
+ }
+ return item;
+ });
+ this.setState(this.todoItems);
+ this.storage.saveItems(this.todoItems);
+ }
+ if (e.key === 'Escape') {
+ this.todoItems = this.todoItems.map((item) => {
+ if (item.id === id) {
+ item.editing = false;
+ }
+ return item;
+ });
+ this.setState(this.todoItems);
+ this.storage.saveItems(this.todoItems);
+ }
+ };
+
+ const onFilter = (type) => {
+ if (type === FilterType.all) {
+ this.setState(this.todoItems);
+ } else if (type === FilterType.active) {
+ const activeItems = this.todoItems.filter(
+ (item) => item.completed === false
+ );
+ this.setState(activeItems);
+ } else if (type === FilterType.completed) {
+ const completedItems = this.todoItems.filter(
+ (item) => item.completed === true
+ );
+ this.setState(completedItems);
+ }
+ };
+
+ const todoInput = new TodoInput();
+ todoInput.setEventListener(onAdd);
+
+ const todoList = new TodoList();
+ todoList.setEventListener(onComplete, onDelete, onEdit, onUpdate);
+
+ const todoCount = new TodoCount();
+
+ const todoFilter = new TodoFilter();
+ todoFilter.setEventListener(onFilter);
+}
diff --git a/src/components/TodoCount.js b/src/components/TodoCount.js
new file mode 100644
index 00000000..b836146a
--- /dev/null
+++ b/src/components/TodoCount.js
@@ -0,0 +1,7 @@
+export default function TodoCount() {
+ this.todoCount = document.querySelector('.todo-count strong');
+
+ this.setState = (todoItems) => {
+ this.todoCount.textContent = todoItems.length;
+ };
+}
diff --git a/src/components/TodoFilter.js b/src/components/TodoFilter.js
new file mode 100644
index 00000000..1be34509
--- /dev/null
+++ b/src/components/TodoFilter.js
@@ -0,0 +1,37 @@
+export const FilterType = Object.freeze({
+ all: 'all',
+ active: 'active',
+ completed: 'completed',
+});
+
+export function TodoFilter() {
+ this.filters = document.querySelector('.filters');
+ this.filtersBtn = this.filters.querySelectorAll('a');
+
+ this.filters.addEventListener('click', (event) => this.handleClick(event));
+
+ this.setEventListener = (onFilter) => {
+ this.onFilter = onFilter;
+ };
+
+ this.handleClick = (event) => {
+ const type = event.target.className;
+
+ this.removeSelectedClass();
+ this.addSelectedClass(event.target);
+
+ this.onFilter && this.onFilter(type);
+ };
+
+ this.removeSelectedClass = () => {
+ this.filtersBtn.forEach((item) => {
+ if (item.classList.contains('selected')) {
+ item.classList.remove('selected');
+ }
+ });
+ };
+
+ this.addSelectedClass = (target) => {
+ target.classList.add('selected');
+ };
+}
diff --git a/src/components/TodoInput.js b/src/components/TodoInput.js
new file mode 100644
index 00000000..502f5d81
--- /dev/null
+++ b/src/components/TodoInput.js
@@ -0,0 +1,18 @@
+// ์
๋ ฅ๋ฐ๋ ์ปดํฌ๋ํธ
+export default function TodoInput() {
+ const todoInput = document.querySelector('#new-todo-title');
+
+ todoInput.addEventListener('keydown', (event) => this.handleOnAdd(event));
+
+ this.setEventListener = (onAdd) => {
+ this.onAdd = onAdd;
+ };
+
+ this.handleOnAdd = (event) => {
+ if (event.key === 'Enter') {
+ const newTodoTarget = event.target;
+ this.onAdd && this.onAdd(newTodoTarget.value);
+ newTodoTarget.value = '';
+ }
+ };
+}
diff --git a/src/components/TodoItem.js b/src/components/TodoItem.js
new file mode 100644
index 00000000..d462363d
--- /dev/null
+++ b/src/components/TodoItem.js
@@ -0,0 +1,8 @@
+export default class TodoItem {
+ constructor(contents, id) {
+ this.id = id;
+ this.contents = contents;
+ this.completed = false;
+ this.editing = false;
+ }
+}
diff --git a/src/components/TodoList.js b/src/components/TodoList.js
new file mode 100644
index 00000000..cbbc883e
--- /dev/null
+++ b/src/components/TodoList.js
@@ -0,0 +1,60 @@
+// todoList ๋ณด์ฌ์ฃผ๋ ์ปดํฌ๋ํธ
+export default function TodoList() {
+ const todoList = document.querySelector('#todo-list');
+
+ todoList.addEventListener('click', (event) => this.handleClick(event));
+ todoList.addEventListener('dblclick', (event) => this.handleDblClick(event));
+ todoList.addEventListener('keydown', (event) => this.handleKeydown(event));
+
+ this.setEventListener = (onComplete, onDelete, onEdit, onUpdate) => {
+ this.onComplete = onComplete;
+ this.onDelete = onDelete;
+ this.onEdit = onEdit;
+ this.onUpdate = onUpdate;
+ };
+
+ this.setState = (updatedTodoItems) => {
+ this.todoItems = updatedTodoItems;
+ this.render(this.todoItems);
+ };
+
+ this.render = (items) => {
+ const template = items.map((item) => {
+ return `
+
+
+
+
+
+
+
+
+ `;
+ });
+ todoList.innerHTML = template.join('');
+ };
+
+ this.handleKeydown = (event) => {
+ const id = parseInt(event.target.parentNode.dataset.id);
+ this.onUpdate && this.onUpdate(event, id);
+ };
+
+ this.handleDblClick = (event) => {
+ const id = parseInt(event.target.parentNode.parentNode.dataset.id);
+ this.onEdit && this.onEdit(id);
+ };
+
+ this.handleClick = (event) => {
+ const id = parseInt(event.target.parentNode.parentNode.dataset.id);
+ if (event.target.className === 'toggle') {
+ this.onComplete && this.onComplete(id);
+ }
+ if (event.target.className === 'destroy') {
+ this.onDelete && this.onDelete(id);
+ }
+ };
+}