diff --git a/README.md b/README.md index d86263b..5497fda 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,73 @@ -# todo -레벨2 +## STEP5-3 : TUNA & YM 리뷰 내용 반영 + +# +#### 1. comment : 전체코드의 들여쓰기가 너무 깊은데요? 좋은 indent 가 무엇인지 한번찾아볼래요? + +- 전체 코드 indent 수정 : tab 인덴트를 space * 4 로 수정 +- 원인 : pair를 위해 구름 ide에서 작업 중, local으로 옮기면서 indent 오반영 + +원래 ide의 설정은 tab이 space * 4 의 indent로 설정되어 있습니다. +앞으로 들여쓰기가 올바르게 되어있는지, 불편하지 않은지 신경쓰도록 하겠습니다 + +# + +#### 2. comment : 주석이 필요한.. arguments.length 로 구별하는 방식을 어떻게 개선할 수 있을까요? + +- 리뷰 이전 +```javascript +if (argument.length === 3) // updaste +... +} else if (arguments.length === 2) { // add, delete +``` + +- 리뷰 이후 : 의미가 불분명해 주석이 필요했던 if statement 수정 + +```javascript +if (command === 'update') { + ... +} else if (command === 'add' || command === 'delete') { + ... +} +``` + +# + +#### 3. utils 클래스의 deepCopy 메서드의 이해 + +- 원래 object.assign으로 얕은 복사를 썼으나, 예상한 방식대로 동작하지 않아 깊은 복사를 지원하는 deepCopy 유틸을 만들었습니다 +- 메서드는 검색을 통해 참고했으나, 참조 레퍼런스의 순수한 값만 복사하는 deep copy의 작동 방식은 이해했습니다 + +# + +#### 4. comment : 시작하는 것은 굳이 class로 만들지 않아도 될거 같아요. 사용일 될 일이 정말 없을 거 같은 부분이기도 하니까요. + +```javascript +const RunTodoApp = class { + constructor() { + this.cmdArr = ['show','delete','update','add','undo','redo']; + this.commandParser = new CommandParser(); +``` + +- 프로그램 실행을 담당하는 app.js의 class를 제거한 후, 리팩토링했습니다. + +# + +#### 5. comment : promise패턴을 사용했는데요. then메서드를 사용했을때의 장점은 무엇이라고 생각되세요? + +```javascript + this.utils.delay(0) + .then(() => {if (cmdList[0] === 'update') return this.utils.delay(3500);}) +``` + +- then메서드를 사용했을 경우, 새로운 프로미스 객체가 반환되어 여러 개의 Promise를 체이닝할 수 있습니다. +- 따라서, 여러 개의 비동기 작업을 수행할 경우, 보다 쉽게 비동기를 제어할 수 있다는 장점이 가장 큰 장점 같습니다. +- error 처리 또한 catch로 손쉽게 할 수 있다는 점도 장점입니다. + +# + +#### 6. comment : 이부분은 객체리터럴 vs. 클래스! 중 어떤 것이 더 어울릴까요? +```javascript +const CustomException = class { +``` +- 에러 처리는 변하는 값이 아닌 특수한 상황에서만 사용되는 고정된 값입니다. 따라서 동적인 인스턴스를 지원하는 class를 사요할 필요 없이 +속도가 빠르고 메모리 자원을 낭비하지 않는 리터럴이 적합하다 생각합니다. diff --git a/app.js b/app.js new file mode 100644 index 0000000..813e242 --- /dev/null +++ b/app.js @@ -0,0 +1,44 @@ +const readline = require('readline').createInterface( { + input:process.stdin, + output:process.stdout, +}); + +const CommandParser = require('./commandParser.js'); +const Utils = require('./utils.js'); +const Instruction = require('./instruction.js'); +const customException = require('./customException.js'); + +const cmdArr = ['show','delete','update','add','undo','redo']; +const commandParser = new CommandParser(); +const utils = new Utils(); +const instruction = new Instruction(); + +const run = (() => { + readline.setPrompt('명령하세요: '); + readline.prompt(); + readline.on('line', (userInput) => { + + try { + customException.missingSeperatorException(userInput); + const cmdList = commandParser.getCmdList(userInput); + + customException.CommandMissingException(cmdList[0], cmdArr); + commandParser.executeCmd(cmdList); + + utils.delay(0) + .then(() => {if (cmdList[0] === 'update') return utils.delay(3500);}) + .then(() => {return utils.delay(1000);}) + .then(() => {if (cmdList[0] !== 'show') instruction.show('all');}) + .catch(function(e) {console.log(e);}) + .then(() => {readline.prompt();}); + + } catch(e) { + console.error(e.message); + readline.prompt(); + } + +}).on('close', () => { + console.log("프로그램을 종료합니다."); + process.exit(); + }); +})(); diff --git a/commandParser.js b/commandParser.js new file mode 100644 index 0000000..655a073 --- /dev/null +++ b/commandParser.js @@ -0,0 +1,40 @@ +const Instruction = require('./instruction.js'); +const customException = require('./customException.js'); + +const CommandParser = class { + + constructor() { + this.instruction = new Instruction(); + } + + getCmdList(input) { + const regexp = /[^\$]+|undo|redo/g; + return input.match(regexp); + } + + executeCmd(command) { + try { + if (command.length === 1) { + this.instruction[command[0]](); + } else if (command.length === 2) { + this.instruction[command[0]](command[1]); + } else if (command.length === 3) { + this.instruction[command[0]](command[1], command[2]); + } else { + customException.CommandMissingException(); + } + + } catch (e) { + console.error(e.message); + return; + } + } + + isValidCommand(command, arr) { + let result = false; + if (arr.includes(command)) result = true; + return result; + } +}; + +module.exports = CommandParser; diff --git a/customException.js b/customException.js new file mode 100644 index 0000000..6af8bae --- /dev/null +++ b/customException.js @@ -0,0 +1,34 @@ +const CustomException = { + missingSeperatorException : function(input) { + if (!this.isValidSeperator(input)) throw new Error("구분자 $가 존재하지 않습니다"); + }, + + notExistIdException : function() { + throw new Error(`찾으시는 id가 존재하지 않습니다`); + }, + + sameStatusException : function() { + throw new Error(`같은 상태로 업데이트 할 수 없습니다`); + }, + + CommandMissingException : function(command, arr) { + if (!this.isValidCommand(command, arr)) + throw new Error(`올바른 명령어가 아닙니다`); + }, + + isValidSeperator : function(input) { + let result = true; + const regexp = /\$|undo|redo/g; + if (input.match(regexp) == null) result = false; + + return result; + }, + + isValidCommand : function(command, arr) { + let result = false; + if (arr.includes(command)) result = true; + return result; + } +}; + +module.exports = CustomException; \ No newline at end of file diff --git a/instruction.js b/instruction.js new file mode 100644 index 0000000..af2e3be --- /dev/null +++ b/instruction.js @@ -0,0 +1,181 @@ +const originData = require('./todosdata.json'); +const convertedData = JSON.parse(JSON.stringify(originData)).data; +const Utils = require('./utils.js'); +const customException = require('./customException.js'); + +const Instruction = class { + constructor() { + this.utils = new Utils(); + this.minIdNum = 1; + this.maxIdNum = 99999; + + this.commandHistory = { + historyArr : [], + pointer : -1, + }; + } + + everyStatus (convertedData) { + let [numOfTodos, numOfDoings, numOfDones] = [0,0,0]; + convertedData.forEach((value) => { + if (value.status === 'todo') numOfTodos++; + else if (value.status === 'doing') numOfDoings++; + else if (value.status === 'done') numOfDones++; + }); + + console.log(`현재상태 : todo: ${numOfTodos}개, doing: ${numOfDoings}개, done: ${numOfDones}개`); + } + + singleStatus (convertedData, status) { + const tasks = this.utils.getArrByCondition(convertedData, (val) => { return (status === val.status);}); + let message = `${status}리스트 총 ${tasks.length}건 : `; + tasks.forEach( obj => { + message += `'${obj.name}, ${obj.id}번,' `; + }); + console.log(message); + } + + show(status) { + const statusArr = ['all', 'todo', 'doing', 'done']; + if (status === 'all') { + this.everyStatus(convertedData); + } else if (statusArr.includes(status)) { + this.singleStatus(convertedData, status); + } + } + + add(name, tags) { + const id = this.utils.getRandomId(this.maxIdNum, this.minIdNum); + let obj = { + name, + tags, + status: 'todo', + id, + }; + convertedData.push(obj); + if (arguments.length !== 3) { + this.createHistoryObj('add' ,obj); + } + const message = `${obj.name} 1개가 추가됐습니다.(id : ${obj.id})`; + console.log(message); + } + + delete(id) { + const targetObj = this.utils.getArrByCondition(convertedData, (val) => { return id == val.id;})[0]; + try { + if (!targetObj) customException.notExistIdException(); + } catch (e) { + console.error(e.message); + return; + } + + convertedData.splice(convertedData.indexOf(targetObj), 1); + + if (arguments.length == 1) { + this.createHistoryObj('delete', obj); + } + let message = `${targetObj.name}이 ${targetObj.status}에서 삭제되었습니다.`; + console.log(message); + } + + update(id, status) { + const targetObj = this.utils.getArrByCondition(convertedData, (val) => { return id == val.id;})[0]; + try { + if (!targetObj) customException.notExistIdException(); + if (targetObj.status === status) customException.sameStatusException(); + } catch (e) { + console.error(e.message); + return; + } + + let preObj = {}; + if (arguments.length === 2) { + preObj = this.utils.deepCopy(targetObj); + } + + targetObj.status = status; + const nextObj = this.utils.deepCopy(targetObj); + + if (arguments.length === 2) { + this.createHistoryObj('update', preObj, nextObj); + } + const message = `${targetObj.name}가 ${targetObj.status}로 상태가 변경되었습니다`; + + setTimeout(() => { + console.log(message); + }, 3000); + } + + pushObj(arr, obj) { + if (arr.length === 3) { + arr.shift(); + arr.push(obj); + } else if (arr.length < 3) { + arr.push(obj); + } + } + + createHistoryObj(command, ...obj) { + let historyObj = {}; + if (command === 'update') { + historyObj = { + cmd: command, + pre: obj[0], + next: obj[1] + }; + } else if (command === 'add' || command === 'delete') { + historyObj = { + cmd: command, + pre: obj[0] + }; + } + this.pushObj(this.commandHistory.historyArr, historyObj); + this.commandHistory.pointer++; + } + + undo() { + const targetObject = this.commandHistory.historyArr[this.commandHistory.pointer]; + const [command, preObj, checkSum] = [targetObject.cmd, targetObject.pre, true]; + + let message = ``; + + if (command === 'add') { + this.delete(preObj.id, checkSum); + message += `${preObj.id}번 항목 '${preObj.name}'가 ${preObj.status} 상태에서 삭제됐습니다`; + + } else if (command === 'delete') { + convertedData.push(preObj); + message += `${preObj.id}번 항목 '${preObj.name}'가 삭제에서 ${preObj.status} 상태로 변경됐습니다`; + + } else if (command === 'update') { + this.update(preObj.id, preObj.status, checkSum); + } + + console.log(message); + + if (this.commandHistory.pointer > -1) this.commandHistory.pointer--; + } + + redo() { + if (this.commandHistory.pointer < 2) this.commandHistory.pointer++; + + const targetObject = this.commandHistory.historyArr[this.commandHistory.pointer]; + const [command, preObj, nextObj, checkSum] = [targetObject.cmd, targetObject.pre, targetObject.next, true]; + + let message = ``; + + if (command === 'add') { + convertedData.push(preObj); + message += `${preObj.id}번 항목 '${preObj.name}'가 ${preObj.status}에 추가되었습니다`; + + } else if (command === 'delete') { + this.delete(preObj.id, checkSum); + message += `${preObj.id}번 항목 '${preObj.name}'가 ${preObj.status} 상태에서 삭제됐습니다`; + } else if (command === 'update') { + this.update(nextObj.id, nextObj.status, checkSum); + } + console.log(message); + } +}; + +module.exports = Instruction; diff --git a/todosdata.json b/todosdata.json new file mode 100644 index 0000000..282d7ba --- /dev/null +++ b/todosdata.json @@ -0,0 +1,23 @@ +{ + "result" : true, + "data": [ + { + "name" : "자바스크립트 공부하기", + "tags" : ["programming", "javascript"], + "status" : "todo", + "id" : 123 + }, + { + "name" : "그림 그리기", + "tags" : ["picture", "favorite"], + "status" : "doing", + "id" : 312323 + }, + { + "name" : "파이썬 공부하기", + "tags" : ["programming", "javascript"], + "status" : "done", + "id" : 213232 + } + ] +} \ No newline at end of file diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..cfc0d4c --- /dev/null +++ b/utils.js @@ -0,0 +1,29 @@ +const Utils = class { + getArrByCondition (arr, condition) { + return arr.reduce((acc, val) => { + if (condition(val)) acc.push(val); + return acc; + }, []); + } + + getRandomId (max, min) { + return Math.floor(Math.random() * (max-min)) + 1; + } + + delay (time) { + return new Promise(function(resolve, reject){ + setTimeout(resolve, time); + }); + } + + deepCopy (obj) { + if (obj === null || typeof(obj) !== "object") return obj; + let copy = {}; + for(let key in obj) { + copy[key] = this.deepCopy(obj[key]); + } + return copy; + } +}; + +module.exports = Utils; \ No newline at end of file