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

[자동차 경주] 김태인 미션 제출합니다. #450

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,37 @@
# javascript-racingcar-precourse

## 기능 구현 목록

### 1. 사용자 입력 처리

- [x] 경주할 자동차 이름 입력받기
- [x] 쉼표(,)를 기준으로 문자열 구분
- [x] 시도할 횟수 입력받기

### 2. 자동차 전진 기능

- [x] 자동차 당 0~9 사이 무작위 값 구하기
- [x] 4 이상일 시 전진

### 3. 결과 출력

- [x] 각 횟수마다 자동차들의 전진 현황 출력(-)
- [x] 마지막 횟수가 끝나면 우승자 출력
- [x] 공동 우승자일 경우 , (쉼표와 띄어쓰기)로 구분

### 4. 예외처리

- 시도 횟수 예외
- [x] 시도 횟수가 공백일 경우 Error 발생
- [x] 입력된 문자가 숫자가 아닐 경우Error 발생
- 자동차 이름 예외
- [x] 각 이름이 5자 이하인지 확인 후 아닐 시 Error 발생
- [x] 자동차가 2개 미만일 경우 Error 발생
- [x] 자동차의 이름이 공백일 경울 Error 발생
- [x] 자동차의 이름이 중복될 경우 Error 발생

### 5. 단위 테스트

- [x] 자동차 이름 입력 유효성 테스트
- [x] 시도 횟수 입력 유효성 테스트
- [x] 전체 테스트
78 changes: 78 additions & 0 deletions __tests__/ValidationTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { validateCarNames, validateTryCount } from "../src/util/validation";
import { CAR_ERROR, NUMBER_ERROR } from "../src/util/constants";

describe("자동차 이름 입력 유효성 검사", () => {
test("정상적인 자동차 이름 입력", () => {
const input = "pobi,woni,jun";
const expected = ["pobi", "woni", "jun"];
expect(validateCarNames(input)).toEqual(expected);
});

test("자동차 이름이 공백인 경우", () => {
const input = " ";
expect(() => validateCarNames(input)).toThrow(CAR_ERROR.CAR_BLANK);
});

test("자동차 이름에 공백이 포함된 경우 trim 처리", () => {
const input = "pobi , woni , jun";
const expected = ["pobi", "woni", "jun"];
expect(validateCarNames(input)).toEqual(expected);
});

test("자동차가 1대인 경우", () => {
const input = "pobi";
expect(() => validateCarNames(input)).toThrow(CAR_ERROR.CAR_OVER_2);
});

test("자동차 이름이 5자를 초과하는 경우", () => {
const input = "pobi,woni,junius";
expect(() => validateCarNames(input)).toThrow(CAR_ERROR.CAR_UNDER_5);
});

test("자동차 이름이 중복되는 경우", () => {
const input = "pobi,woni,pobi";
expect(() => validateCarNames(input)).toThrow(CAR_ERROR.CAR_DUPLICATE);
});

test("자동차 이름 중 하나가 공백인 경우", () => {
const input = "pobi,,woni";
expect(() => validateCarNames(input)).toThrow(CAR_ERROR.CAR_BLANK);
});
});

describe("시도 횟수 입력 유효성 검사", () => {
test("정상적인 시도 횟수 입력", () => {
const input = "5";
expect(validateTryCount(input)).toBe(5);
});

test("시도 횟수가 공백인 경우", () => {
const input = " ";
expect(() => validateTryCount(input)).toThrow(NUMBER_ERROR.NUMBER_BLANK);
});

test("시도 횟수가 숫자가 아닌 경우", () => {
const input = "abc";
expect(() => validateTryCount(input)).toThrow(NUMBER_ERROR.NUMBER_NONNUM);
});

test("시도 횟수가 음수인 경우", () => {
const input = "-1";
expect(() => validateTryCount(input)).toThrow(NUMBER_ERROR.NUMBER_NONNUM);
});

test("시도 횟수가 0인 경우", () => {
const input = "0";
expect(() => validateTryCount(input)).toThrow(NUMBER_ERROR.NUMBER_NONNUM);
});

test("시도 횟수가 소수인 경우", () => {
const input = "1.5";
expect(() => validateTryCount(input)).toThrow(NUMBER_ERROR.NUMBER_NONNUM);
});

test("시도 횟수가 공백을 포함한 숫자인 경우", () => {
const input = " 5 ";
expect(validateTryCount(input)).toBe(5);
});
});
32 changes: 31 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
import { readInput } from "./util/missionUtil";
import { validateCarNames, validateTryCount } from "./util/validation";
import Race from "./Race";

