diff --git a/README.md b/README.md index e078fd4..c5c26fa 100644 --- a/README.md +++ b/README.md @@ -1 +1,59 @@ # javascript-racingcar-precourse +# 2주차 미션 : 자동차 경주🚗 + +## Todo List +- ✅ 입력 + - 문자열 입력 기능 + - 숫자 입력 기능 + +- ✅ 전처리 + - 쉼표를 기준으로 이름을 구분 + +- ✅ 전진/멈춤할지 고르기 + - 입력받은 시도 횟수만큼 반복 + - 0-9 사이 무작위값 받기 + - 전진(4 이상) / 멈춤 (그 외) + +- ✅ 우승자 고르기 + - 전진한 횟수 비교 + +- ✅ 예외처리 + - 입력 오류 : 이름이 잘못 입력된 경우 + - 입력 오류 : 몇 번 이동할 것인지 입력하는 것이 숫자가 아닌 경우 + - 입력 처리 : 이름 양쪽 공백은 삭제한다. 즉, '지윤' = ' 지윤 ' + - 예외 처리 : 이름이 중복되는 경우, 한 사람으로 처리한다. + +- ✅ 리펙토링 + - MVC 패턴... + - 복잡한 코드 흐름 개선 + +## 코드 흐름 +1. "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" 출력 후 입력 대기 +2. "시도할 횟수는 몇 회인가요?" 출력 후 입력 대기 +3. 문자열 전처리 + a. ','기준으로 나누기 + b. 문자열 양 옆 공백 지우기 + + 예외처리 : 중복제거, 잘못된 입력 오류 처리 +4. 경주 시작 + a. 랜덤값으로 전진/멈춤 고르기 + b. 전진 수 저장 + c. n번 반복 +5. 실행 결과 출력 + a. 저장된 전진 수 만큼 '-'를 반복 출력 + b. n번 반복 +6. 우승자 결정 및 출력 + a. 전진 수 비교 + b. 우승자 저장 + +## MVC 패턴 +src/ +│ +├── App.js # Controller (App 클래스) +├── Model.js # Model (Car, Racing 클래스) +├── View.js # View (출력 및 사용자 인터페이스 담당) +└── index.js # 애플리케이션 시작점 + +### Model (Data) - Car, Racing +### View (UI) - Console 입출력 +### Controller (Interface) - App + diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 0260e7e..078ddbe 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -1,6 +1,7 @@ import App from "../src/App.js"; import { MissionUtils } from "@woowacourse/mission-utils"; +//메서드 mocking : 실제 입력대신, 테스트에서 주어진 inputs 배열에서 값 차례대로 반환 const mockQuestions = (inputs) => { MissionUtils.Console.readLineAsync = jest.fn(); @@ -57,4 +58,75 @@ describe("자동차 경주", () => { // then await expect(app.run()).rejects.toThrow("[ERROR]"); }); + + test("이름 오류 처리", async () => { + const inputs = ["pobi, ,woni"]; + mockQuestions(inputs); + const app = new App(); + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); + + test("이름 길이 오류 처리", async () => { + const inputs = ["longname,short"]; + mockQuestions(inputs); + const app = new App(); + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); + + test("횟수 오류 처리", async () => { + const inputs = ["abc"]; // 잘못된 숫자 입력 + mockQuestions(inputs); + const app = new App(); + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); + + test("횟수 숫자 오류 처리", async () => { + const inputs = ["pobi,woni", "abc"]; + mockQuestions(inputs); + const app = new App(); + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); + + test("이름 양쪽 공백 처리", async () => { + // given + const MOVING_FORWARD = 4; + const STOP = 3; + const inputs = [" pobi , woni ", "1"]; + const logs = ["pobi : -", "woni : ", "최종 우승자 : pobi"]; + const logSpy = getLogSpy(); + + mockQuestions(inputs); + mockRandoms([MOVING_FORWARD, STOP]); + + // when + const app = new App(); + await app.run(); + + // then + logs.forEach((log) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + }); + }); + + test("이름 중복 처리", async () => { + // given + const MOVING_FORWARD = 4; + const STOP = 3; + const inputs = ["pobi,pobi ", "1"]; + const logs = ["pobi : -", "최종 우승자 : pobi"]; + const logSpy = getLogSpy(); + + mockQuestions(inputs); + mockRandoms([MOVING_FORWARD, STOP]); + + // when + const app = new App(); + await app.run(); + + // then + logs.forEach((log) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + }); + }); }); + diff --git a/src/App.js b/src/App.js index 091aa0a..fdce6f0 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,29 @@ +import { Racing } from './Model.js'; +import View from './View.js'; + class App { - async run() {} + async run() { + try { + const racing = new Racing(); + + const inputName = await View.readLine("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분): "); + const nameArray = racing.validateCarNames(inputName); + + const inputNum = await View.readLine("시도할 횟수는 몇 회인가요?: "); + if (isNaN(inputNum) || inputNum <= 0) { + throw new Error("[ERROR] IllegalArgumentException"); + } + + View.print('실행 결과'); + const results = racing.advanceCount(nameArray, inputNum); + View.printResults(results); + View.printWinner(racing.getWinner()); + + } catch (error) { + View.printError(error.message); + return Promise.reject(error); + } + } } export default App; diff --git a/src/Model.js b/src/Model.js new file mode 100644 index 0000000..67f8245 --- /dev/null +++ b/src/Model.js @@ -0,0 +1,56 @@ +import { Random } from "@woowacourse/mission-utils"; + +class Car { + constructor(name) { + this.name = name; + this.count = 0; + } + + // 전진 조건을 적용하는 메서드 + move() { + if (Random.pickNumberInRange(0, 9) >= 4) { + this.count++; + } + } + + // 자동차 상태 출력 + print() { + return `${this.name} : ${'-'.repeat(this.count)}`; + } + } + + class Racing { + constructor() { + this.carInstances = []; + } + + // 쉼표 기준 분리 | 공백 처리 | 5자 이하 처리 | 중복 처리 + validateCarNames(names) { + return [...new Set(names.split(",") + .map(name => name.trim()) + .filter(name => name && name.length <= 5))]; + } + + // 전진 카운트 + advanceCount(nameArray, num) { + // 인스턴스화 + this.carInstances = nameArray.map(name => new Car(name)); + + const result = []; + for (let i = 0; i < num; i++) { + result.push(this.carInstances.map(car => { + car.move(); + return car.print(); + })); + } + return result; + } + getWinner() { + // 가장 큰 값 찾기 + const maxCount = Math.max(...this.carInstances.map(car => car.count)); + // 가장 큰 값과 같은 값을 가진 CarName 만 추출 + return this.carInstances.filter(car => car.count === maxCount).map(car => car.name); + } + } + + export { Car, Racing }; \ No newline at end of file diff --git a/src/View.js b/src/View.js new file mode 100644 index 0000000..ae140c0 --- /dev/null +++ b/src/View.js @@ -0,0 +1,30 @@ +import { Console } from "@woowacourse/mission-utils"; + +class View { + static print(message) { + Console.print(message); + } + + static async readLine(message) { + return await Console.readLineAsync(message); + } + + static printResults(results) { + results.forEach(result => { + result.forEach(carOutput => { + View.print(carOutput); + }); + }); + } + + static printWinner(winner) { + View.print(`최종 우승자 : ${winner.join(', ')}`); + } + + static printError(message) { + View.print(message); + } + } + + export default View; + \ No newline at end of file