Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d7344cf
docs(readme): 기능 목록 작성
nunomi0 Oct 27, 2025
9249640
feat(input): 자동차 이름 입력
nunomi0 Oct 27, 2025
f09e3d6
feat(input): 시도할 횟수 입력
nunomi0 Oct 27, 2025
7d53620
feat(app): 입력 파싱
nunomi0 Oct 27, 2025
b082a21
feat(validator): 자동차 이름 유효성 검사
nunomi0 Oct 27, 2025
17c20d5
feat(validator): 시도 횟수 유효성 검사
nunomi0 Oct 27, 2025
61dfbf6
refactor(validator): 자동차 이름 최대 길이 상수 분리
nunomi0 Oct 27, 2025
30e959d
test(validator): 자동차 이름 유효성 검사 테스트 추가
nunomi0 Oct 27, 2025
584c82c
feat(car, racinggame): 자동차 생성 기능 추가
nunomi0 Oct 27, 2025
82784fa
test(car): 자동차 클래스 테스트 추가
nunomi0 Oct 27, 2025
c96c685
feat(racinggame): 랜덤 값에 따른 자동차 이동 로직 추가
nunomi0 Oct 27, 2025
44a88dd
test(car, racinggame): 랜덤 값에 따른 전진 로직 테스트 추가
nunomi0 Oct 27, 2025
769e710
feat(racinggame): 시도 횟수만큼 게임 진행
nunomi0 Oct 27, 2025
b84f3d0
feat(output): 라운드별 실행 결과 출력
nunomi0 Oct 27, 2025
2c78b95
test(output): 라운드별 실행 결과 출력 테스트
nunomi0 Oct 27, 2025
8c38bc2
feat(racinggame): 우승자 선출
nunomi0 Oct 27, 2025
9ce6932
feat(ouput): 최종 우승자 출력
nunomi0 Oct 27, 2025
6152556
test(racinggame, output): 우승자 선출 테스트 추가
nunomi0 Oct 27, 2025
d6885b9
feat(validator): 자동차 이름 중복 검사 추가
nunomi0 Oct 27, 2025
c069b29
docs(readme): 테스트 목록, 프로젝트 구조, 실행 결과 추가
nunomi0 Oct 27, 2025
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
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,69 @@
# javascript-racingcar-precourse
# 자동차 경주
자동차 이름과 시도 횟수를 입력 받아 자동차 게임을 진행한다.

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

### 파싱
- [x] 자동차 이름 입력값을 쉼표(,) 기준으로 나누어 배열로 변환
- [x] 각 이름의 공백 제거

### 유효성 검사
- [x] 이름 5자 이하
- [x] 이름 공백 불가능
- [x] 이름 중복 불가능
- [x] 시도 횟수는 0보다 큰 정수

### 게임 진행
- [x] 자동차 생성
- [x] 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상인 경우 자동차 전진
- [x] 시도 횟수만큼 게임 진행
- [x] 우승자 선출

### 출력
- [x] 라운드별 실행 결과 출력
- [x] 게임 종료 후 우승자 안내 문구 출력

### 테스트
- [x] validator
- [x] car
- [x] RacingGame
- [x] output

## 프로젝트 구조
```
src/
├── App.js # 프로그램 실행 흐름 관리 (입력 → 검증 → 게임 → 출력)
├── index.js # 프로그램 실행 진입점
├── Car.js # 자동차 클래스 (이름, 위치, 이동 로직)
├── RacingGame.js # 게임 진행 및 우승자 계산
├── input.js # 사용자 입력 처리
├── output.js # 게임 결과 및 우승자 출력
├── validator.js # 입력값 유효성 검사
└── utils/
└── randomUtils.js # 랜덤 숫자 생성 (0~9)
__tests__/ # 단위 테스트 모음
```

## 실행 결과
```bash
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
시도할 횟수는 몇 회인가요?
3
pobi :
woni : -
jun : -

pobi : -
woni : --
jun : -

pobi : -
woni : ---
jun : -

최종 우승자 : woni
```
67 changes: 67 additions & 0 deletions __tests__/RacingGame.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { RacingGame } from "../src/RacingGame.js";
import * as randomUtils from "../src/utils/randomUtils.js";
import { Car } from "../src/Car.js";

