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

[Feat] todo list 구현 #13

Open
wants to merge 9 commits into
base: main
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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,38 @@
- (선택) 디자인 적용하기
- (선택) 무료로 배포하기


## 구현 요약
- MVVM 패턴을 적용하여 Model, View, ViewModel 로 분리
- todoList를 localStorage에 저장하여 데이터 관리

## 기능

1. 생성(Create) 및 조회
- '할 일을 입력해주세요.' input에 입력 후 `enter` 또는 `+` 버튼을 눌러 생성

<img width="400" alt="image" src="https://github.com/42organization/42gg-onboarding-fe-01/assets/74870834/b0c71a1c-9470-430a-a92f-3233f2e84c8a">

2. 완료 상태 체크
- 체크 박스를 클릭하여 완료 상태 체크
- 완료된 항목은 완료되지 않은 항목 아래로 이동

<img width="400" alt="image" src="https://github.com/42organization/42gg-onboarding-fe-01/assets/74870834/164d98e5-fd4f-456e-a0bc-411ec7727af3">

3. 수정(Update)
- 리스트의 항목을 더블클릭하여 수정 가능

<div>
<img width="400" alt="image" src="https://github.com/42organization/42gg-onboarding-fe-01/assets/74870834/a69fee65-33bc-4911-a3ae-8b4ec5dcce76">
<img width="400" alt="image" src="https://github.com/42organization/42gg-onboarding-fe-01/assets/74870834/84a36828-e1c7-4c72-9ff2-676b6c5e231a">
</div>

4. 삭제(Delete)
- 각 항목의 오른쪽 `X` 버튼을 클릭하여 삭제

<img width="400" alt="image" src="https://github.com/42organization/42gg-onboarding-fe-01/assets/74870834/1b9c429d-7cce-4e3c-b85e-5aeca776039c">


## 참고

- 데이터 관리는 하단의 방식 중 하나 선택하시면 됩니다.
Expand Down
20 changes: 20 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo List</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="root">
<h1>Todo List</h1>
<form id="todo-form">
<input type="text" id="todo-input" placeholder="할일을 입력해주세요." autofocus >
<button type="submit">+</button>
</form>
<ul class="todo-list"></ul>
</div>
<script type="module" src="./src/App.js"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ViewModel from './ViewModel.js';
import View from './View.js';
import Model from './Model.js';

export default function App() {
this.model = new Model();
this.viewModel = new ViewModel(this.model);
this.view = new View(this.viewModel);
}

