Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,42 @@
# javascript-racingcar-precourse

**에러 처리 기능**

- try ~ catch 문으로 에러를 catch했다면 [ERROR]로 시작하는 에러 메세지를 반환

**경주할 자동차 입력을 받고 이름을 검증하는 기능**

- 입력: 입력한 자동차를 split으로 분리해 배열에 저장
- `Console.readLineAsync()`
- 출력: “경주할 자동차 이름을 입력하세요.”
- `Console.readLineAsync()`
- 검증: 길이 5이하의 문자열인지 확인, 아니라면 에러 throw / 중복된 이름이 있는지 확인, 있다면 에러 throw

**시도할 횟수를 입력받고 검증하는 기능**

- 입력: 시도할 횟수를 입력받아 저장
- `Console.readLineAsync()`
- 출력: “시도할 횟수는 몇 회인가요?”
- `Console.readLineAsync()`
- 검증: 숫자인지 확인, 아니라면 에러 throw

**시도할 횟수만큼 반복문으로 돌며 자동차마다 랜덤값을 생성하는 기능**

- `MissionUtils.Random.pickNumberInRange(0, 9);`

**자동차를 전진하는 기능**

- 랜덤값이 4 이상일 시 1 전진
- 전진한 자동차 정보를 저장

**실행 결과를 출력하는 기능**

- 출력: 처음에는 “실행 결과”를 출력, 이후 각 자동차 이름과 전진 횟수를 -로 표시
- `Console.print()`

**우승 결과를 출력하는 기능**

- 출력: 최종 우승자 :
- 가장 많은 전진 횟수를 가진 자동차를 출력
- 동일한 전진 횟수를 가진 자동차가 여러대라면 쉼표로 구분해 출력
- `Console.print()`
116 changes: 115 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,119 @@
import { Console, MissionUtils } from "@woowacourse/mission-utils";

export const PROGRESS_BAR = "-";

export const validateCarName = (carName) => {
const trimmed = String(carName).trim();
if (!trimmed) {
throw new Error("자동차 이름을 입력해주세요.");
}
if (trimmed.length > 5) {
throw new Error("자동차 이름은 5글자 이하로 입력해주세요.");
}
return trimmed;
};

export const checkNameDuplicate = (carNames) => {
const nameSet = new Set();
for (const name of carNames) {
if (nameSet.has(name)) {
throw new Error(`중복된 자동차 이름이 있습니다. 이름: ${name}`);
}
nameSet.add(name);
}
};

export const readCarNames = async () => {
const input = await Console.readLineAsync(
"경주할 자동차 이름을 입력하세요. (이름은 쉼표(,) 기준으로 구분) \n"
);
const rawNames = input.split(",");
const carNames = rawNames.map((name) => validateCarName(name));
checkNameDuplicate(carNames);

return carNames;
};

// 시도 횟수 유효성 검사
export const validateAttemptCount = (count) => {
if (count === undefined || count === null || String(count).trim() === "") {
throw new Error("시도할 횟수를 입력해주세요.");
}

const numCount = Number(count);

if (Number.isNaN(numCount)) {
throw new Error("시도할 횟수는 숫자로 입력해주세요.");
} else if (!Number.isInteger(numCount)) {
throw new Error("시도할 횟수는 정수로 입력해주세요.");
} else if (numCount <= 0) {
throw new Error("시도할 횟수는 1 이상의 정수로 입력해주세요.");
} else {
return numCount;
}
};

export const readAttemptCount = async () => {
const input = await Console.readLineAsync("시도할 횟수는 몇 회인가요? \n");
const value = validateAttemptCount(input);
return value;
};

export const randomMove = () => {
const randomValue = MissionUtils.Random.pickNumberInRange(0, 9);
return randomValue >= 4;
};

export const printProgress = (currentCar, movedCount) => {
const currentMove = movedCount[currentCar] || 0;
const currentProgress = PROGRESS_BAR.repeat(currentMove);
Console.print(`${currentCar} : ${currentProgress}`);
};

export const startRound = (carNames, movedCount) => {
carNames.forEach((car) => {
if (randomMove()) {
movedCount[car] = (movedCount[car] || 0) + 1;
}
printProgress(car, movedCount);
});
Console.print("\n");
};

export const printWinner = (movedCount) => {
const maxMove = Math.max(...Object.values(movedCount));
const winners = Object.keys(movedCount).filter(
(key) => movedCount[key] === maxMove
);
Console.print(`최종 우승자 : ${winners.join(", ")}`);
};

