diff --git a/README.md b/README.md index e078fd41..9c1c76c0 100644 --- a/README.md +++ b/README.md @@ -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()` diff --git a/src/App.js b/src/App.js index 091aa0a5..8ec9cc7f 100644 --- a/src/App.js +++ b/src/App.js @@ -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; diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 00000000..83e38f12 --- /dev/null +++ b/src/App.test.js @@ -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"]); + }); +});