Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a1969bf
docs: update README.md
yerimming Oct 24, 2025
c08f8b7
feat(message): 메시지 상수 분리 및 에러 메시지 구분
yerimming Oct 24, 2025
960b4af
docs: 기능 구현 목록 추가
yerimming Oct 24, 2025
f000e51
feat(message): 자동차 최소 개수에 대한 에러 메시지 추가
yerimming Oct 24, 2025
b01377a
feat(ConsoleView): ConsoleView 클래스 생성 및 자동차 이름 입력 기능 구현
yerimming Oct 24, 2025
10047e4
feat(GameNumbers): 자동차 경주 게임 로직용 상수 MOVE_THOUSHOLD, MAX_CAR_LENGTH 추가
yerimming Oct 24, 2025
a59bebf
feat(GameNumbers): 최소 자동차 개수를 의미하는 상수 MIN_CAR_NUM 추가
yerimming Oct 24, 2025
58ebf03
feat(Validator): Validator 클래스 생성 및 자동차 이름 검증 로직 구현
yerimming Oct 24, 2025
4a86368
feat(RacingController): RacingController 클래스 생성 및 자동차 이름 입력 검증 연결
yerimming Oct 24, 2025
909ff4f
feat(ConsoleView): 시도 횟수 입력 및 에러 출력 기능 추가
yerimming Oct 24, 2025
da3208c
feat(Validator): 시도 횟수 검증 기능 추가
yerimming Oct 24, 2025
016f41e
fix(ConsoleView): export 오타 수정
yerimming Oct 24, 2025
3218d40
feat(RacingController): 시도 횟수 입력과 검증 및 에러 처리 기능 추가
yerimming Oct 24, 2025
524e2ca
feat(App): RacingController 실행 로직 추가
yerimming Oct 24, 2025
fe0af01
feat(CarRace): CarRace 클래스 생성 및 자동차 경주 로직 구현
yerimming Oct 24, 2025
fa14601
feat(ConsoleView): 게임 시작 및 라운드 출력 기능 추가
yerimming Oct 24, 2025
a33d0d7
feat(RacingController): 자동차 경주 게임 전체 실행 로직 구현
yerimming Oct 24, 2025
f145890
refactor(Validator): validateTryCount 메서드에 static 추가
yerimming Oct 25, 2025
b6312e0
feat(CarRace): 우승자 계산 메서드 getWinners 추가
yerimming Oct 25, 2025
e7d6145
feat(ConsoleView): 우승자 출력 기능 추가
yerimming Oct 25, 2025
6dd9721
chore(App): import 경로에 .js 확장자 명시
yerimming Oct 25, 2025
fd14c1d
feat(RacingController): 우승자 출력 기능 추가
yerimming Oct 25, 2025
0d280a3
refactor(RacingController): 예외를 재전파하도록 throw error 추가
yerimming Oct 25, 2025
faab503
test: Validator 클래스의 입력 검증 단위 테스트 추가
yerimming Oct 26, 2025
6109b98
test: CarRace 클래스의 단위 테스트 추가
yerimming Oct 26, 2025
d1c95ef
fix(RacingController): 코드 내 오타 수정
yerimming Oct 26, 2025
8816c72
docs: 기능 구현 목록 체크 및 실행 결과와 테스트 코드 실행 결과 작성
yerimming Oct 26, 2025
2ed3219
test: 로컬 전용 테스트 코드로 변경
yerimming Oct 27, 2025
d1bb48a
test: 로컬 전용 테스트 코드로 변경
yerimming Oct 27, 2025
7d17fc5
chore: 테스트 파일 삭제
yerimming Oct 27, 2025
11856a1
chore: 테스트 파일 삭제
yerimming 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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
# javascript-racingcar-precourse

## ✅ 기능 구현 목록

- [x] 자동차 이름 입력받기
- [x] `,` 기준으로 이름 구분
- [x] `,` 기준으로 이름 구분 가능한지 확인
- [x] 자동차가 2개 이상인지 확인
- [x] 이름 5자 이하인지 확인
- [x] 이동 시도할 횟수 입력받기
- [x] 자동차마다 무작위 값 구해 4 이상인 경우 전진
- [x] 이동 횟수만큼 반복
- [x] 우승자 구하기
- [x] 여러 명인 경우 `,`로 구분
- [x] 잘못된 값 입력 시 에러 발생 후 종료

## ⭐️ 실행 결과

<img width="582" height="521" alt="Image" src="https://github.com/user-attachments/assets/f36b09ae-5315-4b69-b310-48a992abac6c" />

## 테스트 코드 실행 결과

