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

[1주차] 조유담 미션 제출합니다 #11

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all",
"semi": true
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# 서론

##

안녕하세요 🙌🏻 19기 프론트엔드 운영진 **변지혜**입니다.

이번 미션은 개발 환경 구축과 스터디 진행 방식에 익숙해지실 수 있도록 간단한 **to-do list** 만들기를 진행합니다. 무작정 첫 스터디부터 React를 다루는 것보다는 왜 React가 필요한지, React가 없으면 무엇이 불편한지 느껴 보고 본격적인 스터디에 들어가는 것이 React를 이해하는 데 더 많은 도움이 될 것이라 생각합니다.
Expand Down
35 changes: 32 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,40 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vanilla Todo</title>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="./style/style.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/all.min.css" />
<meta property="og:title" content="TODO-LIST" />
<meta property="og:url" content="https://vanilla-todo-19th-navy.vercel.app/" />
<meta property="og:type" content="website" />
<meta property="og:description" content="오늘의 일정을 관리해보세요." />
Comment on lines +7 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

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

og 태그 작성해주신 점 좋네요! 미리보기까지 챙겨주신 섬세함이 돋보이는 것 같습니다ㅎㅎ

</head>

<body>
<div class="container"></div>
<header class="flex-center">
<p class="date flex-center" />
<h1><span class="userName"></span>TODO-LIST</h1>
</header>
<main>
<form class="input-form flex-center">
<div class="input-container">
<div class="input-wrapper">
<input class="input" placeholder="할 일 입력" />
<button class="input-button">입력</button>
</div>
<p class="error"></p>
</div>
</form>
<div class="list-container flex-center">
<div class="list-wrapper">
<h2 class="label">Todo <span class="todo-count"> / 0개</span></h2>
<ul class="list todo-list"></ul>
</div>
<div class="list-wrapper">
<h2 class="label">Done <span class="done-count"> / 0개</span></h2>
<ul class="list done-list"></ul>
</div>
</div>
</main>
</body>
<script src="script.js"></script>
<script src="./js/index.js" type="module"></script>
</html>
196 changes: 196 additions & 0 deletions js/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { $, $all } from './util.js';

// 오늘 날짜 문자열 반환
const getTodayDate = () => {
const today = new Date();
const year = today.getFullYear();
const month = (today.getMonth() + 1).toString().padStart(2, '0');
const date = today.getDate().toString().padStart(2, '0');

const dayToString = ['일', '월', '화', '수', '목', '금', '토'];
const day = dayToString[today.getDay()];

return `${year}년 ${month}월 ${date}일 ${day}요일`;
};
Comment on lines +3 to +14
Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 작성해주셨을 때 확실히 세세한 설정을 할 수 있어서 좋지만, 디테일한 부분이 필요하지 않을 때는 JS의 toLocaleDateString 를 써봐도 좋을 것 같아요. 설정을 한국어로 바꿔준 뒤 세부 설정을 long으로 해주면 읽기 편한 방식으로 바꿔주는 편리한 기능이라 참고링크 놓고 갑니다!


// 할 일 아이템 생성
const createItem = (text, isDone = false) => {
const newItem = document.createElement('li');

// draggable 속성 추가
newItem.setAttribute('draggable', true);
newItem.classList.add('draggable');

// 동그라미 아이콘
const circleIcon = document.createElement('i');
circleIcon.classList.add('fa-regular', 'fa-circle', 'cursor-pointer');

if (isDone) {
circleIcon.classList.add('fa-check-circle');
}
// 누르면 done으로 이동하는 이벤트 리스너 등록
circleIcon.addEventListener('click', moveItem);
newItem.append(circleIcon);

// 입력받은 Text
const todoText = document.createTextNode(text);
newItem.append(todoText);

// 쓰레기통 아이콘
const trashIcon = document.createElement('i');
trashIcon.classList.add('fa-solid', 'fa-trash-can', 'cursor-pointer');

// 누르면 삭제
trashIcon.addEventListener('click', deleteItem);
newItem.append(trashIcon);

return newItem;
};

