diff --git a/mission001/pci2676/css/style.css b/mission001/pci2676/css/style.css
new file mode 100644
index 0000000..905a92a
--- /dev/null
+++ b/mission001/pci2676/css/style.css
@@ -0,0 +1,338 @@
+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;
+}
+
+.d-none {
+ display: none;
+}
diff --git a/mission001/pci2676/index.html b/mission001/pci2676/index.html
new file mode 100644
index 0000000..224db60
--- /dev/null
+++ b/mission001/pci2676/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+ 이벤트 - TODOS
+
+
+
+
+
+
+
diff --git a/mission001/pci2676/js/app.js b/mission001/pci2676/js/app.js
new file mode 100644
index 0000000..dfbfa4e
--- /dev/null
+++ b/mission001/pci2676/js/app.js
@@ -0,0 +1,25 @@
+import Controller from "./todo/Controller.js";
+import Service from "./todo/Service.js";
+import View from "./todo/View.js";
+import Storage from "./todo/Storage.js";
+
+function App() {
+
+ function initialize(app) {
+ const $inputTextBox = document.querySelector("#new-todo-title");
+ const $filters = document.querySelector(".filters");
+ const $todoList = document.querySelector("#todo-list");
+ const $todoCount = document.querySelector(".count");
+
+ const view = new View($todoList, $todoCount);
+ const storage = new Storage();
+ const service = new Service(storage);
+ const controller = new Controller(service, view);
+
+ controller.init($inputTextBox, $todoList, $filters);
+ }
+
+ initialize(this);
+}
+
+new App();
\ No newline at end of file
diff --git a/mission001/pci2676/js/todo/Controller.js b/mission001/pci2676/js/todo/Controller.js
new file mode 100644
index 0000000..47985cb
--- /dev/null
+++ b/mission001/pci2676/js/todo/Controller.js
@@ -0,0 +1,74 @@
+import {EVENT_KEY} from "../utils/Contants.js"
+import {EVENT_VALIDATOR, STRING_VALIDATOR} from "../utils/Validators.js";
+
+function Controller(service, view) {
+
+ Controller.prototype.inputListener = (event, $inputTextBox) => {
+ const inputItem = $inputTextBox.value;
+ if (EVENT_VALIDATOR.isEnter(event) && STRING_VALIDATOR.isNotEmpty(inputItem)) {
+ $inputTextBox.value = "";
+ service.addTodoItem(view.addNewItem, inputItem);
+ }
+ };
+
+ Controller.prototype.toggleClick = (event) => {
+ event.preventDefault()
+
+ if (event.target.classList.contains('toggle')) {
+ const $li = event.target.closest('li');
+ service.changeStatus(view.toggle, $li);
+ }
+ };
+
+ Controller.prototype.deleteClick = (event) => {
+ event.preventDefault()
+
+ if (event.target.classList.contains('destroy')) {
+ const $li = event.target.closest('li');
+ service.delete(view.remove, $li.id);
+ }
+ };
+
+ Controller.prototype.editContent = (event) => {
+ event.preventDefault()
+
+ const $li = event.target.closest('li');
+
+ if (EVENT_VALIDATOR.isEsc(event)) {
+ view.editExit(event);
+ return;
+ }
+ if (EVENT_VALIDATOR.isEnter(event)) {
+ const edited = event.target.value;
+ service.update(view.update, $li.id, edited);
+ }
+ };
+
+ Controller.prototype.filterClick = (event) => {
+ event.preventDefault()
+
+ const $target = event.target.closest('a');
+ if ($target) {
+ view.select($target);
+ view.updateCount();
+ }
+ };
+
+ Controller.prototype.initEventListener = ($inputTextBox, $todoList, $filters) => {
+ $inputTextBox.addEventListener(EVENT_KEY.KEY_UP, event => Controller.prototype.inputListener(event, $inputTextBox));
+
+ $todoList.addEventListener(EVENT_KEY.CLICK, (event) => Controller.prototype.toggleClick(event));
+ $todoList.addEventListener(EVENT_KEY.CLICK, (event) => Controller.prototype.deleteClick(event));
+ $todoList.addEventListener(EVENT_KEY.CLICK, (event) => Controller.prototype.filterClick(event));
+ $todoList.addEventListener(EVENT_KEY.DOUBLE_CLICK, (event) => view.editMode(event));
+ $todoList.addEventListener(EVENT_KEY.KEY_UP, (event) => Controller.prototype.editContent(event));
+
+ $filters.addEventListener(EVENT_KEY.CLICK, (event) => Controller.prototype.filterClick(event));
+ };
+
+ Controller.prototype.init = ($inputTextBox, $todoList, $filters) => {
+ Controller.prototype.initEventListener($inputTextBox, $todoList, $filters);
+ }
+}
+
+export default Controller;
\ No newline at end of file
diff --git a/mission001/pci2676/js/todo/Service.js b/mission001/pci2676/js/todo/Service.js
new file mode 100644
index 0000000..cdf351e
--- /dev/null
+++ b/mission001/pci2676/js/todo/Service.js
@@ -0,0 +1,43 @@
+function Service(storage) {
+
+ Service.prototype.addTodoItem = (callback, inputItem) => {
+ const entity = {};
+ entity.value = inputItem;
+ entity.status = "active";
+ storage.save(callback, entity);
+ };
+
+ Service.prototype.delete = (callback, id) => {
+ const todoId = extractTodoId(id);
+ storage.delete(callback, todoId);
+ };
+
+ Service.prototype.changeStatus = (callback, target) => {
+ const todoId = extractTodoId(target.id);
+ const status = target.className;
+
+ const changeStatus = toggle(status);
+
+ storage.changeStatus(callback, todoId, changeStatus);
+ };
+
+ function toggle(status) {
+ if (status === 'active') {
+ return 'completed';
+ } else if (status === 'completed') {
+ return 'active';
+ }
+ }
+
+ Service.prototype.update = (callback, id, edited) => {
+ const todoId = extractTodoId(id);
+ storage.update(callback, todoId, edited);
+ };
+
+ function extractTodoId(id) {
+ return id.replace('todo-', '');
+ }
+
+}
+
+export default Service;
\ No newline at end of file
diff --git a/mission001/pci2676/js/todo/Storage.js b/mission001/pci2676/js/todo/Storage.js
new file mode 100644
index 0000000..ebc30fb
--- /dev/null
+++ b/mission001/pci2676/js/todo/Storage.js
@@ -0,0 +1,40 @@
+function Storage() {
+ let id = 0;
+
+ Storage.prototype.save = (callback, entity) => {
+ id++;
+ entity.id = id.toString();
+ save(entity);
+ callback(entity);
+ };
+
+ Storage.prototype.delete = (callback, id) => {
+ window.localStorage.removeItem(id);
+ callback(id);
+ };
+
+ Storage.prototype.changeStatus = (callback, id, changeStatus) => {
+ const entity = findById(id);
+ entity.status = changeStatus;
+ save(entity);
+ callback(entity);
+ };
+
+ Storage.prototype.update = (callback, id, edited) => {
+ const entity = findById(id);
+ entity.value = edited;
+ save(entity);
+ callback(entity);
+ }
+
+ function save(entity) {
+ window.localStorage.setItem(entity.id, JSON.stringify(entity));
+ }
+
+ function findById(id) {
+ return JSON.parse(window.localStorage.getItem(id));
+ }
+
+}
+
+export default Storage;
\ No newline at end of file
diff --git a/mission001/pci2676/js/todo/Template.js b/mission001/pci2676/js/todo/Template.js
new file mode 100644
index 0000000..11df4f1
--- /dev/null
+++ b/mission001/pci2676/js/todo/Template.js
@@ -0,0 +1,16 @@
+const Template = {
+ getNewItem(entity) {
+ return `
+
+
+
+
+
+
+
+
+ `;
+ }
+};
+
+export default Template;
\ No newline at end of file
diff --git a/mission001/pci2676/js/todo/View.js b/mission001/pci2676/js/todo/View.js
new file mode 100644
index 0000000..5c42773
--- /dev/null
+++ b/mission001/pci2676/js/todo/View.js
@@ -0,0 +1,90 @@
+import Template from "./Template.js";
+
+function View($todoList, $todoCount) {
+
+ View.prototype.addNewItem = (entity) => {
+ const item = Template.getNewItem(entity);
+ $todoList.insertAdjacentHTML('afterbegin', item);
+ View.prototype.updateCount();
+ };
+
+ View.prototype.editMode = (event) => {
+ event.preventDefault();
+
+ const $label = event.target.closest('label');
+ if ($label) {
+ const $li = event.target.closest('li');
+ $li.className = 'editing';
+ }
+ };
+
+ View.prototype.toggle = (entity) => {
+ const id = '#todo-' + entity.id;
+ const status = entity.status;
+ const li = document.querySelector(id);
+ li.className = status;
+ View.prototype.updateCount();
+ };
+
+ View.prototype.remove = (id) => {
+ const target = document.querySelector('#todo-' + id.toString());
+ target.parentElement.removeChild(target);
+ View.prototype.updateCount();
+ };
+
+ View.prototype.editExit = (event) => {
+ event.preventDefault();
+
+ const li = event.target.offsetParent;
+
+ li.className = 'ready';
+ event.target.value = li.querySelector('.label').textContent;
+ };
+
+ View.prototype.update = (entity) => {
+ const id = '#todo-' + entity.id;
+ const $li = document.querySelector(id);
+ $li.querySelector('.label').textContent = entity.value;
+ $li.querySelector('.edit').value = entity.value;
+ $li.className = 'ready';
+ };
+
+ View.prototype.updateCount = () => {
+ const target = document.querySelector('.selected').classList.item(0);
+
+ if (target === 'all') {
+ $todoCount.innerHTML = $todoList.getElementsByClassName('view').length.toString();
+ } else {
+ $todoCount.innerHTML = $todoList.getElementsByClassName(target).length.toString();
+ }
+ showSelected();
+ };
+
+ View.prototype.select = (target) => {
+ removeAllSelected();
+ target.classList.add('selected');
+ showSelected();
+ };
+
+ function removeAllSelected() {
+ document.querySelector('.selected').classList.remove('selected');
+ }
+
+ function showSelected() {
+ for (let $li of $todoList.getElementsByTagName('li')) {
+ $li.classList.remove('d-none');
+ }
+
+ const filter = document.querySelector('.selected').dataset.filter;
+ if (filter === 'all') {
+ return;
+ }
+ for (let $li of $todoList.getElementsByTagName('li')) {
+ if ($li.className !== filter) {
+ $li.classList.add('d-none')
+ }
+ }
+ }
+}
+
+export default View;
\ No newline at end of file
diff --git a/mission001/pci2676/js/utils/Contants.js b/mission001/pci2676/js/utils/Contants.js
new file mode 100644
index 0000000..22fd56e
--- /dev/null
+++ b/mission001/pci2676/js/utils/Contants.js
@@ -0,0 +1,9 @@
+const EVENT_KEY = {
+ CLICK: 'click',
+ KEY_UP: 'keyup',
+ DOUBLE_CLICK: 'dblclick'
+};
+
+export {
+ EVENT_KEY
+}
\ No newline at end of file
diff --git a/mission001/pci2676/js/utils/Validators.js b/mission001/pci2676/js/utils/Validators.js
new file mode 100644
index 0000000..05e845f
--- /dev/null
+++ b/mission001/pci2676/js/utils/Validators.js
@@ -0,0 +1,19 @@
+const EVENT_VALIDATOR = {
+ isEsc: function (event) {
+ return event.key === 'Escape';
+ },
+ isEnter: function (event) {
+ return event.key === 'Enter';
+ }
+};
+
+const STRING_VALIDATOR = {
+ isNotEmpty: function (string) {
+ return string && string.trim().length !== 0;
+ }
+};
+
+export {
+ EVENT_VALIDATOR,
+ STRING_VALIDATOR
+};
\ No newline at end of file