-
Notifications
You must be signed in to change notification settings - Fork 172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[1단계 - 자동차 경주] 지니(손진영) 미션 제출합니다. #275
Changes from all commits
ab4fe61
f8d43c4
898bb38
8ec0d80
9ce6d4e
b2ab97d
389320b
b14db1c
27b4f4f
6ff6a4c
8960e06
51f756f
67063f0
410ac93
9c45c53
21fcd5f
2355080
31fd77c
57b59e1
bff467f
6bff77f
4cc1f97
2c20203
bb02fbd
2dca485
e433863
482d0fc
23f786c
7529bc4
623fceb
ce3c376
082d4ba
001a7d6
af88ae3
318e70a
e4528f4
264b647
a7debd2
a6b77bd
835405d
b02d738
ca2e771
5c1d2b9
c1efc23
95585b1
2f18fd5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,31 @@ | ||
{ | ||
"rules": {}, | ||
"env": { | ||
"es6": true, | ||
"node": true | ||
"es2021": true, | ||
"node": true, | ||
"jest": true | ||
}, | ||
"extends": ["airbnb-base", "prettier", "plugin:jsdoc/recommended", "plugin:jest/recommended"], | ||
"overrides": [ | ||
{ | ||
"files": ["*.test.js", "console.js"], | ||
"rules": { | ||
"max-lines-per-function": ["off"] | ||
} | ||
} | ||
], | ||
"parserOptions": { | ||
"ecmaVersion": "latest", | ||
"sourceType": "module" | ||
}, | ||
"extends": [ | ||
"eslint:recommended", | ||
"plugin:prettier/recommended" | ||
] | ||
} | ||
"rules": { | ||
"no-use-before-define": ["off"], | ||
"import/prefer-default-export": ["off"], | ||
"import/extensions": ["off"], | ||
"class-methods-use-this" : ["off"], | ||
"max-depth": ["error", 2], | ||
"max-lines-per-function": ["error", { "max": 10, "ignoreComments": true }], | ||
"jsdoc/require-returns" : ["off"], | ||
"jsdoc/require-jsdoc" : ["off"], | ||
"no-param-reassign" : ["off"] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"singleQuote": true, | ||
"semi": true, | ||
"useTabs": false, | ||
"tabWidth": 2, | ||
"trailingComma": "all", | ||
"printWidth": 120, | ||
"bracketSpacing": true, | ||
"arrowParens": "always", | ||
"endOfLine": "auto" | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
// Set the default | ||
"editor.formatOnSave": false, | ||
// Enable per-language | ||
"[javascript]": { | ||
"editor.formatOnSave": true | ||
}, | ||
"editor.codeActionsOnSave": { | ||
"source.fixAll.eslint": "explicit" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# 🎯 기능 구현 목록 | ||
## 공통 | ||
- [x] 공백이 포함되었을 경우, '입력한 값에 공백이 존재합니다.'와 함께 재입력 해야 한다. | ||
- [x] 입력 값이 빈 값일 경우 '아무것도 입력하지 않았으므로 다시 입력해주세요.'와 함께 재입력 해야 한다. | ||
|
||
## 예외 처리 | ||
- [x] 사용자가 잘못된 입력 값을 작성한 경우 에러 메시지를 보여주고, 다시 입력할 수 있게 한다. | ||
- [x] 에러가 발생 시 prefix로 `[ERROR]`를 추가한다. | ||
|
||
## 자동차 이름 입력 기능 (phase 1) | ||
- [x] 자동차 이름은 쉼표(,)를 기준으로 구분한다. | ||
- ,로 구분하지 않았을 경우 '자동차 이름은 ,로만 구분 가능합니다.' 라는 에러 메시지와 함께 재입력 해야 한다. | ||
- [x] 자동차 이름은 1 ~ 5자만 가능하다. | ||
- 자동차 이름이 범위에서 벗어날 경우 '자동차 이름은 1 ~ 5자의 범위만 가능합니다.'라는 에러메시지와 함께 재입력 해야 한다. | ||
|
||
## 자동차 시도 횟수 입력 기능 (phase 2) | ||
- [x] 사용자는 정수만 입력 가능하다. | ||
- 정수를 입력하지 않았을 경우 '정수만 입력 가능합니다.' 라는 에러 메시지와 함께 재입력 해야 한다. | ||
- [x] 사용자는 1 ~ 10의 범위만 입력 가능하다. | ||
- 1 ~ 10의 범위가 아닐 경우 '시도 횟수는 1 ~ 10만 입력 가능합니다.'라는 에러 메시지와 함께 재입력 해야 한다. | ||
|
||
## 자동차 이동 기능 (phase 3) | ||
- [x] 자동차에 이름을 부여할 수 있다. | ||
- [x] 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. | ||
|
||
## 우승자 확인 기능 (phase 4) | ||
- [x] 우승자는 한 명 이상일 수 있다. | ||
|
||
## 자동차 경주 결과 출력 기능 (phase 5) | ||
- [x] 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. | ||
- [x] 최종 우승자를 출력한다. | ||
|
||
# ✅ 프로그래밍 요구사항 | ||
- 프리코스 때 사용한 @woowacourse/mission-utils 라이브러리 사용을 금지한다. | ||
- 입출력(readline)과 관련된 테스트는 작성하지 않는다. | ||
- 입력 또는 출력이 잘 되었는지를 확인하는 테스트 | ||
- 입력 후 출력 값을 기반으로 결괏값이 잘 도출되었는지를 확인하는 테스트 | ||
- 코드 스타일 가이드에 따라 컨벤션을 준수하며 개발한다. | ||
- 변수 선언시 var를 사용하지 않는다. let, const를 사용한다. | ||
- 전역 변수를 만들지 않는다. | ||
- 축약하지 않는다. | ||
- 하드 코딩된 값 대신에 의미 있는 상수를 활용한다. | ||
- 동등 연산자는 === 로만 사용한다. | ||
- 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다. | ||
- 함수(또는 메서드)가 한 가지 일만 하도록 만든다. | ||
- 함수(또는 메서드)의 들여쓰기 depth는 2단계까지만 허용한다. | ||
- 예를 들어 while문 안에 if문이 있으면 depth는 2단계 이다. | ||
- 힌트) 함수(또는 메서드) 분리는 들여쓰기 depth를 줄이는 좋은 방법이다. | ||
- 도메인 로직과 UI 로직을 분리한다. | ||
- 모든 도메인 로직에 단위 테스트를 구현한다. (UI 로직은 제외) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import RacingGameController from './controller/RacingGameController.js'; | ||
|
||
const App = Object.freeze({ | ||
async start() { | ||
await RacingGameController.run(); | ||
}, | ||
}); | ||
|
||
export default App; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { SYMBOLS } from '../symbols.js'; | ||
|
||
export const INPUT_MESSAGE = Object.freeze({ | ||
racingCar: '경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)\n', | ||
tryCount: '시도할 횟수는 몇 회인가요?\n', | ||
}); | ||
|
||
export const OUTPUT_MESSAGE = Object.freeze({ | ||
executeResult: '\n실행 결과', | ||
movementIndicator: '-', | ||
}); | ||
|
||
export const FORMAT_MESSAGE = Object.freeze({ | ||
racingResultToString(racingResult) { | ||
const extractCarNameToString = ({ carName, moveCount }) => | ||
`${carName} : ${OUTPUT_MESSAGE.movementIndicator.repeat(moveCount)}`; | ||
|
||
const generatePartialRacingResultToString = (racingTurn) => racingTurn.map(extractCarNameToString).join('\n'); | ||
|
||
return racingResult.map(generatePartialRacingResultToString).join('\n\n'); | ||
}, | ||
|
||
racingWinnersToString(racingWinners) { | ||
return `\n최종 우승자: ${racingWinners.join(`${SYMBOLS.comma} `)}`; | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { FORMAT_MESSAGE } from './messages'; | ||
|
||
describe('자동차 경주 문자열 변환 테스트', () => { | ||
describe('racingResultToString 메서드를 통한 자동차 경주 결과 문자열 변환 테스트', () => { | ||
test.each([ | ||
{ | ||
description: | ||
"racingResult가 [[{ carName: 'CarA', moveCount: 1 }, { carName: 'CarB', moveCount: 1 }]] 일 때, \n 문자열 변환 결과는 'CarA : - CarB : -'이다.", | ||
racingResult: [ | ||
[ | ||
{ carName: 'CarA', moveCount: 1 }, | ||
{ carName: 'CarB', moveCount: 1 }, | ||
], | ||
], | ||
expected: 'CarA : -\nCarB : -', | ||
}, | ||
{ | ||
description: | ||
"racingResult가 [[{ carName: 'CarA', moveCount: 2 }, { carName: 'CarB', moveCount: 0 }], [{ carName: 'CarA', moveCount: 3 },{ carName: 'CarB', moveCount: 1 }]] 일 때, \n 문자열 변환 결과는 'CarA : -- CarB : CarA : --- CarB : -'이다.", | ||
racingResult: [ | ||
[ | ||
{ carName: 'CarA', moveCount: 2 }, | ||
{ carName: 'CarB', moveCount: 0 }, | ||
], | ||
[ | ||
{ carName: 'CarA', moveCount: 3 }, | ||
{ carName: 'CarB', moveCount: 1 }, | ||
], | ||
], | ||
expected: 'CarA : --\nCarB : \n\nCarA : ---\nCarB : -', | ||
}, | ||
])('$description', ({ racingResult, expected }) => { | ||
expect(FORMAT_MESSAGE.racingResultToString(racingResult)).toMatch(expected); | ||
}); | ||
}); | ||
|
||
describe('racingWinnersToString 메서드를 통한 우승자 문자열 변환 테스트', () => { | ||
test.each([ | ||
{ | ||
racingWinners: ['CarA'], | ||
expected: '\n최종 우승자: CarA', | ||
}, | ||
{ | ||
racingWinners: ['CarA', 'CarB'], | ||
expected: '\n최종 우승자: CarA, CarB', | ||
}, | ||
])('racingWinners가 $racingWinners일 때, 문자열 변환 결과는 "$expected다."', ({ racingWinners, expected }) => { | ||
expect(FORMAT_MESSAGE.racingWinnersToString(racingWinners)).toMatch(expected); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** | ||
* 현재 single export로 인해 import/prefer-default-export 관련 airbnb 컨벤션을 위배하고 있지만 | ||
* constants 모듈이 default export 셩격에 맞지 않다는 점과 random과 관련된 상수가 추가될 수 있다고 판단되어 | ||
* export 방식을 유지하기로 결정 | ||
*/ | ||
export const SYMBOLS = Object.freeze({ | ||
emptyString: '', | ||
space: ' ', | ||
comma: ',', | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import InputView from '../views/InputView.js'; | ||
import OutputView from '../views/OutputView.js'; | ||
import RacingGame from '../models/RacingGame/RacingGame.js'; | ||
import RandomMoveCountMaker from '../models/RandomMoveCountMaker/RandomMoveCountMaker.js'; | ||
import RacingWinnerRecorder from '../models/RacingWinnerRecorder/RacingWinnerRecorder.js'; | ||
import RetryHandler from '../errors/RetryHandler/RetryHandler.js'; | ||
|
||
const RacingGameController = Object.freeze({ | ||
async run() { | ||
const { racingCarNames, tryCount } = await processUserInput(); | ||
processRacingGame({ racingCarNames, tryCount }); | ||
}, | ||
}); | ||
|
||
export default RacingGameController; | ||
|
||
async function processUserInput() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 객체 내부에 존재하게 되면 public 하게 외부 모듈에서 접근이 가능하게 되기 때문입니다. controller 내부 로직을 굳이 외부에 알리지 않고 캡슐화 하고 싶은 목적에 외부에 작성하게 되었습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
라는 것은, 이것이 controller 내부 로직이라고 생각하신 건가요? 외부 로직이라고 생각하신 건가요? 외부에 작성했다면 더 이상 controller 내부 로직이 아닙니다. 그런 의도였다면 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다시 생각해보니 지그 의견 처럼 내부 동작에 관련된 로직을 객체 외부로 두게 될 경우 그 모듈의 내부 로직이 아닌 그 모듈과 관련된 함수 정도로 인식할 수도 있겠네요.. 제가 생각이 짧았던거 같습니다 👏 |
||
const racingCarNames = await RetryHandler.errorWithLogging(() => InputView.readRacingCarNames()); | ||
const tryCount = await RetryHandler.errorWithLogging(() => InputView.readTryCount()); | ||
|
||
return { racingCarNames, tryCount }; | ||
} | ||
|
||
function processRacingGame({ racingCarNames, tryCount }) { | ||
const racingGame = new RacingGame({ racingCarNames, tryCount }); | ||
|
||
const randomMoveCounts = RandomMoveCountMaker.execute(tryCount, racingCarNames.length); | ||
const racingResult = racingGame.startRace(randomMoveCounts); | ||
|
||
const finalRacingResult = racingResult.at(-1); | ||
const racingWinners = RacingWinnerRecorder.createRacingWinners(finalRacingResult); | ||
|
||
OutputView.printRacingResult(racingResult); | ||
OutputView.printRacingWinners(racingWinners); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/** | ||
* @module AppError | ||
* '일관된 에러 메시지 제공'의 역할을 수행 | ||
*/ | ||
class AppError extends Error { | ||
/** | ||
* @static | ||
* @public | ||
* @constant | ||
* @type {string} | ||
*/ | ||
static PREFIX = '[ERROR]'; | ||
|
||
/** | ||
* @param {string} message - 에러 메시지 | ||
*/ | ||
constructor(message) { | ||
super(`\n${AppError.PREFIX} ${message}\n`); | ||
} | ||
} | ||
|
||
export default AppError; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import AppError from './AppError'; | ||
|
||
describe('AppError 테스트', () => { | ||
const throwAppError = () => { | ||
throw new AppError('test'); | ||
}; | ||
test(`발생 된 에러 메시지는 ${AppError.PREFIX}으로 시작 한다.`, () => { | ||
// given - when - then | ||
expect(throwAppError).toThrow(AppError.PREFIX); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import Console from '../../utils/console.js'; | ||
|
||
/** | ||
* @module RetryHandler | ||
* 시스템 작업 중 발생하는 예외 처리 및 재 실행을 위한 모듈 | ||
*/ | ||
const RetryHandler = { | ||
/** | ||
* 제공된 비동기 함수를 실행 후 오류가 발생하지 않을 때 까지 오류 메시지를 출력하고 함수를 재 실행 | ||
* @template T | ||
* @param {Function} executeFunction - 실행할 비동기 함수 | ||
* @returns {Promise<T>} 비동기 함수가 성공적으로 완료되면 그 결과를 반환 | ||
*/ | ||
async errorWithLogging(executeFunction) { | ||
try { | ||
return await executeFunction(); | ||
} catch (error) { | ||
Console.print(error.message); | ||
return this.errorWithLogging(executeFunction); | ||
} | ||
}, | ||
}; | ||
|
||
export default RetryHandler; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import Console from '../../utils/console.js'; | ||
import RetryHandler from './RetryHandler.js'; | ||
|
||
jest.mock('../../utils/console.js', () => ({ | ||
print: jest.fn(), | ||
})); | ||
|
||
describe('입력 관련 예외 처리 테스트', () => { | ||
let executeTest; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
// given | ||
executeTest = jest.fn().mockRejectedValueOnce(new Error('Test Error')).mockResolvedValueOnce('Success'); | ||
}); | ||
|
||
test('함수가 두 번 호출된다.', async () => { | ||
// when | ||
await RetryHandler.errorWithLogging(executeTest); | ||
// then | ||
expect(executeTest).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
test('첫 번째 호출은 실패 후 에러 로깅이 발생한다.', async () => { | ||
// when | ||
await RetryHandler.errorWithLogging(executeTest); | ||
// then | ||
expect(Console.print).toHaveBeenCalledWith('Test Error'); | ||
}); | ||
|
||
test('두 번째 호출은 성공적인 결과를 반환한다.', async () => { | ||
// when | ||
const result = await RetryHandler.errorWithLogging(executeTest); | ||
// then | ||
expect(result).toMatch('Success'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import App from './App.js'; | ||
|
||
App.start(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
class Car { | ||
static #MIN_MOVABLE_VALUE = 4; | ||
|
||
static #CAR_MOVE_COUNT = 1; | ||
|
||
#carDetails; | ||
|
||
constructor(carName) { | ||
this.#carDetails = { | ||
carName, | ||
moveCount: 0, | ||
}; | ||
} | ||
|
||
move(randomMoveCount) { | ||
this.#updateMoveCount(randomMoveCount); | ||
|
||
return { ...this.#carDetails }; | ||
} | ||
|
||
#updateMoveCount(randomMoveCount) { | ||
if (this.#isMovable(randomMoveCount)) { | ||
this.#carDetails.moveCount += Car.#CAR_MOVE_COUNT; | ||
} | ||
} | ||
|
||
#isMovable(randomMoveCount) { | ||
return randomMoveCount >= Car.#MIN_MOVABLE_VALUE; | ||
} | ||
} | ||
|
||
export default Car; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
App
에Object.freeze()
를 사용하신 특별한 이유가 있으실까요?한 군데(index.js)서만 사용되고, 딱히 외부에서 변경될 가능성이 없는 객체라면 굳이 모든 것을
Object.freeze()
해줄 필요는 없을 것 같습니다.App
객체 안에 중요한 정보들이 있는 것도 아닌 것 같아서요There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다른 곳들(ex.
RacingGameController
)도 마찬가지입니다There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다른 곳에서 수정할 수 있는 여지를 막으려는 목적으로 read-only 성격의 Object.freeze()를 사용하게 되었습니다.
프로덕션(cli 기반 애플리케이션) 코드라고 가정 했을 때 App 객체 내 start 메서드가 변경 된다면 앱 전체가 멈춰버릴 수 있다고 생각했던거 같습니다. 🥲
지그가 언급해주신 중요한 정보라고 한다면 비즈니스 로직들이 있는 model layer 라고 생각하면 되는지 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아아 '중요한 정보'라고 언급한 건 특별한 의미는 아니었구요, 외부에서 의도적으로 변경할 만한 프로퍼티가 아니라고 생각했습니다.
말씀주신 대로 Car의 moveCount 등이 외부에서 변경될 가능성이 있는 요소들이겠죠?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아! 좋은 인사이트 감사합니다 !! 👍