// 할 일 추가 이벤트 핸들러
const addItem = (e) => {
e.preventDefault();

// 빈 값 입력시 에러 처리
const inputValue = $('.input').value.trim();
if (!inputValue) {
$('.error').textContent = '내용을 입력해주세요';
return;
}
$('.error').textContent = '';
Comment on lines +55 to +60
Copy link

Choose a reason for hiding this comment

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

이부분은

Suggested change
const inputValue = $('.input').value.trim();
if (!inputValue) {
$('.error').textContent = '내용을 입력해주세요';
return;
}
$('.error').textContent = '';
const inputValue = document.querySelector('.input');
if (!inputValue.value.trim()) {
alert = ('내용을 입력해주세요');
}

로 하면 더 깔끔할거 같아요!

Copy link

Choose a reason for hiding this comment

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

앗 다시 확인해보니 error을 쓰신 이유가 있었네요! alert 부분은 무시해 주셔도 됩니다! 개인적으로는 inputValue와 같은 변수에는 하나의 클래스만 선택하도록 하는 것이 또 쓰일 경우를 대비해서 더 좋더라구요. if문안에도 직관적인 내용이 들어왔을 때가 유지 보수하기 좋았던 것 같은데 이부분은 개인 취향이 아닐까.. 생각합니다!


// 새로운 list 아이템 만들기
const newItem = createItem(inputValue);
Copy link

Choose a reason for hiding this comment

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

이부분은 입력 버튼을 눌렀을 때로 이벤트를 주면 좀 더 직관적인 코드가 되었을거 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

form 태그에 이벤트를 걸어둔 거라 입력 버튼을 누르거나 엔터를 눌렀을 때 addItem 함수 내부의 createItem이 실행돼요!

Copy link

Choose a reason for hiding this comment

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

아하! 제 코드에도 달아주신 리뷰도 확인했습니다! 많이 배워가요!


// list에 추가
$('.todo-list').append(newItem);

// 인풋 비워주기
$('.input').value = '';

updateListCount();
saveToLocalStorage();
};

// 할 일 이동 이벤트 핸들러
const moveItem = (e) => {
const circleIcon = e.target;

// 부모 요소 찾기 (아이콘의 부모 요소가 li)
const clickedItem = circleIcon.parentNode;

// 이동할 리스트 정하기
const targetList = clickedItem.closest('.todo-list') ? '.done-list' : '.todo-list';

// 동그라미 <-> 체크 동그라미
circleIcon.classList.toggle('fa-circle');
circleIcon.classList.toggle('fa-check-circle');
$(targetList).append(clickedItem);
Comment on lines +86 to +88
Copy link

Choose a reason for hiding this comment

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

저두 todo와 done에 따라 다른 아이콘을 주고 싶어서 css로만 조작을 했었는데, 이렇게 따로 append를 해주니 더 좋은거 같아요! 배워갑니다~


updateListCount();
saveToLocalStorage();
};

// 할 일 삭제 이벤트 핸들러
const deleteItem = (e) => {
// 쓰레기통 아이콘의 부모 요소 찾기
const clickedItem = e.target.closest('li');
clickedItem.remove();

updateListCount();
saveToLocalStorage();
Comment on lines +100 to +101
Copy link
Collaborator

Choose a reason for hiding this comment

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

list count를 업데이트할 때마다 로컬 스토리지에 저장하다보니, updateListCount 함수에 saveToLocalStorage 함수를 넣으면 타 함수 작성시에 둘 중 한 가지 기능을 빼먹을 가능성이 줄어들 것 같아요!

};

// 할 일 리스트 개수 업데이트
const updateListCount = () => {
$('.todo-count').textContent = `/ ${$('.todo-list').childElementCount}개`;
$('.done-count').textContent = `/ ${$('.done-list').childElementCount}개`;
};

// Local Storage에 저장
const saveToLocalStorage = () => {
const todoListItems = Array.from($('.todo-list').children).map((item) => item.textContent);
const doneListItems = Array.from($('.done-list').children).map((item) => item.textContent);

localStorage.setItem('todoList', JSON.stringify(todoListItems));
localStorage.setItem('doneList', JSON.stringify(doneListItems));
};

// Local Storage에서 할 일 목록 불러오기
const loadFromLocalStorage = () => {
const todoList = JSON.parse(localStorage.getItem('todoList')) || [];
todoList.forEach((item) => $('.todo-list').append(createItem(item)));

const doneList = JSON.parse(localStorage.getItem('doneList')) || [];
doneList.forEach((item) => $('.done-list').append(createItem(item, true)));

updateListCount();
};

// 사용자 이름 설정
const showUserName = () => {
Comment on lines +130 to +131
Copy link

Choose a reason for hiding this comment

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

사용자 이름 설정까지!

Copy link
Collaborator

Choose a reason for hiding this comment

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

저도 결과 화면 이용해보면서 이거 보고 재치있고 귀엽다고 생각했어요ㅎㅎ 디테일 굿입니당👍👍

const savedUserName = localStorage.getItem('userName');
if (savedUserName) {
$('.userName').textContent = `${savedUserName}의 `;
return;
}
const userName = prompt('이름을 알려주세요!');
if (userName) {
$('.userName').textContent = `${userName}의 `;
localStorage.setItem('userName', userName);
}
};
Comment on lines +132 to +142
Copy link
Collaborator