<img width="879" height="692" alt="Image" src="https://github.com/user-attachments/assets/8b4e4a42-4a0a-4b1a-9361-4a3c98fcab7d" />
37 changes: 37 additions & 0 deletions __tests__local__/CarRaceTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import CarRace from "../src/model/CarRace.js";

describe("CarRace", () => {
test("자동차 초기화", () => {
const race = new CarRace();
race.initCars(["Car1", "Car2"]);
expect(race.getCars()).toEqual({ Car1: 0, Car2: 0 });
});

test("playRound: 랜덤 전진 (값 범위 확인)", () => {
const race = new CarRace();
race.initCars(["Car1", "Car2"]);
race.playRound();
const cars = race.getCars();
Object.values(cars).forEach(val => {
expect(val).toBeGreaterThanOrEqual(0);
});
});

test("getWinners: 최고 값 자동차 반환", () => {
const race = new CarRace();
race.initCars(["Car1", "Car2"]);
race.getCars().Car1 = 3;
race.getCars().Car2 = 2;
const winners = race.getWinners();
expect(winners).toEqual(["Car1"]);
});

test("getWinners: 동점 처리", () => {
const race = new CarRace();
race.initCars(["Car1", "Car2"]);
race.getCars().Car1 = 3;
race.getCars().Car2 = 3;
const winners = race.getWinners();
expect(winners).toEqual(["Car1", "Car2"]);
});
});
59 changes: 59 additions & 0 deletions __tests__local__/ValidatorTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Validator from "../src/model/Validator.js";
import { ERROR_MESSAGES } from "../src/constants/message.js";

describe("Validator", () => {
describe("checkSeparated", () => {
test("배열이 아니면 에러 발생", () => {
expect(() => Validator.checkSeparated("Car1,Car2")).toThrow(ERROR_MESSAGES.INPUT);
});

test("빈 배열이면 에러 발생", () => {
expect(() => Validator.checkSeparated([])).toThrow(ERROR_MESSAGES.INPUT);
});

test("올바른 배열이면 통과", () => {
expect(() => Validator.checkSeparated(["Car1", "Car2"])).not.toThrow();
});
});

describe("checkMinimumCars", () => {
test("자동차 2대 미만이면 에러 발생", () => {
expect(() => Validator.checkMinimumCars(["Car1"])).toThrow("최소 2대 이상의 자동차가 필요합니다.");
});

test("자동차 2대 이상이면 통과", () => {
expect(() => Validator.checkMinimumCars(["Car1", "Car2"])).not.toThrow();
});
});

describe("checkEmptyNames", () => {
test("빈 문자열이 있으면 에러 발생", () => {
expect(() => Validator.checkEmptyNames(["", "Car2"])).toThrow(ERROR_MESSAGES.INPUT);
});

test("빈 문자열 없으면 통과", () => {
expect(() => Validator.checkEmptyNames(["Car1", "Car2"])).not.toThrow();
});
});

describe("checkNameLength", () => {
test("이름 길이 초과 시 에러 발생", () => {
expect(() => Validator.checkNameLength(["Car123", "Car2"])).toThrow(ERROR_MESSAGES.NAME_LENGTH);
});

test("길이 정상 시 통과", () => {
expect(() => Validator.checkNameLength(["Car1", "Car2"])).not.toThrow();
});
});

describe("validateTryCount", () => {
test("0 이하나 NaN이면 에러 발생", () => {
expect(() => Validator.validateTryCount(0)).toThrow(ERROR_MESSAGES.TRY_COUNT);
expect(() => Validator.validateTryCount(NaN)).toThrow(ERROR_MESSAGES.TRY_COUNT);
});

test("1 이상이면 통과", () => {
expect(() => Validator.validateTryCount(3)).not.toThrow();
});
});
});
7 changes: 6 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import RacingController from "./controller/RacingController.js";

class App {
async run() {}
async run() {
const controller = new RacingController();
await controller.run();
}
}

export default App;
3 changes: 3 additions & 0 deletions src/constants/GameNumbers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const MOVE_THOUSHOLD = 4; // 전진 기준 값
export const MIN_CAR_NUM = 2;
export const MAX_CAR_NAME_LENGTH = 5; // 자동차 이름 최대 길이
15 changes: 15 additions & 0 deletions src/constants/message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const MESSAGES = {
INPUT_CAR_NAMES: "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n",
INPUT_TRY_COUNT: "\n시도할 횟수는 몇 회인가요?\n",
START: "\n실행 결과",
WINNER: "최종 우승자 : ",
NO_WINNER: "우승자가 없습니다.",
};