class App {
async run() {}
async play() {
const carNames = await this.#getCarNames();
const tryCount = await this.#getTryCount();

const race = new Race(carNames);
race.race(tryCount);
}

async #getCarNames() {
const input = await readInput(
"경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"
);
return validateCarNames(input);
}

async #getTryCount() {
const input = await readInput("시도할 횟수는 몇 회인가요?");
return validateTryCount(input);
}

async run() {
try {
await this.play();
} catch (error) {
throw error;
}
}
}

export default App;
33 changes: 33 additions & 0 deletions src/Car.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { pickNumberInRange, printOutput } from "./util/missionUtil";

export default class Car {
#name;
#position = "";
#min = 0;
#max = 9;

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

moveForward() {
const RANDOM_NUMBER = pickNumberInRange(this.#min, this.#max);
if (RANDOM_NUMBER >= 4) {
this.setPosition("-");
}
const STATUS = `${this.#name} : ${this.#position}`;
return STATUS;
}

setPosition(position) {
this.#position += position;
}

getName() {
return this.#name;
}

getPosition() {
return this.#position;
}
}
42 changes: 42 additions & 0 deletions src/Race.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { printOutput } from "./util/missionUtil";
import Car from "./Car";

export default class Race {
#cars;

constructor(carNames) {
this.#cars = carNames.map((name) => new Car(name));
}

race(tryCount) {
printOutput("\n실행 결과");

for (let i = 0; i < tryCount; i++) {
this.#moveAllCars();
printOutput("");
}

this.#announceWinners();
}

#moveAllCars() {
this.#cars.forEach((car) => {
const status = car.moveForward();
printOutput(status);
});
}

#getMaxPosition() {
return Math.max(...this.#cars.map((car) => car.getPosition().length));
}

#announceWinners() {
const maxPosition = this.#getMaxPosition();
const winners = this.#cars
.filter((car) => car.getPosition().length === maxPosition)
.map((car) => car.getName())
.join(", ");

printOutput(`최종 우승자 : ${winners}`);
}
}
11 changes: 11 additions & 0 deletions src/util/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const NUMBER_ERROR = Object.freeze({
NUMBER_BLANK: "[ERROR] 입력하신 시도 횟수가 공백입니다.",
NUMBER_NONNUM: "[ERROR] 입력하신 시도 횟수가 숫자가 아닙니다.",
});

export const CAR_ERROR = Object.freeze({
CAR_UNDER_5: "[ERROR] 입력하신 자동차의 이름이 5자를 초과합니다.",
CAR_OVER_2: "[ERROR] 입력하신 자동차의 개수가 2개 미만입니다.",
CAR_BLANK: "[ERROR] 입력하신 자동차의 이름이 공백입니다.",
CAR_DUPLICATE: "[ERROR] 입력하신 자동차의 이름이 중복입니다.",
});
13 changes: 13 additions & 0 deletions src/util/missionUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Console, Random } from "@woowacourse/mission-utils";

export const readInput = (content) => {
return Console.readLineAsync(content);
};

export const printOutput = (content) => {
return Console.print(content);
};

export const pickNumberInRange = (min, max) => {
return Random.pickNumberInRange(min, max);
};
42 changes: 42 additions & 0 deletions src/util/validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { NUMBER_ERROR, CAR_ERROR } from "./constants";

export const validateCarNames = (names) => {
if (!names.trim()) {
throw new Error(CAR_ERROR.CAR_BLANK);
}

const carNames = names.split(",").map((name) => name.trim());

if (carNames.length < 2) {
throw new Error(CAR_ERROR.CAR_OVER_2);
}

const uniqueNames = new Set(carNames);
if (uniqueNames.size !== carNames.length) {
throw new Error(CAR_ERROR.CAR_DUPLICATE);
}

carNames.forEach((name) => {
if (name.length > 5) {
throw new Error(CAR_ERROR.CAR_UNDER_5);
}
if (!name) {
throw new Error(CAR_ERROR.CAR_BLANK);
}
});

return carNames;
};

export const validateTryCount = (count) => {
if (!count.trim()) {
throw new Error(NUMBER_ERROR.NUMBER_BLANK);
}

const number = Number(count);
if (isNaN(number) || !Number.isInteger(number) || number <= 0) {
throw new Error(NUMBER_ERROR.NUMBER_NONNUM);
}

return number;
};