diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 000000000..fb72f754c
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,31 @@
+export default {
+ env: {
+ browser: true,
+ es2021: true,
+ },
+ extends: ['airbnb-base', 'prettier'],
+ overrides: [
+ {
+ env: {
+ jest: true,
+ node: true,
+ },
+ files: [
+ '.eslintrc.{js,cjs}',
+ ],
+ parserOptions: {
+ sourceType: 'script',
+ },
+ },
+ ],
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
+ rules: {
+ 'max-depth': ['error', 2],
+ 'max-params': ['error', 3],
+ 'max-lines-per-function': ['error', { max: 10 }],
+ 'import/extensions': 'off',
+ },
+};
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 000000000..1b4ce05b2
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,11 @@
+export default {
+ singleQuote: true,
+ semi: true,
+ useTabs: false,
+ tabWidth: 2,
+ trailingComma: "all",
+ printWidth: 80,
+ bracketSpacing: true,
+ arrowParens: "always",
+ endOfLine: "auto",
+};
diff --git a/__tests__/BeforeRefactor.test.js b/__tests__/BeforeRefactor.test.js
new file mode 100644
index 000000000..137f8da46
--- /dev/null
+++ b/__tests__/BeforeRefactor.test.js
@@ -0,0 +1,341 @@
+import App from "../src/App.js";
+import { Console } from "@woowacourse/mission-utils"
+import { Random } from "@woowacourse/mission-utils"
+
+const mockInput = (input) => {
+ Console.readLineAsync = jest.fn(() => Promise.resolve(input));
+};
+
+describe("App class", () => {
+ describe("getCarName method", () => {
+ test("사용자 입력", async () => {
+ const testInput = "pobi,woni,jun";
+ mockInput(testInput);
+
+ const app = new App();
+ const result = await app.getCarName();
+
+ expect(Console.readLineAsync).toHaveBeenCalledTimes(1);
+ expect(Console.readLineAsync).toHaveBeenCalledWith("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n");
+ expect(result).toEqual(["pobi", "woni", "jun"]);
+ });
+ });
+
+ describe("sanitizeCarName method", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("이름 사이에 공백이 없을 때", () => {
+ const input = "pobi,woni,jun";
+ const result = app.sanitizeCarName(input);
+ expect(result).toEqual(["pobi", "woni", "jun"]);
+ });
+
+ test("이름 앞뒤에 공백이 있을 때", () => {
+ const input = " pobi ,woni , jun ";
+ const result = app.sanitizeCarName(input);
+ expect(result).toEqual(["pobi", "woni", "jun"]);
+ });
+
+ test("공백만 있는 이름이 있을 때", () => {
+ const input = "pobi, , jun";
+ const result = app.sanitizeCarName(input);
+ expect(result).toEqual(["pobi", "jun"]);
+ });
+
+ test("연속적으로 여러 개의 콤마가 나올 때", () => {
+ const input = "pobi,,,woni,,jun";
+ const result = app.sanitizeCarName(input);
+ expect(result).toEqual(["pobi", "woni", "jun"]);
+ });
+ });
+
+ describe("Error Handling in validateCarName", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("자동차 이름이 5자를 초과할 때", async () => {
+ mockInput("pobipobi,woni,jun");
+ await expect(app.getCarName()).rejects.toThrow("[ERROR] 자동차 이름은 5자를 초과할 수 없습니다.");
+ });
+
+ test("자동차 이름이 중복될 때", async () => {
+ mockInput("pobi,pobi,jun");
+ await expect(app.getCarName()).rejects.toThrow("[ERROR] 중복되는 자동차 이름이 있습니다.");
+ });
+
+ test("자동차 이름이 최소 2대 미만일 때", async () => {
+ mockInput("pobi");
+ await expect(app.getCarName()).rejects.toThrow("[ERROR] 최소 두 대 이상의 자동차 이름을 입력해주세요.");
+ });
+ });
+
+ describe("getRound method", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("사용자로부터 시도할 횟수 입력 받기", async () => {
+ const testInput = "5";
+ mockInput(testInput);
+
+ const result = await app.getRoundNumber();
+
+ expect(Console.readLineAsync).toHaveBeenCalledWith("시도할 횟수는 몇 회인가요?\n");
+ expect(result).toBe(5);
+ });
+ });
+
+ describe("sanitizeRoundNumber method", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("숫자에 공백이 없을 때", () => {
+ const input = "3";
+ const result = app.sanitizeRoundNumber(input);
+ expect(result).toEqual(3);
+ });
+
+ test("앞뒤에 공백이 있을 때", () => {
+ const input = " 3 ";
+ const result = app.sanitizeRoundNumber(input);
+ expect(result).toEqual(3);
+ });
+
+ test("숫자 사이에 공백이 있을 때", () => {
+ const input = "1 0";
+ const result = app.sanitizeRoundNumber(input);
+ expect(result).toEqual(10);
+ });
+ });
+
+ describe("Error Handling in validateRound", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("시도 횟수가 1 미만일 때", async () => {
+ mockInput("0");
+ await expect(app.getRoundNumber()).rejects.toThrow("[ERROR] 최소 1 이상의 횟수를 입력해주세요.");
+ });
+
+ test("시도 횟수가 10 초과일 때", async () => {
+ mockInput("11");
+ await expect(app.getRoundNumber()).rejects.toThrow("[ERROR] 최대 10 이하의 횟수를 입력해주세요.");
+ });
+
+ test("시도 횟수가 자연수 형식이 아닐 때", async () => {
+ mockInput("2.5");
+ await expect(app.getRoundNumber()).rejects.toThrow("[ERROR] 횟수는 자연수 형식이어야 합니다");
+ });
+
+ test("시도 횟수가 문자일 때", async () => {
+ mockInput("삼");
+ await expect(app.getRoundNumber()).rejects.toThrow("[ERROR] 횟수는 자연수 형식이어야 합니다");
+ });
+ });
+
+ describe("makeCar method", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("자동차 생성하기", () => {
+ const carName = ["pobi", "woni", "jun"];
+ app.makeCar(carName);
+
+ expect(app.car).toEqual([
+ { name: "pobi", distance: 0, move: expect.any(Function), shouldMove: expect.any(Function) },
+ { name: "woni", distance: 0, move: expect.any(Function), shouldMove: expect.any(Function) },
+ { name: "jun", distance: 0, move: expect.any(Function), shouldMove: expect.any(Function) }
+ ]);
+ });
+ });
+
+ describe("getRandomNumber method", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("0 ~ 9 사이 무작위 값 생성 테스트", () => {
+ const results = new Set();
+
+ for (let i = 0; i < 100; i++) {
+ const randomValue = app.getRandomNumber();
+ results.add(randomValue);
+
+ expect(randomValue).toBeGreaterThanOrEqual(0);
+ expect(randomValue).toBeLessThanOrEqual(9);
+ }
+
+ expect(results.size).toBe(10);
+ });
+ });
+
+ describe("makeCar method movement test", () => {
+ let app;
+ let car;
+ beforeEach(() => {
+ app = new App();
+ const carName = ["testCar"];
+ app.makeCar(carName);
+ car = app.car[0];
+ });
+
+ test("랜덤 숫자가 4 이상이면 전진", () => {
+ jest.spyOn(Random, 'pickNumberInRange').mockReturnValue(4);
+ expect(car.shouldMove()).toBeTruthy();
+
+ jest.spyOn(Random, 'pickNumberInRange').mockReturnValue(5);
+ expect(car.shouldMove()).toBeTruthy();
+
+ jest.spyOn(Random, 'pickNumberInRange').mockReturnValue(9);
+ expect(car.shouldMove()).toBeTruthy();
+ });
+
+ test("랜덤 숫자가 4 미만이면 정지", () => {
+ jest.spyOn(Random, 'pickNumberInRange').mockReturnValue(1);
+ expect(car.shouldMove()).toBeFalsy();
+
+ jest.spyOn(Random, 'pickNumberInRange').mockReturnValue(2);
+ expect(car.shouldMove()).toBeFalsy();
+
+ jest.spyOn(Random, 'pickNumberInRange').mockReturnValue(3);
+ expect(car.shouldMove()).toBeFalsy();
+ });
+ });
+
+ describe("runRace method", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("자동차는 총 횟수 만큼 움직여야 한다", async () => {
+ jest.spyOn(app, 'getCarName').mockResolvedValue(["pobi"]);
+ jest.spyOn(app, 'getRoundNumber').mockResolvedValue(3);
+ jest.spyOn(Console, 'readLineAsync').mockResolvedValue("");
+
+ jest.spyOn(app, 'getRandomNumber').mockReturnValue(4);
+
+ await app.play();
+
+ const moveCount = app.car[0].distance;
+
+ expect(moveCount).toBe(3);
+ });
+ });
+
+ describe("getWinner method", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("단독 우승자 결정하기", () => {
+ const carName = ["pobi", "woni", "jun"];
+ app.makeCar(carName);
+
+ jest.spyOn(Random, 'pickNumberInRange').mockReturnValueOnce(4)
+ .mockReturnValueOnce(4)
+ .mockReturnValueOnce(4)
+ .mockReturnValueOnce(4)
+ .mockReturnValueOnce(3);
+
+ app.runRace(3);
+
+ const winner = app.getWinner();
+ expect(winner).toEqual(["pobi"]);
+ });
+
+ test("다중 우승자 결정하기", () => {
+ const carName = ["pobi", "woni", "jun"];
+ app.makeCar(carName);
+
+ jest.spyOn(Random, 'pickNumberInRange').mockReturnValueOnce(4)
+ .mockReturnValueOnce(4)
+ .mockReturnValueOnce(4)
+ .mockReturnValueOnce(4)
+ .mockReturnValueOnce(4)
+ .mockReturnValueOnce(4)
+ .mockReturnValueOnce(4)
+ .mockReturnValueOnce(4)
+ .mockReturnValueOnce(4);
+
+ app.runRace(3);
+
+ const winner = app.getWinner();
+ expect(winner).toEqual(["pobi", "woni", "jun"]);
+ });
+ });
+
+ describe("printResultHeader method", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ jest.spyOn(Console, 'print');
+ });
+
+ test("실행 결과 출력", () => {
+ app.printResultHeader();
+ expect(Console.print).toHaveBeenCalledWith("\n실행 결과");
+ });
+ });
+
+ describe("printRoundResult method", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("차수별 실행 결과 출력", () => {
+ const carName = ["pobi", "woni", "jun"];
+ app.makeCar(carName);
+ jest.spyOn(Console, 'print').mockImplementation(() => {});
+
+ app.car[0].distance = 3; // pobi
+ app.car[1].distance = 2; // woni
+ app.car[2].distance = 1; // jun
+
+ app.printRoundResult();
+
+ expect(Console.print).toHaveBeenCalledWith("pobi : ---");
+ expect(Console.print).toHaveBeenCalledWith("woni : --");
+ expect(Console.print).toHaveBeenCalledWith("jun : -");
+ });
+ });
+
+ describe("printWinner method", () => {
+ let app;
+ beforeEach(() => {
+ app = new App();
+ });
+
+ test("최종 우승자 메세지 출력", () => {
+ app.car = [
+ { name: "pobi", distance: 5 },
+ { name: "woni", distance: 5 },
+ { name: "jun", distance: 3 }
+ ];
+
+ const printSpy = jest.spyOn(Console, 'print');
+
+ const winners = app.getWinner();
+ app.printWinner(winners);
+
+ expect(printSpy).toHaveBeenCalledWith("최종 우승자 : pobi, woni");
+
+ printSpy.mockRestore();
+ });
+ });
+});
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..0db87d98c
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,156 @@
+# 🚗 자동차 경주 게임 🚗
+
+``` html
+ 경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
+ pobi,woni,jun
+ 시도할 횟수는 몇 회인가요?
+ 5
+
+ 실행 결과
+ pobi : -
+ woni :
+ jun : -
+
+ pobi : --
+ woni : -
+ jun : --
+
+ pobi : ---
+ woni : --
+ jun : ---
+
+ pobi : ----
+ woni : ---
+ jun : ----
+
+ pobi : -----
+ woni : ----
+ jun : -----
+
+ 최종 우승자 : pobi, jun
+
+```
+
+---
+
+## ✅ 기능 구현 목록
+
+1. [] 자동차 이름 입력 받기
+ - 1-1. [✔️] 이름 입력 메세지 출력
+ - ex) "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"
+ - 1-2. [✔️] 이름 입력 처리
+ - 이름 입력은 쉼표로 구분
+ - 이름에 공백이 들어갈 경우 제거
+ - 빈 문자열 제거
+ - ex) pobi,woni,jun
+ - 1-3. [✔️] 이름 입력 예외 처리
+ - 이름이 두 개 미만인 경우
+ - 각 이름이 5글자 초과인 경우
+ - 중복되는 자동차 이름이 있는 경우
+ - throw문을 사용해 "[ERROR]" 로 시작하는 메세지를 가지는 예외 발생
+ - 애플리케이션 즉시 종료
+
+2. [] 시도 횟수 입력 받기
+ - 2-1. [✔️] 시도 횟수 입력 메세지 출력
+ - ex) "시도할 횟수는 몇 회인가요?"
+ - 2-2. [✔️] 시도 횟수 입력 처리
+ - 공백이 들어갈 경우 제거
+ - ex) 3
+ - 2-3. [✔️] 시도 횟수 입력 예외처리
+ - 1보다 작은 숫자를 입력한 경우
+ - 자연수가 아닌 형식을 입력한 경우
+ - 시도 횟수가 10 이상인 경우(임의 설정)
+ - throw문을 사용해 "[ERROR]" 로 시작하는 메세지를 가지는 예외 발생
+ - 애플리케이션 즉시 종료
+
+3. [] 자동차 만들기
+ - 3-1. [✔️] 입력 값에 따라 자동차 생성
+
+4. [] 자동차 경주
+ - 4-1. [✔️] 0 ~ 9 사이 무작위 값 생성
+ - 4-2. [✔️] 4 이상일 경우 한칸 전진
+ - 4-3. [✔️] 시도 횟수 만큼 경주 진행
+ - 4-4. [✔️] 마지막 경주로 최종 우승자 구하기
+ - 우승자 한 명 이상 가능
+
+5. [] 실행 결과 나타내기
+ - 5-1. [✔️] 실행 결과 메세지 출력
+ - "실행 결과"
+ - 5-2. [✔️] 각 차수별 실행 결과 메세지 출력
+
+6. [] 최종 우승자 나타내기
+ - 6-1. [✔️] 최종 우승자 메세지 출력
+ - 우승자가 여려명일 경우 쉼표로 구분
+ - ex) 최종 우승자 : pobi, jun
+
+---
+
+## ✔️ 테스트 구현 목록
+
+1. [] 자동차 이름 입력 받기
+ - T-1-1. [✔️] 이름 입력 메세지 출력 테스트
+ - T-1-2. [✔️] 이름 입력 처리 테스트
+ - T-1-3. [✔️] 이름 입력 예외처리 테스트
+
+2. [] 시도 횟수 입력 받기
+ - T-2-1. [✔️] 시도 횟수 입력 메세지 출력 테스트
+ - T-2-2. [✔️] 시도 횟수 입력 처리 테스트
+ - T-2-3. [✔️] 시도 횟수 입력 예외처리 테스트
+
+3. [] 자동차 만들기
+ - T-3-1. [✔️] 입력 값에 따른 자동차 생성 테스트
+
+4. [] 자동차 경주
+ - T-4-1. [✔️] 0 ~ 9 사이 무작위 값 생성 테스트
+ - T-4-2. [✔️] 4 이상일 경우 한칸 전진 테스트
+ - T-4-3. [✔️] 시도 횟수 만큼 경주 진행 테스트
+ - T-4-4. [✔️] 마지막 경주로 최종 우승자 구하기 테스트
+
+5. [] 실행 결과 나타내기
+ - T-5-1. [✔️] 실행 결과 메세지 출력 테스트
+ - T-5-2. [✔️] 각 차수별 실행 결과 메세지 출력 테스트
+
+6. [] 최종 우승자 나타내기
+ - T-6-1. [✔️] 최종 우승자 메세지 출력 테스트
+
+---
+
+## 🖋️ 요구사항
+
+- [] Node.js 18.17.1 버전에서 실행 가능해야 한다.
+- [] 프로그램 실행의 시작점은 App.js의 play 메서드이다.
+- [] package.json을 변경할 수 없다.
+- [] 순수 Vanila JS로만 구현한다.
+- [] 외부 라이브러리(jQuery, Lodash 등)를 사용하지 않는다.
+- [] JavaScript 코드 컨벤션을 지키면서 프로그래밍 한다
+- [] 프로그램 종료 시 process.exit()를 호출하지 않는다
+- [] 프로그램 구현이 완료되면 ApplicationTest의 모든 테스트가 성공해야 한다
+- [] 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.
+- [] indent(인덴트, 들여쓰기) 2까지만 허용한다.
+ - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
+- [] jest를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
+- [] @woowacourse/mission-utils에서 제공하는 Random 및 Console API를 사용하여 구현한다
+- [] Random 값 추출은 Random.pickNumberInRange()를 활용한다.
+- [] 사용자의 값을 입력 받고 출력하기 위해서는 Console.readLineAsync, Console.print를 활용한다
+
+---
+
+## 📢 1주차 공통 피드백
+
+- 요구사항을 정확히 준수한다
+- 커밋 메시지를 의미 있게 작성한다
+- git을 통해 관리할 자원에 대해서도 고려한다
+- Pull Request를 보내기 전 브랜치를 확인한다
+- PR을 한 번 작성했다면 닫지 말고 추가 커밋을 한다
+- 이름을 통해 의도를 드러낸다
+- 축약하지 않는다
+- 공백도 코딩 컨벤션이다
+- 공백 라인을 의미 있게 사용한다
+- space와 tab을 혼용하지 않는다
+- 의미 없는 주석을 달지 않는다
+- linter와 Code Formatter의 기능을 활용한다
+- EOL(End Of Line)
+- 불필요한 console.log를 남기지 않는다
+- JavaScript에서 제공하는 API를 적극 활용한다
+
+---
diff --git a/docs/StudyLog.md b/docs/StudyLog.md
new file mode 100644
index 000000000..25a35e1b5
--- /dev/null
+++ b/docs/StudyLog.md
@@ -0,0 +1,548 @@
+# 📖 배운 내용 📖
+
+## 1. 자동차 이름 입력 받기
+
+
+
+### 1-1. 이름 입력 메세지 출력
+
+- `Class`
+ - 객체 지향 프로그래밍에서, 객체를 생성하기 위한 '틀' 또는 '설계도'의 개념
+ - 데이터(속성 or 멤버 변수) + 데이터를 조작하는 메서드(함수)를 포함한다
+- `Method`
+ - 클래스 내에서 정의된 함수
+ - 객체가 수행할 수 있는 동작이나 행위
+ - 객체의 속성(데이터)을 조작하거나 처리한다
+ - '달린다', '멈춘다' 등
+- `this 키워드`
+ - 현재 객체 또는 현재 컨텍스트를 참조하는 키워드
+- `async 와 await`
+ - 비동기 처리 패턴으로 어떤 동작의 완료를 기다려야 할 때 사용한다
+ - 예를 들어 사용자의 입력을 기다리는 작업등은 시간이 얼마나 걸릴지 모르므로 비동기로 처리한다
+ - async - 함수가 비동기로 작동함을 선언한다
+ - await - 비동기 동작의 완료를 기다린다
+- `readLine(query, callback)`
+ - 주어진 질문을 화면에 출력한다
+ - 사용자가 답변을 입력할 때까지 기다린다
+ - 이후 입력된 답변을 인수로 전달하는 콜백 함수를 호출한다
+
+ ``` javascript
+ Console.readLine('닉네임을 입력해주세요.', (answer) => {
+ console.log(`닉네임: ${answer}`);
+ });
+ ```
+
+- `readLineAsync(query)`
+ - 주어진 질문을 화면에 출력한다
+ - 사용자가 입력한 답변을 Promise를 통해 반환한다
+
+ ``` javascript
+ async function getUername() {
+ try{
+ const username = await Console.readLineAsync('닉네임을 입력해주세요.');
+ } catch (error) {
+ // reject 되는 경우
+ }
+ }
+ ```
+
+- `print(message)`
+ - 주어진 문자열을 콘솔에 출력한다
+
+ ``` javascript
+ async function getUername() {
+ try{
+ const username = await Console.readLineAsync('닉네임을 입력해주세요.');
+ } catch (error) {
+ // reject 되는 경우
+ }
+ }
+ ```
+
+
+
+### T-1-1. 이름 입력 메세지 출력 테스트
+
+- `Mocking`
+ - 실제 구현을 바꾸지 않고도 테스트의 격리를 보장하고 실행한다
+- `Spy`
+ - 함수나 메서드가 어떻게 호출되었는지 추적한다
+- `Mocking Clear`
+ - 각 테스트 사이에서 Mock과 Spy의 상태를 초기화한다
+ - 다른 테스트의 결과에 영향을 주지 않기 위한 목적
+- `Describe`
+ - 관련 테스트들을 그룹화하는데 사용된다
+ - 블록 안 테스트들이 특정 클래스나 함수, 기능 등에 관한 것이라는 것을 알려준다
+- `Test`
+ - 개별 테스트 케이스를 정의한다
+ - 첫 번째 인자는 테스트의 이름 또는 설명을 문자열로 받는다
+ - 두번째 인자는 테스트 로직을 포함하는 함수를 받는다
+- `Expectation`
+ - 값에 대한 특정 조건을 설정한다.
+
+
+
+### 1-2. 이름 입력 처리
+
+- `this 키워드`
+ - 같은 클래스 내부의 다른 메서드나 속성을 접근할 수 있다
+ - 인자를 해당 메서드에 넘겨줄 수 있다
+- `객체`
+ - 데이터와 관련된 기능을 함께 포함하는 항목을 의미
+ - 생성의 기원이되는 정보 명시 (X)
+ - 모든 객체는 인스턴스 (X)
+- `인스턴스`
+ - 클래스를 기반으로 생성된 메모리에 할당된 실제 구현체를 의미
+ - 클래스 - 쿠키를 만드는 틀
+ - 인스턴스 - 그 틀로 만든 실제 쿠키
+ - 생성의 기원이 되는 클래스나 프로토타입에 대한 정보 명시 (O)
+ - 모든 인스턴스는 객체 (O)
+ - 생성 : const 인스턴스명 = new 클래스명(매개변수);
+
+ ``` javascript
+ //Dog : 클래스, 설계도나 틀의 기능
+ class Dog {
+ constructor(name) {
+ this.name = name;
+ }
+
+ bark() {
+ console.log(`${this.name} says mung!`);
+ }
+ }
+
+ //myDog : Dog 클래스를 바탕으로 실제 개를 생성
+ const myDog = new Dog("Bobby");
+
+ //객체를 생성했으므로 bark의 기능 수행 가능
+ //출력 : Bobby says mung!
+ myDog.bark();
+ ```
+
+- `화살표 함수`
+ - ES6에서 도입된 새로운 함수 표현 방식
+ - 간결한 문법 - 본문이 한 줄인 경우 중괄호 및 'return'문 생략 가능
+ - this바인딩 - 자신만의 this를 가지지 않고, 상위 스코프 this 참조
+- `전역 객체`
+ - 브라우저 : window 객체, 전역으로 선언된 변수나 함수
+ - Node.js : global 객체, 명시적으로 할당된 변수나 함수
+ - 모든 스코프에서 접근 가능하기 때문에 주의가 필요하다
+- `split`
+ - 문자열 메서드
+ - 특정 구분자를 기준으로 문자열을 여러 부분으로 나누어 배열로 반환한다
+
+ ``` javascript
+ const str = "apple,banana,grape";
+ const fruits = str.split(',');
+ // ["apple", "banana", "grape"]
+ ```
+
+- `map`
+ - 배열 메서드
+ - 배열의 각 요소에 대해 주어진 함수를 호출한다
+ - 그 결과로 구성된 새로운 배열을 반환한다
+ - 불변성 - 원본 배열을 수정하지 않고 새로운 배열을 반환한다
+ - 순회 - 배열의 모든 요소에 대해 주어진 함수를 순서대로 실행한다
+
+
+
+### T-1-2. 이름 입력 처리 테스트
+
+- `matcher`
+ - 테스팅 라이브러리에서 제공하는 함수나 메서드
+ - 실제 결과와 기대값을 비교하여 테스트의 성공 여부를 결정하는데 사용된다
+- `toBe`
+ - 기본 타입의 값이 일치하는지 확인한다
+- `not`
+ - matcher의 반대 결과를 반환한다
+ - 주로 두 값이 다른지 확인하고 싶을 때 사용한다
+- `toBeTruthy`
+ - 값이 boolean에서 true로 평가되는지 확인한다
+- `toBeFalsy`
+ - 값이 boolean에서 false로 평가되는지 확인한다
+- `toContain`
+ - 배열이나 문자열이 특정 항목 또는 문자열을 포함하는지 확인한다
+
+
+
+### 1-3. 이름 입력 예외 처리
+
+- `Set의 개념`
+ - ES6에서 도입된 데이터 구조
+ - 유일한 값만 저장되는 컬렉션으로 중복을 허용하지 않는다
+- `Set의 특징`
+ - 값은 유일하게 하나만 존재한다
+ - 각 요소는 순서를 가지지 않는다
+ - 배열과 유사하지만 중복을 허용하지 않는다는 점이 주요 차이점이다
+- `중복체크와 Set`
+ - 배열에 중복되는 이름이 있다면, Set으로 변환했을때의 길이와 원래 배열의 길이가 달라진다
+ - Set은 중복을 허용하지 않기 때문에 중복 값을 제거하게 된다
+ - 따라서 두 길이를 비교함으로써 원래 배열에 중복 요소가 있는지 확인할 수 있다
+- `_언더스코어, #`
+ - 일반적으로 private 또는 내부에서만 사용되는 메서드나 변수를 나타내기 위해 사용된다
+ - 즉 변수나 메서드를 외부에서 접근하거나 사용하면 안된다는 의미를 전달한다
+ - JavaScript에는 private키워드 대신 컨벤션으로 언더스코어를 사용해왔다
+ - ES6부터는 #을 사용하여 private필드를 정의할 수 있게되었지만 많은 개발자들은 여전히 언더스코어 컨벤션을 사용한다.
+ - 에어비앤비 스타일 가이드는 언더스코어를 붙이는 것을 권장하지 않는다
+
+
+
+### T-1-3. 이름 입력 예외 처리 테스트
+
+- `Private 메서드 테스트`
+ - Priavate 메서드는 직접 테스트 할 수 없다
+ - 간접적으로 public 메서드를 사용하여 테스트해야한다
+ - 특히 private 메서드에 영향을 받는 public 메서드를 통해 그 효과를 확인 할 수 ㅣ있다
+- `비동기 에러 처리 테스트`
+ - await expect(...).rejects.toThrow(...) 구문을 사용한다
+
+ ``` javascript
+ test("비동기 에러 예상하기", async () => {
+ await expect(asyncFunction()).rejects.toThrow("예상되는 에러 메시지");
+ });
+ ```
+
+ - 이를 통해 비동기 함수가 특정 에러를 반환하는지 테스트 할 수 있다
+ - 결론적으로, 직접적인 방법으로 테스트 할 수 없는 코드나 상황들에도 간접적인 방법을 통해 코드의 품질을 확보할 수 있다
+
+
+
+---
+
+## 2. 시도 횟수 입력 받기
+
+
+
+### 2-1. 시도 횟수 입력 메세지 출력
+
+- `parseInt`
+ - 문자열을 정수로 변환하는 함수
+ - 사용자에게 숫자를 입력받도록 요청하더라도, 반환값은 문자열 형태가 된다
+ - 그렇기 때문에 문자열을 실제 정수로 사용하기 위해, parseInt함수가 쓰인다
+ - 형식은 다음과 같다
+
+ ``` javascript
+ parseInt(string, radix);
+ ```
+
+ - String - 변환될 문자열
+ - radix - 기수, 문자열이 표시되는 진수를 나타내는 2에서 36사이 정수이다
+ - ex) 10은 parseInt의 두 번째 인자로, 10진수로 변환하라는 의미이다
+ - radix를 생략하면 문자열에 따라 다른 진수를 사용한다
+ - 0x 또는 0X - 16진수로 간주
+ - 0o - 8진수로 간주
+ - 나머지 경우 - 기본적으로 10진수로 간주
+ - 하지만 예상치 못한 결과를 방지하기 위해 radix 값을 항상 제공하는 것이 좋다
+
+
+
+### T-2-1. 시도 횟수 입력 메세지 출력 테스트
+
+- `toHaveBeen 관련 matcher`
+ - toHaveBeen으로 시작하는 matcher들은 특히 mock함수의 호출과 관련된 검증에 사용된다
+- `toHaveBeenCalled`
+ - mock 함수가 호출되었는지 확인한다
+- `toHaveBeenCalledTimes`
+ - mock 함수가 특정 횟수만큼 호출되었는지 확인한다
+
+ ``` javascript
+ expect(mockFunc).toHaveBeenCalledTimes(3);
+ // mockFunc이 3번 호출되었는지 확인
+ ```
+
+- `toHaveBeenCalledWith`
+ - mock 함수가 특정 인수로 호출되었는지 확인한다
+
+ ``` javascript
+ expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
+ // mockFunc이 arg1, arg2로 호출되었는지 확인
+ ```
+
+- `toHaveBeenLastCalledWith`
+ - mock 함수가 마지막으로 호출될 때 주어진 인수로 호출되었는지 확인한다
+
+ ``` javascript
+ expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
+ // mockFunc이 마지막으로 arg1, arg2로 호출되었는지 확인
+ ```
+
+- `toHaveBeenNthCalledWith`
+ - mock 함수가 n번째로 호출될 때 특정 인수로 호출되었는지 확인한다
+
+ ``` javascript
+ expect(mockFunc).toHaveBeenNthCalledWith(1, arg1, arg2);
+ // mockFunc이 첫 번째로 arg1, arg2로 호출되었는지 확인
+ ```
+
+- `toHaveReturned`
+ - mock 함수가 값을 반환했는지 확인한다
+- `toHaveReturnedWith`
+ - mock 함수가 특정 값을 반환했는지 확인한다
+
+ ``` javascript
+ expect(mockFunc).toHaveReturnedWith(returnValue);
+ // mockFunc이 returnValue를 반환했는지 확인
+ ```
+
+- `toHaveBeenLastReturnedWith`
+ - mock 함수가 마지막 호출에서 특정 값을 반환했는지 확인한다
+
+ ``` javascript
+ expect(mockFunc).toHaveBeenLastReturnedWith(returnValue);
+ // mockFunc의 마지막 호출에서 returnValue를 반환했는지 확인
+ ```
+
+- `toHaveNthReturnedWith`
+ - mock 함수의 n번째 호출에서 특정 값을 반환했는지 확인한다
+
+ ``` javascript
+ expect(mockFunc).toHaveNthReturnedWith(1, returnValue);
+ // mockFunc의 첫 번째 호출에서 returnValue를 반환했는지 확인
+ ```
+
+
+
+### 2-2. 시도 횟수 입력 처리
+
+- `trim`
+ - 문자열의 앞 뒤 공백을 제거하기 위해 trim() 메서드를 사용한다
+
+ ``` javascript
+ const result = " example ".trim();
+ ```
+
+- `parseInt`
+ - 문자열 내에 숫자와 문자가 함께 있을 때
+ - 숫자가 아닌 문자를 만나면 해당 문자까지만 파싱하고, 그 이후의 문자는 무시한다
+
+ ``` javascript
+ const number = parseInt("123abc", 10);
+ // number : 123
+ ```
+
+ - 공백만 있는 문자열 일때
+ - 결과값은 NaN이다
+
+ ``` javascript
+ const input = " ";
+ const result = parseInt(input.trim().replace(/\s+/g, ''), 10);
+ // result : NaN
+ ```
+
+- `replace`
+ - 문자열의 일부를 다른 문자열로 대체하기 위해 사용한다
+ - 두 가지 인자를 받는다
+ - 검색 패턴 - 대체될 문자열 또는 정규 표현식
+ - 대체 문자열 - 대체될 새로운 문자열 또는 함수
+
+ ``` javascript
+ const str = "Hello, World!";
+ const newStr = str.replace("World", "Seojin");
+ console.log(newStr);
+ // 출력: "Hello, Seojin!"
+
+ ```
+
+- `정규표현식`
+ - 문자열의 특정 패턴을 찾기 위한 도구이다
+ - /.../로 둘러싸여 있는 형식으로 표현된다
+ - \d: 모든 숫자(0-9와 동일)
+ - \s: 모든 공백 문자(스페이스, 탭, 줄바꿈 등)
+ - \w: 모든 단어 문자(a-z, A-Z, 0-9, _와 동일)
+ - g: 전역 검색, 문자열 내의 모든 일치 항목을 검색
+ - i: 대소문자를 구분하지 않는 검색
+
+ ``` javascript
+ const str = "Hello 123 World 456!";
+ const newStr = str.replace(/\d+/g, '');
+ console.log(newStr);
+ // 출력: "Hello World !"
+
+ ```
+
+- 결론
+ - 문자열을 적절하게 정제한 후 숫자로 변환하는 것이 효율적이다
+
+
+
+### 2-3. 시도 횟수 입력 예외 처리
+
+- `includes()`
+ - 문자열이 특정 부분 문자열을 포함하고 있는지 확인하기 위해 사용된다
+ - 지정된 부분 문자열을 포함하면 true, 그렇지 않으면 false 반환
+ - 정규 표현식 사용할 필요 없이 빠르게 확인할 수 있다
+
+ ``` javascript
+ string.includes(searchString, position)
+ ```
+
+ - searchString - 메인 문자열에서 찾을 부분 문자열
+ - position - 선택 사항, 문자열 내에서 검색을 시작할 위치, 기본값 0
+
+- `Number.isInteger()`
+ - 전달된 값이 정수인지 여부를 결정한다
+ - 값이 정수면 true, 그렇지 않으면 false 반환
+ - 사용자 입력이나 데이터 타입이 다양할 수 있는 데이터를 검증할 때 유용하다
+
+ ``` javascript
+ Number.isInteger(value)
+ ```
+
+ - value - 확인할 값
+
+
+
+---
+
+## 3. 자동차 만들기
+
+
+
+### 3-1. 입력 값에 따른 자동차 생성
+
+- `constructor`
+ - 생성자
+ - 클래스 내에서 객체의 초기 상태를 설정하는 데 사용되는 특별한 메서드
+ - 클래스를 통해 객체가 생성될 때 constructor가 자동으로 호출된다
+ - 이를 통해 초기 속성 값을 설정하거나, 필요한 초기화 작업을 수행할 수 있다
+ - 생성자는 필요에 따라 파라미터를 받을 수도 있다
+ - 이를 통해 객체 생성 시 외부에서 값을 전달하여 객체의 상태를 설정할 수 있다
+
+ ``` javascript
+ class App {
+ constructor() {
+ this.car = [];
+ }
+ }
+ ```
+
+ - App 클래스의 constructor는 this.car라는 빈 배열을 초기화하고 있다
+ - 이는 App 클래스의 인스턴스가 생성될 때마다 this.car가 빈 배열로 초기화된다는 것을 의미한다
+
+
+
+---
+
+## 4. 자동차 경주
+
+
+
+### 4-1. 0 ~ 9 사이 무작위 값 생성
+
+- `pickNumberInRange(startInclusive, endInclusive)`
+ - 숫자 범위를 지정하면 시작 또는 끝 숫자를 포함하여 범위의 숫자를 반환한다
+ - 형식은 다음과 같다
+
+ ``` javascript
+ Random.pickNumberInRange(1, 10); // 1
+ Random.pickNumberInRange(1, 10); // 10
+ Random.pickNumberInRange(1, 10); // 4
+ Random.pickNumberInRange(1, 10); // 5parseInt(string, radix);
+ ```
+
+- `pickNumberInList(array)`
+ - 목록에 있는 숫자 중 하나를 반환한다
+ - 형식은 다음과 같다
+
+ ``` javascript
+ Random.pickNumberInList([1, 3, 10]); // 1
+ Random.pickNumberInList([1, 3, 10]); // 10
+ Random.pickNumberInList([1, 3, 10]); // 3
+ ```
+
+- `pickUniqueNumbersInRange(startInclusive, endInclusive, count)`
+ - 숫자 범위 내에서 지정된 개수만큼 겹치지 않는 숫자를 반환한다
+ - 형식은 다음과 같다
+
+ ``` javascript
+ Random.pickUniqueNumbersInRange(1, 10, 2); // [1, 2]
+ Random.pickUniqueNumbersInRange(1, 10, 5); // [1, 10, 7, 8, 5]
+ ```
+
+- `shuffle(array)`
+ - 무작위로 섞인 새 목록을 반환한다
+ - 형식은 다음과 같다
+
+ ``` javascript
+ Random.shuffle([1, 2, 3, 4, 5]); // [2, 4, 1, 3, 5]
+ ```
+
+
+
+### T-4-2. 값이 4 이상일 경우 한칸 전진 테스트
+
+- `jest.fn()`
+ - Jest에서 제공하는 모의 함수
+ - 호출되었을 때, 어떻게 호출되었는지, 몇 번 호출되었는지, 어떤 인자로 호출되었는지 등의 정보를 추적한다
+- `jest.spyOn()`
+ - 객체의 메서드를 스파이하는 데 사용
+ - 해당 메서드가 어떻게 호출되었는지 검증하면서, 원래의 구현을 유지하거나 모의 구현으로 대체할 수 있다
+- `mockReturnValue(value)`
+ - 모의 함수가 호출될 때 특정 값을 반환하도록 설정한다
+- `.mockImplementation(fn)`
+ - 원래의 함수 대신 제공된 fn 함수를 호출하도록 설정한다
+- `.mockImplementationOnce(fn)`
+ - 다음 호출에 대해서만 제공된 fn 함수를 호출하도록 설정한다
+
+
+
+### T-4-3. 시도 횟수 만큼 경주 진행 테스트
+
+- `기본적인 컨텍스트`
+ - 함수는 기본적으로 전역 컨텍스트에서 실행된다
+ - 즉, 일반 함수에서 this를 참조하면 전역 객체 (브라우저에서는 window)를 가리키게 된다
+
+ ``` javascript
+ function checkThis() {
+ console.log(this);
+ }
+
+ checkThis(); // Window
+ ```
+
+- `메서드 호출에서의 컨텍스트`
+ - 객체의 메서드로 함수를 호출할 때, this는 해당 객체를 가리키게 된다
+
+ ``` javascript
+ const obj = {
+ checkThis: function() {
+ console.log(this);
+ }
+ }
+
+ obj.checkThis();
+ ```
+
+- `컨텍스트 손실`
+ - 하지만, 메서드 내부에서 다른 함수나 메서드를 호출하게 되면 this의 바인딩이 손실될 수 있다
+
+ ``` javascript
+ const obj = {
+ name: "Seojin",
+ sayHello: function() {
+ function innerFunc() {
+ console.log(this.name);
+ }
+ innerFunc();
+ }
+ }
+
+ obj.sayHello();
+ ```
+
+- `명시적 바인딩`
+ - 이러한 문제를 해결하기 위해, bind, call, apply와 같은 메서드를 사용하여 함수의 this 값을 명시적으로 설정한다
+
+ ``` javascript
+ const obj = {
+ name: "Seojin",
+ sayHello: function() {
+ const innerFunc = function() {
+ console.log(this.name);
+ }.bind(this);
+ innerFunc();
+ }
+ }
+
+ obj.sayHello();
+ ```
diff --git a/src/App.js b/src/App.js
index c38b30d5b..6ca78b5bb 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,5 +1,141 @@
+import { Console } from "@woowacourse/mission-utils";
+import { Random } from "@woowacourse/mission-utils"
+
class App {
- async play() {}
+
+ constructor() {
+ this.car = [];
+ }
+
+ async play() {
+ const carName = await this.getCarName();
+ const round = await this.getRoundNumber();
+
+ this.makeCar(carName);
+ this.printResultHeader();
+ this.runRace(round);
+
+ const winner = this.getWinner();
+ this.printWinner(winner);
+ }
+
+ async getCarName() {
+ const inputCar = await Console.readLineAsync("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n");
+ return this.sanitizeCarName(inputCar);
+ }
+
+ sanitizeCarName(inputCar) {
+ const sanitizedCar = inputCar.split(',').map(name => name.trim()).filter(name => name);
+ return this.#validateCarName(sanitizedCar);
+ }
+
+ #validateCarName(sanitizedCar) {
+ this.#checkMinimumCar(sanitizedCar);
+ this.#checkCarLength(sanitizedCar);
+ this.#checkCarDuplicate(sanitizedCar);
+ return sanitizedCar;
+ }
+
+ #checkMinimumCar(sanitizedCar) {
+ if (sanitizedCar.length < 2) {
+ throw new Error("[ERROR] 최소 두 대 이상의 자동차 이름을 입력해주세요.");
+ }
+ }
+
+ #checkCarLength(sanitizedCar) {
+ if (sanitizedCar.some(name => name.length > 5)) {
+ throw new Error("[ERROR] 자동차 이름은 5자를 초과할 수 없습니다.");
+ }
+ }
+
+ #checkCarDuplicate(sanitizedCar) {
+ if (new Set(sanitizedCar).size !== sanitizedCar.length) {
+ throw new Error("[ERROR] 중복되는 자동차 이름이 있습니다.");
+ }
+ }
+
+ async getRoundNumber() {
+ const inputRound = await Console.readLineAsync("시도할 횟수는 몇 회인가요?\n");
+ return this.sanitizeRoundNumber(inputRound);
+ }
+
+ sanitizeRoundNumber(inputRound) {
+ const sanitizedRound = parseInt(inputRound.trim().replace(/\s+/g, ''), 10);
+ if (inputRound.includes('.') || !Number.isInteger(sanitizedRound)) {
+ throw new Error("[ERROR] 횟수는 자연수 형식이어야 합니다");
+ }
+ return this.#validateRound(sanitizedRound);
+ }
+
+ #validateRound(sanitizedRound) {
+ this.#checkMinimumRound(sanitizedRound);
+ this.#checkMaximumRound(sanitizedRound);
+ return sanitizedRound;
+ }
+
+ #checkMinimumRound(sanitizedRound) {
+ if (sanitizedRound < 1) {
+ throw new Error("[ERROR] 최소 1 이상의 횟수를 입력해주세요.");
+ }
+ }
+
+ #checkMaximumRound(sanitizedRound) {
+ if (sanitizedRound > 10) {
+ throw new Error("[ERROR] 최대 10 이하의 횟수를 입력해주세요.");
+ }
+ }
+
+ makeCar(carName) {
+ const getRandom = this.getRandomNumber.bind(this);
+ this.car = carName.map(name =>({
+ name,
+ distance: 0,
+ move() {
+ if (this.shouldMove()) {
+ this.distance += 1;
+ }
+ },
+ shouldMove() {
+ const randomValue = getRandom();
+ return randomValue >= 4;
+ }
+ }));
+ }
+
+ getRandomNumber() {
+ const randomValue = Random.pickNumberInRange(0, 9);
+ return randomValue;
+ }
+
+ runRace(totalRound) {
+ for (let i = 0; i < totalRound; i++) {
+ this.car.forEach(car => car.move());
+ this.printRoundResult();
+ Console.print("\n");
+ }
+ }
+
+ getWinner() {
+ const maxDistance = Math.max(...this.car.map(car => car.distance));
+ return this.car.filter(car => car.distance === maxDistance).map(car => car.name);
+ }
+
+ printResultHeader() {
+ Console.print("\n실행 결과");
+ }
+
+ printRoundResult() {
+ this.car.forEach(car => {
+ Console.print(`${car.name} : ${'-'.repeat(car.distance)}`);
+ });
+ }
+
+ printWinner(winner) {
+ Console.print(`최종 우승자 : ${winner.join(", ")}`);
+ }
}
+const app = new App();
+app.play();
+
export default App;