-
Notifications
You must be signed in to change notification settings - Fork 454
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
[자동차 경주] 임현우 미션 제출합니다. #434
base: main
Are you sure you want to change the base?
Changes from all commits
8352340
1331915
85a6cf0
bc408b3
9ddef92
c7cc5ee
0c10bb2
7704d12
33e6f2d
c98c96b
0685160
1937fb9
07acad7
a017a80
55e4822
a9be75b
8305a11
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 +1,87 @@ | ||
# javascript-racingcar-precourse | ||
|
||
## 구현할 기능 목록 | ||
|
||
### 1. 기능 구현 (App.js에서 모든 기능 구현) | ||
|
||
1. **입력 기능** | ||
- 사용자의 입력을 통해 경주에 참가할 자동차 이름과 시도할 횟수를 입력받습니다. | ||
- `경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)` | ||
- 예시 입력: `pobi,woni , jun` | ||
- 자동차 이름은 5자 이하로 제한되며, 앞뒤 공백이 제거됩니다. | ||
- 길이가 0인 이름은 입력될 수 없습니다. | ||
- 자동차를 **2개 이상** 입력해야 합니다. | ||
- **중복되는 자동차 이름**은 입력할 수 없습니다. | ||
- `시도할 횟수는 몇 회인가요?` | ||
- 예시 입력: `5` | ||
- 시도 횟수는 **1 이상의 양수**여야 합니다. | ||
- 잘못된 입력 시 `[ERROR]`로 시작하는 오류 메시지를 출력하고 프로그램을 종료합니다. | ||
|
||
2. **자동차 전진 기능** | ||
- 각 자동차는 **주어진 시도 횟수만큼 전진**하거나 **멈추는** 과정을 반복합니다. | ||
- **전진 조건**: 0에서 9 사이의 랜덤 숫자를 생성하여, 숫자가 **4 이상일 경우** 자동차가 전진합니다. | ||
- 전진 여부는 `Random.pickNumberInRange(0,9)`를 이용하여 결정합니다. | ||
|
||
3. **경주 진행 및 결과 출력 기능** | ||
- 각 시도마다 **모든 자동차의 위치**를 출력합니다. | ||
- 출력 예시: | ||
``` | ||
실행 결과 | ||
pobi : - | ||
woni : - | ||
jun : | ||
``` | ||
- 각 **라운드마다 경주 결과**를 모두 출력합니다. | ||
|
||
4. **우승자 출력 기능** | ||
- 모든 시도가 완료된 후 가장 멀리 전진한 자동차를 **우승자**로 선정합니다. | ||
- **우승자가 여러 명**일 경우 쉼표`(,)`로 구분하여 출력합니다. | ||
- 예시 출력: `최종 우승자 : pobi, jun` | ||
|
||
|
||
### 2. 리팩토링 계획 (기능 구현 후) | ||
|
||
1. **모듈화 작업** | ||
- 입력 처리, 게임 로직, 유효성 검사를 각각의 **독립적인 모듈**로 분리합니다. | ||
- 상수값 (메시지, 에러 메시지 등)을 **constants.js** 파일로 분리하여 관리합니다. | ||
- 주요 기능들을 각각 별도의 함수나 클래스로 분리하여 가독성을 높입니다. | ||
|
||
2. **네이밍 규칙 적용** | ||
- 변수, 함수, 클래스 이름을 **Airbnb JavaScript 스타일 가이드**에 맞춰 정리합니다. | ||
- 예를 들어, **상수**는 대문자와 밑줄(`_`)로 구분하고, 변수와 함수는 **카멜 케이스**를 사용합니다. | ||
|
||
3. **깊이(depth) 규칙 준수** | ||
- 들여쓰기 깊이를 최대 **2까지만 허용**하도록 코드를 정리합니다. | ||
- 중첩된 로직이 있을 경우, 이를 **함수로 분리**하여 깊이를 줄이는 방식으로 개선합니다. | ||
|
||
4. **코드 리뷰 및 클린 코드 적용** | ||
- 작성한 코드를 다시 검토하여 **중복 코드 제거**, **불필요한 주석 삭제** 등 **클린 코드** 원칙을 적용합니다. | ||
### 3. 구현 및 리팩토링 과정 | ||
- **1단계**: App.js에서 **모든 기능을 구현**하고 기능이 정상적으로 동작하는지 확인합니다. | ||
- **2단계**: 기능 구현이 완료되면, 코드를 **리팩토링**하여 가독성과 유지보수성을 높입니다. | ||
- **3단계**: Jest를 이용하여 **테스트 케이스 작성**을 진행합니다. | ||
- **입력 값 검증 테스트**: 자동차 이름이 올바른지, 시도 횟수가 유효한지를 검증합니다. | ||
- **파라미터화 테스트**: 다양한 입력 조합에 대해 반복 테스트하기 위해 `test.each()` 또는 `describe.each()`를 사용합니다. | ||
|
||
## 실행 예시 | ||
``` | ||
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) | ||
pobi,woni,jun | ||
시도할 횟수는 몇 회인가요? | ||
3 | ||
|
||
실행 결과 | ||
pobi : - | ||
woni : | ||
jun : - | ||
|
||
pobi : -- | ||
woni : - | ||
jun : -- | ||
|
||
pobi : -- | ||
woni : - | ||
jun : -- | ||
|
||
최종 우승자 : pobi, jun | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import App from '../src/App.js'; | ||
import { MissionUtils } from '@woowacourse/mission-utils'; | ||
import { | ||
VARIOUS_NAME_TEST_CASES, | ||
ROUND_TEST_CASE, | ||
TWO_WINNERS_TEST_CASE, | ||
EXCEPTION_TEST_CASES, | ||
} from '../data/testCasesData.js'; | ||
|
||
const mockQuestions = (inputs) => { | ||
MissionUtils.Console.readLineAsync = jest.fn(); | ||
MissionUtils.Console.readLineAsync.mockImplementation(() => { | ||
const input = inputs.shift(); | ||
return Promise.resolve(input); | ||
}); | ||
}; | ||
|
||
const mockRandoms = (numbers) => { | ||
MissionUtils.Random.pickNumberInRange = jest.fn(); | ||
numbers.reduce((acc, number) => { | ||
return acc.mockReturnValueOnce(number); | ||
}, MissionUtils.Random.pickNumberInRange); | ||
}; | ||
|
||
const getLogSpy = () => { | ||
const logSpy = jest.spyOn(MissionUtils.Console, 'print'); | ||
logSpy.mockClear(); | ||
return logSpy; | ||
}; | ||
|
||
describe('자동차 경주', () => { | ||
describe('기능 테스트', () => { | ||
test.each(VARIOUS_NAME_TEST_CASES)( | ||
'$description', | ||
async ({ inputs, logs }) => { | ||
const MOVING_FORWARD = 4; | ||
const STOP = 3; | ||
const logSpy = getLogSpy(); | ||
|
||
mockQuestions(inputs); | ||
mockRandoms([MOVING_FORWARD, STOP]); | ||
|
||
const app = new App(); | ||
await app.run(); | ||
|
||
for (const log of logs) { | ||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); | ||
} | ||
} | ||
); | ||
}); | ||
|
||
describe('기능 테스트 - 각 라운드 출력', () => { | ||
test('각 라운드 출력 확인', async () => { | ||
const { inputs, logs } = ROUND_TEST_CASE; | ||
const MOVING_FORWARD = 4; | ||
const STOP = 3; | ||
const logSpy = getLogSpy(); | ||
|
||
mockQuestions(inputs); | ||
mockRandoms([ | ||
MOVING_FORWARD, | ||
STOP, | ||
MOVING_FORWARD, | ||
STOP, | ||
MOVING_FORWARD, | ||
STOP, | ||
]); | ||
|
||
const app = new App(); | ||
await app.run(); | ||
|
||
for (const log of logs) { | ||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); | ||
} | ||
}); | ||
}); | ||
|
||
describe('기능 테스트 - 우승자 2인', () => { | ||
test('우승자 2인', async () => { | ||
const { inputs, logs } = TWO_WINNERS_TEST_CASE; | ||
const MOVING_FORWARD = 4; | ||
const logSpy = getLogSpy(); | ||
|
||
mockQuestions(inputs); | ||
mockRandoms([MOVING_FORWARD, MOVING_FORWARD]); | ||
|
||
const app = new App(); | ||
await app.run(); | ||
|
||
for (const log of logs) { | ||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); | ||
} | ||
}); | ||
}); | ||
|
||
describe('자동차 경주 예외 테스트', () => { | ||
test.each(EXCEPTION_TEST_CASES)( | ||
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. 이런식으로 한번에 예외처리도 가능했군요!! |
||
'입력 값 검증 - 예외 테스트', | ||
async ({ inputs, expectedError }) => { | ||
mockQuestions(inputs); | ||
|
||
const app = new App(); | ||
|
||
await expect(app.run()).rejects.toThrow(expectedError); | ||
} | ||
); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { ERROR_MESSAGE } from '../src/constants/errorMessages.js'; | ||
|
||
const { | ||
INVALID_CAR_NAMES_LENGTH, | ||
INVALID_CAR_NAME_LENGTH, | ||
DUPLICATE_CAR_NAMES, | ||
INVALID_TRY_COUNT, | ||
} = ERROR_MESSAGE; | ||
|
||
const VARIOUS_NAME_TEST_CASES = [ | ||
{ | ||
description: '한글, 숫자, 특수문자 통과 테스트', | ||
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. 한글 및 영어 여부를 테스트 케이스로 분리하신 이유가 있으실까요? 한글이든 영어든 둘다 결국 string 값이고 5자의 글자 제한이 들어간건 동일해서 굳이 분리하신 이유가 따로 있을지 궁금합니다! 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. 테스트 케이스여서 넣었었습니다. 사실 저는 이런 테스트 케이스 작성은 처음이라 이런 걸 넣어도 작동을 한다~ 보여주는 의미인가 싶어서 넣었죠.... |
||
inputs: ['한글1,한글2', '1'], | ||
logs: ['한글1 : -', '한글2 : ', '최종 우승자 : 한글1'], | ||
}, | ||
{ | ||
description: '숫자만 포함된 자동차 이름 테스트', | ||
inputs: ['1234,5678', '1'], | ||
logs: ['1234 : -', '5678 : ', '최종 우승자 : 1234'], | ||
}, | ||
{ | ||
description: '한글과 숫자가 혼합된 자동차 이름 테스트', | ||
inputs: ['한글123,한글456', '1'], | ||
logs: ['한글123 : -', '한글456 : ', '최종 우승자 : 한글123'], | ||
}, | ||
{ | ||
description: '특수문자만 포함된 자동차 이름 테스트', | ||
inputs: ['!@#$,%^&*', '1'], | ||
logs: ['!@#$ : -', '%^&* : ', '최종 우승자 : !@#$'], | ||
}, | ||
{ | ||
description: '한글과 특수문자가 혼합된 자동차 이름 테스트', | ||
inputs: ['한글!@#,한글%^&', '1'], | ||
logs: ['한글!@# : -', '한글%^& : ', '최종 우승자 : 한글!@#'], | ||
}, | ||
{ | ||
description: '한글과 영문자가 혼합된 자동차 이름 테스트', | ||
inputs: ['한글abc,한글xyz', '1'], | ||
logs: ['한글abc : -', '한글xyz : ', '최종 우승자 : 한글abc'], | ||
}, | ||
]; | ||
|
||
const ROUND_TEST_CASE = { | ||
inputs: ['pobi,woni', '3'], | ||
logs: [ | ||
'pobi : -', | ||
'woni : ', | ||
'', // 첫 라운드 후 빈 줄 출력 | ||
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. 감사합니다. 저는 저렇게 됐을 때 입력 값이 pobi, woni, '' 로 착각할 수 있지 않을까? 라는 우려가 있었습니다. |
||
'pobi : --', | ||
'woni : ', | ||
'', // 두 번째 라운드 후 빈 줄 출력 | ||
'pobi : ---', | ||
'woni : ', | ||
'', // 세 번째 라운드 후 빈 줄 출력 | ||
'최종 우승자 : pobi', | ||
], | ||
}; | ||
|
||
const TWO_WINNERS_TEST_CASE = { | ||
inputs: ['pobi,woni', '1'], | ||
logs: ['pobi : -', 'woni : -', '최종 우승자 : pobi, woni'], | ||
}; | ||
|
||
const EXCEPTION_TEST_CASES = [ | ||
{ | ||
inputs: ['pobi'], | ||
expectedError: INVALID_CAR_NAMES_LENGTH, | ||
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. 개인적인 취향이 될 수도 있지만, 자칫
|
||
}, | ||
{ | ||
inputs: ['pobi,moon,'], | ||
expectedError: INVALID_CAR_NAME_LENGTH, | ||
}, | ||
{ | ||
inputs: ['pobicar,moon'], | ||
expectedError: INVALID_CAR_NAME_LENGTH, | ||
}, | ||
{ | ||
inputs: ['pobi,pobi'], | ||
expectedError: DUPLICATE_CAR_NAMES, | ||
}, | ||
Comment on lines
+78
to
+80
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. 지금보니 공백인 경우 |
||
{ | ||
inputs: ['pobi,woni', '0'], | ||
expectedError: INVALID_TRY_COUNT, | ||
}, | ||
{ | ||
inputs: ['pobi, woni', '-1'], | ||
expectedError: INVALID_TRY_COUNT, | ||
}, | ||
{ | ||
inputs: ['pobi,woni ', 'a'], | ||
expectedError: INVALID_TRY_COUNT, | ||
}, | ||
]; | ||
Comment on lines
+90
to
+93
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. 여기도 마찬가지로 입력값이 공백과 빈 값이 들어왔을때에 대한 예외처리가 빠져있는 것 같습니다. |
||
|
||
export { | ||
VARIOUS_NAME_TEST_CASES, | ||
ROUND_TEST_CASE, | ||
TWO_WINNERS_TEST_CASE, | ||
EXCEPTION_TEST_CASES, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,50 @@ | ||
import { userInputCarNames, userInputTryCount } from './utils/getInputUtils.js'; | ||
import { splitAndRemoveWhitespace } from './utils/carNameUtils.js'; | ||
import { | ||
printRoundStatus, | ||
printToStartGame, | ||
printWinners, | ||
} from './utils/printUtils.js'; | ||
import { validateCarNames, validateTryCount } from './validate/validateion.js'; | ||
import { createCarObjectsFromNames } from './createCarObjects.js'; | ||
import { moveCars } from './gameLogic.js'; | ||
import { | ||
getMaxPosition, | ||
filterCarsByMaxPosition, | ||
extractCarNames, | ||
} from './utils/raceResultUtils.js'; | ||
|
||
class App { | ||
async run() {} | ||
async run() { | ||
try { | ||
const inputCarNames = await userInputCarNames(); | ||
const processedCarNames = splitAndRemoveWhitespace(inputCarNames); | ||
|
||
validateCarNames(processedCarNames); | ||
|
||
const cars = createCarObjectsFromNames(processedCarNames); | ||
|
||
const inputTryCount = await userInputTryCount(); | ||
|
||
const tryCount = Number(inputTryCount); | ||
validateTryCount(tryCount); | ||
|
||
printToStartGame(); | ||
for (let i = 0; i < tryCount; i++) { | ||
moveCars(cars); | ||
printRoundStatus(cars); | ||
} | ||
|
||
const maxPosition = getMaxPosition(cars); | ||
|
||
const result = filterCarsByMaxPosition(cars, maxPosition); | ||
|
||
const winners = extractCarNames(result); | ||
printWinners(winners); | ||
} catch (error) { | ||
throw error; | ||
} | ||
Comment on lines
+44
to
+46
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. error 를 catch해서 다시 throw 해 주는 특별한 이유가 있나요?? 여기서 catch 해주지 않아도 동일한 결과가 나올 것 같아서요! 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. 감사합니다. 처음에 배울 때 이렇게 해라~ 를 듣고 관습적으로 항상 해왔던 것 같아요..! |
||
} | ||
} | ||
|
||
export default App; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
class Car { | ||
constructor(name) { | ||
this.name = name; | ||
this.position = 0; | ||
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. 외부에서 클래스 내의 변수에 접근할 수 없도록 private 변수로 활용해보시는 것은 어떨까요? ( 이 부분은 저도 받았던 피드백이라 공유드리고 싶어서 말씀드려요 !) 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. 감사합니다. 이번주 미션은 그렇게 해봐야겠어요! |
||
} | ||
|
||
move() { | ||
this.position += 1; | ||
} | ||
} | ||
|
||
export { Car }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
const ERROR_MESSAGE = { | ||
INVALID_CAR_NAMES_LENGTH: '[ERROR] 자동차는 2개 이상 입력해야 합니다.', | ||
INVALID_CAR_NAME_LENGTH: | ||
'[ERROR] 자동차 이름은 1글자 이상 5글자 이하로 입력해야 합니다.', | ||
DUPLICATE_CAR_NAMES: '[ERROR] 중복되는 자동차 이름은 입력할 수 없습니다.', | ||
INVALID_TRY_COUNT: '[ERROR] 시도할 횟수는 양의 정수여야 합니다.', | ||
}; | ||
|
||
export { ERROR_MESSAGE }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Car } from './Car.js'; | ||
|
||
const createCarObjectsFromNames = (carNames) => { | ||
return carNames.map((name) => new Car(name)); | ||
}; | ||
|
||
export { createCarObjectsFromNames }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Random } from '@woowacourse/mission-utils'; | ||
|
||
const moveOnFourOrOver = () => { | ||
const randomNumber = Random.pickNumberInRange(0, 9); | ||
return randomNumber >= 4; | ||
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 moveCars = (cars) => { | ||
for (const car of cars) { | ||
if (moveOnFourOrOver()) { | ||
car.move(); | ||
} | ||
} | ||
}; | ||
|
||
export { moveCars }; |
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.
개인적인 의견이고 저도 아직 정답이 뭔지 모르겠지만,, 테스트 코드 안에서 쓰이는 테스트 케이스들을 변수화해서 테스트코드 밖에서 관리하게 된다면 실제 쓰이는 데이터들을 확인하기 위해서 읽던 파일을 이동해야 하니 가독성에 좋지 않다는 생각이 들기도 합니다...! 재사용되는 것이 아니라면 test.each에 바로 작성해주는 것도 좋을 것 같아요!!