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 @@ ์ด๋ฒคํŠธ - TODOS +
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); + } + }; +}