Choose a reason for hiding this comment

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

로컬 스토리지에 userName이 있으면 해당 텍스트를 띄우고, 없으면 이름을 받아 그걸 저장한 뒤 띄우는 기능을 하는 함수로 보여요! 디테일이 정말 굿입니다...👍 여기서 else if 문을 따로 쓰지 않으시고 if문을 따로 2개 쓰신 이유가 있는지 궁금해요! 제가 인지하지 못한 다른 이유가 없었다면 else if로 되어있으면 조금 더 가독성이 좋아지지 않을까 조심스럽게 의견 내봅니다 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

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

else if를 사용하는 것보다 early return을 해주는 걸 선호하는 편입니다! 코드 깊이가 얕아지는 장점이 있어서요 ㅎㅎ

무조건 early return을 하는 게 적절하다고 할 수는 없지만, 저는 유효하지 않은 경우를 앞 부분에서 처리해주는 게 더 가독성 있게 여겨지는 거 같아요~~
early return


const addDragDropEvents = () => {
Copy link

Choose a reason for hiding this comment

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

이부분은 스크롤이 안되는 부분 때문에 추가하신 걸까요? 저도 스크롤이 안되는 문제가있어서 고민 중인데 css 문제인지 잘 모르겠어서🥲 혹시 좋은 방안이 있다면 같이 이야기 해봐요!

Copy link
Author

Choose a reason for hiding this comment

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

앗 아니요 ! 드래그앤드랍 기능을 만드려고 추가한 코드였어요 ㅋㅋ 드래그앤드랍이 되긴 하는데, 반쪽짜리 코드입니다 ㅠ 스크롤은 그냥 css에서 처리해주면 됩니다!

const draggableItems = $all('.draggable');

Choose a reason for hiding this comment

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

드래그가 인식은 되는데,
드래그해서 할일을 이동시키는 기능은 아직 구현이 안되신 것 같은데,
저도 드래그 기능을 구현하고 싶었으나 시간 관계로 구현하지 못했는데
이에 대해 이따가 같이 이야기해보면 좋을 것 같아요!

const containers = $all('.list');

draggableItems.forEach((el) => {
el.addEventListener('dragstart', () => {
el.classList.add('dragging');
});

el.addEventListener('dragend', () => {
el.classList.remove('dragging');
});
});

function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.draggable:not(.dragging)')];

return draggableElements.reduce(
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
},
{ offset: Number.NEGATIVE_INFINITY },
).element;
}

containers.forEach((container) => {
container.addEventListener('dragover', (e) => {
e.preventDefault();
const afterElement = getDragAfterElement(container, e.clientY);
const draggable = document.querySelector('.dragging');
container.insertBefore(draggable, afterElement);
});
});
};

// 초기 실행 함수
const init = () => {
showUserName();
$('.date').textContent = getTodayDate();
$('.input-form').addEventListener('submit', addItem);

loadFromLocalStorage();
addDragDropEvents();
};

// 초기 실행
init();
9 changes: 9 additions & 0 deletions js/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function $(selector) {
return document.querySelector(selector);
}

function $all(selector) {
return document.querySelectorAll(selector);
}

export { $, $all };
Comment on lines +1 to +9
Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 자주 쓰는 내용들을 묶어서 깔끔하게 정리해주신 것 좋네요!!👍👍👍

Choose a reason for hiding this comment

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

'document.querySelectorAll(selector)'를 바로 리턴해서
const draggableItems = $all('.draggable');
const containers = $all('.list');
이런식으로 html 요소를 받아서 활용할 수 있게 하셔서 되게 좋은 아이디어 인 것 같다는 생각이 듭니다!

함수 이름에 $를 붙인 이유가 궁금한데 오늘 이따 만나면 여쭤볼게용!!!

1 change: 0 additions & 1 deletion script.js

This file was deleted.

1 change: 0 additions & 1 deletion style.css

This file was deleted.

9 changes: 9 additions & 0 deletions style/common.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}

.cursor-pointer {
cursor: pointer;
}
40 changes: 40 additions & 0 deletions style/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@font-face {
font-family: 'Ownglyph_meetme-Rg';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/[email protected]/Ownglyph_meetme-Rg.woff2')
format('woff2');
}

html {
font-size: 62.5%;
font-family: 'Ownglyph_meetme-Rg';
}

html,
body,
button,
input,
h1,
h2 {
line-height: normal;
font-style: normal;
font-weight: 400;
}

a,
button {
cursor: pointer;
}

#__next {
height: 100%;
}

.root {
display: flex;
flex-direction: column;
height: 100%;
}

.content {
flex: 1;
}
Loading