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

[자동차 경주] 김진우 미션 제출합니다. #447

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,38 @@
# javascript-racingcar-precourse
# 🏫 우아한테크코스 7기 프리코스 2주차 미션: 자동차 경주
## 🎯 학습 목표
- 여러 역할을 수행하는 큰 함수를 단일 역할을 수행하는 작은 함수로 분리한다.
- 테스트 도구를 사용하는 방법을 배우고 프로그램이 제대로 작동하는지 테스트한다.
- 1주 차 공통 피드백을 최대한 반영한다.

## 📝 구현할 기능 목록

- [x] 경주할 자동차 이름 입력
- [x] 이동(시도)할 횟수 입력
- [x] 사용자가 잘못된 값을 입력할 경우 예외처리
- [x] 자동차 경주 차수별 실행 결과 출력
- [x] 단독 또는 공동 우승자 안내 문구 출력


## ❕ 프로그래밍 요구 사항
- [x] Node.js 20.17.0 버전에서 실행 가능해야 한다.
- [x] 프로그램 실행의 시작점은 App.js의 run()이다.
- [x] package.json 파일은 변경할 수 없으며, 제공된 라이브러리와 스타일 라이브러리 이외의 외부 라이브러리는 사용하지 않는다.
- [x] 프로그램 종료 시 process.exit()를 호출하지 않는다.
- [x] 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다.
- [x] 자바스크립트 코드 컨벤션을 지키면서 프로그래밍한다.
- [x] 기본적으로 JavaScript Style Guide를 원칙으로 한다.
- [x] indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- [x] 3항 연산자를 쓰지 않는다.
- [x] 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- [x] Jest를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코드로 확인한다.
- [x] @woowacourse/mission-utils에서 제공하는 Random 및 Console API를 사용하여 구현하기

## 예외 처리
- [x] 자동차 이름 다섯 글자 초과일 경우
- [x] 자동차 이름 공백이 포함될 경우
- [x] 자동차 이름 중복일 경우
- [x] 시도 횟수 숫자 아닐 경우
- [x] 시도 횟수 음수일 경우
- [x] 시도 횟수 소수일 경우
- [x] 자동차 이름의 입력 값이 없는 경우
- [x] 시도 횟수의 입력 값이 없는 경우
19 changes: 16 additions & 3 deletions __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import App from "../src/App.js";
import { MissionUtils } from "@woowacourse/mission-utils";
import { ERROR_MESSAGES } from "../src/constant.js";

const mockQuestions = (inputs) => {
MissionUtils.Console.readLineAsync = jest.fn();
Expand Down Expand Up @@ -46,15 +47,27 @@ describe("자동차 경주", () => {
});
});

test("예외 테스트", async () => {
// Parameterized Test
test.each([
{ name: ERROR_MESSAGES.CAR_NAMES_EMPTY, inputs: ["", "1"] },
{ name: ERROR_MESSAGES.CAR_NAME_CONTAINS_SPACE, inputs: ["pobi, woni"] },
{
name:ERROR_MESSAGES.CAR_NAME_TOO_LONG,
inputs: ["pobi,javada"],
},
{ name: ERROR_MESSAGES.CAR_NAME_DUPLICATE, inputs: ["pobi,pobi"] },
{ name: ERROR_MESSAGES.ATTEMPTS_EMPTY, inputs: ["pobi,woni", ""] },
{ name: ERROR_MESSAGES.ATTEMPTS_NOT_NUMBER, inputs: ["pobi,woni", "abc"] },
{ name: ERROR_MESSAGES.ATTEMPTS_NOT_INTEGER, inputs: ["pobi,woni", "1.5"] },
{ name: ERROR_MESSAGES.ATTEMPTS_NOT_POSITIVE, inputs: ["pobi,woni", "-1"] },
])("$name", async ({ name, inputs }) => {
// given
const inputs = ["pobi,javaji"];
mockQuestions(inputs);

// when
const app = new App();

// then
await expect(app.run()).rejects.toThrow("[ERROR]");
await expect(app.run()).rejects.toThrow(`${name}`);
});
});
25 changes: 24 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import { Console } from "@woowacourse/mission-utils";
import Car from "./models/Car.js";
import Race from "./models/Race.js";
import { validateAttempts, validateCarNames } from "./validators.js";
import { MESSAGES } from "./constant.js";

