diff --git a/README.md b/README.md index 74c6f58..a41e73d 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,5 @@ # 1주차 미션: Vanilla-Todo -# 서론 - -안녕하세요 🙌🏻 18기 프론트엔드 운영진 **배성준**입니다. - -이번 미션은 개발 환경 구축과 스터디 진행 방식에 익숙해지실 수 있도록 간단한 **to-do list** 만들기를 진행합니다. 무작정 첫 스터디부터 React를 다루는 것보다는 왜 React가 필요한지, React가 없으면 무엇이 불편한지 느껴 보고 본격적인 스터디에 들어가는 것이 React를 이해하는 데 더 많은 도움이 될 것이라 생각합니다. - -비교적 가벼운 미션인 만큼 코드를 짜는 데 있어 여러분의 **창의성**을 충분히 발휘해 보시기 바랍니다. 작동하기만 하면 되는 것보다 같은 코드를 짜는 여러가지 방식과 패턴에 대해 고민해 보시고, 본인이 생각한 가장 창의적인 방법으로 코드를 작성해 주세요. 여러분이 미션을 수행하는 과정에서 겪는 고민과 생각의 깊이만큼 스터디에서 더 많은 것을 얻어가실 수 있을 것입니다. - -막히는 부분이 있더라도 우선은 스스로 공부하고 찾아보는 방법을 권고드리지만, 운영진의 도움이 필요하시다면 얼마든지 슬랙 Q&A 채널이나 프론트엔드 카톡방에 질문을 남겨 주세요! - -# 미션 ## 미션 목표 @@ -26,33 +15,29 @@ ## Key Questions - DOM은 무엇인가요? + +DOM은 문서 객체 모델의 약어인데, 브라우저가 웹 페이지를 인식하는 방식을 트리 구조로 계층화시켜 프로그래밍 방식으로 조작하게 해주는 API입니다. 트리 구조의 최상단에는 document 객체가 있고, 이 객체를 통하여 DOM 트리의 여러 노드에 접근할 수 있습니다. 이번 과제에서는 Vanilla JS를 통하여 DOM을 조작하여 클릭, 엔터와 같은 이벤트를 처리했습니다. - HTML (tag) Element를 JavaScript로 생성하는 방법은 어떤 것이 있고, 어떤 방법이 가장 적합할까요? + + 이번 과제에 Element를 생성할 때 사용한 DOM API는 .createElement()였는데, innerHTML()과 insertAdjacent() 와 비교했을 때 생성한 요소를 삽입하는 추가 코드가 필요하여 번거로운 부분이 있으나 이번 과제에서 DOM 트리 구조가 그렇게 복잡하지 않아 사용하였습니다. + 하지만 이런 간단한 작업에도 상대적으로 코드가 길어져, 아무래도 일반적인 프로젝트에서는 리액트, 뷰 같은 라이브러리를 사용하는 게 좋을 것 같습니다. + - Semantic tag에는 어떤 것이 있으며, 이를 사용하는 이유는 무엇일까요? + +Semantic tag는 "의미가 있는 태그" 라는 뜻으로 header, nav, main, footer, article, section 등이 있습니다. 이러한 시멘틱 태그를 적절히 사용하면, 키워드를 통한 검색결과에 잘 노출되게 할 수 있고, 페이지 탐색에 도움을 줄 수 있습니다. +개인적으로 느꼈던 시멘틱 태그의 큰 장점은 html의 구조를 설계할 때, 시멘틱 태그를 통해 설계하면 좀 더 코드 가독성이 늘었던 거 같습니다. + + - Flexbox Layout은 무엇이며, 어떻게 사용하나요? -- JavaScript가 다른 언어들에 비해 주목할 만한 점에는 어떤 것들이 있나요? -- 코드에서 주석을 다는 바람직한 방법은 무엇일까요? -## 필수 요건 +Flexbox layout 은 요소를 가로 정렬할 때는 물론 요소의 배치와 정렬하는 데 사용되는 CSS 레이아웃 모델입니다. +기본적으로 정렬할 요소의 부모 요소에다가 display: flex 를 부여해주어 Flexbox layout을 개시하게 됩니다. 기호에 따라 부모 요소에 justify-content, align-items, flex-wrap 와 같은 속성들을 정의해주고, 자식 요소에선 align-self, flex-grow 와 같은 속성들을 정의하여 정렬을 반응형으로 어렵지 않게 구현하는데 정말 유용하게 쓰이는 것 같습니다. +- JavaScript가 다른 언어들에 비해 주목할 만한 점에는 어떤 것들이 있나요? -- [결과 화면](https://vanilla-todo-17th-qras.vercel.app/)의 기능을 그대로 구현합니다. -- 결과 링크의 화면 디자인 그대로 구현해도 좋고, 자신만의 디자인을 적용해도 좋습니다. -- CSS의 Flexbox를 이용하여 레이아웃을 구성합니다. -- JQuery, React, Bootstrap 등 외부 라이브러리를 사용하지 않습니다. -- 함수와 변수의 이름은 lowerCamelCase로 짓습니다. -- 코딩의 단위를 기능별로 나누어 Commit 메세지를 작성합니다. +이번에 학교 수업에서 프론트, 백엔드, DB를 모두 자바스크립트만으로 구현하는 법을 배우고 있는데 (React, Express, MySQL) 이처럼 하나의 언어로 모든 분야를 조작할 수 있다는 점에서 자바스크립트는 굉장히 자유도가 높으며 활용도가 높은 언어인 것 같고, 앞으로도 수많은 프레임워크와 라이브러리가 나올 것 같습니다. +- 코드에서 주석을 다는 바람직한 방법은 무엇일까요? -## 선택 요건 +코드가 복잡할 때 주석을 많이 달기 보다는, 그럴 때 일수록 핵심적인 부분만 주석을 다는 게 좋은 것 같습니다. -- 외부 폰트([눈누 상업용 무료폰트](https://noonnu.cc/))로 입맛에 맞게 꾸밉니다. -- 브라우저의 `localStorage` 혹은 `sessionStorage`를 이용하여 다음 번 접속 시에 기존의 투두 데이터를 불러옵니다. -- 이 외에도 추가하고 싶은 기능이 있다면 마음껏 추가하셔도 됩니다. -# 링크 및 참고자료 -- [HTML/CSS 기초](https://heropy.blog/2019/04/24/html-css-starter/) -- [HTML 태그](https://heropy.blog/2019/05/26/html-elements/) -- [FlexBox 가이드](https://heropy.blog/2018/11/24/css-flexible-box/) -- [JS를 통한 DOM 조작](https://velog.io/@bining/javascript-DOM-%EC%A1%B0%EC%9E%91%ED%95%98%EA%B8%B0#append) -- [localStorage, sessionStorage](https://www.daleseo.com/js-web-storage/) -- [git 사용법](https://wayhome25.github.io/git/2017/07/08/git-first-pull-request-story/) -- [좋은 코드리뷰 방법](https://tech.kakao.com/2022/03/17/2022-newkrew-onboarding-codereview/) diff --git a/images/blackhole.gif b/images/blackhole.gif new file mode 100644 index 0000000..1f02635 Binary files /dev/null and b/images/blackhole.gif differ diff --git a/images/delete.png b/images/delete.png new file mode 100644 index 0000000..022e0bc Binary files /dev/null and b/images/delete.png differ diff --git a/images/galaxy-icon.svg b/images/galaxy-icon.svg new file mode 100644 index 0000000..1615648 --- /dev/null +++ b/images/galaxy-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/giphy.gif b/images/giphy.gif new file mode 100644 index 0000000..810ccfb Binary files /dev/null and b/images/giphy.gif differ diff --git a/images/restore.png b/images/restore.png new file mode 100644 index 0000000..314729f Binary files /dev/null and b/images/restore.png differ diff --git a/images/rock.png b/images/rock.png new file mode 100644 index 0000000..b32c4c0 Binary files /dev/null and b/images/rock.png differ diff --git a/index.html b/index.html index 420961c..d1a3f79 100644 --- a/index.html +++ b/index.html @@ -3,12 +3,49 @@ - Vanilla Todo + Universe-Todo + + - -
+
+
+
+ 아이콘 +
+
+ +
+ +
+
Carl Seagan - " 사멸은 법칙이다. "
+
+
+
+
+