const app = new App();
57 changes: 57 additions & 0 deletions src/Model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export default class Model {
constructor() {
this.loadLocalStorage();
}

loadLocalStorage() {
const storage = localStorage.getItem('todoList');

if (storage) {
this.todoList = JSON.parse(storage);
} else {
this.todoList = [];
}
const storageNextId = localStorage.getItem('nextId');
this.nextId = storageNextId ? parseInt(storageNextId) : 0;
}

updateLocalStorage() {
localStorage.setItem('todoList', JSON.stringify(this.todoList));
localStorage.setItem('nextId', this.nextId.toString());
}

add(todoText) {
const item = {
id: this.nextId++,
text: todoText,
completed: false,
};
this.todoList.push(item);
this.updateLocalStorage();
}

edit(id, newText) {
const item = this.todoList.find(item => item.id === id);
if (item) {
item.text = newText;
this.updateLocalStorage();
}
}

delete(id) {
const index = this.todoList.findIndex(item => item.id === id);

if (index !== -1) {
this.todoList.splice(index, 1);
this.updateLocalStorage();
}
}

toggle(id) {
const item = this.todoList.find(item => item.id === id);
if (item) {
item.completed = !item.completed;
this.updateLocalStorage();
}
}
}
133 changes: 133 additions & 0 deletions src/View.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
export default class View {
constructor(viewModel) {
this.app = document.getElementById('root');

this.todoForm = document.getElementById('todo-form');
this.todoInput = document.getElementById('todo-input');
this.todoList = document.querySelector('.todo-list');

this.viewModel = viewModel;
this.viewModel.todoListChanged = this.updateView; // callback

this.addSubmitEventListeners();
this.init();
}

init() {
const todoList = this.viewModel.todoList;
this.updateView(todoList);
}


createTodoItem({ id, text, completed, checked }) {
return`
<li data-id="${id}" class="item ${completed}">
<div class="item-line">
<input class="toggle" type="checkbox" ${checked}>
<label>${text}</label>
<button class="destroy">X</button>
</div>
</li>
`;
}

updateView = (todoList) => {
let checked = '';
let unchecked = '';

todoList.forEach(todo => {
const todoItem = this.createTodoItem({
id: todo.id,
text: todo.text,
completed: todo.completed ? 'completed' : '',
checked: todo.completed ? 'checked' : '',
});

if (todo.completed) {
checked += todoItem;
} else {
unchecked += todoItem;
}
});

this.todoList.innerHTML = unchecked + checked;

this.addEventListeners();
}


addSubmitEventListeners()
{
this.todoForm.addEventListener('submit', (e) => {
e.preventDefault();

const todoText = this.todoInput.value.trim();

if (todoText !== '') {
this.viewModel.addTodo(todoText);
this.todoInput.value = '';
}
});
}

addEventListeners() {
this.addDeleteListeners();
this.addEditListeners();
this.addToggleListeners();
}

addDeleteListeners() {
this.todoList.querySelectorAll('.destroy').forEach(button => {
button.addEventListener('click', (e) => {
const id = parseInt(e.target.closest('li').dataset.id);
this.viewModel.deleteTodo(id);
});
});
}

addEditListeners() {
this.todoList.querySelectorAll('label').forEach(label => {
label.addEventListener('dblclick', (e) => {
this.makeLabelEditable(label);
});
});
}

makeLabelEditable(label) {
const id = parseInt(label.closest('li').dataset.id);
const oldText = label.innerText;

label.contentEditable = true;
label.focus();

const finishEdit = () => {
const newText = label.innerText.trim();
if (newText && newText !== oldText) {
this.viewModel.editTodo(id, newText);
}
label.contentEditable = false;
label.removeEventListener('blur', finishEdit);
};

label.addEventListener('blur', finishEdit);

label.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();

const newText = label.innerText.trim();
if (!newText) label.innerText = oldText;
label.blur();
}
});
}

addToggleListeners() {
this.todoList.querySelectorAll('.toggle').forEach(check => {
check.addEventListener('change', (e) => {
const id = parseInt(e.target.closest('li').dataset.id);
this.viewModel.toggleTodo(id);
});
});
}
}
28 changes: 28 additions & 0 deletions src/ViewModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export default class ViewModel {
constructor(model) {
this.model = model;
this.todoList = this.model.todoList;

this.todoListChanged = null;
}

addTodo = (todoText) => {
this.model.add(todoText);
this.todoListChanged(this.todoList);
}

deleteTodo = (id) => {
this.model.delete(id);
this.todoListChanged(this.todoList);
}

editTodo = (id, newText) => {
this.model.edit(id, newText);
this.todoListChanged(this.todoList);
}

toggleTodo = (id) => {
this.model.toggle(id);
this.todoListChanged(this.todoList);
}
}
90 changes: 90 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
body {
background-color: #f5f5f5;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}

h1 {
color: #333;
text-align: center;
}

#root {
width: 70%;
max-width: 400px;
}

#todo-form {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}

#todo-input {
width: 80%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}

#todo-form button {
width: 10%;
background-color: #448cd8;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 16px;
}

.todo-list {
list-style: none;
padding: 0;
}

.item {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
margin: 10px 0;
}

.item.completed label {
text-decoration: line-through;
}

.item-line {
display: flex;
align-items: center;
width: 100%;
}

.toggle {
margin-right: 10px;
}

label {
flex-grow: 1;
margin: 0 10px;
}

.destroy {
background-color: #fb7676;
border: none;
border-radius: 5px;
color: white;
cursor: pointer;
padding: 5px 10px;
}

.destroy:hover {
background-color: #e04848;
}