describe("RacingGame 클래스", () => {
afterEach(() => {
jest.restoreAllMocks();
});

test("랜덤 값이 4 이상이면 자동차가 전진한다.", () => {
const carNames = ["pobi", "woni"];
const game = new RacingGame(carNames);

jest.spyOn(randomUtils, "getRandomNumber").mockReturnValue(7);
game.playRound();

game.cars.forEach((car) => {
expect(car.position).toBe(1);
});
});

test("랜덤 값이 4 미만이면 자동차가 전진하지 않는다.", () => {
const carNames = ["pobi", "woni"];
const game = new RacingGame(carNames);

jest.spyOn(randomUtils, "getRandomNumber").mockReturnValue(2);

game.playRound();
game.cars.forEach((car) => {
expect(car.position).toBe(0);
});
});

describe("우승자 선출", () => {
test("가장 멀리 간 자동차가 단독 우승자가 된다.", () => {
const game = new RacingGame(["pobi", "woni", "jun"]);
game.cars = [
new Car("pobi"),
new Car("woni"),
new Car("jun"),
];

game.cars[0].position = 2;
game.cars[1].position = 5;
game.cars[2].position = 3;

const winners = game.getWinners();
expect(winners).toEqual(["woni"]);
});

test("가장 멀리 간 자동차가 여러 대면 공동 우승자가 된다.", () => {
const game = new RacingGame(["pobi", "woni", "jun"]);
game.cars = [
new Car("pobi"),
new Car("woni"),
new Car("jun"),
];

game.cars[0].position = 4;
game.cars[1].position = 4;
game.cars[2].position = 2;

const winners = game.getWinners();
expect(winners).toEqual(["pobi", "woni"]);
});
});
});
36 changes: 36 additions & 0 deletions __tests__/car.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Car } from "../src/Car.js";

describe("Car 클래스", () => {
test("자동차는 이름과 초기 위치를 가진다", () => {
const car = new Car("pobi");

expect(car.name).toBe("pobi");
expect(car.position).toBe(0);
});

test("randomValue가 4인 경우 canMove는 true를 반환한다.", () => {
const car = new Car("pobi");
expect(car.canMove(4)).toBe(true);
});

test("randomValue가 3인 경우 canMove는 false를 반환한다.", () => {
const car = new Car("pobi");
expect(car.canMove(3)).toBe(false);
});

test("move()를 호출하면 position이 1 증가한다", () => {
const car = new Car("pobi");
car.move();

expect(car.position).toBe(1);
});

test("move()를 여러 번 호출하면 그 횟수만큼 position이 증가한다", () => {
const car = new Car("pobi");
car.move();
car.move();
car.move();

expect(car.position).toBe(3);
});
});
47 changes: 47 additions & 0 deletions __tests__/output.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { printRoundResult, printWinners } from "../src/output.js";
import { Console } from "@woowacourse/mission-utils";

describe("printRoundResult", () => {
beforeEach(() => {
jest.spyOn(Console, "print").mockImplementation(() => {});
});

afterEach(() => {
jest.restoreAllMocks();
});

test("자동차의 현재 위치를 '-'로 출력한다.", () => {
const cars = [
{ name: "pobi", position: 2 },
{ name: "crong", position: 3 },
];

printRoundResult(cars);

expect(Console.print).toHaveBeenCalledWith("pobi : --");
expect(Console.print).toHaveBeenCalledWith("crong : ---");
expect(Console.print).toHaveBeenCalledWith("");
});
});

describe("printWinners", () => {
beforeEach(() => {
jest.spyOn(Console, "print").mockImplementation(() => {});
});

afterEach(() => {
jest.restoreAllMocks();
});

test("우승자가 한 명일 경우 이름을 출력한다.", () => {
const winners = ["pobi"];
printWinners(winners);
expect(Console.print).toHaveBeenCalledWith("최종 우승자 : pobi");
});

test("우승자가 여러 명일 경우 쉼표로 구분해 출력한다.", () => {
const winners = ["pobi", "woni", "jun"];
printWinners(winners);
expect(Console.print).toHaveBeenCalledWith("최종 우승자 : pobi, woni, jun");
});
});
53 changes: 53 additions & 0 deletions __tests__/validator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { validateCarNames, validateTryCount, MAX_CAR_NAME_LENGTH } from "../src/validator.js";

describe("자동차 이름 유효성 검사", () => {
test("이름이 비어있으면 에러를 발생시킨다", () => {
expect(() => validateCarNames(["", "pobi"])).toThrow(
"[ERROR] 이름에 공백이 포함되어 있습니다."
);
});

test(`이름이 ${MAX_CAR_NAME_LENGTH}자를 초과하면 에러를 발생시킨다`, () => {
const overLengthName = "a".repeat(MAX_CAR_NAME_LENGTH + 1);
expect(() => validateCarNames([overLengthName, "jun"])).toThrow(
`[ERROR] 자동차 이름은 ${MAX_CAR_NAME_LENGTH}자 이하만 가능합니다.`
);
});

test("이름이 중복되면 에러를 발생시킨다", () => {
expect(() => validateCarNames(["pobi", "woni", "pobi"])).toThrow(
"[ERROR] 자동차 이름은 중복될 수 없습니다."
);
});

test("모든 이름이 조건을 만족하면 통과한다", () => {
expect(() => validateCarNames(["pobi", "jun", "woni"])).not.toThrow();
});
});

