From a651ed018906f5f8d3ef6d9ad9ec7bd819ac3e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Sun, 26 Oct 2025 23:20:32 +0900 Subject: [PATCH 01/17] =?UTF-8?q?docs(readme):=20=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 요구사항을 바탕으로 구조화한 설계와 기능 목록을 정리하고, 설계 단계에서 고민한 내용들을 정리했습니다. --- README.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/README.md b/README.md index e078fd41..c88c66f2 100644 --- a/README.md +++ b/README.md @@ -1 +1,87 @@ # javascript-racingcar-precourse +# 학습 목표 +- 여러 역할을 하는 큰 함수를 단일 역할을 하는 작은 함수로 분리한다. +- 테스트 도구를 사용하는 방법을 배우고 프로그램이 제대로 작동하는 지 테스트 한다. +- 1주차 피드백을 반영한다. +# 요구사항 정리 +### 1주차 피드백 정리 +- git 명령어, git 자원, 관리 파일(package.json, package-lock.json 등) 역할 학습 +- 디버거 사용에 익숙해지기 +- 좋은 이름 짓기 +- 정확한 공백 사용 +- 의미 없는 주석 달지 않기 +- JavaScript API 적극 활용 +### 기능 요구사항 +자동차 경주 게임을 구현 +- 사용자는 자동차의 이름과 몇 번 이동할 것 인지 입력할 수 있다. + - 이름에 대한 입력은 쉼표를 기준으로 구분하며, 각 자동차 명은 5자 이하 +- 주어진 횟수 동안 전진/정지 할 수 있다. + - 0~9 무작위 값이 4 이상일 경우 전진 / 4 미만 정지 +- 자동차 경주 게임이 종료된 후 누가 우승했는지를 알려준다. + - 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분 +- 사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨 후 애플리케이션은 종료된다. +### 프로그래밍 요구사항 +- 함수가 한가지 일만 하도록 작게 만들기 + - 들어쓰기 깊이 2까지만 허용 +- 3항 연산자 사용 X +- Jest로 테스트 코드 작성하여 기능 정상 작동 확인 + - Jest 활용법 공부하기 + +# 설계 +## 함수 구조 +```mermaid +flowchart TD + %% 최상위 (부수효과 담당) + main["App.run()"] + %% 핵심 로직 (순수) + main --> Race + + subgraph Race["Race (순수 함수 계층)"] + direction TB + R1["runEntireRace()"] --> R2["moveCarsOneLap()"] + R2 --> R3["decideCarMoveOrNot()"] + R1 --> R4["applySingleLapResult()"] + R1 --> R6["selectWinningCars()"] + end + + %% 입력 처리 + main --> input["getInputUsingMissionUtils()"] + %% 결과 출력 + main --> output["printOutputUsingMissionUtils()"] + %% 결과 렌더링 + main --> render["renderHistory()"] + render --> output + + %% 외부 난수 API + main --> random["makeRandomNumberArray()"] +``` +## 기능 목록 +- [ ] 입력 기능 + - [ ] 자동차 명 입력 받기 + - [ ] 검증: 각 자동차의 명은 5자 이하이다. + - [ ] 진행 랩 수(몇번 진행할 것인지) 입력 받기 + - [ ] 검증: 랩 수는 양의 정수이다. +- [ ] 랜덤 넘버 데이터 생성 + - [ ] `자동차 수 * 랩 수` 만큼의 랜덤 숫자(0~9사이 정수) 생성 +- [ ] 레이스 진행(전진 or 정지) + - [ ] 랜덤 숫자가 4 이상(4,5,6,7,8,9)이면 전진, 4미만(0,1,2,3)이면 정지 +- [ ] 우승자 결정 + - [ ] 가장 많이 전진한 자동차가 우승자로 결정된다. + - [ ] 공동 우승도 인정된다. +- [ ] 레이스 히스토리 출력 + - 각 랩 순으로 레이스 기록을 출력한다. +- [ ] 우승자명 출력 + - 동률의 경우 `,`로 구분하여 출력한다. + +# 문제 해결 과정 +### 설계 단계 +> OOP + FP(함수형 프로그래밍) +- 해결해야하는 문제: 프리코스 기간 동안 직접 함수형 패러다임을 공부하고 적용을 시도해보고 있습니다. 함수형 패러다임을 OOP에 융합 시키기 위한 구조 설계에 어려움을 겪고 있습니다. +- 어떻게 해결했는지: 큰 틀에서 보면 `App 클래스`가 `부수효과(입출력, API)`를 모두 담당하고 레이스를 진행하는 부분은 순수함수로 작성하려고 합니다. 이를 바탕으로 함수명 구조를 짜보았는데, 아마 구현을 진행하면서 많은 수정이 있을 것 같습니다. +> Car 클래스 분리 +- 해결해야하는 문제: Car을 객체로 만들어서 관리한다는 아이디어가 자연스레 떠올랐습니다. 의미적으로 확실하게 분리가 되니까 관리와 가독성에 장점이 있다고 생각합니다. 하지만 자동차가 복잡도 높은 기능을 수행하는 것도 아니기 때문에 고민이 되었습니다. +- 어떻게 해결했는지: 클래스보다는 객체와 같은 구조로 관리를 하여 함수형 중심적으로 설계를 했습니다. +> 랜덤 넘버 생성 +- 해결해야하는 문제: 함수형 패러다임의 적용과 연결되는 부분입니다. API를 통해서 생성하는 난수들을 어떻게 생성하고 사용할 지에 대한 문제입니다. 처음에는 별 생각없이 매 라운드마다 난수를 만들어서 판별하도록 하면 되겠다고 생각했지만 설계를 하다보니 레이스마다 매번 난수를 생성하면 순수성을 유지하지 못하고, 테스트에도 어려움이 있을 것이라 판단했습니다. +- 어떻게 해결했는지: App 단에서 필요한 개수의 난수를 모두 생성하여 넘겨주는 방식으로 문제를 해결해보려 합니다. 마찬가지로 매 랩읙 결과를 모두 끝이 나고 한 번에 순서대로 출력하는 방식으로 진행하려고 합니다. +### 구현 단계 \ No newline at end of file From 922135f54d29ebd832ede06be60a30dd4525ae4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 00:00:38 +0900 Subject: [PATCH 02/17] =?UTF-8?q?fix(readme):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EA=B2=80=EC=A6=9D=20=EC=84=B8=EB=B6=84?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기능 목록의 검증 기능을 따로 두어 세분화하였습니다. --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c88c66f2..bf868d3f 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,15 @@ flowchart TD main --> random["makeRandomNumberArray()"] ``` ## 기능 목록 -- [ ] 입력 기능 +- [ ] 입력 - [ ] 자동차 명 입력 받기 - - [ ] 검증: 각 자동차의 명은 5자 이하이다. + - [ ] `,` 기준으로 자동차명을 구분 - [ ] 진행 랩 수(몇번 진행할 것인지) 입력 받기 - - [ ] 검증: 랩 수는 양의 정수이다. +- [ ] 입력값 검증(예외) + - [ ] 사용자가 잘못된 값을 입력할 경우 `[ERROR]`로 시작하는 문구와 함께 에러를 발생시키고 프로그램을 종료시킨다. + - [ ] 각 자동차명은 공백을 포함하지 않은 5자 이하의 문자열이다. + - [ ] 각 자동차명은 중복을 허용하지 않는다. + - [ ] 랩 수는 양의 정수이다. - [ ] 랜덤 넘버 데이터 생성 - [ ] `자동차 수 * 랩 수` 만큼의 랜덤 숫자(0~9사이 정수) 생성 - [ ] 레이스 진행(전진 or 정지) From fdfacb9f99765a2756807c25d5e7a7c328775166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 01:42:42 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat(app):=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit woowacourse/mission-utils의 Console을 사용하여 사용자로 부터 입력을 받는 함수를 구현하였습니다. 해당 함수를 사용해 받는 입력은 쉼표로 구분된 자동차명과 진행될 랩 수입니다. --- README.md | 4 ++-- __tests__/InputTest.js | 26 ++++++++++++++++++++++++++ src/App.js | 10 +++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 __tests__/InputTest.js diff --git a/README.md b/README.md index bf868d3f..98c2dfca 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ flowchart TD ``` ## 기능 목록 - [ ] 입력 - - [ ] 자동차 명 입력 받기 + - [x] 자동차 명 입력 받기 - [ ] `,` 기준으로 자동차명을 구분 - - [ ] 진행 랩 수(몇번 진행할 것인지) 입력 받기 + - [x] 진행 랩 수(몇번 진행할 것인지) 입력 받기 - [ ] 입력값 검증(예외) - [ ] 사용자가 잘못된 값을 입력할 경우 `[ERROR]`로 시작하는 문구와 함께 에러를 발생시키고 프로그램을 종료시킨다. - [ ] 각 자동차명은 공백을 포함하지 않은 5자 이하의 문자열이다. diff --git a/__tests__/InputTest.js b/__tests__/InputTest.js new file mode 100644 index 00000000..c875abc9 --- /dev/null +++ b/__tests__/InputTest.js @@ -0,0 +1,26 @@ +import App from "../src/App.js"; +import { MissionUtils } from "@woowacourse/mission-utils"; + +const mockQuestions = (inputs) => { + MissionUtils.Console.readLineAsync = jest.fn(); + + MissionUtils.Console.readLineAsync.mockImplementation(() => { + const input = inputs.shift(); + return Promise.resolve(input); + }); +}; + + +describe("woowacourse/mission-utils readLineAsync 입력 테스트", () => { + test.each([ + ["경주할 자동차 이름을 입력해주세요.", "a,b,c"], + ["경주할 자동차의 이름을 입력해주십쇼.","hihi, woowa, pre"], + ["시도할 횟수는 몇 회인가요?", "5"] + ])("입력값(자동차명 or 랩 수)가 정상적으로 처리되어 가져온다.", async (question, answer) => { + mockQuestions([answer]); + const app = new App(); + const userReply = await app.readInputAsyncUsingWoowaMissionApi(question); + expect(MissionUtils.Console.readLineAsync).toHaveBeenCalledWith(question); + expect(userReply).toBe(answer); + }) +}) \ No newline at end of file diff --git a/src/App.js b/src/App.js index 091aa0a5..f5f8cd79 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,13 @@ +import { Console } from "@woowacourse/mission-utils"; + class App { - async run() {} + async run() { + const namesOfCarUserRequest = await this.readInputAsyncUsingWoowaMissionApi("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + const countOfLapUserRequest = await this.readInputAsyncUsingWoowaMissionApi("시도할 횟수는 몇 회인가요?"); + } + async readInputAsyncUsingWoowaMissionApi(questionStr) { + return await Console.readLineAsync(questionStr); + } } export default App; From 0c0f8912e5d384440e2842cbbefa2a1e5cc70bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 02:26:47 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat(parsing):=20=EC=89=BC=ED=91=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=ED=8C=8C=EC=8B=B1=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 입력받은 자동차를 쉼표를 기준으로 파싱하기 위해서 parseByComma 함수를 구현하였습니다. --- README.md | 4 ++-- __tests__/UtilTest.js | 13 +++++++++++++ src/App.js | 4 +++- src/utils/parsing.js | 4 ++++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 __tests__/UtilTest.js create mode 100644 src/utils/parsing.js diff --git a/README.md b/README.md index 98c2dfca..9bec8dc8 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,9 @@ flowchart TD main --> random["makeRandomNumberArray()"] ``` ## 기능 목록 -- [ ] 입력 +- [x] 입력 - [x] 자동차 명 입력 받기 - - [ ] `,` 기준으로 자동차명을 구분 + - [x] `,` 기준으로 자동차명을 구분 - [x] 진행 랩 수(몇번 진행할 것인지) 입력 받기 - [ ] 입력값 검증(예외) - [ ] 사용자가 잘못된 값을 입력할 경우 `[ERROR]`로 시작하는 문구와 함께 에러를 발생시키고 프로그램을 종료시킨다. diff --git a/__tests__/UtilTest.js b/__tests__/UtilTest.js new file mode 100644 index 00000000..53507eca --- /dev/null +++ b/__tests__/UtilTest.js @@ -0,0 +1,13 @@ +import { parseByComma } from "../src/utils/parsing"; + +describe("유틸 함수 테스트", () => { + test.each(([ + ["a,b,c", ["a", "b", "c"]], + ["ab,cd,e,f ", ["ab", "cd", "e", "f"]], + [" a , b, c, d ", ["a", "b", "c", "d"]], + ["a,b,,d", ["a", "b", "", "d"]] + ]))("쉼표를 기준으로 문자열을 파싱해주는 parseByComma 테스트", (input, parsed) => { + const parsingInput = parseByComma(input); + expect(parsingInput).toStrictEqual(parsed); + }) +}) \ No newline at end of file diff --git a/src/App.js b/src/App.js index f5f8cd79..a10ead1a 100644 --- a/src/App.js +++ b/src/App.js @@ -1,8 +1,10 @@ import { Console } from "@woowacourse/mission-utils"; +import { parseByComma } from "./utils/parsing"; class App { async run() { - const namesOfCarUserRequest = await this.readInputAsyncUsingWoowaMissionApi("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + const stringOfCarNamesUserRequest = await this.readInputAsyncUsingWoowaMissionApi("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)").split(","); + const arrayOfCarNamesUserRequest = parseByComma(stringOfCarNamesUserRequest); const countOfLapUserRequest = await this.readInputAsyncUsingWoowaMissionApi("시도할 횟수는 몇 회인가요?"); } async readInputAsyncUsingWoowaMissionApi(questionStr) { diff --git a/src/utils/parsing.js b/src/utils/parsing.js new file mode 100644 index 00000000..9e4f86bd --- /dev/null +++ b/src/utils/parsing.js @@ -0,0 +1,4 @@ +export function parseByComma(input) { + return input.split(",").map(n => n.trim()); +} + From 8d347c3d632a4e132238e4ec5c67ce7311d269db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 09:52:09 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat(app):=20=EB=9E=9C=EB=8D=A4=20?= =?UTF-8?q?=EC=88=AB=EC=9E=90=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit missionutil의Random을사용해 무작위 수를 생성하는 함수를 추가하였습니다. 그리고 해당 함수를 호출하여 필요로 하는 개수(차량의 수*랩 수)의 난수 배열을 반환하는 함수를 구현했습니다. --- README.md | 4 ++-- __tests__/{InputTest.js => APIsTest.js} | 24 ++++++++++++++++++++---- __tests__/ApplicationTest.js | 9 ++++++++- src/App.js | 22 +++++++++++++++++----- 4 files changed, 47 insertions(+), 12 deletions(-) rename __tests__/{InputTest.js => APIsTest.js} (53%) diff --git a/README.md b/README.md index 9bec8dc8..fdca5bba 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ flowchart TD - [ ] 각 자동차명은 공백을 포함하지 않은 5자 이하의 문자열이다. - [ ] 각 자동차명은 중복을 허용하지 않는다. - [ ] 랩 수는 양의 정수이다. -- [ ] 랜덤 넘버 데이터 생성 - - [ ] `자동차 수 * 랩 수` 만큼의 랜덤 숫자(0~9사이 정수) 생성 +- [x] 랜덤 넘버 데이터 생성 + - [x] `자동차 수 * 랩 수` 만큼의 랜덤 숫자(0~9사이 정수) 생성 - [ ] 레이스 진행(전진 or 정지) - [ ] 랜덤 숫자가 4 이상(4,5,6,7,8,9)이면 전진, 4미만(0,1,2,3)이면 정지 - [ ] 우승자 결정 diff --git a/__tests__/InputTest.js b/__tests__/APIsTest.js similarity index 53% rename from __tests__/InputTest.js rename to __tests__/APIsTest.js index c875abc9..056b0f2b 100644 --- a/__tests__/InputTest.js +++ b/__tests__/APIsTest.js @@ -11,16 +11,32 @@ const mockQuestions = (inputs) => { }; -describe("woowacourse/mission-utils readLineAsync 입력 테스트", () => { +describe("woowacourse/mission-utils api 테스트", () => { + let app; + beforeEach(() => { + app = new App(); + }) + test.each([ ["경주할 자동차 이름을 입력해주세요.", "a,b,c"], ["경주할 자동차의 이름을 입력해주십쇼.","hihi, woowa, pre"], ["시도할 횟수는 몇 회인가요?", "5"] - ])("입력값(자동차명 or 랩 수)가 정상적으로 처리되어 가져온다.", async (question, answer) => { + ])("readLineAsync가 입력값(자동차명 or 랩 수)을 정상적으로 처리되어 가져온다.", async (question, answer) => { mockQuestions([answer]); - const app = new App(); const userReply = await app.readInputAsyncUsingWoowaMissionApi(question); expect(MissionUtils.Console.readLineAsync).toHaveBeenCalledWith(question); expect(userReply).toBe(answer); - }) + }); + test("0 이상 9 이하의 정수를 반환한다", () => { + // 여러 번 호출해서 랜덤성 포함 확인 + const results = Array.from({ length: 100 }, () => + app.pickRandomNumberInRangeUsingWoowaMissionApi(0, 9) + ); + + for (const num of results) { + expect(Number.isInteger(num)).toBe(true); + expect(num).toBeGreaterThanOrEqual(0); + expect(num).toBeLessThanOrEqual(9); + } + }); }) \ No newline at end of file diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 0260e7e8..843a815d 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -45,7 +45,14 @@ describe("자동차 경주", () => { expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); }); }); - + test.each(([ + [["a", "b", "c"], 4, 12], + [["a", "b", "c", "d"], 1, 4] + ]))("필요한 만큼(차량 수 * 랩 수)의 난수 테이프 생성", (cars, laps, expected) => { + const app = new App(); + const randomNumberTape = app.makeRandomNumbersTape(cars, laps); + expect(randomNumberTape.length).toBe(expected); // cars.length * laps + }); test("예외 테스트", async () => { // given const inputs = ["pobi,javaji"]; diff --git a/src/App.js b/src/App.js index a10ead1a..3c445572 100644 --- a/src/App.js +++ b/src/App.js @@ -1,15 +1,27 @@ -import { Console } from "@woowacourse/mission-utils"; +import { MissionUtils } from "@woowacourse/mission-utils"; import { parseByComma } from "./utils/parsing"; class App { async run() { const stringOfCarNamesUserRequest = await this.readInputAsyncUsingWoowaMissionApi("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)").split(","); const arrayOfCarNamesUserRequest = parseByComma(stringOfCarNamesUserRequest); - const countOfLapUserRequest = await this.readInputAsyncUsingWoowaMissionApi("시도할 횟수는 몇 회인가요?"); + const stringOfLapUserRequest = await this.readInputAsyncUsingWoowaMissionApi("시도할 횟수는 몇 회인가요?"); + const countOfLapUserRequest = Number(stringOfLapUserRequest); + const randomNumberTape = this.makeRandomNumbersTape(arrayOfCarNamesUserRequest, countOfLapUserRequest); } async readInputAsyncUsingWoowaMissionApi(questionStr) { - return await Console.readLineAsync(questionStr); + return await MissionUtils.Console.readLineAsync(questionStr); } -} - + makeRandomNumbersTape(cars, laps) { // 필요한 만큼의 난수를 만들고 레이스 진행 + let numberTape = []; + const countOfNeededNumber = cars.length * laps; + for(let cnt = 0; cnt < countOfNeededNumber; cnt++) { + numberTape.push(this.pickRandomNumberInRangeUsingWoowaMissionApi(0,9)) + } + return numberTape; + } + pickRandomNumberInRangeUsingWoowaMissionApi(min,max) { + return MissionUtils.Random.pickNumberInRange(min, max); + } +}; export default App; From 3a32f8cc2128ecf759dc573d99301258ab474d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 18:13:59 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat(race):=20=EA=B2=BD=EC=A3=BC=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 자동차명, 랩수, 난수를 받아 경주를 진행하는 도메인 함수를 구현하였습니다. 반환값은 경주 히스토리와 최종 결과를 모두 사용할 수 있도록 랩을 기준으로 분리된 2차원 배열로 결정했습니다. --- README.md | 4 ++-- __tests__/APIsTest.js | 6 +++--- __tests__/DomainTest.js | 10 ++++++++++ src/App.js | 9 +++++---- src/domains/race.js | 16 ++++++++++++++++ 5 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 __tests__/DomainTest.js create mode 100644 src/domains/race.js diff --git a/README.md b/README.md index fdca5bba..9b200817 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ flowchart TD - [ ] 랩 수는 양의 정수이다. - [x] 랜덤 넘버 데이터 생성 - [x] `자동차 수 * 랩 수` 만큼의 랜덤 숫자(0~9사이 정수) 생성 -- [ ] 레이스 진행(전진 or 정지) - - [ ] 랜덤 숫자가 4 이상(4,5,6,7,8,9)이면 전진, 4미만(0,1,2,3)이면 정지 +- [x] 레이스 진행(전진 or 정지) + - [x] 랜덤 숫자가 4 이상(4,5,6,7,8,9)이면 전진, 4미만(0,1,2,3)이면 정지 - [ ] 우승자 결정 - [ ] 가장 많이 전진한 자동차가 우승자로 결정된다. - [ ] 공동 우승도 인정된다. diff --git a/__tests__/APIsTest.js b/__tests__/APIsTest.js index 056b0f2b..fdd80539 100644 --- a/__tests__/APIsTest.js +++ b/__tests__/APIsTest.js @@ -27,12 +27,12 @@ describe("woowacourse/mission-utils api 테스트", () => { expect(MissionUtils.Console.readLineAsync).toHaveBeenCalledWith(question); expect(userReply).toBe(answer); }); - test("0 이상 9 이하의 정수를 반환한다", () => { - // 여러 번 호출해서 랜덤성 포함 확인 + + test("Random.pickNumberInRange를 사용해 0 이상 9 이하의 정수를 생성한다", () => { const results = Array.from({ length: 100 }, () => app.pickRandomNumberInRangeUsingWoowaMissionApi(0, 9) ); - + for (const num of results) { expect(Number.isInteger(num)).toBe(true); expect(num).toBeGreaterThanOrEqual(0); diff --git a/__tests__/DomainTest.js b/__tests__/DomainTest.js new file mode 100644 index 00000000..0134dc68 --- /dev/null +++ b/__tests__/DomainTest.js @@ -0,0 +1,10 @@ +import { runEntireRace } from "../src/domains/race"; +describe("레이스 진행 도메인 테스트", () => { + test.each([ + [["a", "b", "c"], 2, [4, 1, 2, 9, 0, 4], [[1, 0, 1], [1, 0, 2]]], + [["red", "blue", "black"], 3, [5, 2, 4, 9, 5, 9, 9, 4, 6], [[1, 1, 1], [2, 2, 2], [3, 2, 3]]] + ])("", (carnames, laps, randomTape, raceHistory) => { + const result = runEntireRace(carnames, laps, randomTape); + expect(result).toStrictEqual(raceHistory); + }) +}) \ No newline at end of file diff --git a/src/App.js b/src/App.js index 3c445572..29f94de4 100644 --- a/src/App.js +++ b/src/App.js @@ -3,24 +3,25 @@ import { parseByComma } from "./utils/parsing"; class App { async run() { - const stringOfCarNamesUserRequest = await this.readInputAsyncUsingWoowaMissionApi("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)").split(","); + const stringOfCarNamesUserRequest = await this.readInputAsyncUsingWoowaMissionApi("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); const arrayOfCarNamesUserRequest = parseByComma(stringOfCarNamesUserRequest); const stringOfLapUserRequest = await this.readInputAsyncUsingWoowaMissionApi("시도할 횟수는 몇 회인가요?"); const countOfLapUserRequest = Number(stringOfLapUserRequest); const randomNumberTape = this.makeRandomNumbersTape(arrayOfCarNamesUserRequest, countOfLapUserRequest); + const historyOfRace = runEntireRace(arrayOfCarNamesUserRequest, countOfLapUserRequest, randomNumberTape); } async readInputAsyncUsingWoowaMissionApi(questionStr) { return await MissionUtils.Console.readLineAsync(questionStr); } - makeRandomNumbersTape(cars, laps) { // 필요한 만큼의 난수를 만들고 레이스 진행 + makeRandomNumbersTape(cars, laps) { // 부수효과를 없애기 위해 필요한 만큼의 난수를 만들고 레이스 진행 let numberTape = []; const countOfNeededNumber = cars.length * laps; for(let cnt = 0; cnt < countOfNeededNumber; cnt++) { - numberTape.push(this.pickRandomNumberInRangeUsingWoowaMissionApi(0,9)) + numberTape.push(this.pickRandomNumberInRangeUsingWoowaMissionApi(0, 9)) } return numberTape; } - pickRandomNumberInRangeUsingWoowaMissionApi(min,max) { + pickRandomNumberInRangeUsingWoowaMissionApi(min, max) { return MissionUtils.Random.pickNumberInRange(min, max); } }; diff --git a/src/domains/race.js b/src/domains/race.js new file mode 100644 index 00000000..09970a04 --- /dev/null +++ b/src/domains/race.js @@ -0,0 +1,16 @@ +export const meetMoveCondition = (number, threshold = 4) => number >= threshold; // 요구사항: 4이상이면 전진, 아니면 정지 + +export function runEntireRace(arrOfCarNames, cntOfLaps, randomTape) { + let scoreAfterCurrentLap = Array(arrOfCarNames.length).fill(0); + const raceHistoryByLap = []; // 최종결과(마지막랩)로 쉽게 활용할 수 있도록 랩을 기준으로 데이터 저장 + for(let currentLap = 0 ; currentLap < cntOfLaps ; currentLap++) { + scoreAfterCurrentLap = scoreAfterCurrentLap.slice().map(prev => { + if(meetMoveCondition(randomTape.pop())) return prev + 1; + return prev; + }); + raceHistoryByLap.push(scoreAfterCurrentLap.slice()); + } + return raceHistoryByLap; +}; + + From a8f602a9a0c9c1edfc29c9a1f753d728c13c3a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 19:15:01 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat(queries):=20=EC=9A=B0=EC=8A=B9?= =?UTF-8?q?=EC=9E=90=20=EA=B2=B0=EC=A0=95=20=EB=8F=84=EB=A9=94=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 최종 점수를 가지고 가장 높은 점수를 뽑아 우승자를 결정하는 함수를 구현하였습니다. 동률일 경우 공동 우승으로 처리됩니다. --- README.md | 6 +++--- __tests__/DomainTest.js | 14 +++++++++++++- src/App.js | 3 +++ src/domains/queries.js | 8 ++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/domains/queries.js diff --git a/README.md b/README.md index 9b200817..b28106b3 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,9 @@ flowchart TD - [x] `자동차 수 * 랩 수` 만큼의 랜덤 숫자(0~9사이 정수) 생성 - [x] 레이스 진행(전진 or 정지) - [x] 랜덤 숫자가 4 이상(4,5,6,7,8,9)이면 전진, 4미만(0,1,2,3)이면 정지 -- [ ] 우승자 결정 - - [ ] 가장 많이 전진한 자동차가 우승자로 결정된다. - - [ ] 공동 우승도 인정된다. +- [x] 우승자 결정 + - [x] 가장 많이 전진한 자동차가 우승자로 결정된다. + - [x] 공동 우승도 인정된다. - [ ] 레이스 히스토리 출력 - 각 랩 순으로 레이스 기록을 출력한다. - [ ] 우승자명 출력 diff --git a/__tests__/DomainTest.js b/__tests__/DomainTest.js index 0134dc68..454b62ff 100644 --- a/__tests__/DomainTest.js +++ b/__tests__/DomainTest.js @@ -1,10 +1,22 @@ import { runEntireRace } from "../src/domains/race"; +import { determineWinnerOfRace, findMaxValue } from "../src/domains/queries"; + describe("레이스 진행 도메인 테스트", () => { test.each([ [["a", "b", "c"], 2, [4, 1, 2, 9, 0, 4], [[1, 0, 1], [1, 0, 2]]], [["red", "blue", "black"], 3, [5, 2, 4, 9, 5, 9, 9, 4, 6], [[1, 1, 1], [2, 2, 2], [3, 2, 3]]] - ])("", (carnames, laps, randomTape, raceHistory) => { + ])("레이스 진행하여 전체 히스토리를 기록한다.", (carnames, laps, randomTape, raceHistory) => { const result = runEntireRace(carnames, laps, randomTape); expect(result).toStrictEqual(raceHistory); }) + test("숫자 배열에서 최댓값을 찾는다.", () => { + expect(findMaxValue([2, 3, 8])).toBe(8); + }) + test.each([ + [["a", "ab", "c"], [2, 5, 4], ["ab"]], + [["red", "black", "blue"], [9, 19, 19], ["black", "blue"]] + ])("최종 점수 배열에서 우승자를 선택한다.", (names, scores, expectWinner) => { + const winner = determineWinnerOfRace(names, scores); + expect(winner).toStrictEqual(expectWinner); + }) }) \ No newline at end of file diff --git a/src/App.js b/src/App.js index 29f94de4..d57f08a5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,7 @@ import { MissionUtils } from "@woowacourse/mission-utils"; import { parseByComma } from "./utils/parsing"; +import { runEntireRace } from "./domains/race"; +import { determineWinnerOfRace } from "./domains/queries"; class App { async run() { @@ -9,6 +11,7 @@ class App { const countOfLapUserRequest = Number(stringOfLapUserRequest); const randomNumberTape = this.makeRandomNumbersTape(arrayOfCarNamesUserRequest, countOfLapUserRequest); const historyOfRace = runEntireRace(arrayOfCarNamesUserRequest, countOfLapUserRequest, randomNumberTape); + const namesOfWinner = determineWinnerOfRace(arrayOfCarNamesUserRequest, historyOfRace[historyOfRace.length -1]); } async readInputAsyncUsingWoowaMissionApi(questionStr) { return await MissionUtils.Console.readLineAsync(questionStr); diff --git a/src/domains/queries.js b/src/domains/queries.js new file mode 100644 index 00000000..db94d32d --- /dev/null +++ b/src/domains/queries.js @@ -0,0 +1,8 @@ +export const findMaxValue = (values) => Math.max(...values); // 명확성과 재사용성이 높아보여 분리 + +export function determineWinnerOfRace(carNames, finalScores) { + const scoreOfWinner = findMaxValue(finalScores); + const winner = carNames.filter((name, carIdx) => finalScores[carIdx] === scoreOfWinner) + return winner; +} + From b95acb0e3311e68d348e7935c178ea3192803ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 20:48:02 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat(app):=20=ED=9E=88=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=9C=EB=A0=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit utils/parsing에서 변환한 히스토리 로그를 app에서 mission util api로 출력하는 함수를 구현했습니다. --- README.md | 2 +- __tests__/APIsTest.js | 16 +++++++++++++++- src/App.js | 11 ++++++++++- src/utils/parsing.js | 5 +++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b28106b3..fe787fd4 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ flowchart TD - [x] 우승자 결정 - [x] 가장 많이 전진한 자동차가 우승자로 결정된다. - [x] 공동 우승도 인정된다. -- [ ] 레이스 히스토리 출력 +- [x] 레이스 히스토리 출력 - 각 랩 순으로 레이스 기록을 출력한다. - [ ] 우승자명 출력 - 동률의 경우 `,`로 구분하여 출력한다. diff --git a/__tests__/APIsTest.js b/__tests__/APIsTest.js index fdd80539..c87a9e33 100644 --- a/__tests__/APIsTest.js +++ b/__tests__/APIsTest.js @@ -10,6 +10,11 @@ const mockQuestions = (inputs) => { }); }; +const getLogSpy = () => { + const logSpy = jest.spyOn(MissionUtils.Console, "print"); + logSpy.mockClear(); + return logSpy; +}; describe("woowacourse/mission-utils api 테스트", () => { let app; @@ -32,11 +37,20 @@ describe("woowacourse/mission-utils api 테스트", () => { const results = Array.from({ length: 100 }, () => app.pickRandomNumberInRangeUsingWoowaMissionApi(0, 9) ); - + for (const num of results) { expect(Number.isInteger(num)).toBe(true); expect(num).toBeGreaterThanOrEqual(0); expect(num).toBeLessThanOrEqual(9); } }); + + test("Console.print를 사용해 레이스의 히스토리를 출력한다.", () => { + const logs = ["a : -", "b : ", "a : --", "a : --\nb : -\nc : -"] + const logSpy = getLogSpy(); + app.printHistoryOfRace([[1, 0, 1], [1, 1, 1], [2, 1, 1]], ["a", "b", "c"]); + logs.forEach((log) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + }); + }) }) \ No newline at end of file diff --git a/src/App.js b/src/App.js index d57f08a5..d218a4f9 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,5 @@ import { MissionUtils } from "@woowacourse/mission-utils"; -import { parseByComma } from "./utils/parsing"; +import { parseByComma, parseToHistoryFormat } from "./utils/parsing"; import { runEntireRace } from "./domains/race"; import { determineWinnerOfRace } from "./domains/queries"; @@ -12,6 +12,7 @@ class App { const randomNumberTape = this.makeRandomNumbersTape(arrayOfCarNamesUserRequest, countOfLapUserRequest); const historyOfRace = runEntireRace(arrayOfCarNamesUserRequest, countOfLapUserRequest, randomNumberTape); const namesOfWinner = determineWinnerOfRace(arrayOfCarNamesUserRequest, historyOfRace[historyOfRace.length -1]); + this.printHistoryOfRace(historyOfRace, arrayOfCarNamesUserRequest); } async readInputAsyncUsingWoowaMissionApi(questionStr) { return await MissionUtils.Console.readLineAsync(questionStr); @@ -27,5 +28,13 @@ class App { pickRandomNumberInRangeUsingWoowaMissionApi(min, max) { return MissionUtils.Random.pickNumberInRange(min, max); } + printOutputUsingWoowaMissionApi(output) { + MissionUtils.Console.print(output); + } + printHistoryOfRace(history, name) { + history.forEach(log => { + this.printOutputUsingWoowaMissionApi(parseToHistoryFormat(log, name)) + }) + } }; export default App; diff --git a/src/utils/parsing.js b/src/utils/parsing.js index 9e4f86bd..d742ad1f 100644 --- a/src/utils/parsing.js +++ b/src/utils/parsing.js @@ -2,3 +2,8 @@ export function parseByComma(input) { return input.split(",").map(n => n.trim()); } +export function parseToHistoryFormat(oneRoundHistory, name) { // [1,1,1] ["a","b","c"] + return oneRoundHistory + .map((result, idx) => `${name[idx]} : ${"-".repeat(result)}`) + .join("\n"); +} \ No newline at end of file From 21d7292e95040dc58bdeaaf52e3eda6b08f08777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 21:18:26 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat(app):=20=EC=9A=B0=EC=8A=B9=EC=9E=90?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 우승자를 문자열로 파싱한 뒤 misson util로 우승자를 출력하는 함수를 구현하였습니다. 우승자가 여러 명일 경우 쉼표로 구분되어 출력됩니다. --- README.md | 2 +- __tests__/APIsTest.js | 7 +++++++ src/App.js | 6 +++++- src/utils/parsing.js | 4 ++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fe787fd4..ec3dcb28 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ flowchart TD - [x] 공동 우승도 인정된다. - [x] 레이스 히스토리 출력 - 각 랩 순으로 레이스 기록을 출력한다. -- [ ] 우승자명 출력 +- [x] 우승자명 출력 - 동률의 경우 `,`로 구분하여 출력한다. # 문제 해결 과정 diff --git a/__tests__/APIsTest.js b/__tests__/APIsTest.js index c87a9e33..304bf878 100644 --- a/__tests__/APIsTest.js +++ b/__tests__/APIsTest.js @@ -52,5 +52,12 @@ describe("woowacourse/mission-utils api 테스트", () => { logs.forEach((log) => { expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); }); + }); + + test("Console.print를 사용해 우승자를 출력한다.", () => { + const log = "최종 우승자 : a, b"; + const logSpy = getLogSpy(); + app.printWinnerOfRace(["a", "b"]); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); }) }) \ No newline at end of file diff --git a/src/App.js b/src/App.js index d218a4f9..393e087f 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,5 @@ import { MissionUtils } from "@woowacourse/mission-utils"; -import { parseByComma, parseToHistoryFormat } from "./utils/parsing"; +import { parseByComma, parseToHistoryFormat, parserToWinnerFormat } from "./utils/parsing"; import { runEntireRace } from "./domains/race"; import { determineWinnerOfRace } from "./domains/queries"; @@ -13,6 +13,7 @@ class App { const historyOfRace = runEntireRace(arrayOfCarNamesUserRequest, countOfLapUserRequest, randomNumberTape); const namesOfWinner = determineWinnerOfRace(arrayOfCarNamesUserRequest, historyOfRace[historyOfRace.length -1]); this.printHistoryOfRace(historyOfRace, arrayOfCarNamesUserRequest); + this.printWinnerOfRace(namesOfWinner); } async readInputAsyncUsingWoowaMissionApi(questionStr) { return await MissionUtils.Console.readLineAsync(questionStr); @@ -36,5 +37,8 @@ class App { this.printOutputUsingWoowaMissionApi(parseToHistoryFormat(log, name)) }) } + printWinnerOfRace(winner) { + this.printOutputUsingWoowaMissionApi(parserToWinnerFormat(winner)); + } }; export default App; diff --git a/src/utils/parsing.js b/src/utils/parsing.js index d742ad1f..274f0501 100644 --- a/src/utils/parsing.js +++ b/src/utils/parsing.js @@ -6,4 +6,8 @@ export function parseToHistoryFormat(oneRoundHistory, name) { // [1,1,1] ["a","b return oneRoundHistory .map((result, idx) => `${name[idx]} : ${"-".repeat(result)}`) .join("\n"); +} + +export function parserToWinnerFormat(winner) { + return `최종 우승자 : ${winner.join(", ")}`; } \ No newline at end of file From fdf4494e3d2158e726194972707377b6bf649da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 21:30:59 +0900 Subject: [PATCH 10/17] =?UTF-8?q?fix(race):=20=EB=82=9C=EC=88=98=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20FIFO=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 처음에 주어진 테스트 케이스를 통과하지 못하는 이유가 생성한 난수 배열을 stack의 pop()을 통해 값을 하나씩 가져오고 있기 때문이었습니다. 그래서 FIFO로 읽어 올 수 있도록 shift()를 사용하도록 수정하였습니다. --- __tests__/DomainTest.js | 4 ++-- src/domains/race.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/__tests__/DomainTest.js b/__tests__/DomainTest.js index 454b62ff..fa8cf5c9 100644 --- a/__tests__/DomainTest.js +++ b/__tests__/DomainTest.js @@ -3,8 +3,8 @@ import { determineWinnerOfRace, findMaxValue } from "../src/domains/queries"; describe("레이스 진행 도메인 테스트", () => { test.each([ - [["a", "b", "c"], 2, [4, 1, 2, 9, 0, 4], [[1, 0, 1], [1, 0, 2]]], - [["red", "blue", "black"], 3, [5, 2, 4, 9, 5, 9, 9, 4, 6], [[1, 1, 1], [2, 2, 2], [3, 2, 3]]] + [["a", "b", "c"], 2, [4, 1, 2, 9, 0, 4], [[1, 0, 0], [2, 0, 1]]], + [["red", "blue", "black"], 3, [5, 2, 4, 9, 5, 9, 9, 4, 6], [[1, 0, 1], [2, 1, 2], [3, 2, 3]]] ])("레이스 진행하여 전체 히스토리를 기록한다.", (carnames, laps, randomTape, raceHistory) => { const result = runEntireRace(carnames, laps, randomTape); expect(result).toStrictEqual(raceHistory); diff --git a/src/domains/race.js b/src/domains/race.js index 09970a04..67b38ad5 100644 --- a/src/domains/race.js +++ b/src/domains/race.js @@ -5,7 +5,7 @@ export function runEntireRace(arrOfCarNames, cntOfLaps, randomTape) { const raceHistoryByLap = []; // 최종결과(마지막랩)로 쉽게 활용할 수 있도록 랩을 기준으로 데이터 저장 for(let currentLap = 0 ; currentLap < cntOfLaps ; currentLap++) { scoreAfterCurrentLap = scoreAfterCurrentLap.slice().map(prev => { - if(meetMoveCondition(randomTape.pop())) return prev + 1; + if(meetMoveCondition(randomTape.shift())) return prev + 1; return prev; }); raceHistoryByLap.push(scoreAfterCurrentLap.slice()); From d5d0950f833db78e3a7ae76315c36d83df0962e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 22:01:43 +0900 Subject: [PATCH 11/17] =?UTF-8?q?feat(validator):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=EB=A9=B4=205=EC=9E=90=20=EC=9D=B4=ED=95=98=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- __tests__/UtilTest.js | 7 +++++++ src/App.js | 2 ++ src/utils/validator.js | 5 +++++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/utils/validator.js diff --git a/README.md b/README.md index ec3dcb28..24c8484c 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ flowchart TD - [x] 진행 랩 수(몇번 진행할 것인지) 입력 받기 - [ ] 입력값 검증(예외) - [ ] 사용자가 잘못된 값을 입력할 경우 `[ERROR]`로 시작하는 문구와 함께 에러를 발생시키고 프로그램을 종료시킨다. - - [ ] 각 자동차명은 공백을 포함하지 않은 5자 이하의 문자열이다. + - [x] 각 자동차명은 공백을 포함하지 않은 5자 이하의 문자열이다. - [ ] 각 자동차명은 중복을 허용하지 않는다. - [ ] 랩 수는 양의 정수이다. - [x] 랜덤 넘버 데이터 생성 diff --git a/__tests__/UtilTest.js b/__tests__/UtilTest.js index 53507eca..8de53c3d 100644 --- a/__tests__/UtilTest.js +++ b/__tests__/UtilTest.js @@ -1,4 +1,5 @@ import { parseByComma } from "../src/utils/parsing"; +import { validateCarNameRule } from "../src/utils/validator"; describe("유틸 함수 테스트", () => { test.each(([ @@ -9,5 +10,11 @@ describe("유틸 함수 테스트", () => { ]))("쉼표를 기준으로 문자열을 파싱해주는 parseByComma 테스트", (input, parsed) => { const parsingInput = parseByComma(input); expect(parsingInput).toStrictEqual(parsed); + }); + + test.each(([ + [["a", "aaaa", "bbbbbb"], "[ERROR] : 자동차 명은 5자 이하여야 합니다."] + ]))("자동차명 검증 테스트", (carNameHasErr, errMsg) => { + expect(() => validateCarNameRule(carNameHasErr)).toThrow(errMsg); }) }) \ No newline at end of file diff --git a/src/App.js b/src/App.js index 393e087f..687bb874 100644 --- a/src/App.js +++ b/src/App.js @@ -2,11 +2,13 @@ import { MissionUtils } from "@woowacourse/mission-utils"; import { parseByComma, parseToHistoryFormat, parserToWinnerFormat } from "./utils/parsing"; import { runEntireRace } from "./domains/race"; import { determineWinnerOfRace } from "./domains/queries"; +import { validateCarNameRule } from "./utils/validator"; class App { async run() { const stringOfCarNamesUserRequest = await this.readInputAsyncUsingWoowaMissionApi("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); const arrayOfCarNamesUserRequest = parseByComma(stringOfCarNamesUserRequest); + validateCarNameRule(arrayOfCarNamesUserRequest); const stringOfLapUserRequest = await this.readInputAsyncUsingWoowaMissionApi("시도할 횟수는 몇 회인가요?"); const countOfLapUserRequest = Number(stringOfLapUserRequest); const randomNumberTape = this.makeRandomNumbersTape(arrayOfCarNamesUserRequest, countOfLapUserRequest); diff --git a/src/utils/validator.js b/src/utils/validator.js new file mode 100644 index 00000000..0cd36dc8 --- /dev/null +++ b/src/utils/validator.js @@ -0,0 +1,5 @@ +export function validateCarNameRule(names) { + names.forEach(n => { + if(n.length > 5) throw new Error("[ERROR] : 자동차 명은 5자 이하여야 합니다.") + }); +} \ No newline at end of file From 22897e727d32805dc38a888d2dac3fd721b3ed9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 22:16:23 +0900 Subject: [PATCH 12/17] =?UTF-8?q?feat(validator):=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- __tests__/UtilTest.js | 3 ++- src/utils/validator.js | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 24c8484c..e18389fa 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ flowchart TD - [ ] 입력값 검증(예외) - [ ] 사용자가 잘못된 값을 입력할 경우 `[ERROR]`로 시작하는 문구와 함께 에러를 발생시키고 프로그램을 종료시킨다. - [x] 각 자동차명은 공백을 포함하지 않은 5자 이하의 문자열이다. - - [ ] 각 자동차명은 중복을 허용하지 않는다. + - [x] 각 자동차명은 중복을 허용하지 않는다. - [ ] 랩 수는 양의 정수이다. - [x] 랜덤 넘버 데이터 생성 - [x] `자동차 수 * 랩 수` 만큼의 랜덤 숫자(0~9사이 정수) 생성 diff --git a/__tests__/UtilTest.js b/__tests__/UtilTest.js index 8de53c3d..0e2ed7ab 100644 --- a/__tests__/UtilTest.js +++ b/__tests__/UtilTest.js @@ -13,7 +13,8 @@ describe("유틸 함수 테스트", () => { }); test.each(([ - [["a", "aaaa", "bbbbbb"], "[ERROR] : 자동차 명은 5자 이하여야 합니다."] + [["a", "aaaa", "bbbbbb"], "[ERROR] : 자동차 명은 5자 이하여야 합니다."], + [["a", "a", "ab"], "[ERROR] : 중복된 이름을 사용할 수 없습니다."] ]))("자동차명 검증 테스트", (carNameHasErr, errMsg) => { expect(() => validateCarNameRule(carNameHasErr)).toThrow(errMsg); }) diff --git a/src/utils/validator.js b/src/utils/validator.js index 0cd36dc8..9b922d5a 100644 --- a/src/utils/validator.js +++ b/src/utils/validator.js @@ -1,5 +1,6 @@ export function validateCarNameRule(names) { + if(new Set(names).size != names.length) throw new Error("[ERROR] : 중복된 이름을 사용할 수 없습니다.") names.forEach(n => { - if(n.length > 5) throw new Error("[ERROR] : 자동차 명은 5자 이하여야 합니다.") + if(n.length > 5 || n.length === 0) throw new Error("[ERROR] : 자동차 명은 5자 이하여야 합니다."); }); } \ No newline at end of file From 5a12cfb1a4240860041da9383a30729e1bcd3233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 22:52:01 +0900 Subject: [PATCH 13/17] =?UTF-8?q?feat(validator):=20=ED=9A=9F=EC=88=98=20?= =?UTF-8?q?=EC=96=91=EC=9D=98=20=EC=A0=95=EC=88=98=EC=9E=84=EC=9D=84=20=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- __tests__/UtilTest.js | 12 +++++++++++- src/App.js | 3 ++- src/utils/validator.js | 5 +++++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e18389fa..38b3a158 100644 --- a/README.md +++ b/README.md @@ -60,11 +60,11 @@ flowchart TD - [x] 자동차 명 입력 받기 - [x] `,` 기준으로 자동차명을 구분 - [x] 진행 랩 수(몇번 진행할 것인지) 입력 받기 -- [ ] 입력값 검증(예외) - - [ ] 사용자가 잘못된 값을 입력할 경우 `[ERROR]`로 시작하는 문구와 함께 에러를 발생시키고 프로그램을 종료시킨다. +- [x] 입력값 검증(예외) + - [x] 사용자가 잘못된 값을 입력할 경우 `[ERROR]`로 시작하는 문구와 함께 에러를 발생시키고 프로그램을 종료시킨다. - [x] 각 자동차명은 공백을 포함하지 않은 5자 이하의 문자열이다. - [x] 각 자동차명은 중복을 허용하지 않는다. - - [ ] 랩 수는 양의 정수이다. + - [x] 랩 수는 양의 정수이다. - [x] 랜덤 넘버 데이터 생성 - [x] `자동차 수 * 랩 수` 만큼의 랜덤 숫자(0~9사이 정수) 생성 - [x] 레이스 진행(전진 or 정지) diff --git a/__tests__/UtilTest.js b/__tests__/UtilTest.js index 0e2ed7ab..bc87036e 100644 --- a/__tests__/UtilTest.js +++ b/__tests__/UtilTest.js @@ -1,5 +1,5 @@ import { parseByComma } from "../src/utils/parsing"; -import { validateCarNameRule } from "../src/utils/validator"; +import { validateCarNameRule, validateLapNumberRule } from "../src/utils/validator"; describe("유틸 함수 테스트", () => { test.each(([ @@ -17,5 +17,15 @@ describe("유틸 함수 테스트", () => { [["a", "a", "ab"], "[ERROR] : 중복된 이름을 사용할 수 없습니다."] ]))("자동차명 검증 테스트", (carNameHasErr, errMsg) => { expect(() => validateCarNameRule(carNameHasErr)).toThrow(errMsg); + }); + + test.each(([ + [2.1, "[ERROR] : 횟수는 양의 정수이어야 합니다."], + [-1, "[ERROR] : 횟수는 양의 정수이어야 합니다."], + [NaN, "[ERROR] : 횟수는 양의 정수이어야 합니다."], + [null, "[ERROR] : 횟수는 양의 정수이어야 합니다."], + [undefined, "[ERROR] : 횟수는 양의 정수이어야 합니다."], + ]))("횟수 검증 테스트", (numberHasErr, errMsg) => { + expect(() => validateLapNumberRule(numberHasErr)).toThrow(errMsg); }) }) \ No newline at end of file diff --git a/src/App.js b/src/App.js index 687bb874..a9c37136 100644 --- a/src/App.js +++ b/src/App.js @@ -2,7 +2,7 @@ import { MissionUtils } from "@woowacourse/mission-utils"; import { parseByComma, parseToHistoryFormat, parserToWinnerFormat } from "./utils/parsing"; import { runEntireRace } from "./domains/race"; import { determineWinnerOfRace } from "./domains/queries"; -import { validateCarNameRule } from "./utils/validator"; +import { validateCarNameRule, validateLapNumberRule } from "./utils/validator"; class App { async run() { @@ -11,6 +11,7 @@ class App { validateCarNameRule(arrayOfCarNamesUserRequest); const stringOfLapUserRequest = await this.readInputAsyncUsingWoowaMissionApi("시도할 횟수는 몇 회인가요?"); const countOfLapUserRequest = Number(stringOfLapUserRequest); + validateLapNumberRule(countOfLapUserRequest); const randomNumberTape = this.makeRandomNumbersTape(arrayOfCarNamesUserRequest, countOfLapUserRequest); const historyOfRace = runEntireRace(arrayOfCarNamesUserRequest, countOfLapUserRequest, randomNumberTape); const namesOfWinner = determineWinnerOfRace(arrayOfCarNamesUserRequest, historyOfRace[historyOfRace.length -1]); diff --git a/src/utils/validator.js b/src/utils/validator.js index 9b922d5a..2937951b 100644 --- a/src/utils/validator.js +++ b/src/utils/validator.js @@ -3,4 +3,9 @@ export function validateCarNameRule(names) { names.forEach(n => { if(n.length > 5 || n.length === 0) throw new Error("[ERROR] : 자동차 명은 5자 이하여야 합니다."); }); +} + +export function validateLapNumberRule(lap) { + if(lap <= 0 || !Number.isInteger(lap)) + throw new Error("[ERROR] : 횟수는 양의 정수이어야 합니다.") } \ No newline at end of file From de75d0d249d2f8c666a4601122710c25b1d566d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 23:15:30 +0900 Subject: [PATCH 14/17] =?UTF-8?q?docs(readme):=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=ED=95=B4=EA=B2=B0=20=EA=B3=BC=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 38b3a158..b07693e7 100644 --- a/README.md +++ b/README.md @@ -88,4 +88,10 @@ flowchart TD > 랜덤 넘버 생성 - 해결해야하는 문제: 함수형 패러다임의 적용과 연결되는 부분입니다. API를 통해서 생성하는 난수들을 어떻게 생성하고 사용할 지에 대한 문제입니다. 처음에는 별 생각없이 매 라운드마다 난수를 만들어서 판별하도록 하면 되겠다고 생각했지만 설계를 하다보니 레이스마다 매번 난수를 생성하면 순수성을 유지하지 못하고, 테스트에도 어려움이 있을 것이라 판단했습니다. - 어떻게 해결했는지: App 단에서 필요한 개수의 난수를 모두 생성하여 넘겨주는 방식으로 문제를 해결해보려 합니다. 마찬가지로 매 랩읙 결과를 모두 끝이 나고 한 번에 순서대로 출력하는 방식으로 진행하려고 합니다. -### 구현 단계 \ No newline at end of file +### 구현 단계 +> 데이터 구조 +- 해결해야하는 문제: 난수 테이프 부분에서 필요한 모든 난수를 미리 만들어서 레이스를 진행하는 흐름을 처음 구현하다보니 데이터 구조 결정에 문제있었습니다. 기능 중심으로만 설계하다 보니, 각 함수가 어떤 데이터를 주고받을지에 대한 구조적 고려가 부족했던 것이 원인이었습니다. +- 어떻게 해결했는지: 처음에는 단순히 HashMap을 사용하여 <자동차명-히스토리 배열>을 이루면 쉽게 구현애 가능하겠구나 생각했습니다. 여러 데이터를 파라미터로 넘겨주다 보니 시간복잡도와 메모리 비용이 너무 크게 발생함을 깨닫고, 2차원 배열로 히스토리를 저장하는 방식으로 문제를 해결했습니다. +> FIFO vs LIFO +- 해결해야하는 문제: 모든 테스트 코드 중 제공된 테스트만 통과하지 못하는 문제가 있었습니다. +- 어떻게 해결했는지: 난수를 생성해서 배열에 push함수를 사용해서 난수테이프를 만들었기에, 자연스레 pop()을 사용했던 것이 문제였습니다. 먼저 push된 것을 먼저 사용할 수 있도록 shift()를 사용하여 문제를 해결했습니다. \ No newline at end of file From 4a08e8e9dc27d69e772026eccf4874a32e8bf149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 23:19:19 +0900 Subject: [PATCH 15/17] =?UTF-8?q?refactor(app):=ED=94=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=9E=A8=20=ED=9D=90=EB=A6=84=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/App.js b/src/App.js index a9c37136..f4db2019 100644 --- a/src/App.js +++ b/src/App.js @@ -6,18 +6,22 @@ import { validateCarNameRule, validateLapNumberRule } from "./utils/validator"; class App { async run() { + // 입력(자동차명, 횟수) const stringOfCarNamesUserRequest = await this.readInputAsyncUsingWoowaMissionApi("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); const arrayOfCarNamesUserRequest = parseByComma(stringOfCarNamesUserRequest); validateCarNameRule(arrayOfCarNamesUserRequest); const stringOfLapUserRequest = await this.readInputAsyncUsingWoowaMissionApi("시도할 횟수는 몇 회인가요?"); const countOfLapUserRequest = Number(stringOfLapUserRequest); validateLapNumberRule(countOfLapUserRequest); + // 레이스 진행 const randomNumberTape = this.makeRandomNumbersTape(arrayOfCarNamesUserRequest, countOfLapUserRequest); const historyOfRace = runEntireRace(arrayOfCarNamesUserRequest, countOfLapUserRequest, randomNumberTape); const namesOfWinner = determineWinnerOfRace(arrayOfCarNamesUserRequest, historyOfRace[historyOfRace.length -1]); + // 결과 출력(레이스 히스토리, 우승자) this.printHistoryOfRace(historyOfRace, arrayOfCarNamesUserRequest); this.printWinnerOfRace(namesOfWinner); } + async readInputAsyncUsingWoowaMissionApi(questionStr) { return await MissionUtils.Console.readLineAsync(questionStr); } From a5355f33ececd3223d55a96710a3a3d83d2c2fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 23:35:52 +0900 Subject: [PATCH 16/17] =?UTF-8?q?fix(race):=20=EB=82=9C=EC=88=98=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=20=EB=B6=88=EB=B3=80=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/race.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domains/race.js b/src/domains/race.js index 67b38ad5..57f8d4e8 100644 --- a/src/domains/race.js +++ b/src/domains/race.js @@ -1,11 +1,12 @@ export const meetMoveCondition = (number, threshold = 4) => number >= threshold; // 요구사항: 4이상이면 전진, 아니면 정지 export function runEntireRace(arrOfCarNames, cntOfLaps, randomTape) { + let randomNumbers = randomTape.slice(); // 불변성 방지 복사 let scoreAfterCurrentLap = Array(arrOfCarNames.length).fill(0); const raceHistoryByLap = []; // 최종결과(마지막랩)로 쉽게 활용할 수 있도록 랩을 기준으로 데이터 저장 for(let currentLap = 0 ; currentLap < cntOfLaps ; currentLap++) { scoreAfterCurrentLap = scoreAfterCurrentLap.slice().map(prev => { - if(meetMoveCondition(randomTape.shift())) return prev + 1; + if(meetMoveCondition(randomNumbers.shift())) return prev + 1; return prev; }); raceHistoryByLap.push(scoreAfterCurrentLap.slice()); From 1feb6a22b14e5e7972debc900c8145659252b7d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 27 Oct 2025 23:38:44 +0900 Subject: [PATCH 17/17] =?UTF-8?q?docs(readme):=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b07693e7..8b8a89cb 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ - Jest 활용법 공부하기 # 설계 -## 함수 구조 +## 함수 구조(구현 전 설계) ```mermaid flowchart TD %% 최상위 (부수효과 담당)