diff --git a/mission002/kimjieun/components/TodoCheck.js b/mission002/kimjieun/components/TodoCheck.js
new file mode 100644
index 0000000..2fc1ec0
--- /dev/null
+++ b/mission002/kimjieun/components/TodoCheck.js
@@ -0,0 +1,22 @@
+import { ACTIVE, COMPLETED, ALLSELECTED } from '../utils/constants.js'
+
+export default class TodoCheck {
+ constructor({ $selector }) {
+ this.$selector = $selector
+
+ this.init()
+ }
+
+ init = () => {
+ this.$selector.addEventListener('click', (e) => {
+ switch (e.target.className) {
+ case ACTIVE:
+ return this.onTodoCheck(ACTIVE)
+ case COMPLETED:
+ return this.onTodoCheck(COMPLETED)
+ default:
+ return this.onTodoCheck(ALLSELECTED)
+ }
+ })
+ }
+}
diff --git a/mission002/kimjieun/components/TodoCount.js b/mission002/kimjieun/components/TodoCount.js
new file mode 100644
index 0000000..a76c7ae
--- /dev/null
+++ b/mission002/kimjieun/components/TodoCount.js
@@ -0,0 +1,13 @@
+export default class TodoCount {
+ constructor({ $selector }) {
+ this.$selector = $selector
+ }
+
+ createHtmlString = (data) => {
+ return `총 ${data.length} 개`
+ }
+
+ createTodoCount = (data) => {
+ this.$selector.innerHTML = this.createHtmlString(data)
+ }
+}
diff --git a/mission002/kimjieun/components/TodoInput.js b/mission002/kimjieun/components/TodoInput.js
new file mode 100644
index 0000000..d14081d
--- /dev/null
+++ b/mission002/kimjieun/components/TodoInput.js
@@ -0,0 +1,13 @@
+export default class TodoInput {
+ constructor({ $selector }) {
+ this.$selector = $selector
+
+ this.init()
+ }
+
+ init() {
+ this.$selector.addEventListener('keydown', (e) => {
+ this.onKeyDown(e)
+ })
+ }
+}
diff --git a/mission002/kimjieun/components/TodoList.js b/mission002/kimjieun/components/TodoList.js
new file mode 100644
index 0000000..216b86e
--- /dev/null
+++ b/mission002/kimjieun/components/TodoList.js
@@ -0,0 +1,33 @@
+import { DESTROY, TOGGLE } from '../utils/constants.js'
+
+export default class TodoList {
+ constructor({ $selector }) {
+ this.$selector = $selector
+ this.init()
+ }
+
+ init = () => {
+ this.$selector.addEventListener('click', (e) => {
+ if (e.target.className === DESTROY) return this.onDeleteTodo(e.target.parentNode.dataset.idx)
+ if (e.target.className === TOGGLE) return this.onToggleTodo(e.target.parentNode.dataset.idx)
+ })
+ }
+
+ createLiClassName = (isCompleted) => isCompleted ? 'completed' : 'view'
+
+ createTodoListHtmlString = ({ content, isCompleted, _id }) => {
+ return `
+
+
+
+
+
+
+ `
+ }
+
+ render = (data) => {
+ const $todoList = document.querySelector('#todo-list')
+ $todoList.innerHTML = data.map(this.createTodoListHtmlString).join('')
+ }
+}
diff --git a/mission002/kimjieun/css/style.css b/mission002/kimjieun/css/style.css
new file mode 100644
index 0000000..14a905e
--- /dev/null
+++ b/mission002/kimjieun/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/mission002/kimjieun/index.html b/mission002/kimjieun/index.html
new file mode 100644
index 0000000..bc06fd5
--- /dev/null
+++ b/mission002/kimjieun/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ 이벤트 - TODOS
+
+
+
+
+
+
+
diff --git a/mission002/kimjieun/js/App.js b/mission002/kimjieun/js/App.js
new file mode 100644
index 0000000..9f2b6d1
--- /dev/null
+++ b/mission002/kimjieun/js/App.js
@@ -0,0 +1,119 @@
+import { apiHandler } from '../utils/api.js'
+import {
+ ENTER,
+ ACTIVE,
+ COMPLETED,
+ ALLSELECTED,
+ INIT,
+} from '../utils/constants.js'
+import { hostUrl } from '../utils/url.js'
+
+export default class App {
+ constructor({ todoList, todoInput, todoCount, todoCheck }) {
+ this.todoList = todoList
+ this.todoInput = todoInput
+ this.todoCount = todoCount
+ this.todoCheck = todoCheck
+
+ this.init()
+ this.fetchTodoData(INIT)
+ }
+
+ init = () => {
+ this.todoInput.onKeyDown = this.onKeyDown.bind(this)
+ this.todoList.onDeleteTodo = this.onDeleteTodo.bind(this)
+ this.todoList.onToggleTodo = this.onToggleTodo.bind(this)
+ this.todoCheck.onTodoCheck = this.onTodoCheck.bind(this)
+ }
+
+ getStorageData = (storageData) => {
+ this.data = JSON.parse(storageData)
+ this.setState(this.data)
+ }
+
+ fetchTodoData = async (isStatus) => {
+ const storageData = localStorage.getItem('todoData')
+ if (isStatus === INIT && storageData) return this.getStorageData(storageData)
+
+ try {
+ this.data = await apiHandler({ url: hostUrl })
+ localStorage.setItem('todoData', JSON.stringify(this.data))
+ this.setState(this.data)
+ } catch (error) {
+ throw new Error(error)
+ }
+ }
+
+ setState = (data) => {
+ this.render(data)
+ this.createTodoCount(data)
+ }
+
+ render = (data) => {
+ this.todoList.render(data)
+ }
+
+ createTodoCount = (data) => {
+ this.todoCount.createTodoCount(data)
+ }
+
+ onKeyDown = async (e) => {
+ if (!e.target.value) return
+
+ if (e.key === ENTER) {
+ try {
+ const data = await apiHandler({
+ url: hostUrl,
+ method: 'POST',
+ body: JSON.stringify({
+ content: e.target.value,
+ }),
+ })
+
+ if (data) e.target.value = ''
+ } catch (error) {
+ throw new Error(error)
+ }
+
+ this.fetchTodoData()
+ }
+ }
+
+ onDeleteTodo = async (id) => {
+ try {
+ await apiHandler({
+ url: hostUrl,
+ customUrl: id,
+ method: 'DELETE',
+ })
+ } catch (error) {
+ throw new Error(error)
+ }
+
+ this.fetchTodoData()
+ }
+
+ onToggleTodo = async (id) => {
+ try {
+ await apiHandler({
+ url: hostUrl,
+ customUrl: `${id}/toggle`,
+ method: 'PUT',
+ })
+ } catch (error) {
+ throw new Error(error)
+ }
+
+ this.fetchTodoData()
+ }
+
+ onTodoCheck = (todoStatus) => {
+ const todoData = this.data.filter(d => {
+ if (todoStatus === ACTIVE) return !d.isCompleted
+ if (todoStatus === COMPLETED) return d.isCompleted
+ if (todoStatus === ALLSELECTED) return this.data
+ })
+
+ this.setState(todoData)
+ }
+}
diff --git a/mission002/kimjieun/js/main.js b/mission002/kimjieun/js/main.js
new file mode 100644
index 0000000..b93a0d8
--- /dev/null
+++ b/mission002/kimjieun/js/main.js
@@ -0,0 +1,20 @@
+import App from './App.js'
+import TodoList from '../components/TodoList.js'
+import TodoInput from '../components/TodoInput.js'
+import TodoCount from '../components/TodoCount.js'
+import TodoCheck from '../components/TodoCheck.js'
+
+new App({
+ todoList: new TodoList({
+ $selector: document.querySelector('#todo-list'),
+ }),
+ todoInput: new TodoInput({
+ $selector: document.querySelector('#new-todo-title'),
+ }),
+ todoCount: new TodoCount({
+ $selector: document.querySelector('.todo-count'),
+ }),
+ todoCheck: new TodoCheck({
+ $selector: document.querySelector('.filters'),
+ })
+})
diff --git a/mission002/kimjieun/utils/api.js b/mission002/kimjieun/utils/api.js
new file mode 100644
index 0000000..8ed3416
--- /dev/null
+++ b/mission002/kimjieun/utils/api.js
@@ -0,0 +1,22 @@
+import { USERNAME } from './constants.js'
+
+export const apiHandler = async ({ url, method, body, customUrl }) => {
+ const options = {
+ method,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body,
+ }
+
+ try {
+ const res = await fetch(`${url}/${USERNAME}${customUrl ? `/${customUrl}` : ''}`, options)
+
+ if (res.ok) {
+ const data = await res.json()
+ return data
+ }
+ } catch (error) {
+ throw new Error(error)
+ }
+}
diff --git a/mission002/kimjieun/utils/constants.js b/mission002/kimjieun/utils/constants.js
new file mode 100644
index 0000000..8ffaed8
--- /dev/null
+++ b/mission002/kimjieun/utils/constants.js
@@ -0,0 +1,8 @@
+export const ENTER = 'Enter'
+export const DESTROY = 'destroy'
+export const TOGGLE = 'toggle'
+export const ACTIVE = 'active'
+export const COMPLETED = 'completed'
+export const ALLSELECTED = 'all selected'
+export const INIT = 'init'
+export const USERNAME = 'kimjieun'
diff --git a/mission002/kimjieun/utils/url.js b/mission002/kimjieun/utils/url.js
new file mode 100644
index 0000000..437b305
--- /dev/null
+++ b/mission002/kimjieun/utils/url.js
@@ -0,0 +1 @@
+export const hostUrl = 'http://todo-api.roto.codes'