export const ERROR_MESSAGES = {
PREFIX: "[ERROR]",
NAME_LENGTH: "자동차 이름은 5자 이하만 가능합니다.",
TRY_COUNT: "시도 횟수는 1 이상의 숫자여야 합니다.",
INPUT: "입력 오류가 발생했습니다.",
MIN_CARS: "최소 2대 이상의 자동차가 필요합니다.",
}
36 changes: 36 additions & 0 deletions src/controller/RacingController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import ConsoleView from "../view/ConsoleView.js"
import Validator from "../model/Validator.js"
import CarRace from "../model/CarRace.js";

class RacingController {
constructor() {
this.view = new ConsoleView();
this.race = new CarRace();
}

async run() {
try {
const carNames = await this.view.getCarNames();
Validator.validateCarNames(carNames);

const tryCount = await this.view.getTryCount();
Validator.validateTryCount(tryCount);

this.race.initCars(carNames);
this.view.printStart();

for (let i = 0; i < tryCount; i++) {
this.race.playRound();
this.view.printRound(this.race.getCars());
}

const winners = this.race.getWinners();
this.view.printWinners(winners);
} catch (error) {
this.view.printError(error);
throw error;
}
}
}

export default RacingController;
35 changes: 35 additions & 0 deletions src/model/CarRace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { MissionUtils } from "@woowacourse/mission-utils";
import { MOVE_THOUSHOLD } from "../constants/gameNumbers.js";

class CarRace {
constructor() {
this.cars = {};
}

initCars(carNames) {
this.cars = carNames.reduce((acc, name) => {
acc[name] = 0;
return acc;
}, {});
}

playRound() {
Object.keys(this.cars).forEach((name) => {
const random = MissionUtils.Random.pickNumberInRange(0, 9);
if (random >= MOVE_THOUSHOLD) {
this.cars[name]++;
}
});
}

getCars() {
return this.cars;
}

getWinners() {
const max = Math.max(...Object.values(this.cars));
return Object.keys(this.cars).filter(name => this.cars[name] === max);
}
}

export default CarRace;
53 changes: 53 additions & 0 deletions src/model/Validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { MIN_CAR_NUM, MAX_CAR_NAME_LENGTH } from "../constants/gameNumbers.js";
import { ERROR_MESSAGES } from "../constants/message.js";

class Validator {
// 자동차 이름 전체 검증
static validateCarNames(carNames) {
this.checkSeparated(carNames);
this.checkMinimumCars(carNames);
this.checkEmptyNames(carNames);
this.checkNameLength(carNames);
}

// 입력이 배열로 잘 분리되었는지, 비어있지는 않은지 검증
static checkSeparated(carNames) {
if (!Array.isArray(carNames) || carNames.length === 0) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.INPUT}`);
}
}

// 최소 2대 이상인지 검증
static checkMinimumCars(carNames) {
if (carNames.length < MIN_CAR_NUM) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.MIN_CARS}`);
}
}

// 빈 문자열 이름이 있는지 검증
static checkEmptyNames(carNames) {
carNames.forEach(name => {
if (!name.trim()) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.INPUT}`);
}
})
}

// 이름 길이가 5자 이하인지 검증
static checkNameLength(carNames) {
carNames.forEach(name => {
if (name.length > MAX_CAR_NAME_LENGTH) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.NAME_LENGTH}`);
}
});
}

// 시도 횟수가 숫자인지, 한 번 이상인지 확인
static validateTryCount(count) {
if(isNaN(count) || count < 1) {
throw new Error(`${ERROR_MESSAGES.PREFIX} ${ERROR_MESSAGES.TRY_COUNT}`);
}
}
}

export default Validator;
39 changes: 39 additions & 0 deletions src/view/ConsoleView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Console } from "@woowacourse/mission-utils";
import { MESSAGES } from "../constants/message.js";

class ConsoleView {
async getCarNames() {
const input = await Console.readLineAsync(MESSAGES.INPUT_CAR_NAMES);
return input.split(",").map((name) => name.trim());
}

async getTryCount() {
const input = await Console.readLineAsync(MESSAGES.INPUT_TRY_COUNT);
return Number(input);
}

printStart() {
Console.print(MESSAGES.START);
}

printRound(cars) {
Object.entries(cars).forEach(([name, distance]) => {
Console.print(`${name} : ${"-".repeat(distance)}`);
});
Console.print(" ");
}

printWinners(winners) {
if(winners.length === 0) {
Console.print(MESSAGES.NO_WINNER);
throw new Error("[ERROR] 우승자가 없습니다.");
}
Console.print(`${MESSAGES.WINNER}${winners.join(", ")}`);
}

printError(error) {
Console.print(`${error.message}`);
}
}

export default ConsoleView;