✔ 지금 당장 해결하세요.

+

+ 총 개의 할 일이 있어요. +

+
    + +
+
+
+

✔ 최근 해결한 내역

+

당신이 해결한 일들은 이곳에 모두 저장됩니다.

+
    +
    +
    +
    + 블랙홀 이미지 +
    +
    + - - \ No newline at end of file + diff --git a/reset.css b/reset.css new file mode 100644 index 0000000..27acba5 --- /dev/null +++ b/reset.css @@ -0,0 +1,43 @@ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/script.js b/script.js index e69de29..605a4fc 100644 --- a/script.js +++ b/script.js @@ -0,0 +1,155 @@ +let currentTasks = []; +let completedTasks = []; +const maxCompletedTasksToShow = 5; + +const taskForm = document.querySelector(".todo"); +const taskList = document.querySelector(".todo-list"); +const completedList = document.querySelector(".solved-list"); +const submitButton = document.querySelector("button"); + +const resetInputText = () => { + const taskInput = document.querySelector(".todo > input"); + taskInput.value = ""; +}; + +const addTaskToList = (text) => { + if (!currentTasks.includes(text)) { + const listItem = document.createElement("li"); + listItem.innerHTML = text; + currentTasks.push(text); + taskList.appendChild(listItem); + } +}; + +const addCompletedTaskToList = (text) => { + if (!completedTasks.includes(text)) { + const listItem = document.createElement("li"); + const textElement = document.createElement("p"); + textElement.innerHTML = text; + const deleteImg = document.createElement("img"); + deleteImg.src = "./images/delete.png"; + const restoreImg = document.createElement("img"); + restoreImg.src = "./images/restore.png"; + listItem.appendChild(deleteImg); + listItem.appendChild(restoreImg); + listItem.appendChild(textElement); + completedTasks.push(text); + completedList.appendChild(listItem); + const filteredTasks = currentTasks.filter((task) => task !== text); + currentTasks = filteredTasks; + localStorage.setItem("currentTasks", JSON.stringify(currentTasks)); + localStorage.setItem("completedTasks", JSON.stringify(completedTasks)); + if (completedList.children.length > maxCompletedTasksToShow) { + completedList.lastChild.remove(); + } + } +}; + +const loadTasks = (item) => { + const storedTasks = localStorage.getItem(item); + const parsedTasks = JSON.parse(storedTasks); + parsedTasks.forEach((task) => { + addTaskToList(task); + }); +}; + +const loadCompletedTasks = (item) => { + const storedCompletedTasks = localStorage.getItem(item); + const parsedCompletedTasks = JSON.parse(storedCompletedTasks); + parsedCompletedTasks.forEach((completedTask) => { + addCompletedTaskToList(completedTask); + }); +}; + +const updateTaskCount = () => { + const taskCount = document.querySelector(".todo-number"); + taskCount.innerHTML = currentTasks.length; +}; + +const submitHandler = (event) => { + event.preventDefault(); + const taskText = event.target.children[0].value; + if (taskText.length < 3) { + alert("3글자 이상 입력하세요."); + return; + } else if (taskText.length > 25) { + alert("25글자 이하로 요약해서 입력해주세요."); + return; + } + addTaskToList(taskText); + localStorage.setItem("currentTasks", JSON.stringify(currentTasks)); + updateTaskCount(); + resetInputText(); +}; + +const clickHandler = (event) => { + event.preventDefault(); + const taskText = event.target.previousElementSibling.children[0].value; + if (taskText.length < 3) { + alert("3글자 이상 입력하세요."); + return; + } else if (taskText.length > 25) { + alert("25글자 이하로 요약해서 입력해주세요."); + return; + } + addTaskToList(taskText); + localStorage.setItem("currentTasks", JSON.stringify(currentTasks)); + updateTaskCount(); + resetInputText(); +}; + +const taskClickHandler = (event) => { + const filteredTasks = currentTasks.filter( + (task) => task !== event.target.innerHTML + ); + addCompletedTaskToList(event.target.innerHTML); + currentTasks.length = 0; + currentTasks.push(...filteredTasks); + localStorage.setItem("currentTasks", JSON.stringify(currentTasks)); + completedTasks.push(event.target.innerHTML); + localStorage.setItem("completedTasks", JSON.stringify(completedTasks)); + event.target.remove(); + updateTaskCount(); +}; + +const completedTaskClickHandler = (event) => { + if ( + event.target.tagName === "IMG" && + event.target.src.includes("delete.png") + ) { + const deletedTask = + event.target.nextElementSibling.nextElementSibling.innerHTML; + const filteredCompletedTasks = completedTasks.filter( + (task) => task !== deletedTask + ); + completedTasks = filteredCompletedTasks; + console.log(completedTasks); + localStorage.setItem("completedTasks", JSON.stringify(completedTasks)); + event.target.parentElement.remove(); + } else if ( + event.target.tagName == "IMG" && + event.target.src.includes("restore.png") + ) { + const restoredTask = event.target.nextElementSibling.innerHTML; + const filteredCompletedTasks = completedTasks.filter( + (task) => task !== restoredTask + ); + completedTasks = filteredCompletedTasks; + localStorage.setItem("completedTasks", JSON.stringify(completedTasks)); + addTaskToList(restoredTask); + localStorage.setItem("currentTasks", JSON.stringify(currentTasks)); + event.target.parentElement.remove(); + updateTaskCount(); + } +}; + +taskForm.addEventListener("submit", submitHandler); +submitButton.addEventListener("click", clickHandler); +taskList.addEventListener("click", taskClickHandler); +completedList.addEventListener("click", completedTaskClickHandler); + +(() => { + loadTasks("currentTasks"); + loadCompletedTasks("completedTasks"); + updateTaskCount(); +})(); diff --git a/style.css b/style.css index e69de29..1aeb30d 100644 --- a/style.css +++ b/style.css @@ -0,0 +1,169 @@ +@font-face { + font-family: "Pretendard-Regular"; + src: url("https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff") + format("woff"); + font-weight: 400; + font-style: normal; +} +body { + background-color: black; + font-size: 15px; + font-family: "Pretendard-Regular"; + position: relative; +} + +header { + margin-top: 80px; + width: 100%; +} +header .input-section { + width: 45%; + height: 45px; + margin: 0 auto; + background-color: white; + border-radius: 20px; + display: flex; + box-shadow: 0px 0px 100px 50px rgba(255, 255, 255, 0.25); +} +header .input-section .icon { + padding: 0 20px 0px 30px; + display: flex; + align-items: center; +} +header .input-section form { + width: 73%; + align-self: center; +} +header .input-section form input { + width: 100%; + border: none; + outline: none; +} +header .input-section button { + display: flex; + width: 67px; + height: 27px; + align-self: center; + align-items: center; + justify-content: center; + background-color: rgba(212, 212, 212, 1); + border-radius: 10px; + font-weight: 600; +} + +header .quote-section { + width: 100%; + margin-top: 26px; + text-align: center; + color: white; +} + +.blackhole-section { + text-align: center; +} + +main { + margin-top: 19px; + color: white; +} + +main .todos-solves { + display: flex; + width: 90%; + margin: 0px auto; +} +main .todos-solves .todo-section { + margin-right: auto; +} + +.todos-solves .todo-section h1 { + font-size: 24px; +} +.todos-solves .todo-section p.caption { + margin-top: 10px; + color: rgba(255, 255, 255, 0.5); +} +.todos-solves .todo-section ul { + margin-top: 28px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 20px; +} +.todos-solves .todo-section li { + background: radial-gradient( + 50% 50% at 50% 50%, + rgba(18, 18, 18, 0) 0%, + #202020 100% + ); + + box-shadow: 0px 0px 60px 20px rgba(13, 12, 11, 0.25); + box-shadow: 0px 0px 20px 0px white; + border-radius: 20px; + padding: 13px 30px 13px; + cursor: pointer; + transition: 1s; +} +.todos-solves .todo-section li:hover { + background: yellow; + color: black; +} +.todos-solves .solved-section h1 { + text-align: right; + font-size: 24px; +} +.todos-solves .solved-section p.caption { + text-align: right; + margin-top: 10px; + color: rgba(255, 255, 255, 0.5); +} +.todos-solves .solved-section ul { + margin-top: 28px; + display: flex; + flex-direction: column; + align-items: flex-end; +} +.todos-solves .solved-section li { + display: flex; + flex-direction: row-reverse; + align-items: center; + height: 45px; + border-radius: 20px; + background: radial-gradient( + 50% 50% at 50% 50%, + rgba(18, 18, 18, 0) 0%, + #202020 100% + ); + margin-bottom: 20px; +} + +.todos-solves .solved-section li p { + align-self: center; + padding-left: 20px; + padding-right: 30px; +} +.todos-solves .solved-section li img { + width: 20px; +} +.todos-solves .solved-section li img:first-child { + padding-right: 10px; +} +main { + position: relative; +} + +main .blackhole-section { + position: absolute; + top: 200px; + left: calc(35vw); + width: 100px; +} + +footer { + width: 100%; + text-align: center; + font-size: 12px; + color: #675f5f; + position: fixed; + bottom: 0; +}