diff --git a/.gitignore b/.gitignore
index 6704566..74de17d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -102,3 +102,6 @@ dist
# TernJS port file
.tern-port
+
+# VScode
+.vscode/*
\ No newline at end of file
diff --git a/mission001/ganeodolu/css/style.css b/mission001/ganeodolu/css/style.css
new file mode 100644
index 0000000..14a905e
--- /dev/null
+++ b/mission001/ganeodolu/css/style.css
@@ -0,0 +1,334 @@
+html,
+body {
+ margin: 0;
+ padding: 10px;
+}
+
+button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: none;
+ font-size: 100%;
+ vertical-align: baseline;
+ font-family: inherit;
+ font-weight: inherit;
+ color: inherit;
+ -webkit-appearance: none;
+ appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ line-height: 1.4em;
+ background: #f5f5f5;
+ color: #4d4d4d;
+ min-width: 230px;
+ max-width: 550px;
+ margin: 0 auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-weight: 300;
+}
+
+:focus {
+ outline: 0;
+}
+
+.hidden {
+ display: none;
+}
+
+.todoapp {
+ background: #fff;
+ margin: 130px 0 40px 0;
+ position: relative;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
+ 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+}
+
+.todoapp input::-webkit-input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp h1 {
+ position: absolute;
+ top: -125px;
+ width: 100%;
+ font-size: 60px;
+ text-align: center;
+ color: dimgray;
+ font-weight: 100;
+ font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
+}
+
+.new-todo,
+.edit {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ font-size: 24px;
+ font-family: inherit;
+ font-weight: inherit;
+ line-height: 1.4em;
+ border: 0;
+ color: inherit;
+ padding: 6px;
+ box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.new-todo {
+ padding: 16px 16px 16px 60px;
+ border: none;
+ background: rgba(0, 0, 0, 0.003);
+ box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
+}
+
+.main {
+ position: relative;
+ z-index: 2;
+ border-top: 1px solid #e6e6e6;
+}
+
+.toggle-all {
+ width: 1px;
+ height: 1px;
+ border: none;
+ opacity: 0;
+ position: absolute;
+ right: 100%;
+ bottom: 100%;
+}
+
+.toggle-all + label {
+ width: 60px;
+ height: 34px;
+ font-size: 0;
+ position: absolute;
+ top: -52px;
+ left: -13px;
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+
+.toggle-all + label:before {
+ content: '❯';
+ font-size: 22px;
+ color: #e6e6e6;
+ padding: 10px 27px 10px 27px;
+}
+
+.toggle-all:checked + label:before {
+ color: #737373;
+}
+
+.todo-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.todo-list li {
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #ededed;
+}
+
+.todo-list li:last-child {
+ border-bottom: none;
+}
+
+.todo-list li.editing {
+ border-bottom: none;
+ padding: 0;
+}
+
+.todo-list li.editing .edit {
+ display: block;
+ width: calc(100% - 43px);
+ padding: 12px 16px;
+ margin: 0 0 0 43px;
+}
+
+.todo-list li.editing .view {
+ display: none;
+}
+
+.todo-list li .toggle {
+ text-align: center;
+ width: 40px;
+ height: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ border: none;
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.todo-list li .toggle {
+ opacity: 0;
+}
+
+.todo-list li .toggle + label {
+ background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
+ background-repeat: no-repeat;
+ background-position: center left;
+}
+
+.todo-list li .toggle:checked + label {
+ background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
+}
+
+.todo-list li label {
+ word-break: break-all;
+ padding: 15px 15px 15px 60px;
+ display: block;
+ line-height: 1.2;
+ transition: color 0.4s;
+}
+
+.todo-list li.completed label {
+ color: #d9d9d9;
+ text-decoration: line-through;
+}
+
+.todo-list li .destroy {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ bottom: 0;
+ width: 40px;
+ height: 40px;
+ margin: auto 0;
+ font-size: 30px;
+ color: #cc9a9a;
+ margin-bottom: 11px;
+ transition: color 0.2s ease-out;
+ cursor: pointer;
+}
+
+.todo-list li .destroy:hover {
+ color: #af5b5e;
+}
+
+.todo-list li .destroy:after {
+ content: '×';
+}
+
+.todo-list li:hover .destroy {
+ display: block;
+}
+
+.todo-list li .edit {
+ display: none;
+}
+
+.todo-list li.editing:last-child {
+ margin-bottom: -1px;
+}
+
+.count-container {
+ color: #777;
+ padding: 10px 15px;
+ height: 20px;
+ text-align: center;
+ border-top: 1px solid #e6e6e6;
+}
+
+.count-container:before {
+ content: '';
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 50px;
+ overflow: hidden;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
+ 0 8px 0 -3px #f6f6f6,
+ 0 9px 1px -3px rgba(0, 0, 0, 0.2),
+ 0 16px 0 -6px #f6f6f6,
+ 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+.todo-count {
+ float: left;
+ text-align: left;
+}
+
+.todo-count strong {
+ font-weight: 300;
+}
+
+.filters {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ right: 0;
+ left: 0;
+}
+
+.filters li {
+ display: inline;
+}
+
+.filters li a {
+ color: inherit;
+ margin: 3px;
+ padding: 3px 7px;
+ text-decoration: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+}
+
+.filters li a:hover {
+ border-color: rgba(175, 47, 47, 0.1);
+}
+
+.filters li a.selected {
+ border-color: rgba(175, 47, 47, 0.2);
+}
+
+.clear-completed, html .clear-completed:active {
+ float: right;
+ position: relative;
+ line-height: 20px;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.clear-completed:hover {
+ text-decoration: underline;
+}
+
+.info {
+ margin: 65px auto 0;
+ color: #bfbfbf;
+ font-size: 10px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-align: center;
+}
+
+.info p {
+ line-height: 1;
+}
+
+.info a {
+ color: inherit;
+ text-decoration: none;
+ font-weight: 400;
+}
+
+.info a:hover {
+ text-decoration: underline;
+}
diff --git a/mission001/ganeodolu/index.html b/mission001/ganeodolu/index.html
new file mode 100644
index 0000000..8d19eb5
--- /dev/null
+++ b/mission001/ganeodolu/index.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+ 이벤트 - TODOS
+
+
+
+
+
+
+
diff --git a/mission001/ganeodolu/js/App.js b/mission001/ganeodolu/js/App.js
new file mode 100644
index 0000000..e4af7cf
--- /dev/null
+++ b/mission001/ganeodolu/js/App.js
@@ -0,0 +1,88 @@
+import TodoList from "./TodoList.js"
+import TodoCount from "./TodoCount.js"
+import TodoInput from "./TodoInput.js"
+import {error} from "./constant.js"
+
+export default function App(data) {
+ this.data = data;
+ this.render = function (filteredData) {
+ todoList.setState(filteredData)
+ todoCount.render(filteredData)
+ todoCount.setState({
+ totalCount: filteredData.length,
+ })
+ }
+ this.setState = function (nextData) {
+ this.data = nextData;
+ todoList.setState(this.data)
+ todoCount.setState({
+ totalCount: this.data.length,
+ })
+ }
+
+ const $todoList = document.querySelector('.todo-list')
+ const $todoFilter = document.querySelector('.filters');
+ const todoList = new TodoList({
+ $target: $todoList,
+ $targetFilter: $todoFilter,
+ data: this.data,
+ onClickToggle: (index) => {
+ const nextData = [...this.data]
+ nextData[index].isCompleted = !nextData[index].isCompleted
+ this.setState(nextData)
+ },
+ onEditTodo: (index) => {
+ const nextData = [...this.data]
+ if (!nextData[index].isCompleted) {
+ nextData[index].isEditing = !nextData[index].isEditing
+ }
+ this.setState(nextData)
+ },
+ onClickRemoval: (index) => {
+ const nextData = [...this.data]
+ nextData.splice(index, 1)
+ this.setState(nextData)
+ },
+ onChangeTodo: (index, value) => {
+ const nextData = [...this.data]
+ nextData[index] = {
+ text: value,
+ isCompleted: false,
+ isEditing: false
+ };
+ this.setState(nextData)
+ },
+ onClickFilter: (filterBoolean) => {
+ let filteredData = [...this.data]
+ filteredData = this.data.filter(todo => todo.isCompleted !== filterBoolean)
+ this.render(filteredData)
+ }
+ });
+
+ const $todoCount = document.querySelector('.todo-count');
+ const todoCount = new TodoCount({
+ $targetCount: $todoCount,
+ $targetFilter: $todoFilter,
+ data: {
+ totalCount: this.data.length,
+ },
+ })
+
+ const $todoInput = document.querySelector('.new-todo')
+ const todoInput = TodoInput($todoInput,
+ {
+ onAdd: (text) => {
+ const nextData = [...this.data]
+ nextData.push({
+ text: text,
+ isCompleted: false,
+ isEditing: false
+ })
+ this.setState(nextData)
+ }
+ }
+ )
+ if(!todoList instanceof TodoList || !todoCount instanceof TodoCount){
+ throw new Error(error.NO_USED_NEW_KEYWORD)
+ }
+}
diff --git a/mission001/ganeodolu/js/TodoCount.js b/mission001/ganeodolu/js/TodoCount.js
new file mode 100644
index 0000000..53dbf85
--- /dev/null
+++ b/mission001/ganeodolu/js/TodoCount.js
@@ -0,0 +1,21 @@
+export default function TodoCount({$targetCount, $targetFilter, data, onFilterClick}) {
+ this.data = data;
+ this.$targetCount = $targetCount;
+ this.$targetFilter = $targetFilter;
+
+ this.setState = function (nextData) {
+ this.data = nextData;
+ this.render();
+ }
+
+ if (this === window) {
+ throw new Error(error.NO_USED_NEW_KEYWORD)
+ }
+
+ this.render = function () {
+ const { totalCount } = this.data
+ $targetCount.innerHTML = `총 ${totalCount}개`
+ }
+ this.render();
+
+}
diff --git a/mission001/ganeodolu/js/TodoInput.js b/mission001/ganeodolu/js/TodoInput.js
new file mode 100644
index 0000000..7e211b8
--- /dev/null
+++ b/mission001/ganeodolu/js/TodoInput.js
@@ -0,0 +1,11 @@
+import {keyName} from "./constant.js"
+
+export default function TodoInput($targetInput, {onAdd}) {
+
+ $targetInput.addEventListener('keydown', (e) => {
+ if (e.key === keyName.ENTER) {
+ onAdd($targetInput.value)
+ $targetInput.value = '';
+ }
+ })
+}
diff --git a/mission001/ganeodolu/js/TodoList.js b/mission001/ganeodolu/js/TodoList.js
new file mode 100644
index 0000000..b0a3327
--- /dev/null
+++ b/mission001/ganeodolu/js/TodoList.js
@@ -0,0 +1,96 @@
+import { error, keyName } from "./constant.js"
+
+export default function TodoList({ $target, $targetFilter, data, onClickToggle, onEditTodo, onClickRemoval, onChangeTodo, onClickFilter }) {
+ this.$target = $target;
+ this.$targetFilter = $targetFilter
+ this.data = data;
+
+ this.setState = function (nextData) {
+ this.data = nextData;
+ this.render()
+ }
+
+ const filterTypes = document.querySelectorAll(".filters li a")
+
+ if (this === window) {
+ throw new Error(error.NO_USED_NEW_KEYWORD)
+ }
+ else if (Array.isArray(this.data) === false) {
+ throw new Error(error.NOARRAY_DATA)
+ }
+
+ this.$target.addEventListener('click', (e) => {
+ const { className } = e.target;
+ const { index } = e.target.parentNode.parentNode.dataset
+ if (!filterTypes[0].classList.contains("selected")) return
+
+ switch (className) {
+ case 'toggle': onClickToggle(index)
+ break;
+ case 'destroy': onClickRemoval(index)
+ break;
+ }
+ })
+
+ this.$target.addEventListener('dblclick', (e) => {
+ const { className } = e.target;
+ const { index } = e.target.closest('li').dataset
+ if (className === 'label') {
+ onEditTodo(index)
+ }
+ })
+
+ this.$target.addEventListener('keydown', (e) => {
+ const { className } = e.target;
+ const { index } = e.target.parentNode.dataset;
+ if (className === 'edit') {
+ if (e.key === keyName.ENTER) {
+ onChangeTodo(index, e.target.value)
+ } else if (e.key === keyName.ESC) {
+ onEditTodo(index)
+ }
+ }
+ })
+
+ this.$targetFilter.addEventListener('click', (e) => {
+ const { className } = e.target;
+ for (let val of filterTypes) {
+ if (val.classList.contains("selected")) {
+ val.classList.remove("selected")
+ }
+ }
+ e.target.classList.add("selected");
+
+ if (className.includes('all')) {
+ onClickFilter()
+ } else if (className.includes('active')) {
+ onClickFilter(true)
+ } else if (className.includes('completed')) {
+ onClickFilter(false)
+ }
+ })
+
+ this.render()
+}
+
+TodoList.prototype.render = function () {
+ const renderedHTMLText = this.data.map((val, idx) => {
+ if (!val.text) {
+ throw new Error(error.NOT_DATA)
+ }
+ else if (typeof (val.text) !== "string") {
+ throw new Error(error.INVALID_DATA)
+ }
+ return `
+
+
+
+
+
+
+
+ `
+ }).join('');
+ this.$target.innerHTML = renderedHTMLText
+}
+
diff --git a/mission001/ganeodolu/js/constant.js b/mission001/ganeodolu/js/constant.js
new file mode 100644
index 0000000..f5d18fc
--- /dev/null
+++ b/mission001/ganeodolu/js/constant.js
@@ -0,0 +1,22 @@
+export const error = {
+ NO_USED_NEW_KEYWORD: "함수 선언시 new를 사용해주세요.",
+ NOARRAY_DATA: "data타입이 Array가 아닙니다.",
+ NOT_DATA: "data가 null 또는 undefined 입니다.",
+ INVALID_DATA: "data타입이 문자열이 아닙니다.",
+};
+export const keyName = {
+ ENTER: "Enter",
+ ESC: "Escape"
+}
+export const data = [
+ {
+ text: 'JS 공부하기',
+ isCompleted : false,
+ isEditing : false,
+ },
+ {
+ text: 'JS 복습하기',
+ isCompleted: true,
+ isEditing : false,
+ },
+]
diff --git a/mission001/ganeodolu/js/index.js b/mission001/ganeodolu/js/index.js
new file mode 100644
index 0000000..3998931
--- /dev/null
+++ b/mission001/ganeodolu/js/index.js
@@ -0,0 +1,4 @@
+import App from "./App.js"
+import {data} from "./constant.js"
+
+new App(data);
\ No newline at end of file
diff --git a/mission001/origin/.vscode/settings.json b/mission001/origin/.vscode/settings.json
new file mode 100644
index 0000000..6f3a291
--- /dev/null
+++ b/mission001/origin/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "liveServer.settings.port": 5501
+}
\ No newline at end of file
diff --git a/mission001/origin/index.html b/mission001/origin/index.html
index b1180c7..3cdcfdd 100644
--- a/mission001/origin/index.html
+++ b/mission001/origin/index.html
@@ -56,6 +56,6 @@ TODOS
-
+