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 구현 #2

Open
wants to merge 15 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
36 changes: 17 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
# 42gg 프론트엔드 온보딩 1단계
# 42gg 프론트엔드 온보딩 1단계

## 공통 조건
## 📝 ToDo List 만들기

- 온보딩 프로젝트는 개인 계정으로 fork하여 진행하고 PR로 제출합니다.
- git / github / code 컨벤션은 42gg notion에 있는 자료를 적극 반영합니다.
- 기본 기능 외 추가 기능, 디자인 구현은 자유입니다.
- 최종 제출품에는 README 작성이 되있어야 합니다.([예시](https://github.com/42organization/42gg.client/blob/main/README.md))
[✨ 배포 페이지 ✨](https://verysimpletodolist.netlify.app/)

## todo list 만들기
## ✨ 주요 기능 소개

- (필수) Javascript, HTML, CSS
- (필수) todo 생성(Create), 조회(Read) 기능 구현하기 (새로고침 고려 X)
- (선택) todo 수정(Update), 삭제(Delete) 기능 구현하기 (새로고침 고려 X)
- (선택) 디자인 적용하기
- (선택) 무료로 배포하기
### ✏️ 할 일을 추가할 수 있습니다

## 참고
<img width="40%" alt="add_todo_1" src="https://user-images.githubusercontent.com/57761286/228439216-82f423e9-c84a-4361-8e28-608522ca0e92.png">&nbsp;&nbsp;&nbsp;&nbsp;<img width="40%" alt="add_todo_2" src="https://user-images.githubusercontent.com/57761286/228439232-5b2b5ed7-3e09-4368-af9d-e469ac3481e0.png">

- 데이터 관리는 하단의 방식 중 하나 선택하시면 됩니다.
- localstorage
- local server(예. [https://github.com/shal0mdave/todo-api.git](https://github.com/shal0mdave/todo-api.git), lowdb)
- mock api(예. [https://dummyjson.com/](https://dummyjson.com/))
- todo list를 구현하기 위해 필요한 기능들을 미리 생각(그려보고)해보고, 구현해보세요.
- 궁금한 사항은 issue에 등록해주세요.
### ✏️ 할 일을 삭제할 수 있습니다

<img width="40%" alt="SimpleTodo_ScreenShoot_7" src="https://user-images.githubusercontent.com/57761286/228441771-4c83b9ae-6625-4ff3-9693-bca3a2aed86c.png">&nbsp;&nbsp;&nbsp;&nbsp;<img width="40%" alt="SimpleTodo_ScreenShoot_8" src="https://user-images.githubusercontent.com/57761286/228441788-087d1429-a4d8-4d46-8109-b2aefbd483a2.png">

### ✏️ 할 일을 수정할 수 있습니다

<img width="40%" alt="update_todo_1" src="https://user-images.githubusercontent.com/57761286/228439558-fb3ac233-dadf-47fc-9a4f-a7a9e96e999d.png">&nbsp;&nbsp;&nbsp;&nbsp;<img width="40%" alt="update_todo_2" src="https://user-images.githubusercontent.com/57761286/228439549-f6ec3208-9439-4197-8cc8-711c94ec81ef.png">

### ✏️ 완료한 할 일은 클릭하여 완료표시할 수 있습니다

<img width="40%" alt="complete_todo_1" src="https://user-images.githubusercontent.com/57761286/228440361-16b07f81-9e02-4314-8719-ee2dda325fe2.png">&nbsp;&nbsp;&nbsp;&nbsp;<img width="40%" alt="complete_todo_2" src="https://user-images.githubusercontent.com/57761286/228440357-0ae16e3c-5334-4db0-8c4e-c9a913d55f54.png">
26 changes: 26 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lilita+One&family=Nanum+Gothic&display=swap" rel="stylesheet">
Comment on lines +7 to +9
Copy link
Contributor

Choose a reason for hiding this comment

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

폰트 관련해서 link해주셨는데 궁금한게 있어서 질문드립니다!

  1. preconnect 적용하고 안하고의 차이가 궁금합니다.
  2. gstatic에서만 crossorigin을 설정해주셨는데 이유가 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. preconnect를 적용하지 않았을 경우에는 css 파일 로딩을 위한 연결 → css 파일 로드 → 폰트 파일 로딩을 위한 연결 → 폰트 파일 로드 의 과정을 순차적으로 거쳐야 하기 때문에 최종 폰트 파일 로드까지 시간이 걸리지만, preconnect를 적용해서 미리 연결을 위한 초기 설정을 해 두면 최종 폰트 파일 로드까지 걸리는 시간이 preconnect를 적용하지 않았을 때 보다 빨라집니다.
  2. 폰트 파일을 요청하는 css 파일의 도메인과 폰트 파일이 도메인이 다르기 때문에 gstatic에 crossorigin을 설정해주었습니다.

<link rel="stylesheet" href="./styles.css">
<title>Simple ToDo</title>
</head>
<body>
<h1>SIMPLE TODO LIST</h1>
<div id="todo-wrapper">
<form id="todo-form">
<input id="todo-input" autocomplete="off" placeholder="TODO 추가하기" type="text" required>
</form>
<hr />
<ul id="todo-list">
<!-- Dynamically added -->
</ul>
</div>
Comment on lines +15 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

의도하신건지 모르겠다만 고정적인 부분은 미리 선언하셔서 좋은거 같아요 👍

<script src="./todo.js"></script>
</body>
</html>
123 changes: 123 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
* {
font-family: 'Nanum Gothic', sans-serif;
}

html {
height: 100vh;
}
body {
display: flex;
flex-direction: column;
height: 100%;
margin: 0;
background: linear-gradient(45deg, #6190E8, #A7BFE8);
justify-content: center;
align-items: center;
}

h1 {
font-family: 'Lilita One', sans-serif, cursive;
font-size: 3rem;
color: #F5F9FC;
margin: 0 0 10px 0;
}

#todo-wrapper {
display: flex;
flex-direction: column;
width: 500px;
height: 400px;
padding: 30px;
background: #F5F9FC;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
color: #333333;
}

@media (max-height: 500px) {
#todo-wrapper {
height: 300px;
}
}
Comment on lines +37 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

미디어쿼리를 사용하셨는데 해당 스타일이 어떤 목적으로 사용되는지 알려주세요


#todo-wrapper hr {
width: 100%;
height: 2px;
margin: 20px 0;
border: 0;
background-color: #FFD24C;
}

#todo-form {
width: 100%;
}

#todo-form input {
height: 2rem;
width: 100%;
padding: 0 7px;
box-sizing: border-box;
border: 1.5px solid #92B4EC;
border-radius: 10px;
outline: none;
}

#todo-list {
flex: 1;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
list-style: none;
overflow: auto;
}

#todo-list li {
display: flex;
height: fit-content;
padding: 10px;
margin-bottom: 10px;
border: 1.5px solid #6190E8;
border-radius: 10px;
background-color: #FFFFFF;
}

#todo-list li > div {
flex: 1;
padding: 0 10px;
text-align: left;
word-break: break-all;
word-wrap: break-word;
}

