Skip to content
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

[자동차 경주] 정석찬 미션 제출합니다. #433

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
# javascript-racingcar-precourse
# 자동차 경주

## 과제 진행 요구 사항
미션은 자동차 경주 저장소를 포크하고 클론하는 것으로 시작한다.
기능을 구현하기 전 README.md에 구현할 기능 목록을 정리해 추가한다.
Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로 추가한다.
AngularJS Git Commit Message Conventions을 참고해 커밋 메시지를 작성한다.
자세한 과제 진행 방법은 프리코스 진행 가이드 문서를 참고한다.

# 기능 요구 사항

- [x] 자동차 이름, 횟수 입력 받기
- [x] ',' 기준으로 이름 입력 (5자 이하)
- [x] 자연수 숫자 입력받기
- [x] 잘못된 값 입력시 에러처리
- [x] 유효한 숫자 아닌 경우 에러처리
- [x] 자동차 경주
- [x] 전진 기능 구현
- [x] 결과 출력 기능 구현
- [x] 최종 우승자 출력
- [x] 공동 우승 처리
- [x] ERROR 출력
60 changes: 60 additions & 0 deletions __tests__/Test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import App from "../src/App.js";
import { MissionUtils } from "@woowacourse/mission-utils";

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("자동차 경주", () => {
test("기능 테스트", async () => {
// given
const MOVING_FORWARD = 4;
const STOP = 3;
const inputs = ["dao,bazzi", "1"];
const logs = ["dao : -", "bazzi : ", "최종 우승자 : dao"];
const logSpy = getLogSpy();

mockQuestions(inputs);
mockRandoms([MOVING_FORWARD, STOP]);

// when
const app = new App();
await app.run();

// then
logs.forEach((log) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log));
});
});

test("예외 테스트", async () => {
// given
const inputs = ["pazzi,dao,,json"];
mockQuestions(inputs);

// when
const app = new App();

// then
await expect(app.run()).rejects.toThrow("[ERROR]");
});
});
29 changes: 28 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
import View from "./View.js";
import Car from "./Car.js";
import Race from "./Race.js";

class App {
async run() {}
#input = new View();
#race = new Race();

async run() {
const carNames = await this.#input.readInputCar();
const raceCount = await this.#input.readInputRaceCount();

this.#race.setRaceCount(raceCount);

// Race 객체에 Car객체 추가
carNames.forEach(carName => {
this.#race.addRacingCar(new Car(carName));
});

for (let count=0; count < this.#race.getRaceCount(); count ++){
this.#race.racing();
this.#race.showRacingResult();

}

this.#race.Winner();
this.#race.showWinner();

}
}

export default App;
24 changes: 24 additions & 0 deletions src/Car.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class Car {
#forwadCount=0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forwadCount -> forwardCount로 수정되어야 할 것 같습니다!

#name;

constructor(name) {
this.#name = name;
}

forward(random) {
if (random >= 4) {
this.#forwadCount++;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Airbnb JS 스타일 가이드에서는 단항 증감 연산자를 피하고 i+=과 같이 사용하라고 하네요!
https://github.com/airbnb/javascript?tab=readme-ov-file#variables--unary-increment-decrement

}
}

getName() {
return this.#name;
}

getForwardCount() {
return this.#forwadCount;
}
}

export default Car;
59 changes: 59 additions & 0 deletions src/Race.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Random, Console } from "@woowacourse/mission-utils";

class Race {
#cars = [];
#winners = [];
#raceCount;


addRacingCar(car) {
this.#cars.push(car);
}

setRaceCount(raceCount) {
this.#raceCount = raceCount;
}

getRaceCount() {
return this.#raceCount;
}

racing() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메서드나 함수의 이름은 동사로 시작하도록 짓는 것이 가독성이 좋을 것 같습니다!

https://velog.io/@pjc0247/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%98%81%EC%96%B4

this.#cars.forEach(car => {
const random = Random.pickNumberInRange(0, 9);
// 4 이상일 경우 전진
car.forward(random);
})
}