class App {
async run() {}
async run() {
// 경주에 참가할 자동차 이름 입력 받기
Console.print(
MESSAGES.PLEASE_CAR_NAMES
);
const carNames = await Console.readLineAsync("");
validateCarNames(carNames);
const cars = carNames.split(",").map((carName) => new Car(carName));

// 시도할 주행 횟수 입력
Console.print(MESSAGES.PLEASE_NUMBER_OF_ATTEMPTS);
const numOfAttempts = await Console.readLineAsync("");
validateAttempts(numOfAttempts);
const race = new Race(cars, numOfAttempts);

// 입력값을 바탕으로 주행 시작!
race.start();
}
}

export default App;
16 changes: 16 additions & 0 deletions src/constant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const MESSAGES = {
PLEASE_CAR_NAMES: "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)",
PLEASE_NUMBER_OF_ATTEMPTS: "시도할 횟수는 몇 회인가요?",
}

export const ERROR_MESSAGES = {
CAR_NAMES_EMPTY: "[ERROR] 자동차 이름이 입력되지 않았습니다.",
CAR_NAME_CONTAINS_SPACE: "[ERROR] 자동차 이름에 공백이 포함될 수 없습니다.",
CAR_NAME_TOO_LONG: "[ERROR] 이름은 5자 이하만 가능합니다.",
CAR_NAME_DUPLICATE: "[ERROR] 이름은 중복될 수 없습니다.",
ATTEMPTS_EMPTY: "[ERROR] 시도 횟수가 입력되지 않았습니다.",
ATTEMPTS_NOT_NUMBER: "[ERROR] 시도 횟수는 숫자여야 합니다.",
ATTEMPTS_NOT_INTEGER: "[ERROR] 시도 횟수는 정수여야 합니다.",
ATTEMPTS_NOT_POSITIVE: "[ERROR] 시도 횟수는 1 이상의 양수여야 합니다.",
};

29 changes: 29 additions & 0 deletions src/models/Car.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { MissionUtils } from "@woowacourse/mission-utils";

class Car {
#name;
#position;

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

// 0부터 9까지의 무작위 수를 뽑아 4 이상일 시 전진
move() {
const randomValue = MissionUtils.Random.pickNumberInRange(0, 9);
if (randomValue >= 4) {
this.#position++;
}
}

getPosition() {
return this.#position;
}

getName() {
return this.#name;
}
}

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

class Race {
#cars;
#attempts;

constructor(cars, attempts) {
this.#cars = cars;
this.#attempts = attempts;
}

// 정해진 횟수만큼 주행 시작
start() {
for (let i = 0; i < this.#attempts; i++) {
this.#cars.forEach((car) => car.move());
this.printRaceStatus();
}
this.printWinners();
}

// 경주 중간 결과를 출력
printRaceStatus() {
this.#cars.forEach((car) => {
Console.print(`${car.getName()} : ${"-".repeat(car.getPosition())}`);
});
Console.print("");
}

// 경주 최종 결과를 출력
printWinners() {
const maxPosition = Math.max(...this.#cars.map((car) => car.getPosition()));
const winners = this.#cars.filter(
(car) => car.getPosition() === maxPosition
);
const winnerNames = winners.map((car) => car.getName()).join(", ");
Console.print(`최종 우승자 : ${winnerNames}`);
}
}

export default Race;
39 changes: 39 additions & 0 deletions src/validators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ERROR_MESSAGES } from "./constant";

// 자동차 이름 입력에 대한 예외 처리
export function validateCarNames(carNames) {
if (!carNames || carNames.trim() === "") {
throw new Error(ERROR_MESSAGES.CAR_NAMES_EMPTY);
}

const uniqueNames = new Set();
carNames.split(',').forEach((name) => {
if (name.includes(" ")) {
throw new Error(ERROR_MESSAGES.CAR_NAME_CONTAINS_SPACE);
}
if (name.length > 5) {
throw new Error(ERROR_MESSAGES.CAR_NAME_TOO_LONG);
}
if (uniqueNames.has(name)) {
throw new Error(ERROR_MESSAGES.CAR_NAME_DUPLICATE);
}
uniqueNames.add(name);
});
}

// 시도 횟수 입력에 대한 예외 처리
export function validateAttempts(attempts) {
if (!attempts || attempts.trim() === "") {
throw new Error(ERROR_MESSAGES.ATTEMPTS_EMPTY);
}
const attemptsNumber = Number(attempts);
if (isNaN(attemptsNumber)) {
throw new Error(ERROR_MESSAGES.ATTEMPTS_NOT_NUMBER);
}
if (!Number.isInteger(attemptsNumber)) {
throw new Error(ERROR_MESSAGES.ATTEMPTS_NOT_INTEGER);
}
if (attemptsNumber <= 0) {
throw new Error(ERROR_MESSAGES.ATTEMPTS_NOT_POSITIVE);
}
}