#todo-list li.completed {
border: 1.5px solid #FFD24C;
background-color: #F5F9FC;
}

#todo-list li.completed > div {
text-decoration: line-through;
color: #92B4EC;
}

#todo-list li form > input {
height: 2rem;
width: 100%;
padding: 0 7px;
box-sizing: border-box;
border: 1.5px solid #92B4EC;
border-radius: 10px;
outline: none;
}

#todo-list li > button {
border: none;
background: none;
color: #92B4EC;
text-align: right;
}

#todo-list li > button:hover {
color: #6190E8;
cursor: pointer
}
133 changes: 133 additions & 0 deletions todo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
const todoForm = document.getElementById('todo-form');
const todoInput = document.getElementById('todo-input');
const todoList = document.getElementById('todo-list');

// Initialize

const TODO_KEY = 'todoData'
let todoData = JSON.parse(localStorage.getItem(TODO_KEY)) || [];
todoData.map(printTodo);

todoForm.addEventListener('submit', handleTodoSubmit);

// Event Handlers

function handleTodoSubmit(event) {
event.preventDefault(); // 새로고침 방지
const newTodo = {
id: Date.now(), // 현재 시간의 밀리초를 id로 사용
text: todoInput.value,
completed: false,
}
todoInput.value = '';
printTodo(newTodo);
addTodo(newTodo);
}

