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
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

72 changes: 72 additions & 0 deletions __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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));
});
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

아 테스트 코드를 작성한다는 게 무슨 뜻인지 몰랐는데 이렇게 하는거군요,,,
참고하겠습니다

26 changes: 25 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -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;
56 changes: 56 additions & 0 deletions src/Model.js
Original file line number Diff line number Diff line change
@@ -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++;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

만약 가독성, 유지보수 측면을 더 고려할 수 있다면
숫자 4나 9같은 매직 넘버를 상수로 정의하는 방법도 있습니다!
ex)
const BASELINE = 4;
const MAX_RANDOM_NUMBER = 9;


// 자동차 상태 출력
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 };
30 changes: 30 additions & 0 deletions src/View.js
Original file line number Diff line number Diff line change
@@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MVC 패턴으로 깔끔하게 구분하셔서 좋은 것 같습니다
근데 궁금한 점이 있다면, 기본 Console 라이브러리로 수행할 수 있는 작업들을 View 클래스를 새로 생성해서 다시 기능을 정의하면 어떤 이점이 있는지 궁금합니다
MVC 패턴을 아직 제대로 모르는 입장에서 보면 이 파일이 왜 필요할까..? 라는 생각이 들어서요!