export const runCarRace = async () => {
const movedCount = {};
const carNames = await readCarNames();
const attemptCount = await readAttemptCount();

carNames.forEach((name) => {
movedCount[name] = 0;
});

Console.print("\n실행 결과\n");

for (let i = 1; i <= attemptCount; i++) {
startRound(carNames, movedCount);
}

printWinner(movedCount);
};

class App {
async run() {}
async run() {
try {
await runCarRace();
} catch (error) {
throw new Error(`[ERROR] ${error.message}`);
}
}
}

export default App;
120 changes: 120 additions & 0 deletions src/App.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
checkNameDuplicate,
randomMove,
startRound,
validateAttemptCount,
validateCarName,
} from "./App";

describe("경주할 자동차 입력을 검증하는 기능 테스트", () => {
test("정상적인 자동차 이름", () => {
expect(validateCarName("pobi")).toBe("pobi");
});

test("빈 문자열 예외", () => {
expect(() => validateCarName("")).toThrow("자동차 이름을 입력해주세요.");
});

test("5글자 초과 예외", () => {
expect(() => validateCarName("abcdef")).toThrow(
"자동차 이름은 5글자 이하로 입력해주세요."
);
});

test("공백 제거", () => {
expect(validateCarName(" pobi ")).toBe("pobi");
});
});
describe("경주할 자동차 이름 중복 검사 기능 테스트", () => {
test("중복 없는 경우", () => {
expect(() => checkNameDuplicate(["pobi", "woni", "jun"])).not.toThrow();
});

test("중복 있는 경우", () => {
expect(() => checkNameDuplicate(["pobi", "woni", "pobi"])).toThrow(
"중복된 자동차 이름이 있습니다. 이름: pobi"
);
});
});

describe("시도 횟수 검증 기능 테스트", () => {
test("정상적인 시도 횟수", () => {
expect(validateAttemptCount("5")).toBe(5);
expect(validateAttemptCount("1")).toBe(1);
expect(validateAttemptCount("100")).toBe(100);
});

test("빈 값 예외", () => {
expect(() => validateAttemptCount("")).toThrow(
"시도할 횟수를 입력해주세요."
);
expect(() => validateAttemptCount(undefined)).toThrow(
"시도할 횟수를 입력해주세요."
);
expect(() => validateAttemptCount(null)).toThrow(
"시도할 횟수를 입력해주세요."
);
});

test("숫자가 아닌 값 예외", () => {
expect(() => validateAttemptCount("abc")).toThrow(
"시도할 횟수는 숫자로 입력해주세요."
);
expect(() => validateAttemptCount("5.5")).toThrow(
"시도할 횟수는 정수로 입력해주세요."
);
});

test("0 이하 값 예외", () => {
expect(() => validateAttemptCount("0")).toThrow(
"시도할 횟수는 1 이상의 정수로 입력해주세요."
);
expect(() => validateAttemptCount("-1")).toThrow(
"시도할 횟수는 1 이상의 정수로 입력해주세요."
);
});
});

describe("시도한 횟수만큼 반복문으로 랜덤값을 생성하고 전진하는 기능 테스트", () => {
test("랜덤값이 4 이상일 시 전진하는 기능 테스트", () => {
const movedCount = { pobi: 0, woni: 0, jun: 0 };
movedCount["pobi"] = (movedCount["pobi"] || 0) + 1;
expect(movedCount["pobi"]).toBe(1);
});

test("실행 결과를 출력하는 기능 테스트", () => {
const currentCar = "pobi";
const movedCount = { pobi: 3, woni: 2, jun: 1 };
const PROGRESS_BAR = "-";

const currentMove = movedCount[currentCar] || 0;
const currentProgress = PROGRESS_BAR.repeat(currentMove);

expect(currentMove).toBe(3);
expect(currentProgress).toBe("---");
});
});

describe("우승자 출력 기능 테스트", () => {
test("단일 우승자 찾기", () => {
const movedCount = { pobi: 3, woni: 2, jun: 1 };
const maxMove = Math.max(...Object.values(movedCount));
const winners = Object.keys(movedCount).filter(
(key) => movedCount[key] === maxMove
);

expect(maxMove).toBe(3);
expect(winners).toEqual(["pobi"]);
});

test("공동 우승자 찾기", () => {
const movedCount = { pobi: 2, woni: 2, jun: 1 };
const maxMove = Math.max(...Object.values(movedCount));
const winners = Object.keys(movedCount).filter(
(key) => movedCount[key] === maxMove
);

expect(maxMove).toBe(2);
expect(winners).toEqual(["pobi", "woni"]);
});
});