function handleTodoDelete(event) {
const targetId = parseInt(event.target.parentElement.id);
const li = event.target.parentElement; // 삭제할 li
li.remove();
todoData = todoData.filter((todo) => { return (todo.id !== targetId); });
saveTodo();
}

function handleTodoUpdate(event) {
const li = event.target.parentElement; // 수정할 Todo
const div = li.querySelector('div');
const updateButton = li.querySelector('.update');
updateButton.innerText = '취소';
updateButton.removeEventListener('click', handleTodoUpdate);
updateButton.addEventListener('click', handleTodoUpdateCancel);
const form = document.createElement('form');
form.addEventListener('submit', handleChangedTodoSubmit);
const updateInput = document.createElement('input');
updateInput.type = 'text';
updateInput.value = div.innerText;
div.innerText = '';
div.appendChild(form);
form.appendChild(updateInput);
}

function handleChangedTodoSubmit(event) {
event.preventDefault();
const form = event.target;
const todoId = parseInt(form.parentElement.parentElement.id); // 수정할 Todo의 id
const div = form.parentElement; // Form 부모 div
const updateInput = form.querySelector('input').value; // 수정할 내용
const updateButton = div.parentElement.querySelector('.update');
updateButton.innerText = '수정';
updateButton.removeEventListener('click', handleTodoUpdateCancel);
updateButton.addEventListener('click', handleTodoUpdate);
div.innerText = updateInput;
form.remove();
todoData = todoData.map((todo) => {
if (todo.id === todoId) {
todo.text = updateInput;
}
return todo;
}); // 수정한 내용 반영
saveTodo();
}

function handleTodoUpdateCancel(event) {
const li = event.target.parentElement; // 수정할 Todo
const id = parseInt(li.id);
const div = li.querySelector('div');
const form = div.querySelector('form');
form.remove();
const updateButton = li.querySelector('.update');
updateButton.innerText = '수정';
updateButton.removeEventListener('click', handleTodoUpdateCancel);
updateButton.addEventListener('click', handleTodoUpdate);
const oldTodo = todoData.find((todo) => todo.id === id);
div.innerText = oldTodo.text;
}

function handleTodoClick(event) {
const li = event.target.parentElement;
const id = parseInt(li.id);
let completed = true;
todoData.map((todo) => {
if (todo.id === id) {
todo.completed = todo.completed === true ? false : true;
completed = todo.completed;
}
return todo;
});
li.className = completed ? 'completed' : 'not-completed';
saveTodo();
}

// Functions

function printTodo (newTodo) {
const li = document.createElement('li');
li.id = newTodo.id;
li.className = newTodo.completed === true ? 'completed' : 'not-completed';
const div = document.createElement('div');
div.addEventListener('click', handleTodoClick);
const updateButton = document.createElement('button');
updateButton.addEventListener('click', handleTodoUpdate);
updateButton.className = 'update';
updateButton.innerText = '수정';
const deleteButton = document.createElement('button');
deleteButton.addEventListener('click', handleTodoDelete);
deleteButton.className = 'delete';
deleteButton.innerText = '삭제';
div.innerText = newTodo.text;

li.appendChild(div);
li.appendChild(updateButton);
li.appendChild(deleteButton);
todoList.appendChild(li);
}

function addTodo (newTodo) {
todoData.push(newTodo);
saveTodo();
}

function saveTodo () {
localStorage.setItem(TODO_KEY, JSON.stringify(todoData));
}