From eff3a21e281b5a95437012d2cb4e5907f6534c52 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Wed, 22 Oct 2025 22:04:23 +0900 Subject: [PATCH 01/40] =?UTF-8?q?docs(README):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e078fd41..970cac0a 100644 --- a/README.md +++ b/README.md @@ -1 +1,74 @@ -# javascript-racingcar-precourse +# ๐ŸŽ๏ธ ์ž๋™์ฐจ ๊ฒฝ์ฃผ ๊ธฐ๋Šฅ ๋ช…์„ธ + +## 1. Domain + +### 1) `Car` + +- ์ž๋™์ฐจ์˜ ์ด๋ฆ„๊ณผ ์ด๋™ ๊ฑฐ๋ฆฌ๋ฅผ ๋ณด์œ ํ•ฉ๋‹ˆ๋‹ค. +- ์ „์ง„ ๋ช…๋ น์„ ๋ฐ›์•„ ์ด๋™ ๊ฑฐ๋ฆฌ๋ฅผ 1 ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค. +- ์ž๋™์ฐจ ์ด๋ฆ„๊ณผ ํ˜„์žฌ ์ด๋™ ๊ฑฐ๋ฆฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + +## 2. Model + +### 1) `RaceCarFactory` + +- ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์ž๋™์ฐจ ์ด๋ฆ„ ๋ฌธ์ž์—ด์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. +- ๊ฐ ์ด๋ฆ„์„ ๋ถ„๋ฆฌํ•˜๊ณ  ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. +- `Car` ์ธ์Šคํ„ด์Šค ๋ฐฐ์—ด์„ ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + +### 2) `RaceStepProcessor` + +- ์ž๋™์ฐจ ๋ฐฐ์—ด์„ ๋ฐ›์•„ ํ•œ ๋ฒˆ์˜ ์ด๋™ ์‹œ๋„๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. +- ์ž๋™์ฐจ๋งˆ๋‹ค 0~9 ์‚ฌ์ด์˜ ๋‚œ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. +- ๋‚œ์ˆ˜๊ฐ€ 4 ์ด์ƒ์ด๋ฉด ํ•ด๋‹น ์ž๋™์ฐจ๋ฅผ ์ „์ง„์‹œํ‚ต๋‹ˆ๋‹ค. + +### 3) `RaceSimulator` + +- ์ž๋™์ฐจ ์ด๋ฆ„๊ณผ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ๋ฐ›์•„ ๊ฒฝ์ฃผ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. +- `RaceCarFactory`๋กœ ์ž๋™์ฐจ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. +- ๋‚จ์€ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค. +- `RaceStepProcessor`๋ฅผ ํ†ตํ•ด ๊ฐ ๋‹จ๊ณ„๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. +- ํ˜„์žฌ ๋ชจ๋“  ์ž๋™์ฐจ์˜ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. +- ์ตœ์ข… ์šฐ์Šน์ž ๋ชฉ๋ก์„ ๊ณ„์‚ฐํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + +## 3. View + +### 1) `InputView` + +- ์‚ฌ์šฉ์ž์—๊ฒŒ ์ž…๋ ฅ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. +- ์ž๋™์ฐจ ์ด๋ฆ„๋“ค์„ ์ž…๋ ฅ๋ฐ›์Šต๋‹ˆ๋‹ค. +- ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ž…๋ ฅ๋ฐ›์Šต๋‹ˆ๋‹ค. + +### 2) `OutputView` + +- ์‹คํ–‰ ๊ฒฐ๊ณผ ํ—ค๋”๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. +- ๋‹จ๊ณ„๋งˆ๋‹ค ์ž๋™์ฐจ๋ณ„ ํ˜„์žฌ ์œ„์น˜๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. +- ์ตœ์ข… ์šฐ์Šน์ž๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + +## 4. Controller + +### 1) `RaceController` + +- `InputView`๋ฅผ ํ†ตํ•ด ์ž๋™์ฐจ ์ด๋ฆ„์„ ์ž…๋ ฅ๋ฐ›์Šต๋‹ˆ๋‹ค. +- `InputView`๋ฅผ ํ†ตํ•ด ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ž…๋ ฅ๋ฐ›์Šต๋‹ˆ๋‹ค. +- `RaceSimulator`๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๊ฒฝ์ฃผ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. +- `OutputView`๋กœ ๊ฒฐ๊ณผ ํ—ค๋”๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. +- ๋‚จ์€ ์‹œ๋„๊ฐ€ ์žˆ๋Š” ๋™์•ˆ ๋ฐ˜๋ณตํ•˜์—ฌ ๊ฐ ๋‹จ๊ณ„๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. +- ๊ฐ ๋‹จ๊ณ„์˜ ๊ฒฐ๊ณผ๋ฅผ `OutputView`๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. +- ๊ฒฝ์ฃผ ์ข…๋ฃŒ ํ›„ ์šฐ์Šน์ž๋ฅผ `OutputView`๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + +## 5. ์˜ˆ์™ธ์ฒ˜๋ฆฌ + +### 1) ์ž๋™์ฐจ ์ด๋ฆ„ ๊ฒ€์ฆ +- ์ž๋™์ฐจ ์ด๋ฆ„์˜ ๊ธธ์ด๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. +- ์ด๋ฆ„์ด 5์ž๋ฅผ ์ดˆ๊ณผํ•˜๋Š” ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. +- ์ด๋ฆ„์ด ๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. +- ์ด๋ฆ„์— ์‰ผํ‘œ๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. +- ์ด๋ฆ„์ด ์ค‘๋ณต๋œ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + +### 2) ์‹œ๋„ ํšŸ์ˆ˜ ๊ฒ€์ฆ +- ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. +- ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ 0 ์ดํ•˜์ธ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + +### 3) ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ +- ๋ชจ๋“  ์˜ˆ์™ธ๋Š” `[ERROR]`๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. From 94669f31a2c4cb63b26ac501502c923fccf2bb77 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Wed, 22 Oct 2025 22:08:23 +0900 Subject: [PATCH 02/40] =?UTF-8?q?feat(constants):=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=83=81=EC=88=98=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ž๋™์ฐจ ์ด๋ฆ„ ๋ฐ ์‹œ๋„ ํšŸ์ˆ˜ ๊ด€๋ จ ๊ฒ€์ฆ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์ˆ˜๋กœ ์„ ์–ธ - ๊ณตํ†ต ์ ‘๋‘์‚ฌ [ERROR]๋ฅผ ERROR_PREFIX๋กœ ๋ถ„๋ฆฌํ•ด ์ผ๊ด€์„ฑ ์œ ์ง€ --- src/constants/errorMessages.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/constants/errorMessages.js diff --git a/src/constants/errorMessages.js b/src/constants/errorMessages.js new file mode 100644 index 00000000..0eca4b51 --- /dev/null +++ b/src/constants/errorMessages.js @@ -0,0 +1,12 @@ +const ERROR_PREFIX = '[ERROR]'; + +const ERROR_MESSAGES = Object.freeze({ + NAME_TOO_LONG: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์€ 5์ž ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + NAME_EMPTY: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + NAME_CONTAINS_COMMA: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์— ์‰ผํ‘œ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`, + NAME_DUPLICATED: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์€ ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`, + ATTEMPT_COUNT_NON_NUMERIC: `${ERROR_PREFIX} ์‹œ๋„ ํšŸ์ˆ˜๋Š” ์ˆซ์ž๋งŒ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + ATTEMPT_COUNT_NOT_POSITIVE: `${ERROR_PREFIX} ์‹œ๋„ ํšŸ์ˆ˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, +}); + +export default ERROR_MESSAGES; From 079c2701f2c6ea2caf55e3d02307607316ed743b Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Wed, 22 Oct 2025 22:09:14 +0900 Subject: [PATCH 03/40] =?UTF-8?q?feat(constants):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=95=88=EB=82=B4=20=EB=B0=8F=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=83=81?= =?UTF-8?q?=EC=88=98=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ž๋™์ฐจ ์ด๋ฆ„ ๋ฐ ์‹œ๋„ ํšŸ์ˆ˜ ์ž…๋ ฅ์„ ์œ„ํ•œ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ - ์‹คํ–‰ ๊ฒฐ๊ณผ ๋ฐ ์šฐ์Šน์ž ํ‘œ์‹œ์šฉ ์ถœ๋ ฅ ๋ฌธ์ž์—ด ์ •์˜ --- src/constants/promptMessages.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/constants/promptMessages.js diff --git a/src/constants/promptMessages.js b/src/constants/promptMessages.js new file mode 100644 index 00000000..dbc9a759 --- /dev/null +++ b/src/constants/promptMessages.js @@ -0,0 +1,8 @@ +const PROMPT_MESSAGES = Object.freeze({ + CAR_NAMES_INPUT: '๊ฒฝ์ฃผํ•  ์ž๋™์ฐจ ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”.(์ด๋ฆ„์€ ์‰ผํ‘œ(,) ๊ธฐ์ค€์œผ๋กœ ๊ตฌ๋ถ„)\n', + ATTEMPT_COUNT_INPUT: '์‹œ๋„ํ•  ํšŸ์ˆ˜๋Š” ๋ช‡ ํšŒ์ธ๊ฐ€์š”?\n', + RESULT_HEADER: '\n์‹คํ–‰ ๊ฒฐ๊ณผ', + FINAL_WINNERS_PREFIX: '์ตœ์ข… ์šฐ์Šน์ž', +}); + +export default PROMPT_MESSAGES; From 1cf4f1ca14731372f4749fe943d4ee3ae5ea6d77 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Wed, 22 Oct 2025 22:11:06 +0900 Subject: [PATCH 04/40] =?UTF-8?q?feat(Car):=20Car=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ž๋™์ฐจ์˜ ์ด๋ฆ„๊ณผ ์ด๋™ ๊ฑฐ๋ฆฌ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” Car ํด๋ž˜์Šค ์ •์˜ - move ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋™ ๊ฑฐ๋ฆฌ ์ฆ๊ฐ€ - ์ด๋ฆ„๊ณผ ์ด๋™ ๊ฑฐ๋ฆฌ ์ ‘๊ทผ์„ ์œ„ํ•œ getter ๋ฉ”์„œ๋“œ ์ •์˜ --- src/domains/Car.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/domains/Car.js diff --git a/src/domains/Car.js b/src/domains/Car.js new file mode 100644 index 00000000..4f7a7ced --- /dev/null +++ b/src/domains/Car.js @@ -0,0 +1,23 @@ +class Car { + #name; + #distance; + + constructor(name) { + this.#name = name; + this.#distance = 0; + } + + move() { + this.#distance += 1; + } + + getName() { + return this.#name; + } + + getDistance() { + return this.#distance; + } +} + +export default Car; From 7910569ede110ca899a30a967daf54ac0411526e Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Wed, 22 Oct 2025 22:12:55 +0900 Subject: [PATCH 05/40] =?UTF-8?q?feat(RaceCarFactory):=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=B0=A8=20=EC=9D=B4=EB=A6=84=EC=9C=BC=EB=A1=9C=20Car?= =?UTF-8?q?=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=98=EB=8A=94=20=ED=8C=A9=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ž…๋ ฅ๋ฐ›์€ ์ž๋™์ฐจ ์ด๋ฆ„ ๋ฌธ์ž์—ด์„ ์‰ผํ‘œ ๊ธฐ์ค€์œผ๋กœ ๋ถ„๋ฆฌ - ๊ฐ ์ด๋ฆ„์„ trim ์ฒ˜๋ฆฌ ํ›„ Car ์ธ์Šคํ„ด์Šค๋กœ ๋ณ€ํ™˜ - ์ƒ์„ฑ๋œ Car ์ธ์Šคํ„ด์Šค ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ --- src/models/RaceCarFactory.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/models/RaceCarFactory.js diff --git a/src/models/RaceCarFactory.js b/src/models/RaceCarFactory.js new file mode 100644 index 00000000..ffd2f9ed --- /dev/null +++ b/src/models/RaceCarFactory.js @@ -0,0 +1,10 @@ +import Car from '../domains/Car.js'; + +class RaceCarFactory { + static createCars(carNames) { + const splitNames = carNames.split(','); + return splitNames.map((name) => new Car(name.trim())); + } +} + +export default RaceCarFactory; From b8c8d11e50fc2d8cf1853ad992dc01dcb8afd1a6 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Wed, 22 Oct 2025 22:14:30 +0900 Subject: [PATCH 06/40] =?UTF-8?q?feat(RaceStepProcessor):=20=EB=8B=A8?= =?UTF-8?q?=EC=9D=BC=20=EC=8B=A4=ED=96=89=20=EB=8B=A8=EA=B3=84=EB=A5=BC=20?= =?UTF-8?q?=EB=8B=B4=EB=8B=B9=ED=95=98=EB=8A=94=20=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=ED=81=B4=EB=9E=98=EC=8A=A4=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 - 0๋ถ€ํ„ฐ 9 ์‚ฌ์ด์˜ ๋‚œ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜์—ฌ 4 ์ด์ƒ์ผ ๊ฒฝ์šฐ ์ž๋™์ฐจ๋ฅผ ์ „์ง„ - ๊ฐ ์ž๋™์ฐจ ์ธ์Šคํ„ด์Šค์˜ move ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๊ฑฐ๋ฆฌ ์ฆ๊ฐ€ ์ฒ˜๋ฆฌ --- src/models/RaceStepProcessor.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/models/RaceStepProcessor.js diff --git a/src/models/RaceStepProcessor.js b/src/models/RaceStepProcessor.js new file mode 100644 index 00000000..a6792d36 --- /dev/null +++ b/src/models/RaceStepProcessor.js @@ -0,0 +1,13 @@ +import { Random } from '@woowacourse/mission-utils'; + +class RaceStepProcessor { + static runOneStep(cars) { + cars.forEach((car) => { + if (Random.pickNumberInRange(0, 9) >= 4) { + car.move(); + } + }); + } +} + +export default RaceStepProcessor; From 812e193f4a0c0aafc7f0c45e4a40251bb74860ed Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Wed, 22 Oct 2025 22:16:20 +0900 Subject: [PATCH 07/40] =?UTF-8?q?feat(RaceSimulator):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EA=B2=BD=EC=A3=BC=20=EC=8B=9C=EB=AE=AC=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B4=80=EB=A6=AC=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RaceCarFactory๋ฅผ ํ†ตํ•ด ์ดˆ๊ธฐ ์ž๋™์ฐจ ๊ฐ์ฒด ์ƒ์„ฑ - RaceStepProcessor๋ฅผ ํ˜ธ์ถœํ•ด ๋งค ์‹œ๋„๋งˆ๋‹ค ์ „์ง„ ์—ฌ๋ถ€ ๊ฒฐ์ • - ํ˜„์žฌ ์œ„์น˜ ์กฐํšŒ, ๋‚จ์€ ์‹œ๋„ ์—ฌ๋ถ€ ํŒ๋‹จ, ์ตœ์ข… ์šฐ์Šน์ž ๊ณ„์‚ฐ ๊ธฐ๋Šฅ ํฌํ•จ - ํ•œ ๋ฒˆ์˜ ์‹คํ–‰ ๋‹จ๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” executeStep ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ --- src/models/RaceSimulator.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/models/RaceSimulator.js diff --git a/src/models/RaceSimulator.js b/src/models/RaceSimulator.js new file mode 100644 index 00000000..8812c355 --- /dev/null +++ b/src/models/RaceSimulator.js @@ -0,0 +1,35 @@ +import RaceCarFactory from './RaceCarFactory.js'; +import RaceStepProcessor from './RaceStepProcessor.js'; + +class RaceSimulator { + #cars; + #remainingAttempts; + + constructor(carNames, attemptCount) { + this.#cars = RaceCarFactory.createCars(carNames); + this.#remainingAttempts = attemptCount; + } + + #getCurrentPositions() { + return this.#cars.map((car) => [car.getName(), car.getDistance()]); + } + + executeStep() { + this.#remainingAttempts -= 1; + RaceStepProcessor.runOneStep(this.#cars); + return this.#getCurrentPositions(); + } + + hasRemainingAttempts() { + return this.#remainingAttempts > 0; + } + + getRaceWinners() { + const maxDistance = Math.max(...this.#cars.map((car) => car.getDistance())); + return this.#cars + .filter((car) => car.getDistance() === maxDistance) + .map((car) => car.getName()); + } +} + +export default RaceSimulator; From d07a1c09d3b6c468f02d5a369ea4e4bda99ba95a Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Wed, 22 Oct 2025 22:17:45 +0900 Subject: [PATCH 08/40] =?UTF-8?q?feat(InputView):=20=EC=BD=98=EC=86=94=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EC=9D=84=20=EC=B2=98=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20View=20=ED=81=B4=EB=9E=98=EC=8A=A4=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 - Console.readLineAsync๋ฅผ ์‚ฌ์šฉํ•ด ํ”„๋กฌํ”„ํŠธ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ ๋ฐ ์ž…๋ ฅ ๋Œ€๊ธฐ --- src/views/InputView.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/views/InputView.js diff --git a/src/views/InputView.js b/src/views/InputView.js new file mode 100644 index 00000000..6ef314ec --- /dev/null +++ b/src/views/InputView.js @@ -0,0 +1,10 @@ +import { Console } from '@woowacourse/mission-utils'; + +class InputView { + async readString(promptMessage) { + const input = await Console.readLineAsync(promptMessage); + return input; + } +} + +export default InputView; From ec96c3546f8a7f3e1d63c79c527063d6e1dfaa14 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Wed, 22 Oct 2025 22:18:49 +0900 Subject: [PATCH 09/40] =?UTF-8?q?feat(OutputView):=20=EC=BD=98=EC=86=94=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=EC=9D=84=20=EB=8B=B4=EB=8B=B9=ED=95=98?= =?UTF-8?q?=EB=8A=94=20View=20=ED=81=B4=EB=9E=98=EC=8A=A4=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 - ์‹คํ–‰ ๊ฒฐ๊ณผ ํ—ค๋”, ๊ฐ ๋‹จ๊ณ„๋ณ„ ๊ฒฝ์ฃผ ๊ฒฐ๊ณผ, ์ตœ์ข… ์šฐ์Šน์ž ์ถœ๋ ฅ ๊ธฐ๋Šฅ ๊ตฌํ˜„ - Console.print๋ฅผ ์ด์šฉํ•ด ๋‹จ๊ณ„๋ณ„ ๊ฒฐ๊ณผ์™€ ๊ตฌ๋ถ„ ๊ณต๋ฐฑ ๋ผ์ธ ์ฒ˜๋ฆฌ --- src/views/OutputView.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/views/OutputView.js diff --git a/src/views/OutputView.js b/src/views/OutputView.js new file mode 100644 index 00000000..b2ac70bf --- /dev/null +++ b/src/views/OutputView.js @@ -0,0 +1,22 @@ +import { Console } from '@woowacourse/mission-utils'; +import PROMPT_MESSAGES from '../constants/promptMessages.js'; + +class OutputView { + printResultHeader() { + Console.print(PROMPT_MESSAGES.RESULT_HEADER); + } + + printStepResult(result) { + result.forEach((car) => { + const [name, distance] = car; + Console.print(`${name} : ${'-'.repeat(distance)}`); + }); + Console.print(''); + } + + printWinners(winners) { + Console.print(`${PROMPT_MESSAGES.FINAL_WINNERS_PREFIX} : ${winners.join(', ')}`); + } +} + +export default OutputView; From e41fa179bd6a60ee3804ac064d8745d0e8e49be6 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Wed, 22 Oct 2025 22:21:20 +0900 Subject: [PATCH 10/40] =?UTF-8?q?feat(RaceController):=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=B0=A8=20=EA=B2=BD=EC=A3=BC=20=ED=9D=90=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20=EC=A0=9C=EC=96=B4=ED=95=98=EB=8A=94=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ž…๋ ฅ, ์ถœ๋ ฅ, ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋กœ์ง์„ ํ†ตํ•ฉ ๊ด€๋ฆฌํ•˜๋Š” Controller ๊ตฌํ˜„ - ์‚ฌ์šฉ์ž์˜ ์ž๋™์ฐจ ์ด๋ฆ„๊ณผ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ๊ฒฝ์ฃผ ์‹คํ–‰ - ๊ฐ ๋‹จ๊ณ„๋ณ„ ๊ฒฐ๊ณผ์™€ ์ตœ์ข… ์šฐ์Šน์ž๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ์ „์ฒด ์‹คํ–‰ ํ๋ฆ„ ๊ตฌ์„ฑ --- src/controllers/RaceController.js | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/controllers/RaceController.js diff --git a/src/controllers/RaceController.js b/src/controllers/RaceController.js new file mode 100644 index 00000000..bbbd97e9 --- /dev/null +++ b/src/controllers/RaceController.js @@ -0,0 +1,40 @@ +import RaceSimulator from '../models/RaceSimulator.js'; +import InputView from '../views/InputView.js'; +import OutputView from '../views/OutputView.js'; +import PROMPT_MESSAGES from '../constants/promptMessages.js'; + +class RaceController { + #inputView; + #outputView; + + constructor() { + this.#inputView = new InputView(); + this.#outputView = new OutputView(); + } + + async start() { + const carNames = await this.#readCarNames(); + const attemptCount = await this.#readAttemptCount(); + this.#runRace(carNames, attemptCount); + } + + async #readCarNames() { + return await this.#inputView.readString(PROMPT_MESSAGES.CAR_NAMES_INPUT); + } + + async #readAttemptCount() { + return await this.#inputView.readString(PROMPT_MESSAGES.ATTEMPT_COUNT_INPUT); + } + + #runRace(carNames, attemptCount) { + const simulator = new RaceSimulator(carNames, attemptCount); + this.#outputView.printResultHeader(); + while (simulator.hasRemainingAttempts()) { + const positions = simulator.executeStep(); + this.#outputView.printStepResult(positions); + } + this.#outputView.printWinners(simulator.getRaceWinners()); + } +} + +export default RaceController; From e77c0736bae0f3a62cb27ac9dddae7c7b5744a6a Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 06:24:43 +0900 Subject: [PATCH 11/40] =?UTF-8?q?refactor(models):=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=A1=9C=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RaceCarFactory์™€ RaceStepProcessor๋Š” ์ธ์Šคํ„ด์Šค ์ƒํƒœ๋ฅผ ๊ฐ€์ง€์ง€ ์•Š๊ณ , ๋‹จ์ˆœํžˆ ์ž…๋ ฅ๊ฐ’์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ •์  ๋ฉ”์„œ๋“œ๋งŒ ํฌํ•จํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์— ๋”ฐ๋ผ ํด๋ž˜์Šค๋กœ ์œ ์ง€ํ•  ํ•„์š”๊ฐ€ ์—†์–ด, ๊ฐ๊ฐ์˜ ๋กœ์ง์„ ์ˆœ์ˆ˜ ํ•จ์ˆ˜(createCars, runOneStep) ํ˜•ํƒœ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ utils๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. - createCars, runOneStep์„ ์œ ํ‹ธ ํ•จ์ˆ˜ ํ˜•ํƒœ๋กœ ๋ณ€๊ฒฝ - RaceSimulator์—์„œ ํ•ด๋‹น ํ•จ์ˆ˜ ์ง์ ‘ ํ˜ธ์ถœํ•˜์—ฌ ๊ตฌ์กฐ ๋‹จ์ˆœํ™” --- src/models/RaceCarFactory.js | 10 ---------- src/models/RaceSimulator.js | 7 +++---- src/models/RaceStepProcessor.js | 13 ------------- src/models/utils/raceUtils.js | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 27 deletions(-) delete mode 100644 src/models/RaceCarFactory.js delete mode 100644 src/models/RaceStepProcessor.js create mode 100644 src/models/utils/raceUtils.js diff --git a/src/models/RaceCarFactory.js b/src/models/RaceCarFactory.js deleted file mode 100644 index ffd2f9ed..00000000 --- a/src/models/RaceCarFactory.js +++ /dev/null @@ -1,10 +0,0 @@ -import Car from '../domains/Car.js'; - -class RaceCarFactory { - static createCars(carNames) { - const splitNames = carNames.split(','); - return splitNames.map((name) => new Car(name.trim())); - } -} - -export default RaceCarFactory; diff --git a/src/models/RaceSimulator.js b/src/models/RaceSimulator.js index 8812c355..dd1f74b2 100644 --- a/src/models/RaceSimulator.js +++ b/src/models/RaceSimulator.js @@ -1,12 +1,11 @@ -import RaceCarFactory from './RaceCarFactory.js'; -import RaceStepProcessor from './RaceStepProcessor.js'; +import { createCars, runOneStep } from './utils/raceUtils.js'; class RaceSimulator { #cars; #remainingAttempts; constructor(carNames, attemptCount) { - this.#cars = RaceCarFactory.createCars(carNames); + this.#cars = createCars(carNames); this.#remainingAttempts = attemptCount; } @@ -16,7 +15,7 @@ class RaceSimulator { executeStep() { this.#remainingAttempts -= 1; - RaceStepProcessor.runOneStep(this.#cars); + runOneStep(this.#cars); return this.#getCurrentPositions(); } diff --git a/src/models/RaceStepProcessor.js b/src/models/RaceStepProcessor.js deleted file mode 100644 index a6792d36..00000000 --- a/src/models/RaceStepProcessor.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Random } from '@woowacourse/mission-utils'; - -class RaceStepProcessor { - static runOneStep(cars) { - cars.forEach((car) => { - if (Random.pickNumberInRange(0, 9) >= 4) { - car.move(); - } - }); - } -} - -export default RaceStepProcessor; diff --git a/src/models/utils/raceUtils.js b/src/models/utils/raceUtils.js new file mode 100644 index 00000000..2319fb14 --- /dev/null +++ b/src/models/utils/raceUtils.js @@ -0,0 +1,15 @@ +import { Random } from '@woowacourse/mission-utils'; +import Car from '../../domains/Car.js'; + +export const createCars = (carNames) => { + const splitNames = carNames.split(','); + return splitNames.map((name) => new Car(name.trim())); +}; + +export const runOneStep = (cars) => { + cars.forEach((car) => { + if (Random.pickNumberInRange(0, 9) >= 4) { + car.move(); + } + }); +}; From eb04f075bf445ec8af925e50e91cbf16c9f848fb Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 06:58:41 +0900 Subject: [PATCH 12/40] =?UTF-8?q?fix(constants):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EC=9E=85=EB=A0=A5=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EA=B4=80=EB=A0=A8=20=EC=98=A4=EB=A5=98=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NAME_CONTAINS_COMMA๋ฅผ NAME_LIST_FORMAT_INVALID๋กœ ์ƒ์ˆ˜๋ช… ๋ณ€๊ฒฝ - ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์„ ์ž…๋ ฅํ•˜๋„๋ก ๊ตฌ์ฒดํ™” --- src/constants/errorMessages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/errorMessages.js b/src/constants/errorMessages.js index 0eca4b51..e78f0a61 100644 --- a/src/constants/errorMessages.js +++ b/src/constants/errorMessages.js @@ -3,7 +3,7 @@ const ERROR_PREFIX = '[ERROR]'; const ERROR_MESSAGES = Object.freeze({ NAME_TOO_LONG: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์€ 5์ž ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, NAME_EMPTY: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, - NAME_CONTAINS_COMMA: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์— ์‰ผํ‘œ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`, + NAME_LIST_FORMAT_INVALID: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์€ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์œผ๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, NAME_DUPLICATED: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์€ ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`, ATTEMPT_COUNT_NON_NUMERIC: `${ERROR_PREFIX} ์‹œ๋„ ํšŸ์ˆ˜๋Š” ์ˆซ์ž๋งŒ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, ATTEMPT_COUNT_NOT_POSITIVE: `${ERROR_PREFIX} ์‹œ๋„ ํšŸ์ˆ˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, From 4180e62077c6478773ecdd4886f622835be9ba21 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:07:47 +0900 Subject: [PATCH 13/40] =?UTF-8?q?feat(validators):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EB=B0=8F=20=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=20=ED=9A=9F=EC=88=98=20=EA=B2=80=EC=A6=9D=20=EC=9C=A0=ED=8B=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ž๋™์ฐจ ์ด๋ฆ„์ด ๋น„์–ด์žˆ๊ฑฐ๋‚˜ 5์ž๋ฅผ ์ดˆ๊ณผํ•˜๊ฑฐ๋‚˜ ์ค‘๋ณต๋œ ๊ฒฝ์šฐ ์˜ˆ์™ธ ๋ฐœ์ƒ - ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹ˆ๊ฑฐ๋‚˜ 0 ์ดํ•˜์ธ ๊ฒฝ์šฐ ์˜ˆ์™ธ ๋ฐœ์ƒ --- src/utils/validators.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/utils/validators.js diff --git a/src/utils/validators.js b/src/utils/validators.js new file mode 100644 index 00000000..72c01800 --- /dev/null +++ b/src/utils/validators.js @@ -0,0 +1,36 @@ +import ERROR_MESSAGES from '../constants/errorMessages.js'; + +export function validateNameLength(carName) { + if (carName.length > 5) { + throw new Error(ERROR_MESSAGES.NAME_TOO_LONG); + } + if (carName.length === 0) { + throw new Error(ERROR_MESSAGES.NAME_EMPTY); + } +} + +export function validateNameListFormat(rawNames) { + const VALID_NAME_LIST_PATTERN = /^[^,]+(,[^,]+)*$/; + + if (!VALID_NAME_LIST_PATTERN.test(rawNames)) { + throw new Error(ERROR_MESSAGES.NAME_LIST_FORMAT_INVALID); + } +} + +export function validateNameDuplication(carNames) { + if (new Set(carNames).size !== carNames.length) { + throw new Error(ERROR_MESSAGES.NAME_DUPLICATED); + } +} + +export function validateNumericValue(attemptInput) { + if (isNaN(attemptInput)) { + throw new Error(ERROR_MESSAGES.ATTEMPT_COUNT_NON_NUMERIC); + } +} + +export function validatePositiveNumber(number) { + if (number <= 0) { + throw new Error(ERROR_MESSAGES.ATTEMPT_COUNT_NOT_POSITIVE); + } +} From 96fbbbc98ab651a8ed3f5a3bc9661d7d2b3cff40 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:09:40 +0900 Subject: [PATCH 14/40] =?UTF-8?q?feat(Car):=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=B4=EB=A6=84=20=EA=B8=B8=EC=9D=B4=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Car ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ์‹œ validateNameLength ํ˜ธ์ถœ --- src/domains/Car.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/domains/Car.js b/src/domains/Car.js index 4f7a7ced..97a8f198 100644 --- a/src/domains/Car.js +++ b/src/domains/Car.js @@ -1,8 +1,11 @@ +import { validateNameLength } from '../utils/validators.js'; + class Car { #name; #distance; constructor(name) { + validateNameLength(name); this.#name = name; this.#distance = 0; } From 10e48e4b7c5ef5f9924d4f76db065a7befc15c6d Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:10:27 +0900 Subject: [PATCH 15/40] =?UTF-8?q?feat(RaceController):=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EA=B0=92=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ž๋™์ฐจ ์ด๋ฆ„ ์ž…๋ ฅ ์‹œ validateNameListFormat ํ˜ธ์ถœ - ์‹œ๋„ ํšŸ์ˆ˜ ์ž…๋ ฅ ์‹œ validateNumericValue ํ˜ธ์ถœ - ์ปจํŠธ๋กค๋Ÿฌ ์ˆ˜์ค€์—์„œ ์ž…๋ ฅ ๊ฒ€์ฆ ์ฑ…์ž„ ์ˆ˜ํ–‰ --- src/controllers/RaceController.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controllers/RaceController.js b/src/controllers/RaceController.js index bbbd97e9..21e89039 100644 --- a/src/controllers/RaceController.js +++ b/src/controllers/RaceController.js @@ -1,6 +1,7 @@ import RaceSimulator from '../models/RaceSimulator.js'; import InputView from '../views/InputView.js'; import OutputView from '../views/OutputView.js'; +import { validateNameListFormat, validateNumericValue } from '../utils/validators.js'; import PROMPT_MESSAGES from '../constants/promptMessages.js'; class RaceController { @@ -14,7 +15,9 @@ class RaceController { async start() { const carNames = await this.#readCarNames(); + validateNameListFormat(carNames); const attemptCount = await this.#readAttemptCount(); + validateNumericValue(attemptCount); this.#runRace(carNames, attemptCount); } From c74388f32db979db24ecca3e0264cb14e5c014e4 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:11:13 +0900 Subject: [PATCH 16/40] =?UTF-8?q?feat(RaceSimulator):=20=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=20=ED=9A=9F=EC=88=98=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ƒ์„ฑ์ž์—์„œ validatePositiveNumber ํ˜ธ์ถœ --- src/models/RaceSimulator.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/RaceSimulator.js b/src/models/RaceSimulator.js index dd1f74b2..22579ff9 100644 --- a/src/models/RaceSimulator.js +++ b/src/models/RaceSimulator.js @@ -1,4 +1,5 @@ import { createCars, runOneStep } from './utils/raceUtils.js'; +import { validatePositiveNumber } from '../utils/validators.js'; class RaceSimulator { #cars; @@ -6,6 +7,7 @@ class RaceSimulator { constructor(carNames, attemptCount) { this.#cars = createCars(carNames); + validatePositiveNumber(attemptCount); this.#remainingAttempts = attemptCount; } From fc8936e5682825444146266987e69393b8db4c8c Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:11:39 +0900 Subject: [PATCH 17/40] =?UTF-8?q?feat(raceUtils):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EC=A4=91=EB=B3=B5=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - createCars ๋‚ด๋ถ€์—์„œ validateNameDuplication ํ˜ธ์ถœ --- src/models/utils/raceUtils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/utils/raceUtils.js b/src/models/utils/raceUtils.js index 2319fb14..d108269a 100644 --- a/src/models/utils/raceUtils.js +++ b/src/models/utils/raceUtils.js @@ -1,8 +1,10 @@ import { Random } from '@woowacourse/mission-utils'; import Car from '../../domains/Car.js'; +import { validateNameDuplication } from '../../utils/validators.js'; export const createCars = (carNames) => { const splitNames = carNames.split(','); + validateNameDuplication(splitNames); return splitNames.map((name) => new Car(name.trim())); }; From 29ad891825268ed7874935c3f36b9b4de29726c9 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:14:12 +0900 Subject: [PATCH 18/40] =?UTF-8?q?feat(App):=20=EC=95=A0=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=85=98=20=EC=8B=A4=ED=96=89=20=EC=8B=9C=20?= =?UTF-8?q?RaceController=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/App.js b/src/App.js index 091aa0a5..69b7624e 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,10 @@ +import RaceController from './controllers/RaceController.js'; + class App { - async run() {} + async run() { + const controller = new RaceController(); + await controller.start(); + } } export default App; From 0fd082f9d9e9f8118aabe8010574e803ea8e77a6 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:14:50 +0900 Subject: [PATCH 19/40] =?UTF-8?q?style(index):=20import=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=9E=91=EC=9D=80=EB=94=B0=EC=98=B4=ED=91=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 02a1d389..9daefc93 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import App from "./App.js"; +import App from './App.js'; const app = new App(); await app.run(); From f2ae8440d20ec0af58d57e13ad371089f989982a Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:19:43 +0900 Subject: [PATCH 20/40] =?UTF-8?q?docs(README):=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EB=9E=A8=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=9D=84=20=EB=B0=98=EC=98=81=ED=95=98=EC=97=AC=20README.md=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 970cac0a..05111502 100644 --- a/README.md +++ b/README.md @@ -10,26 +10,28 @@ ## 2. Model -### 1) `RaceCarFactory` +### 1) `RaceSimulator` -- ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์ž๋™์ฐจ ์ด๋ฆ„ ๋ฌธ์ž์—ด์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. -- ๊ฐ ์ด๋ฆ„์„ ๋ถ„๋ฆฌํ•˜๊ณ  ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. -- `Car` ์ธ์Šคํ„ด์Šค ๋ฐฐ์—ด์„ ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. +- ์ž๋™์ฐจ ์ด๋ฆ„๊ณผ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ๋ฐ›์•„ ๊ฒฝ์ฃผ ์ „์ฒด๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. +- `createCars` ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ์ž๋™์ฐจ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. +- ๋‚จ์€ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ถ”์ ํ•˜๋ฉฐ, ๊ฐ ํ„ด๋งˆ๋‹ค `runOneStep` ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด ์ด๋™์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. +- ๊ฐ ๋‹จ๊ณ„์˜ ์ž๋™์ฐจ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. +- ์ตœ์ข… ์šฐ์Šน์ž๋ฅผ ๊ณ„์‚ฐํ•ด ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. -### 2) `RaceStepProcessor` +### 2) `raceUtils` -- ์ž๋™์ฐจ ๋ฐฐ์—ด์„ ๋ฐ›์•„ ํ•œ ๋ฒˆ์˜ ์ด๋™ ์‹œ๋„๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. -- ์ž๋™์ฐจ๋งˆ๋‹ค 0~9 ์‚ฌ์ด์˜ ๋‚œ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. -- ๋‚œ์ˆ˜๊ฐ€ 4 ์ด์ƒ์ด๋ฉด ํ•ด๋‹น ์ž๋™์ฐจ๋ฅผ ์ „์ง„์‹œํ‚ต๋‹ˆ๋‹ค. +- ๊ฒฝ์ฃผ ๊ด€๋ จ ๋กœ์ง์„ ์œ ํ‹ธ ํ•จ์ˆ˜ ํ˜•์‹์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +#### `createCars(carNames)` -### 3) `RaceSimulator` +- ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์ž๋™์ฐจ ์ด๋ฆ„ ๋ฌธ์ž์—ด์„ ์ž…๋ ฅ๋ฐ›์•„, ๊ฐ ์ด๋ฆ„์˜ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. +- ๊ฐ ์ด๋ฆ„์— ๋Œ€ํ•ด `Car` ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ด ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. -- ์ž๋™์ฐจ ์ด๋ฆ„๊ณผ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ๋ฐ›์•„ ๊ฒฝ์ฃผ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. -- `RaceCarFactory`๋กœ ์ž๋™์ฐจ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. -- ๋‚จ์€ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค. -- `RaceStepProcessor`๋ฅผ ํ†ตํ•ด ๊ฐ ๋‹จ๊ณ„๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. -- ํ˜„์žฌ ๋ชจ๋“  ์ž๋™์ฐจ์˜ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. -- ์ตœ์ข… ์šฐ์Šน์ž ๋ชฉ๋ก์„ ๊ณ„์‚ฐํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. +#### `runOneStep(cars)` + +- ์ž๋™์ฐจ ๋ฐฐ์—ด์„ ๋ฐ›์•„ ํ•œ ๋ฒˆ์˜ ์ด๋™ ์‹œ๋„๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. +- ๊ฐ ์ž๋™์ฐจ๋งˆ๋‹ค 0~9 ๋ฒ”์œ„์˜ ๋‚œ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜๊ณ , + ๋‚œ์ˆ˜๊ฐ€ 4 ์ด์ƒ์ผ ๊ฒฝ์šฐ ์ „์ง„์‹œํ‚ต๋‹ˆ๋‹ค. ## 3. View @@ -49,26 +51,28 @@ ### 1) `RaceController` -- `InputView`๋ฅผ ํ†ตํ•ด ์ž๋™์ฐจ ์ด๋ฆ„์„ ์ž…๋ ฅ๋ฐ›์Šต๋‹ˆ๋‹ค. -- `InputView`๋ฅผ ํ†ตํ•ด ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ž…๋ ฅ๋ฐ›์Šต๋‹ˆ๋‹ค. +- `InputView`๋ฅผ ํ†ตํ•ด ์ž๋™์ฐจ ์ด๋ฆ„๊ณผ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ž…๋ ฅ๋ฐ›์Šต๋‹ˆ๋‹ค. - `RaceSimulator`๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๊ฒฝ์ฃผ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. - `OutputView`๋กœ ๊ฒฐ๊ณผ ํ—ค๋”๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. - ๋‚จ์€ ์‹œ๋„๊ฐ€ ์žˆ๋Š” ๋™์•ˆ ๋ฐ˜๋ณตํ•˜์—ฌ ๊ฐ ๋‹จ๊ณ„๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. - ๊ฐ ๋‹จ๊ณ„์˜ ๊ฒฐ๊ณผ๋ฅผ `OutputView`๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. - ๊ฒฝ์ฃผ ์ข…๋ฃŒ ํ›„ ์šฐ์Šน์ž๋ฅผ `OutputView`๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. -## 5. ์˜ˆ์™ธ์ฒ˜๋ฆฌ +## 5. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ### 1) ์ž๋™์ฐจ ์ด๋ฆ„ ๊ฒ€์ฆ + +- ์ž๋™์ฐจ ์ด๋ฆ„ ๋ชฉ๋ก์ด ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์ด ์•„๋‹Œ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. - ์ž๋™์ฐจ ์ด๋ฆ„์˜ ๊ธธ์ด๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. - ์ด๋ฆ„์ด 5์ž๋ฅผ ์ดˆ๊ณผํ•˜๋Š” ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. - ์ด๋ฆ„์ด ๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. -- ์ด๋ฆ„์— ์‰ผํ‘œ๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. - ์ด๋ฆ„์ด ์ค‘๋ณต๋œ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ### 2) ์‹œ๋„ ํšŸ์ˆ˜ ๊ฒ€์ฆ + - ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. - ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ 0 ์ดํ•˜์ธ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ### 3) ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + - ๋ชจ๋“  ์˜ˆ์™ธ๋Š” `[ERROR]`๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. From d4ab24c723dcf5feb33d615de7e002ccadf4ab05 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:22:58 +0900 Subject: [PATCH 21/40] =?UTF-8?q?refactor(raceUtils):=20runOneStep=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EB=82=B4=EB=B6=80=20=EB=A7=A4=EC=A7=81=20?= =?UTF-8?q?=EB=84=98=EB=B2=84=20=EC=83=81=EC=88=98=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/utils/raceUtils.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/models/utils/raceUtils.js b/src/models/utils/raceUtils.js index d108269a..1056ffeb 100644 --- a/src/models/utils/raceUtils.js +++ b/src/models/utils/raceUtils.js @@ -9,8 +9,12 @@ export const createCars = (carNames) => { }; export const runOneStep = (cars) => { + const MOVE_THRESHOLD = 4; + const MIN_RANDOM_VALUE = 0; + const MAX_RANDOM_VALUE = 9; + cars.forEach((car) => { - if (Random.pickNumberInRange(0, 9) >= 4) { + if (Random.pickNumberInRange(MIN_RANDOM_VALUE, MAX_RANDOM_VALUE) >= MOVE_THRESHOLD) { car.move(); } }); From 6c76a26aed72fb7f690147e746a69095e3ca30be Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:25:02 +0900 Subject: [PATCH 22/40] =?UTF-8?q?refactor(validators):=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EC=B5=9C=EB=8C=80=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=EB=A5=BC=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/validators.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/validators.js b/src/utils/validators.js index 72c01800..ded496bd 100644 --- a/src/utils/validators.js +++ b/src/utils/validators.js @@ -1,7 +1,9 @@ import ERROR_MESSAGES from '../constants/errorMessages.js'; export function validateNameLength(carName) { - if (carName.length > 5) { + const MAX_CAR_NAME_LENGTH = 5; + + if (carName.length > MAX_CAR_NAME_LENGTH) { throw new Error(ERROR_MESSAGES.NAME_TOO_LONG); } if (carName.length === 0) { From 6e8eb9a118de7eaef359817b2b70401da037b7a1 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Thu, 23 Oct 2025 07:29:22 +0900 Subject: [PATCH 23/40] =?UTF-8?q?refactor(raceUtils):=20splitNames?= =?UTF-8?q?=EB=A5=BC=20carNameList=EB=A1=9C=20=EB=B3=80=EC=88=98=EB=AA=85?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/utils/raceUtils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/utils/raceUtils.js b/src/models/utils/raceUtils.js index 1056ffeb..a7087077 100644 --- a/src/models/utils/raceUtils.js +++ b/src/models/utils/raceUtils.js @@ -3,9 +3,9 @@ import Car from '../../domains/Car.js'; import { validateNameDuplication } from '../../utils/validators.js'; export const createCars = (carNames) => { - const splitNames = carNames.split(','); - validateNameDuplication(splitNames); - return splitNames.map((name) => new Car(name.trim())); + const carNameList = carNames.split(','); + validateNameDuplication(carNameList); + return carNameList.map((name) => new Car(name.trim())); }; export const runOneStep = (cars) => { From 92fe804196c51e7c1d27fe9858f027b1842e9a4c Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 04:11:37 +0900 Subject: [PATCH 24/40] =?UTF-8?q?feat(constants):=20=EC=8B=9C=EB=8F=84=20?= =?UTF-8?q?=ED=9A=9F=EC=88=98=20=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/errorMessages.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/constants/errorMessages.js b/src/constants/errorMessages.js index e78f0a61..e037fbd4 100644 --- a/src/constants/errorMessages.js +++ b/src/constants/errorMessages.js @@ -5,8 +5,10 @@ const ERROR_MESSAGES = Object.freeze({ NAME_EMPTY: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, NAME_LIST_FORMAT_INVALID: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์€ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์œผ๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, NAME_DUPLICATED: `${ERROR_PREFIX} ์ž๋™์ฐจ ์ด๋ฆ„์€ ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`, + ATTEMPT_COUNT_EMPTY: `${ERROR_PREFIX} ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, ATTEMPT_COUNT_NON_NUMERIC: `${ERROR_PREFIX} ์‹œ๋„ ํšŸ์ˆ˜๋Š” ์ˆซ์ž๋งŒ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, ATTEMPT_COUNT_NOT_POSITIVE: `${ERROR_PREFIX} ์‹œ๋„ ํšŸ์ˆ˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + ATTEMPT_COUNT_NOT_INTEGER: `${ERROR_PREFIX} ์‹œ๋„ ํšŸ์ˆ˜๋Š” ์ •์ˆ˜๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, }); export default ERROR_MESSAGES; From a75190fc374399423b3c0a3f1b55d6d972afe02b Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 04:13:36 +0900 Subject: [PATCH 25/40] =?UTF-8?q?feat(validators):=20=EC=8B=9C=EB=8F=84=20?= =?UTF-8?q?=ED=9A=9F=EC=88=98=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๋นˆ ๋ฌธ์ž์—ด ์ž…๋ ฅ ์‹œ ์˜ˆ์™ธ ์ถ”๊ฐ€ - validatePositiveNumber๋ฅผ validatePositiveInteger๋กœ ๋ณ€๊ฒฝ - ์ •์ˆ˜ ์—ฌ๋ถ€ ๊ฒ€์‚ฌ ๋กœ์ง ๋ฐ ๊ด€๋ จ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ --- src/utils/validators.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils/validators.js b/src/utils/validators.js index ded496bd..53b90488 100644 --- a/src/utils/validators.js +++ b/src/utils/validators.js @@ -26,13 +26,19 @@ export function validateNameDuplication(carNames) { } export function validateNumericValue(attemptInput) { + if (attemptInput === '') { + throw new Error(ERROR_MESSAGES.ATTEMPT_COUNT_EMPTY); + } if (isNaN(attemptInput)) { throw new Error(ERROR_MESSAGES.ATTEMPT_COUNT_NON_NUMERIC); } } -export function validatePositiveNumber(number) { +export function validatePositiveInteger(number) { if (number <= 0) { throw new Error(ERROR_MESSAGES.ATTEMPT_COUNT_NOT_POSITIVE); } + if (!Number.isInteger(number)) { + throw new Error(ERROR_MESSAGES.ATTEMPT_COUNT_NOT_INTEGER); + } } From 351903725109b108e03ead21b5844fd9b4a49ca4 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 04:14:45 +0900 Subject: [PATCH 26/40] =?UTF-8?q?fix(RaceSimulator):=20=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=20=ED=9A=9F=EC=88=98=20=EA=B2=80=EC=A6=9D=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/RaceSimulator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/RaceSimulator.js b/src/models/RaceSimulator.js index 22579ff9..8e7a1904 100644 --- a/src/models/RaceSimulator.js +++ b/src/models/RaceSimulator.js @@ -1,5 +1,5 @@ import { createCars, runOneStep } from './utils/raceUtils.js'; -import { validatePositiveNumber } from '../utils/validators.js'; +import { validatePositiveInteger } from '../utils/validators.js'; class RaceSimulator { #cars; @@ -7,7 +7,7 @@ class RaceSimulator { constructor(carNames, attemptCount) { this.#cars = createCars(carNames); - validatePositiveNumber(attemptCount); + validatePositiveInteger(attemptCount); this.#remainingAttempts = attemptCount; } From c1355edbcebe5b12069e7103f59c12be8dc6cc59 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 04:48:25 +0900 Subject: [PATCH 27/40] =?UTF-8?q?fix(RaceController):=20=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=20=ED=9A=9F=EC=88=98=20=EB=AC=B8=EC=9E=90=EC=97=B4=EC=9D=84=20?= =?UTF-8?q?Number=EB=A1=9C=20=EB=B3=80=ED=99=98=20=ED=9B=84=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/RaceController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/RaceController.js b/src/controllers/RaceController.js index 21e89039..6ea26fec 100644 --- a/src/controllers/RaceController.js +++ b/src/controllers/RaceController.js @@ -18,7 +18,7 @@ class RaceController { validateNameListFormat(carNames); const attemptCount = await this.#readAttemptCount(); validateNumericValue(attemptCount); - this.#runRace(carNames, attemptCount); + this.#runRace(carNames, Number(attemptCount)); } async #readCarNames() { From 9af4068d8e5674ad5f6b53649de8231c82f29dce Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 04:57:01 +0900 Subject: [PATCH 28/40] =?UTF-8?q?refactor(raceUtils):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EA=B3=B5=EB=B0=B1=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=EC=A4=91=EB=B3=B5=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - carNames.split(,) ํ›„ ๊ฐ ์ด๋ฆ„์˜ ์•ž๋’ค ๊ณต๋ฐฑ ์ œ๊ฑฐ - validateNameDuplication ์ ์šฉ ์ „ ๊ณต๋ฐฑ ์ œ๊ฑฐ๋œ ๋ฆฌ์ŠคํŠธ ์‚ฌ์šฉ --- src/models/utils/raceUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/utils/raceUtils.js b/src/models/utils/raceUtils.js index a7087077..d51be9fc 100644 --- a/src/models/utils/raceUtils.js +++ b/src/models/utils/raceUtils.js @@ -3,9 +3,9 @@ import Car from '../../domains/Car.js'; import { validateNameDuplication } from '../../utils/validators.js'; export const createCars = (carNames) => { - const carNameList = carNames.split(','); + const carNameList = carNames.split(',').map((name) => name.trim()); validateNameDuplication(carNameList); - return carNameList.map((name) => new Car(name.trim())); + return carNameList.map((name) => new Car(name)); }; export const runOneStep = (cars) => { From a242ccfdc8e40e82dff648b17cb44196891733c2 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 05:08:10 +0900 Subject: [PATCH 29/40] =?UTF-8?q?refactor(Car,=20RaceSimulator):=20#distan?= =?UTF-8?q?ce=EB=A5=BC=20#position=EC=9C=BC=EB=A1=9C=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Car ํด๋ž˜์Šค ๋‚ด๋ถ€ ํ•„๋“œ #distance๋ฅผ #position์œผ๋กœ ์ˆ˜์ • - ๊ด€๋ จ ๋ฉ”์„œ๋“œ getDistance๋ฅผ getPosition์œผ๋กœ ์ˆ˜์ • - RaceSimulator์—์„œ getDistance์—์„œ getPosition์œผ๋กœ ์—…๋ฐ์ดํŠธ --- src/domains/Car.js | 10 +++++----- src/models/RaceSimulator.js | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/domains/Car.js b/src/domains/Car.js index 97a8f198..eefc66e4 100644 --- a/src/domains/Car.js +++ b/src/domains/Car.js @@ -2,24 +2,24 @@ import { validateNameLength } from '../utils/validators.js'; class Car { #name; - #distance; + #position; constructor(name) { validateNameLength(name); this.#name = name; - this.#distance = 0; + this.#position = 0; } move() { - this.#distance += 1; + this.#position += 1; } getName() { return this.#name; } - getDistance() { - return this.#distance; + getPosition() { + return this.#position; } } diff --git a/src/models/RaceSimulator.js b/src/models/RaceSimulator.js index 8e7a1904..631e94db 100644 --- a/src/models/RaceSimulator.js +++ b/src/models/RaceSimulator.js @@ -12,7 +12,7 @@ class RaceSimulator { } #getCurrentPositions() { - return this.#cars.map((car) => [car.getName(), car.getDistance()]); + return this.#cars.map((car) => [car.getName(), car.getPosition()]); } executeStep() { @@ -26,9 +26,9 @@ class RaceSimulator { } getRaceWinners() { - const maxDistance = Math.max(...this.#cars.map((car) => car.getDistance())); + const maxDistance = Math.max(...this.#cars.map((car) => car.getPosition())); return this.#cars - .filter((car) => car.getDistance() === maxDistance) + .filter((car) => car.getPosition() === maxDistance) .map((car) => car.getName()); } } From 0ccd23d91e6a5ef9cbcfdb982ac5330f0a28c66f Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 05:23:37 +0900 Subject: [PATCH 30/40] =?UTF-8?q?refactor(RaceController):=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20await=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - readString ํ˜ธ์ถœ ์‹œ return await ๋Œ€์‹  ๋ฐ”๋กœ Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ • --- src/controllers/RaceController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/RaceController.js b/src/controllers/RaceController.js index 6ea26fec..8ecedc3c 100644 --- a/src/controllers/RaceController.js +++ b/src/controllers/RaceController.js @@ -22,11 +22,11 @@ class RaceController { } async #readCarNames() { - return await this.#inputView.readString(PROMPT_MESSAGES.CAR_NAMES_INPUT); + return this.#inputView.readString(PROMPT_MESSAGES.CAR_NAMES_INPUT); } async #readAttemptCount() { - return await this.#inputView.readString(PROMPT_MESSAGES.ATTEMPT_COUNT_INPUT); + return this.#inputView.readString(PROMPT_MESSAGES.ATTEMPT_COUNT_INPUT); } #runRace(carNames, attemptCount) { From dd4e12a3910abda675e8f9a25ccee280223a0086 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 08:59:45 +0900 Subject: [PATCH 31/40] =?UTF-8?q?test(Car):=20Car=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domains/Car.test.js | 57 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/domains/Car.test.js diff --git a/src/domains/Car.test.js b/src/domains/Car.test.js new file mode 100644 index 00000000..e862a28f --- /dev/null +++ b/src/domains/Car.test.js @@ -0,0 +1,57 @@ +import Car from './Car.js'; +import ERROR_MESSAGES from '../constants/errorMessages.js'; + +describe('Car', () => { + describe('์ƒ์„ฑ์ž', () => { + test('์œ ํšจํ•œ ์ž…๋ ฅ์ด ์ฃผ์–ด์ง€๋ฉด Car ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค', () => { + expect(() => new Car('pobi')).not.toThrow(); + }); + + test('์ด๋ฆ„์ด 6์ž ์ด์ƒ์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', () => { + expect(() => new Car('longname')).toThrow(ERROR_MESSAGES.NAME_TOO_LONG); + }); + + test('์ด๋ฆ„์ด ๋น„์–ด ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', () => { + expect(() => new Car('')).toThrow(ERROR_MESSAGES.NAME_EMPTY); + }); + }); + + describe('move ๋ฉ”์„œ๋“œ', () => { + test('move ํ˜ธ์ถœ ์‹œ ์œ„์น˜๊ฐ€ 1 ์ฆ๊ฐ€ํ•œ๋‹ค', () => { + // Arrange + const car = new Car('pobi'); + + // Act + car.move(); + + // Assert + expect(car.getPosition()).toBe(1); + }); + }); + + describe('getName ๋ฉ”์„œ๋“œ', () => { + test('์ž๋™์ฐจ์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { + // Arrange + const car = new Car('pobi'); + + // Act + const name = car.getName(); + + // Assert + expect(name).toBe('pobi'); + }); + }); + + describe('getDistance ๋ฉ”์„œ๋“œ', () => { + test('์ž๋™์ฐจ์˜ ํ˜„์žฌ ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { + // Arrange + const car = new Car('pobi'); + + // Act + const position = car.getPosition(); + + // Assert + expect(position).toBe(0); + }); + }); +}); From d04b9c2eaf23b9ce6eb4f925ad85c1c502789e59 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 09:01:49 +0900 Subject: [PATCH 32/40] =?UTF-8?q?test(raceUtils):=20raceUtils=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/utils/raceUtils.test.js | 80 ++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/models/utils/raceUtils.test.js diff --git a/src/models/utils/raceUtils.test.js b/src/models/utils/raceUtils.test.js new file mode 100644 index 00000000..62015b66 --- /dev/null +++ b/src/models/utils/raceUtils.test.js @@ -0,0 +1,80 @@ +import { Random } from '@woowacourse/mission-utils'; +import { createCars, runOneStep } from './raceUtils.js'; +import ERROR_MESSAGES from '../../constants/errorMessages.js'; + +Random.pickNumberInRange = jest.fn(); + +describe('raceUtils', () => { + describe('createCars', () => { + test('์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์ด๋ฆ„์œผ๋กœ ์ž๋™์ฐจ ๋ฐฐ์—ด์„ ์ƒ์„ฑํ•œ๋‹ค', () => { + // Arrange + const names = 'pobi,woni,jun'; + + // Act + const cars = createCars(names); + + // Assert + expect(cars).toHaveLength(3); + expect(cars[0].getName()).toBe('pobi'); + expect(cars[1].getName()).toBe('woni'); + expect(cars[2].getName()).toBe('jun'); + }); + + test('์ด๋ฆ„ ์•ž๋’ค์— ๊ณต๋ฐฑ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•œ๋‹ค', () => { + // Arrange + const names = ' pobi , woni , jun '; + + // Act + const cars = createCars(names); + + // Assert + expect(cars[0].getName()).toBe('pobi'); + expect(cars[1].getName()).toBe('woni'); + expect(cars[2].getName()).toBe('jun'); + }); + + test('์ค‘๋ณต๋œ ์ด๋ฆ„์ด ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', () => { + expect(() => createCars('pobi,woni,pobi')).toThrow(ERROR_MESSAGES.NAME_DUPLICATED); + expect(() => createCars('pobi, woni, pobi')).toThrow(ERROR_MESSAGES.NAME_DUPLICATED); + }); + }); + + describe('runOneStep', () => { + test('๋žœ๋ค ๊ฐ’์ด 4 ์ด์ƒ์ด๋ฉด ์ž๋™์ฐจ๊ฐ€ ์ „์ง„ํ•œ๋‹ค', () => { + // Arrange + Random.pickNumberInRange.mockReturnValue(4); + const cars = createCars('pobi'); + + // Act + runOneStep(cars); + + // Assert + expect(cars[0].getPosition()).toBe(1); + }); + + test('๋žœ๋ค ๊ฐ’์ด 4 ๋ฏธ๋งŒ์ด๋ฉด ์ž๋™์ฐจ๊ฐ€ ์ „์ง„ํ•˜์ง€ ์•Š๋Š”๋‹ค', () => { + // Arrange + Random.pickNumberInRange.mockReturnValue(3); + const cars = createCars('pobi'); + + // Act + runOneStep(cars); + + // Assert + expect(cars[0].getPosition()).toBe(0); + }); + + test('๊ฐ ์ž๋™์ฐจ์˜ ์ „์ง„ ์—ฌ๋ถ€๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๊ฒฐ์ •๋œ๋‹ค', () => { + // Arrange + Random.pickNumberInRange.mockReturnValueOnce(4).mockReturnValueOnce(3); + const cars = createCars('pobi,woni'); + + // Act + runOneStep(cars); + + // Assert + expect(cars[0].getPosition()).toBe(1); + expect(cars[1].getPosition()).toBe(0); + }); + }); +}); From a93183e26aca88989b1b1933514bb1d54cd3c6b9 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 09:02:26 +0900 Subject: [PATCH 33/40] =?UTF-8?q?test(RaceSimulator):=20RaceSimulator=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/RaceSimulator.test.js | 126 +++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/models/RaceSimulator.test.js diff --git a/src/models/RaceSimulator.test.js b/src/models/RaceSimulator.test.js new file mode 100644 index 00000000..25329624 --- /dev/null +++ b/src/models/RaceSimulator.test.js @@ -0,0 +1,126 @@ +import { Random } from '@woowacourse/mission-utils'; +import RaceSimulator from './RaceSimulator.js'; +import ERROR_MESSAGES from '../constants/errorMessages.js'; + +Random.pickNumberInRange = jest.fn(); + +describe('RaceSimulator', () => { + beforeEach(() => { + Random.pickNumberInRange.mockClear(); + }); + + describe('์ƒ์„ฑ์ž', () => { + test('์œ ํšจํ•œ ์ž…๋ ฅ์ด ์ฃผ์–ด์ง€๋ฉด RaceSimulator ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค', () => { + expect(() => new RaceSimulator('pobi,woni', 5)).not.toThrow(); + }); + + test('์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์–‘์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', () => { + expect(() => new RaceSimulator('pobi,woni', 0)).toThrow( + ERROR_MESSAGES.ATTEMPT_COUNT_NOT_POSITIVE + ); + expect(() => new RaceSimulator('pobi,woni', -1)).toThrow( + ERROR_MESSAGES.ATTEMPT_COUNT_NOT_POSITIVE + ); + }); + + test('์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ •์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', () => { + expect(() => new RaceSimulator('pobi,woni', 1.1)).toThrow( + ERROR_MESSAGES.ATTEMPT_COUNT_NOT_INTEGER + ); + }); + }); + + describe('executeStep ๋ฉ”์„œ๋“œ', () => { + test('ํ•œ ์Šคํ… ์‹คํ–‰ ํ›„ ๊ฐ ์ž๋™์ฐจ์˜ ์ด๋ฆ„๊ณผ ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { + // Arrange + Random.pickNumberInRange.mockReturnValue(4); + const simulator = new RaceSimulator('pobi,woni', 5); + + // Act + const result = simulator.executeStep(); + + // Assert + expect(result).toEqual([ + ['pobi', 1], + ['woni', 1], + ]); + }); + + test('์Šคํ… ์‹คํ–‰ ์‹œ ๋‚จ์€ ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ๊ฐ์†Œํ•œ๋‹ค', () => { + // Arrange + Random.pickNumberInRange.mockReturnValue(4); + const simulator = new RaceSimulator('pobi,woni', 2); + + // Assert: ์ดˆ๊ธฐ ์ƒํƒœ + expect(simulator.hasRemainingAttempts()).toBe(true); + + // Act & Assert: 1ํšŒ ์‹คํ–‰ ํ›„ + simulator.executeStep(); + expect(simulator.hasRemainingAttempts()).toBe(true); + + // Act & Assert: 2ํšŒ ์‹คํ–‰ ํ›„ + simulator.executeStep(); + expect(simulator.hasRemainingAttempts()).toBe(false); + }); + }); + + describe('hasRemainingAttempts ๋ฉ”์„œ๋“œ', () => { + test('๋‚จ์€ ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์žˆ์œผ๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { + // Arrange + const simulator = new RaceSimulator('pobi,woni', 5); + + // Act + const result = simulator.hasRemainingAttempts(); + + // Assert + expect(result).toBe(true); + }); + + test('๋‚จ์€ ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์—†์œผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { + // Arrange + Random.pickNumberInRange.mockReturnValue(4); + const simulator = new RaceSimulator('pobi,woni', 1); + + // Act + simulator.executeStep(); + const result = simulator.hasRemainingAttempts(); + + // Assert + expect(result).toBe(false); + }); + }); + + describe('getRaceWinners ๋ฉ”์„œ๋“œ', () => { + test('๊ฐ€์žฅ ๋ฉ€๋ฆฌ ๊ฐ„ ์ž๋™์ฐจ์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { + // Arrange + Random.pickNumberInRange + .mockReturnValueOnce(4) + .mockReturnValueOnce(3) + .mockReturnValueOnce(4) + .mockReturnValueOnce(4); + const simulator = new RaceSimulator('pobi,woni', 2); + + // Act + simulator.executeStep(); + simulator.executeStep(); + const winners = simulator.getRaceWinners(); + + // Assert + expect(winners).toEqual(['pobi']); + }); + + test('๋™๋ฅ ์ด๋ฉด ๋ชจ๋“  ์šฐ์Šน์ž๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { + // Arrange + Random.pickNumberInRange.mockReturnValue(4); + const simulator = new RaceSimulator('pobi,woni', 2); + + // Act + simulator.executeStep(); + simulator.executeStep(); + const winners = simulator.getRaceWinners(); + + // Assert + expect(winners).toEqual(['pobi', 'woni']); + }); + }); +}); From 930736051b30b8ed1e09e37af53c3fcadc2710ee Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 18:06:00 +0900 Subject: [PATCH 34/40] =?UTF-8?q?chore(constants):=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=9A=A9=20=EC=84=A4=EC=A0=95=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ž๋™์ฐจ ์ด๋™ ์กฐ๊ฑด์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ์ƒ์ˆ˜ ์ถ”๊ฐ€ --- src/constants/testConfig.js | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/constants/testConfig.js diff --git a/src/constants/testConfig.js b/src/constants/testConfig.js new file mode 100644 index 00000000..407abe6d --- /dev/null +++ b/src/constants/testConfig.js @@ -0,0 +1,6 @@ +const TEST_CONFIG = Object.freeze({ + MOVING_FORWARD: 4, + STOP: 3, +}); + +export default TEST_CONFIG; From 4f58fa0d56a627c2a87492900992c2c67596ecd0 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 18:07:42 +0900 Subject: [PATCH 35/40] =?UTF-8?q?test(models):=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=20=EB=82=B4=20=EB=A7=A4=EC=A7=81?= =?UTF-8?q?=20=EB=84=98=EB=B2=84=EB=A5=BC=20TEST=5FCONFIG=20=EC=83=81?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/RaceSimulator.test.js | 17 +++++++++-------- src/models/utils/raceUtils.test.js | 9 ++++++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/models/RaceSimulator.test.js b/src/models/RaceSimulator.test.js index 25329624..ea422db7 100644 --- a/src/models/RaceSimulator.test.js +++ b/src/models/RaceSimulator.test.js @@ -1,5 +1,6 @@ import { Random } from '@woowacourse/mission-utils'; import RaceSimulator from './RaceSimulator.js'; +import TEST_CONFIG from '../constants/testConfig.js'; import ERROR_MESSAGES from '../constants/errorMessages.js'; Random.pickNumberInRange = jest.fn(); @@ -33,7 +34,7 @@ describe('RaceSimulator', () => { describe('executeStep ๋ฉ”์„œ๋“œ', () => { test('ํ•œ ์Šคํ… ์‹คํ–‰ ํ›„ ๊ฐ ์ž๋™์ฐจ์˜ ์ด๋ฆ„๊ณผ ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { // Arrange - Random.pickNumberInRange.mockReturnValue(4); + Random.pickNumberInRange.mockReturnValue(TEST_CONFIG.MOVING_FORWARD); const simulator = new RaceSimulator('pobi,woni', 5); // Act @@ -48,7 +49,7 @@ describe('RaceSimulator', () => { test('์Šคํ… ์‹คํ–‰ ์‹œ ๋‚จ์€ ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ๊ฐ์†Œํ•œ๋‹ค', () => { // Arrange - Random.pickNumberInRange.mockReturnValue(4); + Random.pickNumberInRange.mockReturnValue(TEST_CONFIG.MOVING_FORWARD); const simulator = new RaceSimulator('pobi,woni', 2); // Assert: ์ดˆ๊ธฐ ์ƒํƒœ @@ -78,7 +79,7 @@ describe('RaceSimulator', () => { test('๋‚จ์€ ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์—†์œผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { // Arrange - Random.pickNumberInRange.mockReturnValue(4); + Random.pickNumberInRange.mockReturnValue(TEST_CONFIG.MOVING_FORWARD); const simulator = new RaceSimulator('pobi,woni', 1); // Act @@ -94,10 +95,10 @@ describe('RaceSimulator', () => { test('๊ฐ€์žฅ ๋ฉ€๋ฆฌ ๊ฐ„ ์ž๋™์ฐจ์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { // Arrange Random.pickNumberInRange - .mockReturnValueOnce(4) - .mockReturnValueOnce(3) - .mockReturnValueOnce(4) - .mockReturnValueOnce(4); + .mockReturnValueOnce(TEST_CONFIG.MOVING_FORWARD) + .mockReturnValueOnce(TEST_CONFIG.STOP) + .mockReturnValueOnce(TEST_CONFIG.MOVING_FORWARD) + .mockReturnValueOnce(TEST_CONFIG.MOVING_FORWARD); const simulator = new RaceSimulator('pobi,woni', 2); // Act @@ -111,7 +112,7 @@ describe('RaceSimulator', () => { test('๋™๋ฅ ์ด๋ฉด ๋ชจ๋“  ์šฐ์Šน์ž๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { // Arrange - Random.pickNumberInRange.mockReturnValue(4); + Random.pickNumberInRange.mockReturnValue(TEST_CONFIG.MOVING_FORWARD); const simulator = new RaceSimulator('pobi,woni', 2); // Act diff --git a/src/models/utils/raceUtils.test.js b/src/models/utils/raceUtils.test.js index 62015b66..fb4f56b3 100644 --- a/src/models/utils/raceUtils.test.js +++ b/src/models/utils/raceUtils.test.js @@ -1,5 +1,6 @@ import { Random } from '@woowacourse/mission-utils'; import { createCars, runOneStep } from './raceUtils.js'; +import TEST_CONFIG from '../../constants/testConfig.js'; import ERROR_MESSAGES from '../../constants/errorMessages.js'; Random.pickNumberInRange = jest.fn(); @@ -42,7 +43,7 @@ describe('raceUtils', () => { describe('runOneStep', () => { test('๋žœ๋ค ๊ฐ’์ด 4 ์ด์ƒ์ด๋ฉด ์ž๋™์ฐจ๊ฐ€ ์ „์ง„ํ•œ๋‹ค', () => { // Arrange - Random.pickNumberInRange.mockReturnValue(4); + Random.pickNumberInRange.mockReturnValue(TEST_CONFIG.MOVING_FORWARD); const cars = createCars('pobi'); // Act @@ -54,7 +55,7 @@ describe('raceUtils', () => { test('๋žœ๋ค ๊ฐ’์ด 4 ๋ฏธ๋งŒ์ด๋ฉด ์ž๋™์ฐจ๊ฐ€ ์ „์ง„ํ•˜์ง€ ์•Š๋Š”๋‹ค', () => { // Arrange - Random.pickNumberInRange.mockReturnValue(3); + Random.pickNumberInRange.mockReturnValue(TEST_CONFIG.STOP); const cars = createCars('pobi'); // Act @@ -66,7 +67,9 @@ describe('raceUtils', () => { test('๊ฐ ์ž๋™์ฐจ์˜ ์ „์ง„ ์—ฌ๋ถ€๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๊ฒฐ์ •๋œ๋‹ค', () => { // Arrange - Random.pickNumberInRange.mockReturnValueOnce(4).mockReturnValueOnce(3); + Random.pickNumberInRange + .mockReturnValueOnce(TEST_CONFIG.MOVING_FORWARD) + .mockReturnValueOnce(TEST_CONFIG.STOP); const cars = createCars('pobi,woni'); // Act From 1ad10fd81ec0333c853c329d1fd2f08b6c146024 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 18:11:24 +0900 Subject: [PATCH 36/40] =?UTF-8?q?test(App):=20=EC=9E=90=EB=8F=99=EC=B0=A8?= =?UTF-8?q?=20=EA=B2=BD=EC=A3=BC=20=EC=A0=84=EC=B2=B4=20=ED=9D=90=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20=EA=B2=80=EC=A6=9D=ED=95=98=EB=8A=94=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App ์‹คํ–‰ ํ๋ฆ„์„ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฐ ๋žœ๋ค๊ฐ’์„ ๋ชจํ‚นํ•˜์—ฌ ๊ฒ€์ฆ - ๋‹จ๋…/๊ณต๋™ ์šฐ์Šน ์‹œ๋‚˜๋ฆฌ์˜ค, ์ด๋ฆ„ ๊ณต๋ฐฑ ์ œ๊ฑฐ, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ผ€์ด์Šค ํฌํ•จ --- __tests__/ApplicationTest.js | 106 ++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 13 deletions(-) diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 0260e7e8..806466c3 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -1,5 +1,7 @@ -import App from "../src/App.js"; -import { MissionUtils } from "@woowacourse/mission-utils"; +import { MissionUtils } from '@woowacourse/mission-utils'; +import App from '../src/App.js'; +import TEST_CONFIG from '../src/constants/testConfig.js'; +import ERROR_MESSAGES from '../src/constants/errorMessages.js'; const mockQuestions = (inputs) => { MissionUtils.Console.readLineAsync = jest.fn(); @@ -19,22 +21,44 @@ const mockRandoms = (numbers) => { }; const getLogSpy = () => { - const logSpy = jest.spyOn(MissionUtils.Console, "print"); + const logSpy = jest.spyOn(MissionUtils.Console, 'print'); logSpy.mockClear(); return logSpy; }; -describe("์ž๋™์ฐจ ๊ฒฝ์ฃผ", () => { - test("๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ", async () => { +describe('์ž๋™์ฐจ ๊ฒฝ์ฃผ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ', () => { + test('์šฐ์Šน์ž๊ฐ€ ํ•œ ๋ช…์ธ ๊ฒฝ์šฐ ๋‹จ๋… ์šฐ์Šน์ž๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค', async () => { // given - const MOVING_FORWARD = 4; - const STOP = 3; - const inputs = ["pobi,woni", "1"]; - const logs = ["pobi : -", "woni : ", "์ตœ์ข… ์šฐ์Šน์ž : pobi"]; + const inputs = ['pobi,woni', '1']; + const logs = ['pobi : -', 'woni : ', '์ตœ์ข… ์šฐ์Šน์ž : pobi']; const logSpy = getLogSpy(); + mockQuestions(inputs); + mockRandoms([TEST_CONFIG.MOVING_FORWARD, TEST_CONFIG.STOP]); + + // when + const app = new App(); + await app.run(); + + // then + logs.forEach((log) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + }); + }); + test('์šฐ์Šน์ž๊ฐ€ ์—ฌ๋Ÿฌ ๋ช…์ธ ๊ฒฝ์šฐ ๋ชจ๋“  ์šฐ์Šน์ž๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,woni,jun', '2']; + const logs = ['pobi : --', 'woni : --', 'jun : -', '์ตœ์ข… ์šฐ์Šน์ž : pobi, woni']; + const logSpy = getLogSpy(); mockQuestions(inputs); - mockRandoms([MOVING_FORWARD, STOP]); + mockRandoms([ + TEST_CONFIG.MOVING_FORWARD, + TEST_CONFIG.MOVING_FORWARD, + TEST_CONFIG.MOVING_FORWARD, + TEST_CONFIG.MOVING_FORWARD, + TEST_CONFIG.MOVING_FORWARD, + TEST_CONFIG.STOP, + ]); // when const app = new App(); @@ -46,15 +70,71 @@ describe("์ž๋™์ฐจ ๊ฒฝ์ฃผ", () => { }); }); - test("์˜ˆ์™ธ ํ…Œ์ŠคํŠธ", async () => { + test('์ž๋™์ฐจ ์ด๋ฆ„ ์•ž๋’ค์˜ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•˜๊ณ  ๊ฒฝ์ฃผ๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค', async () => { // given - const inputs = ["pobi,javaji"]; + const inputs = [' pobi , woni ', '1']; + const logs = ['pobi : -', 'woni : -']; + const logSpy = getLogSpy(); mockQuestions(inputs); + mockRandoms([TEST_CONFIG.MOVING_FORWARD, TEST_CONFIG.MOVING_FORWARD]); // when const app = new App(); + await app.run(); // then - await expect(app.run()).rejects.toThrow("[ERROR]"); + logs.forEach((log) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + }); + }); + + test('์ž๋™์ฐจ ์ด๋ฆ„์ด 5์ž๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,javaji', '1']; + mockQuestions(inputs); + const app = new App(); + + // when & then + await expect(app.run()).rejects.toThrow(ERROR_MESSAGES.NAME_TOO_LONG); + }); + + test('์ค‘๋ณต๋œ ์ž๋™์ฐจ ์ด๋ฆ„์ด ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,woni,pobi', '1']; + mockQuestions(inputs); + const app = new App(); + + // when & then + await expect(app.run()).rejects.toThrow(ERROR_MESSAGES.NAME_DUPLICATED); + }); + + test('์ž๋™์ฐจ ์ด๋ฆ„์— ๋นˆ ๊ฐ’์ด ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,,woni', '1']; + mockQuestions(inputs); + const app = new App(); + + // when & then + await expect(app.run()).rejects.toThrow(ERROR_MESSAGES.NAME_LIST_FORMAT_INVALID); + }); + + test('์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž์—ด์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,woni', 'abc']; + mockQuestions(inputs); + const app = new App(); + + // when & then + await expect(app.run()).rejects.toThrow(ERROR_MESSAGES.ATTEMPT_COUNT_NON_NUMERIC); + }); + + test('์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ 0์ดํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,woni', '0']; + mockQuestions(inputs); + const app = new App(); + + // when & then + await expect(app.run()).rejects.toThrow(ERROR_MESSAGES.ATTEMPT_COUNT_NOT_POSITIVE); }); }); From ee782ff988d30a437d53d027241cc42f96eaf00b Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Fri, 24 Oct 2025 18:30:43 +0900 Subject: [PATCH 37/40] =?UTF-8?q?test(App):=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App ์‹คํ–‰ ํ๋ฆ„์˜ ์ค‘๋ณต์„ runApp์œผ๋กœ ํ†ตํ•ฉ - ๋กœ๊ทธ ๊ฒ€์ฆ ๋กœ์ง์„ expectLogsContain์œผ๋กœ ๋ถ„๋ฆฌ - ์ •์ƒ ๋™์ž‘๊ณผ ์˜ˆ์™ธ ์ผ€์ด์Šค ๊ตฌ์„ฑ์„ ๋ถ„๋ฆฌ --- __tests__/ApplicationTest.js | 180 +++++++++++++++++------------------ 1 file changed, 87 insertions(+), 93 deletions(-) diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 806466c3..b320a358 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -26,115 +26,109 @@ const getLogSpy = () => { return logSpy; }; +const runApp = async (inputs, randoms = []) => { + mockQuestions(inputs); + if (randoms.length > 0) { + mockRandoms(randoms); + } + const app = new App(); + await app.run(); +}; + +const expectLogsContain = (logSpy, logs) => { + logs.forEach((log) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + }); +}; + describe('์ž๋™์ฐจ ๊ฒฝ์ฃผ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ', () => { - test('์šฐ์Šน์ž๊ฐ€ ํ•œ ๋ช…์ธ ๊ฒฝ์šฐ ๋‹จ๋… ์šฐ์Šน์ž๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค', async () => { - // given - const inputs = ['pobi,woni', '1']; - const logs = ['pobi : -', 'woni : ', '์ตœ์ข… ์šฐ์Šน์ž : pobi']; - const logSpy = getLogSpy(); - mockQuestions(inputs); - mockRandoms([TEST_CONFIG.MOVING_FORWARD, TEST_CONFIG.STOP]); - - // when - const app = new App(); - await app.run(); - - // then - logs.forEach((log) => { - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + describe('์ •์ƒ ๋™์ž‘ ์‹œ๋‚˜๋ฆฌ์˜ค', () => { + test('์šฐ์Šน์ž๊ฐ€ ํ•œ ๋ช…์ธ ๊ฒฝ์šฐ ๋‹จ๋… ์šฐ์Šน์ž๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,woni', '1']; + const logs = ['pobi : -', 'woni : ', '์ตœ์ข… ์šฐ์Šน์ž : pobi']; + const logSpy = getLogSpy(); + + // when + await runApp(inputs, [TEST_CONFIG.MOVING_FORWARD, TEST_CONFIG.STOP]); + + // then + expectLogsContain(logSpy, logs); }); - }); - test('์šฐ์Šน์ž๊ฐ€ ์—ฌ๋Ÿฌ ๋ช…์ธ ๊ฒฝ์šฐ ๋ชจ๋“  ์šฐ์Šน์ž๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค', async () => { - // given - const inputs = ['pobi,woni,jun', '2']; - const logs = ['pobi : --', 'woni : --', 'jun : -', '์ตœ์ข… ์šฐ์Šน์ž : pobi, woni']; - const logSpy = getLogSpy(); - mockQuestions(inputs); - mockRandoms([ - TEST_CONFIG.MOVING_FORWARD, - TEST_CONFIG.MOVING_FORWARD, - TEST_CONFIG.MOVING_FORWARD, - TEST_CONFIG.MOVING_FORWARD, - TEST_CONFIG.MOVING_FORWARD, - TEST_CONFIG.STOP, - ]); - - // when - const app = new App(); - await app.run(); - - // then - logs.forEach((log) => { - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + test('์šฐ์Šน์ž๊ฐ€ ์—ฌ๋Ÿฌ ๋ช…์ธ ๊ฒฝ์šฐ ๋ชจ๋“  ์šฐ์Šน์ž๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,woni,jun', '2']; + const logs = ['pobi : --', 'woni : --', 'jun : -', '์ตœ์ข… ์šฐ์Šน์ž : pobi, woni']; + const logSpy = getLogSpy(); + + // when + await runApp(inputs, [ + TEST_CONFIG.MOVING_FORWARD, + TEST_CONFIG.MOVING_FORWARD, + TEST_CONFIG.MOVING_FORWARD, + TEST_CONFIG.MOVING_FORWARD, + TEST_CONFIG.MOVING_FORWARD, + TEST_CONFIG.STOP, + ]); + + // then + expectLogsContain(logSpy, logs); }); - }); - test('์ž๋™์ฐจ ์ด๋ฆ„ ์•ž๋’ค์˜ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•˜๊ณ  ๊ฒฝ์ฃผ๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค', async () => { - // given - const inputs = [' pobi , woni ', '1']; - const logs = ['pobi : -', 'woni : -']; - const logSpy = getLogSpy(); - mockQuestions(inputs); - mockRandoms([TEST_CONFIG.MOVING_FORWARD, TEST_CONFIG.MOVING_FORWARD]); - - // when - const app = new App(); - await app.run(); - - // then - logs.forEach((log) => { - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + test('์ž๋™์ฐจ ์ด๋ฆ„ ์•ž๋’ค์˜ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•˜๊ณ  ๊ฒฝ์ฃผ๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค', async () => { + // given + const inputs = [' pobi , woni ', '1']; + const logs = ['pobi : -', 'woni : -']; + const logSpy = getLogSpy(); + + // when + await runApp(inputs, [TEST_CONFIG.MOVING_FORWARD, TEST_CONFIG.MOVING_FORWARD]); + + // then + expectLogsContain(logSpy, logs); }); }); - test('์ž๋™์ฐจ ์ด๋ฆ„์ด 5์ž๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { - // given - const inputs = ['pobi,javaji', '1']; - mockQuestions(inputs); - const app = new App(); + describe('์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ˆ์™ธ ์ผ€์ด์Šค', () => { + test('์ž๋™์ฐจ ์ด๋ฆ„์ด 5์ž๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,javaji', '1']; - // when & then - await expect(app.run()).rejects.toThrow(ERROR_MESSAGES.NAME_TOO_LONG); - }); + // when & then + await expect(runApp(inputs)).rejects.toThrow(ERROR_MESSAGES.NAME_TOO_LONG); + }); - test('์ค‘๋ณต๋œ ์ž๋™์ฐจ ์ด๋ฆ„์ด ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { - // given - const inputs = ['pobi,woni,pobi', '1']; - mockQuestions(inputs); - const app = new App(); + test('์ค‘๋ณต๋œ ์ž๋™์ฐจ ์ด๋ฆ„์ด ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,woni,pobi', '1']; - // when & then - await expect(app.run()).rejects.toThrow(ERROR_MESSAGES.NAME_DUPLICATED); - }); + // when & then + await expect(runApp(inputs)).rejects.toThrow(ERROR_MESSAGES.NAME_DUPLICATED); + }); - test('์ž๋™์ฐจ ์ด๋ฆ„์— ๋นˆ ๊ฐ’์ด ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { - // given - const inputs = ['pobi,,woni', '1']; - mockQuestions(inputs); - const app = new App(); + test('์ž๋™์ฐจ ์ด๋ฆ„์— ๋นˆ ๊ฐ’์ด ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,,woni', '1']; - // when & then - await expect(app.run()).rejects.toThrow(ERROR_MESSAGES.NAME_LIST_FORMAT_INVALID); - }); + // when & then + await expect(runApp(inputs)).rejects.toThrow(ERROR_MESSAGES.NAME_LIST_FORMAT_INVALID); + }); - test('์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž์—ด์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { - // given - const inputs = ['pobi,woni', 'abc']; - mockQuestions(inputs); - const app = new App(); + test('์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž์—ด์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,woni', 'abc']; - // when & then - await expect(app.run()).rejects.toThrow(ERROR_MESSAGES.ATTEMPT_COUNT_NON_NUMERIC); - }); + // when & then + await expect(runApp(inputs)).rejects.toThrow(ERROR_MESSAGES.ATTEMPT_COUNT_NON_NUMERIC); + }); - test('์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ 0์ดํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { - // given - const inputs = ['pobi,woni', '0']; - mockQuestions(inputs); - const app = new App(); + test('์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ 0์ดํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค', async () => { + // given + const inputs = ['pobi,woni', '0']; - // when & then - await expect(app.run()).rejects.toThrow(ERROR_MESSAGES.ATTEMPT_COUNT_NOT_POSITIVE); + // when & then + await expect(runApp(inputs)).rejects.toThrow(ERROR_MESSAGES.ATTEMPT_COUNT_NOT_POSITIVE); + }); }); }); From efd907326fff7e2599a4981f5e7e3a1d1efd9f14 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Sun, 26 Oct 2025 16:06:05 +0900 Subject: [PATCH 38/40] =?UTF-8?q?refactor(RaceController):=20=EA=B2=BD?= =?UTF-8?q?=EC=A3=BC=20=EC=8B=A4=ED=96=89=20=ED=9D=90=EB=A6=84=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๊ฒฝ์ฃผ ๋‹จ๊ณ„ ์ถœ๋ ฅ ๋กœ์ง์„ #printRaceProgress๋กœ ๋ถ„๋ฆฌ - ์ตœ์ข… ๊ฒฐ๊ณผ ์ถœ๋ ฅ์„ #printFinalResults๋กœ ๋ถ„๋ฆฌ - #runRace๋Š” ์‹คํ–‰ ํ๋ฆ„์„ ๊ด€๋ฆฌํ•˜๋Š” ์—ญํ• ๋งŒ ์ˆ˜ํ–‰ํ•˜๋„๋ก ๋‹จ์ˆœํ™” --- src/controllers/RaceController.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/controllers/RaceController.js b/src/controllers/RaceController.js index 8ecedc3c..d7a4e9d6 100644 --- a/src/controllers/RaceController.js +++ b/src/controllers/RaceController.js @@ -29,15 +29,23 @@ class RaceController { return this.#inputView.readString(PROMPT_MESSAGES.ATTEMPT_COUNT_INPUT); } - #runRace(carNames, attemptCount) { - const simulator = new RaceSimulator(carNames, attemptCount); + #printRaceProgress(simulator) { this.#outputView.printResultHeader(); while (simulator.hasRemainingAttempts()) { const positions = simulator.executeStep(); this.#outputView.printStepResult(positions); } + } + + #printFinalResults(simulator) { this.#outputView.printWinners(simulator.getRaceWinners()); } + + #runRace(carNames, attemptCount) { + const simulator = new RaceSimulator(carNames, attemptCount); + this.#printRaceProgress(simulator); + this.#printFinalResults(simulator); + } } export default RaceController; From 91fb602f3e236f5d7f62a0561fc8fd4bb58c6928 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Sun, 26 Oct 2025 16:07:17 +0900 Subject: [PATCH 39/40] =?UTF-8?q?test(Car):=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?describe=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๋ช…์„ getDistance์—์„œ getPosition์œผ๋กœ ์ˆ˜์ • --- src/domains/Car.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domains/Car.test.js b/src/domains/Car.test.js index e862a28f..99aaec46 100644 --- a/src/domains/Car.test.js +++ b/src/domains/Car.test.js @@ -42,7 +42,7 @@ describe('Car', () => { }); }); - describe('getDistance ๋ฉ”์„œ๋“œ', () => { + describe('getPosition ๋ฉ”์„œ๋“œ', () => { test('์ž๋™์ฐจ์˜ ํ˜„์žฌ ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { // Arrange const car = new Car('pobi'); From 9470f372ac7f821c27542b5b6b7bb1d4570ac259 Mon Sep 17 00:00:00 2001 From: itwillbeoptimal Date: Sun, 26 Oct 2025 16:10:47 +0900 Subject: [PATCH 40/40] =?UTF-8?q?refactor(constants):=20=EA=B2=BD=EC=A3=BC?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=83=81=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RACE_CONFIG ์ƒ์ˆ˜ ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฒฝ์ฃผ ๊ทœ์น™ ๊ด€๋ จ ์ƒ์ˆ˜๋ฅผ ์ค‘์•™ ๊ด€๋ฆฌ - raceUtils, validators์—์„œ ์ƒ์ˆ˜ ์ •์˜ ์ œ๊ฑฐ ๋ฐ RACE_CONFIG๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ์ˆ˜์ • --- src/constants/raceConfig.js | 8 ++++++++ src/models/utils/raceUtils.js | 10 +++++----- src/utils/validators.js | 5 ++--- 3 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 src/constants/raceConfig.js diff --git a/src/constants/raceConfig.js b/src/constants/raceConfig.js new file mode 100644 index 00000000..08de576b --- /dev/null +++ b/src/constants/raceConfig.js @@ -0,0 +1,8 @@ +const RACE_CONFIG = Object.freeze({ + MOVE_THRESHOLD: 4, + MIN_RANDOM_VALUE: 0, + MAX_RANDOM_VALUE: 9, + MAX_CAR_NAME_LENGTH: 5, +}); + +export default RACE_CONFIG; diff --git a/src/models/utils/raceUtils.js b/src/models/utils/raceUtils.js index d51be9fc..daba6766 100644 --- a/src/models/utils/raceUtils.js +++ b/src/models/utils/raceUtils.js @@ -1,6 +1,7 @@ import { Random } from '@woowacourse/mission-utils'; import Car from '../../domains/Car.js'; import { validateNameDuplication } from '../../utils/validators.js'; +import RACE_CONFIG from '../../constants/raceConfig.js'; export const createCars = (carNames) => { const carNameList = carNames.split(',').map((name) => name.trim()); @@ -9,12 +10,11 @@ export const createCars = (carNames) => { }; export const runOneStep = (cars) => { - const MOVE_THRESHOLD = 4; - const MIN_RANDOM_VALUE = 0; - const MAX_RANDOM_VALUE = 9; - cars.forEach((car) => { - if (Random.pickNumberInRange(MIN_RANDOM_VALUE, MAX_RANDOM_VALUE) >= MOVE_THRESHOLD) { + if ( + Random.pickNumberInRange(RACE_CONFIG.MIN_RANDOM_VALUE, RACE_CONFIG.MAX_RANDOM_VALUE) >= + RACE_CONFIG.MOVE_THRESHOLD + ) { car.move(); } }); diff --git a/src/utils/validators.js b/src/utils/validators.js index 53b90488..e9a8952d 100644 --- a/src/utils/validators.js +++ b/src/utils/validators.js @@ -1,9 +1,8 @@ import ERROR_MESSAGES from '../constants/errorMessages.js'; +import RACE_CONFIG from '../constants/raceConfig.js'; export function validateNameLength(carName) { - const MAX_CAR_NAME_LENGTH = 5; - - if (carName.length > MAX_CAR_NAME_LENGTH) { + if (carName.length > RACE_CONFIG.MAX_CAR_NAME_LENGTH) { throw new Error(ERROR_MESSAGES.NAME_TOO_LONG); } if (carName.length === 0) {