describe("시도 횟수 유효성 검사", () => {
test("숫자가 아닌 값을 입력하면 에러를 발생시킨다", () => {
expect(() => validateTryCount("abc")).toThrow(
"[ERROR] 시도 횟수는 숫자여야 합니다."
);
});

test("소수 값을 입력하면 에러를 발생시킨다", () => {
expect(() => validateTryCount("3.5")).toThrow(
"[ERROR] 시도 횟수는 정수여야 합니다."
);
});

test(`0 이하의 값을 입력하면 에러를 발생시킨다`, () => {
expect(() => validateTryCount("0")).toThrow(
`[ERROR] 시도 횟수는 0보다 커야 합니다.`
);
expect(() => validateTryCount("-2")).toThrow(
`[ERROR] 시도 횟수는 0보다 커야 합니다.`
);
});

test(`0보다 큰 양의 정수를 입력하면 통과한다`, () => {
expect(() => validateTryCount("5")).not.toThrow();
});
});
20 changes: 19 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import { readCarNames, readTryCount } from "./input.js";
import { validateCarNames, validateTryCount } from "./validator.js";
import { RacingGame } from "./RacingGame.js";
import { printWinners } from "./output.js";

class App {
async run() {}
async run() {
const carNamesRaw = await readCarNames();
const carNames = carNamesRaw.split(",").map((name) => name.trim());

Choose a reason for hiding this comment

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

메인 로직을 읽는 부분이어서 세부적인 로직을 따로 추상화하면 더 깔끔해질 거 같아요.

validateCarNames(carNames);

const tryCount = await readTryCount();
validateTryCount(tryCount);

const game = new RacingGame(carNames);
game.play(tryCount);

const winners = game.getWinners();
printWinners(winners);
}
}

export default App;
14 changes: 14 additions & 0 deletions src/Car.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export class Car {
constructor(name) {
this.name = name;
this.position = 0;
}

canMove(randomValue){

Choose a reason for hiding this comment

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

move 매서드하고 합쳐도 될 거 같은데 canMove 매서드로 분리하신 이유가 있을까요?

return randomValue >= 4;
}

move() {
this.position += 1;
}
}
32 changes: 32 additions & 0 deletions src/RacingGame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Car } from "./Car.js";
import { getRandomNumber } from "./utils/randomUtils.js";
import { printRoundResult } from "./output.js";

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

playRound() {
this.cars.forEach((car) => {
const randomValue = getRandomNumber();
if (car.canMove(randomValue)) {
car.move();
}
});
Comment on lines +11 to +16

Choose a reason for hiding this comment

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

자동차 이동을 판별하는 로직을 car.move로 옮기는 건 어떨까요?

Suggested change
this.cars.forEach((car) => {
const randomValue = getRandomNumber();
if (car.canMove(randomValue)) {
car.move();
}
});
this.cars.forEach((car) => {
car.move();
printRoundResult(this.cars);
});
// Car.js
...
move() {
const randomValue = getRandomNumber();
if (car.canMove(randomValue)) {
car.move();
}
}

Copy link
Author

Choose a reason for hiding this comment

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

자동차 이동을 판별하는 로직을 car.move로 옮기는 건 어떨까요?

리뷰 감사드립니다! 다음부터는 커밋 바디도 작성해봐야겠네요 :)
이동 판별은 자동차 특성이라기보다 게임의 규칙이라고 생각해서 RacingGame에 작성했는데, 자동차가 더 적합한건가요? 혹시 관련된 이론이나 개념이 있으면 알려주시면 감사하겠습니다 🙇‍♀️

Choose a reason for hiding this comment

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

아하 그런 이유가 있었군요. 저는 자동차가 움직이는 조건을 자동차 특성으로 봤어요.
어느 방식이 적합한 지는 관점마다 다르게 적용될 수 있어서 관점에 따라서 다를 거 같아요!

printRoundResult(this.cars);
}

play(tryCount) {
for (let i = 0; i<tryCount; i++){
this.playRound();
}
}

getWinners() {
const max = Math.max(...this.cars.map((car) => car.position));
return this.cars
.filter((car) => car.position === max)
.map((car) => car.name);
}
}
15 changes: 15 additions & 0 deletions src/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Console } from "@woowacourse/mission-utils";

export async function readCarNames() {
const input = await Console.readLineAsync(
"경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n"
);
return input;
}

export async function readTryCount() {
const input = await Console.readLineAsync(
"시도할 횟수는 몇 회인가요?\n"
);
return input;
}
13 changes: 13 additions & 0 deletions src/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Console } from "@woowacourse/mission-utils";

export function printRoundResult(cars) {
cars.forEach((car) => {
const positionBar = "-".repeat(car.position);

Choose a reason for hiding this comment

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

car.position으로 바로 가져오는 것도 좋은데, Car 클래스의 인스턴스를 직접 가져오는 것 보다는 private으로 선언한 뒤, getPosition()등의 함수를 통해 값을 받아보는 건 어떨까요?

Console.print(`${car.name} : ${positionBar}`);
});
Console.print("");
}

export function printWinners(winners) {
Console.print(`최종 우승자 : ${winners.join(", ")}`);
}
5 changes: 5 additions & 0 deletions src/utils/randomUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Random } from "@woowacourse/mission-utils";

export function getRandomNumber() {
return Random.pickNumberInRange(0,9);
}
Loading