showRacingResult() {
// name : --- 형식으로 출력
this.#cars.forEach( car => {
Console.print(car.getName() + ' : ' + '-'.repeat(car.getForwardCount()));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Airbnb JS 스타일 가이드에서는 문자열을 연결할 때 template string 사용을 권장하는 것 같습니다!

https://github.com/airbnb/javascript?tab=readme-ov-file#es6-template-literals

추가적으로 사용자에게 입력을 받는 역할을 View 클래스에 위임하신 것처럼 결과를 출력하는 역할도 View 클래스에 위임하면 좋을 것 같아요!

현재는 결과 문자열을 만들어서 출력해주다보니 UI와 결합성이 강해 추후 UI가 변경되면 Race 클래스까지 변경해야 될 수도 있을 것 같습니다!

})
Console.print('');
}

Winner() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 함수는 우승자를 설정하는 역할을 하는 것 같은데 이름에서 그 역할을 잘 나타내지 못하는 것 같습니다!

특히 JS에서 변수나 함수, 메서드의 이름은 카멜 케이스로 짓는 것이 일반적이니 다른 이름을 고민해보시면 좋을 것 같네요!

let max = 0;
this.#cars.forEach(car => {
let forwardCount = car.getForwardCount();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forwardCount 변수는 추후에 재할당 되지 않으니 const로 선언해도 될 것 같습니다!

if (max < forwardCount) {
max = forwardCount;
}
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let max = 0;
this.#cars.forEach(car => {
let forwardCount = car.getForwardCount();
if (max < forwardCount) {
max = forwardCount;
}
})
const max = Math.max(...this.#cars.map(car => car.getForwardCount()));

로 짧게 줄일 수 있을 것 같습니다!


this.#cars.forEach(car => {
if (car.getForwardCount() == max){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS에서 == (동등 연산자)는 비교하는 두 값의 타입이 다르면, 타입을 강제로 변환하여 비교하기 때문에 타입 변환을 수행하지 않고 비교하는 === (일치 연산자)를 사용하는 것이 코드의 예측 가능성이 좋습니다!

https://github.com/airbnb/javascript?tab=readme-ov-file#comparison--eqeqeq

this.#winners.push(car.getName());
}
})
}

showWinner() {
Console.print('최종 우승자 : ' + this.#winners.join(','));
}

}

export default Race;
31 changes: 31 additions & 0 deletions src/View.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Console } from "@woowacourse/mission-utils";

class View {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View 클래스로 UI 로직을 분리한게 좋네요👍

#inputCarNameRegx = /^([a-zA-z]{1,5})(,[a-zA-Z]{1,5})*$/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정규식으로 유효성 검증하는게 좋네요👍

Regx 이름은 의도하신게 아니라면 Regex가 보다 일반적인 정규식 이름인 것 같습니다!

Code Spell Checker 익스텐션 설치해서 사용하시면 좋을 것 같네요


#carInputMessage = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n";
#raceCount = "시도할 횟수는 몇 회인가요?\n";

async readInputCar() {
const carNamesStr = await Console.readLineAsync(this.#carInputMessage);

if (!this.#inputCarNameRegx.test(carNamesStr)){
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

유효성 검사는 비즈니스 로직에 해당한다고 생각하는데 View에서 유효성 검사를 수행하면 UI와 비즈니스 로직의 결합이 강해져 추후 유효성 검사 로직이 변경되면 View까지 수정해야 되는 상황이 발생할 수 있을 것 같습니다!

View 클래스는 사용자와의 상호작용에 집중하고 유효성 검사는 별도의 클래스나 모듈로 분리하는 것이 변경에 유연하게 대응할 수 있을 것 같네요

throw new Error("[ERROR] : 잘못된 이름 입력\n");
}

const carNames = carNamesStr.split(',');
return carNames;
}

async readInputRaceCount() {
const raceCountStr = await Console.readLineAsync(this.#raceCount);
const raceCount = Number(raceCountStr);
if (raceCount==NaN || raceCount <= 0){
throw new Error("[ERROR] : 잘못된 숫자 입력\n");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS에서 NaN은 다른 어떤 값과도 같지 않아 raceCount == NaN은 항상 false를 반환합니다!(심지어 NaN == NaN or NaN === NaN도 false를 반환합니다)

raceCount가 NaN인지 검사하려면 Number.isNaN() 함수를 사용하시면 될 것 같습니다

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN


return raceCount;
}
}

export default View;