[코드스쿼드] Lv2 과제



Lv2 과제

코드스쿼드 방학기간 (18년 04월 30일(월) ~ 18년 05월 6일(일)) 동안 @crong이 내준 과제입니다.
  1. TODO MVC 구조분석과 학습관리사이트 만들기

    http://todomvc.com/examples/vanilla-es6

    분석 이후에 나만의 나의 학습관리사이트 만들기.

    • 공부할 거 입력 가능

    • 완료체크 가능

    • 완료된 소요시간 측정 후 리포트(아무렇게나)

    • 분석

      • app.js

        // IMPORT
        import Controller from './controller';
        import {$on} from './helpers';
        import Template from './template';
        import Store from './store';
        import View from './view';
               
        // store.js
        // 첫번 째 분석 : store.js 부터 파보자
        const store = new Store('todos-vanilla-es6');
               
        const template = new Template();
        const view = new View(template);
               
        /**
         * @type {Controller}
         */
        const controller = new Controller(store, view);
               
        const setView = () => controller.setView(document.location.hash);
        $on(window, 'load', setView);
        $on(window, 'hashchange', setView);
        
      • controller.js

        import {emptyItemQuery} from './item';
        import Store from './store';
        import View from './view';
               
        export default class Controller {
        	/**
        	 * @param  {!Store} store A Store instance
        	 * @param  {!View} view A View instance
        	 */
        	constructor(store, view) {
        		this.store = store;
        		this.view = view;
               
        		view.bindAddItem(this.addItem.bind(this));
        		view.bindEditItemSave(this.editItemSave.bind(this));
        		view.bindEditItemCancel(this.editItemCancel.bind(this));
        		view.bindRemoveItem(this.removeItem.bind(this));
        		view.bindToggleItem((id, completed) => {
        			this.toggleCompleted(id, completed);
        			this._filter();
        		});
        		view.bindRemoveCompleted(this.removeCompletedItems.bind(this));
        		view.bindToggleAll(this.toggleAll.bind(this));
               
        		this._activeRoute = '';
        		this._lastActiveRoute = null;
        	}
               
        	/**
        	 * Set and render the active route.
        	 *
        	 * @param {string} raw '' | '#/' | '#/active' | '#/completed'
        	 */
        	setView(raw) {
        		const route = raw.replace(/^#\//, '');
        		this._activeRoute = route;
        		this._filter();
        		this.view.updateFilterButtons(route);
        	}
               
        	/**
        	 * Add an Item to the Store and display it in the list.
        	 *
        	 * @param {!string} title Title of the new item
        	 */
        	addItem(title) {
        		this.store.insert({
        			id: Date.now(),
        			title,
        			completed: false
        		}, () => {
        			this.view.clearNewTodo();
        			this._filter(true);
        		});
        	}
               
        	/**
        	 * Save an Item in edit.
        	 *
        	 * @param {number} id ID of the Item in edit
        	 * @param {!string} title New title for the Item in edit
        	 */
        	editItemSave(id, title) {
        		if (title.length) {
        			this.store.update({id, title}, () => {
        				this.view.editItemDone(id, title);
        			});
        		} else {
        			this.removeItem(id);
        		}
        	}
               
        	/**
        	 * Cancel the item editing mode.
        	 *
        	 * @param {!number} id ID of the Item in edit
        	 */
        	editItemCancel(id) {
        		this.store.find({id}, data => {
        			const title = data[0].title;
        			this.view.editItemDone(id, title);
        		});
        	}
               
        	/**
        	 * Remove the data and elements related to an Item.
        	 *
        	 * @param {!number} id Item ID of item to remove
        	 */
        	removeItem(id) {
        		this.store.remove({id}, () => {
        			this._filter();
        			this.view.removeItem(id);
        		});
        	}
               
        	/**
        	 * Remove all completed items.
        	 */
        	removeCompletedItems() {
        		this.store.remove({completed: true}, this._filter.bind(this));
        	}
               
        	/**
        	 * Update an Item in storage based on the state of completed.
        	 *
        	 * @param {!number} id ID of the target Item
        	 * @param {!boolean} completed Desired completed state
        	 */
        	toggleCompleted(id, completed) {
        		this.store.update({id, completed}, () => {
        			this.view.setItemComplete(id, completed);
        		});
        	}
               
        	/**
        	 * Set all items to complete or active.
        	 *
        	 * @param {boolean} completed Desired completed state
        	 */
        	toggleAll(completed) {
        		this.store.find({completed: !completed}, data => {
        			for (let {id} of data) {
        				this.toggleCompleted(id, completed);
        			}
        		});
               
        		this._filter();
        	}
               
        	/**
        	 * Refresh the list based on the current route.
        	 *
        	 * @param {boolean} [force] Force a re-paint of the list
        	 */
        	_filter(force) {
        		const route = this._activeRoute;
               
        		if (force || this._lastActiveRoute !== '' || this._lastActiveRoute !== route) {
        			/* jscs:disable disallowQuotedKeysInObjects */
        			this.store.find({
        				'': emptyItemQuery,
        				'active': {completed: false},
        				'completed': {completed: true}
        			}[route], this.view.showItems.bind(this.view));
        			/* jscs:enable disallowQuotedKeysInObjects */
        		}
               
        		this.store.count((total, active, completed) => {
        			this.view.setItemsLeft(active);
        			this.view.setClearCompletedButtonVisibility(completed);
               
        			this.view.setCompleteAllCheckbox(completed === total);
        			this.view.setMainVisibility(total);
        		});
               
        		this._lastActiveRoute = route;
        	}
        }
        
      • helpers.js

        /**
         * querySelector wrapper
         *
         * @param {string} selector Selector to query
         * @param {Element} [scope] Optional scope element for the selector
         */
        export function qs(selector, scope) {
        	return (scope || document).querySelector(selector);
        }
               
        /**
         * addEventListener wrapper
         *
         * @param {Element|Window} target Target Element
         * @param {string} type Event name to bind to
         * @param {Function} callback Event callback
         * @param {boolean} [capture] Capture the event
         */
        export function $on(target, type, callback, capture) {
        	target.addEventListener(type, callback, !!capture);
        }
               
        /**
         * Attach a handler to an event for all elements matching a selector.
         *
         * @param {Element} target Element which the event must bubble to
         * @param {string} selector Selector to match
         * @param {string} type Event name
         * @param {Function} handler Function called when the event bubbles to target
         *                           from an element matching selector
         * @param {boolean} [capture] Capture the event
         */
        export function $delegate(target, selector, type, handler, capture) {
        	const dispatchEvent = event => {
        		const targetElement = event.target;
        		const potentialElements = target.querySelectorAll(selector);
        		let i = potentialElements.length;
               
        		while (i--) {
        			if (potentialElements[i] === targetElement) {
        				handler.call(targetElement, event);
        				break;
        			}
        		}
        	};
               
        	$on(target, type, dispatchEvent, !!capture);
        }
               
        /**
         * Encode less-than and ampersand characters with entity codes to make user-
         * provided text safe to parse as HTML.
         *
         * @param {string} s String to escape
         *
         * @returns {string} String with unsafe characters escaped with entity codes
         */
        export const escapeForHTML = s => s.replace(/[&<]/g, c => c === '&' ? '&amp;' : '&lt;');
        
        • qs querySelector
        • $on addEventListener
      • item.js

        /**
         * @typedef {!{id: number, completed: boolean, title: string}}
         */
        export var Item;
               
        /**
         * @typedef {!Array<Item>}
         */
        export var ItemList;
               
        /**
         * Enum containing a known-empty record type, matching only empty records unlike Object.
         *
         * @enum {Object}
         */
        const Empty = {
        	Record: {}
        };
               
        /**
         * Empty ItemQuery type, based on the Empty @enum.
         *
         * @typedef {Empty}
         */
        export var EmptyItemQuery;
               
        /**
         * Reference to the only EmptyItemQuery instance.
         *
         * @type {EmptyItemQuery}
         */
        export const emptyItemQuery = Empty.Record;
               
        /**
         * @typedef {!({id: number}|{completed: boolean}|EmptyItemQuery)}
         */
        export var ItemQuery;
               
        /**
         * @typedef {!({id: number, title: string}|{id: number, completed: boolean})}
         */
        export var ItemUpdate;
        
        • export문

          export 문은 지정된 파일(또는 모듈)에서 함수 또는 오브젝트, 원시 타입들을 export 하는데 사용됩니다.

        • 왜 export 뒤에 var 가 오는것이 있고, const가 오는것이 있을까?

        • 주석의 의미를 잘 모르겠다

          {!({id; number}{complted: boolean}EmptyItemQuery)}

          ItemQuery 는 객체를 뜻하는 것인가?

      • store.js

        // 두번 째 분석
        // item.js 부터..
        import {Item, ItemList, ItemQuery, ItemUpdate, emptyItemQuery} from './item';
               
        export default class Store {
        	/**
        	 * @param {!string} name Database name
        	 * @param {function()} [callback] Called when the Store is ready
        	 */
        	constructor(name, callback) {
        		/**
        		 * @type {Storage}
        		 */
        		const localStorage = window.localStorage;
               
        		/**
        		 * @type {ItemList}
        		 */
        		let liveTodos;
               
        		/**
        		 * Read the local ItemList from localStorage.
        		 *
        		 * @returns {ItemList} Current array of todos
        		 */
        		this.getLocalStorage = () => {
        			return liveTodos || JSON.parse(localStorage.getItem(name) || '[]');
        		};
               
        		/**
        		 * Write the local ItemList to localStorage.
        		 *
        		 * @param {ItemList} todos Array of todos to write
        		 */
        		this.setLocalStorage = (todos) => {
        			localStorage.setItem(name, JSON.stringify(liveTodos = todos));
        		};
               
        		if (callback) {
        			callback();
        		}
        	}
               
        	/**
        	 * Find items with properties matching those on query.
        	 *
        	 * @param {ItemQuery} query Query to match
        	 * @param {function(ItemList)} callback Called when the query is done
        	 *
        	 * @example
        	 * db.find({completed: true}, data => {
        	 *	 // data shall contain items whose completed properties are true
        	 * })
        	 */
        	find(query, callback) {
        		const todos = this.getLocalStorage();
        		let k;
               
        		callback(todos.filter(todo => {
        			for (k in query) {
        				if (query[k] !== todo[k]) {
        					return false;
        				}
        			}
        			return true;
        		}));
        	}
               
        	/**
        	 * Update an item in the Store.
        	 *
        	 * @param {ItemUpdate} update Record with an id and a property to update
        	 * @param {function()} [callback] Called when partialRecord is applied
        	 */
        	update(update, callback) {
        		const id = update.id;
        		const todos = this.getLocalStorage();
        		let i = todos.length;
        		let k;
               
        		while (i--) {
        			if (todos[i].id === id) {
        				for (k in update) {
        					todos[i][k] = update[k];
        				}
        				break;
        			}
        		}
               
        		this.setLocalStorage(todos);
               
        		if (callback) {
        			callback();
        		}
        	}
               
        	/**
        	 * Insert an item into the Store.
        	 *
        	 * @param {Item} item Item to insert
        	 * @param {function()} [callback] Called when item is inserted
        	 */
        	insert(item, callback) {
        		const todos = this.getLocalStorage();
        		todos.push(item);
        		this.setLocalStorage(todos);
               
        		if (callback) {
        			callback();
        		}
        	}
               
        	/**
        	 * Remove items from the Store based on a query.
        	 *
        	 * @param {ItemQuery} query Query matching the items to remove
        	 * @param {function(ItemList)|function()} [callback] Called when records matching query are removed
        	 */
        	remove(query, callback) {
        		let k;
               
        		const todos = this.getLocalStorage().filter(todo => {
        			for (k in query) {
        				if (query[k] !== todo[k]) {
        					return true;
        				}
        			}
        			return false;
        		});
               
        		this.setLocalStorage(todos);
               
        		if (callback) {
        			callback(todos);
        		}
        	}
               
        	/**
        	 * Count total, active, and completed todos.
        	 *
        	 * @param {function(number, number, number)} callback Called when the count is completed
        	 */
        	count(callback) {
        		this.find(emptyItemQuery, data => {
        			const total = data.length;
               
        			let i = total;
        			let completed = 0;
               
        			while (i--) {
        				completed += data[i].completed;
        			}
        			callback(total, total - completed, completed);
        		});
        	}
        }
        
        • LocalStorage
        • Store 클래스

          • 생성자

            get set 인상적

          • find(query, callback)

            • query가 ItemQuery 라는것을 주석을 통해서 알 수 있었음
            • 같은 것이 있지 않다면 return false
          • update

            • getLocalStorage 를 통해서 todos 를 불러오고
            • 값을 수정한 후
              • while (t–)
            • setLocalStorage 를 통해서 바뀐 todos 를 저장
          • insert

            • todos 가 배열이라는 것을 확인 (push 메서드)
            • 역시 get, set 이용
          • remove

            • find와 비슷한 구조를 적용
            • filter 메서드를 자주 사용
          • count

            • 이 메서드뿐만 아니라 callback 은 무엇을 하는 용도일까?
      • template.js

        import {ItemList} from './item';
               
        import {escapeForHTML} from './helpers';
               
        export default class Template {
        	/**
        	 * Format the contents of a todo list.
        	 *
        	 * @param {ItemList} items Object containing keys you want to find in the template to replace.
        	 * @returns {!string} Contents for a todo list
        	 *
        	 * @example
        	 * view.show({
        	 *	id: 1,
        	 *	title: "Hello World",
        	 *	completed: false,
        	 * })
        	 */
        	itemList(items) {
        		return items.reduce((a, item) => a + `
        <li data-id="${item.id}"${item.completed ? ' class="completed"' : ''}>
        	<input class="toggle" type="checkbox" ${item.completed ? 'checked' : ''}>
        	<label>${escapeForHTML(item.title)}</label>
        	<button class="destroy"></button>
        </li>`, '');
        	}
               
        	/**
        	 * Format the contents of an "items left" indicator.
        	 *
        	 * @param {number} activeTodos Number of active todos
        	 *
        	 * @returns {!string} Contents for an "items left" indicator
        	 */
        	itemCounter(activeTodos) {
        		return `${activeTodos} item${activeTodos !== 1 ? 's' : ''} left`;
        	}
        }
        
      • view.js

        import {ItemList} from './item';
        import {qs, $on, $delegate} from './helpers';
        import Template from './template';
               
        const _itemId = element => parseInt(element.parentNode.dataset.id, 10);
        const ENTER_KEY = 13;
        const ESCAPE_KEY = 27;
               
        export default class View {
        	/**
        	 * @param {!Template} template A Template instance
        	 */
        	constructor(template) {
        		this.template = template;
        		this.$todoList = qs('.todo-list');
        		this.$todoItemCounter = qs('.todo-count');
        		this.$clearCompleted = qs('.clear-completed');
        		this.$main = qs('.main');
        		this.$toggleAll = qs('.toggle-all');
        		this.$newTodo = qs('.new-todo');
        		$delegate(this.$todoList, 'li label', 'dblclick', ({target}) => {
        			this.editItem(target);
        		});
        	}
               
               
        	/**
        	 * Put an item into edit mode.
        	 *
        	 * @param {!Element} target Target Item's label Element
        	 */
        	editItem(target) {
        		const listItem = target.parentElement;
               
        		listItem.classList.add('editing');
               
        		const input = document.createElement('input');
        		input.className = 'edit';
               
        		input.value = target.innerText;
        		listItem.appendChild(input);
        		input.focus();
        	}
               
        	/**
        	 * Populate the todo list with a list of items.
        	 *
        	 * @param {ItemList} items Array of items to display
        	 */
        	showItems(items) {
        		this.$todoList.innerHTML = this.template.itemList(items);
        	}
               
        	/**
        	 * Remove an item from the view.
        	 *
        	 * @param {number} id Item ID of the item to remove
        	 */
        	removeItem(id) {
        		const elem = qs(`[data-id="${id}"]`);
               
        		if (elem) {
        			this.$todoList.removeChild(elem);
        		}
        	}
               
        	/**
        	 * Set the number in the 'items left' display.
        	 *
        	 * @param {number} itemsLeft Number of items left
        	 */
        	setItemsLeft(itemsLeft) {
        		this.$todoItemCounter.innerHTML = this.template.itemCounter(itemsLeft);
        	}
               
        	/**
        	 * Set the visibility of the "Clear completed" button.
        	 *
        	 * @param {boolean|number} visible Desired visibility of the button
        	 */
        	setClearCompletedButtonVisibility(visible) {
        		this.$clearCompleted.style.display = !!visible ? 'block' : 'none';
        	}
               
        	/**
        	 * Set the visibility of the main content and footer.
        	 *
        	 * @param {boolean|number} visible Desired visibility
        	 */
        	setMainVisibility(visible) {
        		this.$main.style.display = !!visible ? 'block' : 'none';
        	}
               
        	/**
        	 * Set the checked state of the Complete All checkbox.
        	 *
        	 * @param {boolean|number} checked The desired checked state
        	 */
        	setCompleteAllCheckbox(checked) {
        		this.$toggleAll.checked = !!checked;
        	}
               
        	/**
        	 * Change the appearance of the filter buttons based on the route.
        	 *
        	 * @param {string} route The current route
        	 */
        	updateFilterButtons(route) {
        		qs('.filters>.selected').className = '';
        		qs(`.filters>[href="#/${route}"]`).className = 'selected';
        	}
               
        	/**
        	 * Clear the new todo input
        	 */
        	clearNewTodo() {
        		this.$newTodo.value = '';
        	}
               
        	/**
        	 * Render an item as either completed or not.
        	 *
        	 * @param {!number} id Item ID
        	 * @param {!boolean} completed True if the item is completed
        	 */
        	setItemComplete(id, completed) {
        		const listItem = qs(`[data-id="${id}"]`);
               
        		if (!listItem) {
        			return;
        		}
               
        		listItem.className = completed ? 'completed' : '';
               
        		// In case it was toggled from an event and not by clicking the checkbox
        		qs('input', listItem).checked = completed;
        	}
               
        	/**
        	 * Bring an item out of edit mode.
        	 *
        	 * @param {!number} id Item ID of the item in edit
        	 * @param {!string} title New title for the item in edit
        	 */
        	editItemDone(id, title) {
        		const listItem = qs(`[data-id="${id}"]`);
               
        		const input = qs('input.edit', listItem);
        		listItem.removeChild(input);
               
        		listItem.classList.remove('editing');
               
        		qs('label', listItem).textContent = title;
        	}
               
        	/**
        	 * @param {Function} handler Function called on synthetic event.
        	 */
        	bindAddItem(handler) {
        		$on(this.$newTodo, 'change', ({target}) => {
        			const title = target.value.trim();
        			if (title) {
        				handler(title);
        			}
        		});
        	}
               
        	/**
        	 * @param {Function} handler Function called on synthetic event.
        	 */
        	bindRemoveCompleted(handler) {
        		$on(this.$clearCompleted, 'click', handler);
        	}
               
        	/**
        	 * @param {Function} handler Function called on synthetic event.
        	 */
        	bindToggleAll(handler) {
        		$on(this.$toggleAll, 'click', ({target}) => {
        			handler(target.checked);
        		});
        	}
               
        	/**
        	 * @param {Function} handler Function called on synthetic event.
        	 */
        	bindRemoveItem(handler) {
        		$delegate(this.$todoList, '.destroy', 'click', ({target}) => {
        			handler(_itemId(target));
        		});
        	}
               
        	/**
        	 * @param {Function} handler Function called on synthetic event.
        	 */
        	bindToggleItem(handler) {
        		$delegate(this.$todoList, '.toggle', 'click', ({target}) => {
        			handler(_itemId(target), target.checked);
        		});
        	}
               
        	/**
        	 * @param {Function} handler Function called on synthetic event.
        	 */
        	bindEditItemSave(handler) {
        		$delegate(this.$todoList, 'li .edit', 'blur', ({target}) => {
        			if (!target.dataset.iscanceled) {
        				handler(_itemId(target), target.value.trim());
        			}
        		}, true);
               
        		// Remove the cursor from the input when you hit enter just like if it were a real form
        		$delegate(this.$todoList, 'li .edit', 'keypress', ({target, keyCode}) => {
        			if (keyCode === ENTER_KEY) {
        				target.blur();
        			}
        		});
        	}
               
        	/**
        	 * @param {Function} handler Function called on synthetic event.
        	 */
        	bindEditItemCancel(handler) {
        		$delegate(this.$todoList, 'li .edit', 'keyup', ({target, keyCode}) => {
        			if (keyCode === ESCAPE_KEY) {
        				target.dataset.iscanceled = true;
        				target.blur();
               
        				handler(_itemId(target));
        			}
        		});
        	}
        }
        
        • var avar $a
          • 일반 자바스크립트 객체
          • 첫글자에 $를 써서 jQuery 확장객체임을 쉽게 구별하려는 의도
          • javaScript 에서 $를 쓴다고해서 jQuery가 되는것은 아님!
  2. Lodash 메서드 만들기 - 20개 도전

    • lodash에서 제공하는 다양한 메서드를 랜덤하게 선택해서 20개 짜보기

    • function,collection,array, util 골고루 섞어서

    • 예제에서 동작하는 코드들이 똑같이 동작되도록 해보기

    • Array 관련 함수

      • findIndex

        var users = [
          { 'user': 'barney',  'active': false },
          { 'user': 'fred',    'active': false },
          { 'user': 'pebbles', 'active': true }
        ];
         
        _.findIndex(users, function(o) { return o.user == 'barney'; });
        // => 0
         
        // The `_.matches` iteratee shorthand.
        _.findIndex(users, { 'user': 'fred', 'active': false });
        // => 1
         
        // The `_.matchesProperty` iteratee shorthand.
        _.findIndex(users, ['active', false]);
        // => 0
         
        // The `_.property` iteratee shorthand.
        _.findIndex(users, 'active');
        // => 2
        
        // 직접구현
        var users = [
              { 'user': 'barney',  'active': false },
              { 'user': 'fred',    'active': false },
              { 'user': 'pebbles', 'active': true }
        ];
               
        function findIndex(array, target) {
               
            for (index in array) {
                const originalObjectString = JSON.stringify(array[index]);
                const targetObjectString = JSON.stringify(target);
                if (originalObjectString === targetObjectString) {
                    console.log(index);
                    return;
                }
                if (target instanceof Array) {
                    if (originalObjectString.includes(target[0]) && targetObjectString.includes(target[1])) {
                        console.log(index);
                        return;
                    }
                }
                if (typeof(target) === "string") {
                    if (originalObjectString.includes(target) && array[index][target]){
                        console.log(index);
                        return;
                    }   
                }
                if (target instanceof Function) {
                    if(target(array[index])) {
                        console.log(index);
                        return;
                    }
                }
            }
                   
        }
               
        findIndex(users, function(o) { return o.user == 'barney'; });
        findIndex(users, { 'user': 'fred', 'active': false });
        findIndex(users, ['active', false]);
        findIndex(users, 'active');
               
        
      • flatten

        // 배열안의 배열 값을 순서대로 나열합니다.(1depth만)
        console.log(flatten([1, [2, 3, [4]]]));
        // → [1, 2, 3, [4]]
               
        // 배열안의 배열 값을 깊이와 상관없이 순서대로 나열합니다.
        console.log(flatten([1, [2, 3, [4]]], true));
        // → [1, 2, 3, 4]
        
        // 직접 구현
        function flatten(array, mode) {
               
            const resultArray = [];
               
            if (mode) {
                const arrayString = JSON.stringify(array);
                const numbers = arrayString.replace(/[^0-9]/g, '').split("");
                       
                for (index in numbers) {
                    resultArray.push(parseInt(numbers[index]));
                }
               
                return resultArray;
            }
               
            for (index in array) {
                if (array[index] instanceof Array) {
                    for (id in array[index]) {
                        resultArray.push(array[index][id]);
                    }
                    return resultArray;
                }
                resultArray.push(array[index]);
            }
        }
               
        console.log(flatten([1, [2, 3, [4]]]));
        console.log(flatten([1, [2, 3, [4]]], true));
        
      • remove

        var array = [1, 2, 3, 4];
               
        // 원하는 조건에 해당되는 값은 기존 array에서 제거되어 배열로 반환됩니다.
        var evens = _.remove(array, function(n) {
          return n % 2 == 0;
        });
               
        console.log(array);
        // → [1, 3]
               
        console.log(evens);
        // → [2, 4]
        
        // 직접 구현
        var array = [1, 2, 3, 4];
               
        function remove(array, callback) {
            const resultArray = [];
            for (index in array) {
                if (callback(array[index])) {
                    resultArray.push(array[index]);
                    array.splice(index, index);
                }
            }
            return resultArray;
        }
               
        var evens = remove(array, function(n) {
            return n % 2 == 0;
        });
               
        console.log(array);
        console.log(evens);
        





© 2018. by HYEON

Powered by HYEON