From e784a55f86f6e36fbd9b3fd26b0837f39e5ab698 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Tue, 25 Jun 2024 14:25:18 +0900 Subject: [PATCH 01/34] =?UTF-8?q?refactoring:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=82=AC=EC=9D=B4=20=EC=A4=84?= =?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 --- src/__tests__/car.test.js | 118 ++++++++++++++-------------- src/__tests__/main.test.js | 19 ++--- src/__tests__/racing.test.js | 52 +++++++------ src/__tests__/validation.test.js | 128 ++++++++++++++++--------------- 4 files changed, 168 insertions(+), 149 deletions(-) diff --git a/src/__tests__/car.test.js b/src/__tests__/car.test.js index 3f2ef1e..b4489b6 100644 --- a/src/__tests__/car.test.js +++ b/src/__tests__/car.test.js @@ -9,63 +9,69 @@ describe('car 클래스에 대한 단위 테스트.', () => { const nextName = car.setName('Dva'); expect(nextName).toBe('Dva'); - }), - test.each([ - { - name: ['hello'], - }, - { - name: '', - }, - { - name: { name: 'hello' }, - }, - { name: undefined }, - ])( - 'setName 에 문자열이 아닌 값($name)을 할당하면 에러를 반환한다.', - ({ name }) => { - expect(() => { - car.setName(name); - }).toThrow(); - } - ), - test('getName 은 현재 car 인스턴스의 이름을 반환한다.', () => { - const currName = car.getName(); + }); - expect(currName).toBe('Dva'); - }), - test('setPosition 에 올바른 숫자를 할당하면 해당 숫자를 반환한다.', () => { - const nextPosition = car.setPosition(position); + test.each([ + { + name: ['hello'], + }, + { + name: '', + }, + { + name: { name: 'hello' }, + }, + { name: undefined }, + ])( + 'setName 에 문자열이 아닌 값($name)을 할당하면 에러를 반환한다.', + ({ name }) => { + expect(() => { + car.setName(name); + }).toThrow(); + } + ); - expect(nextPosition).toBe(position); - }), - test.each([ - { position: '1' }, - { position: [] }, - { position: {} }, - { position: undefined }, - ])( - 'setPosition 에 숫자가 아닌 값($position)을 할당하면 에러를 반환한다.', - ({ position }) => { - expect(() => { - car.setPosition(position); - }).toThrow(); - } - ), - test('getPosition 은 현재 포지션의 값을 반환해야 한다.', () => { - expect(car.getPosition()).toBe(position); - }), - test.each([ - { distance: 1 }, - { distance: 2 }, - { distance: 3 }, - { distance: 4 }, - ])( - 'move 는 현재 포지션에 ($distance) 값을 추가한 만큼 position 을 증가시켜야 한다.', - ({ distance }) => { - const prevPosition = car.getPosition(); + test('getName 은 현재 car 인스턴스의 이름을 반환한다.', () => { + const currName = car.getName(); - expect(car.move(distance)).toBe(prevPosition + distance); - } - ); + expect(currName).toBe('Dva'); + }); + + test('setPosition 에 올바른 숫자를 할당하면 해당 숫자를 반환한다.', () => { + const nextPosition = car.setPosition(position); + + expect(nextPosition).toBe(position); + }); + + test.each([ + { position: '1' }, + { position: [] }, + { position: {} }, + { position: undefined }, + ])( + 'setPosition 에 숫자가 아닌 값($position)을 할당하면 에러를 반환한다.', + ({ position }) => { + expect(() => { + car.setPosition(position); + }).toThrow(); + } + ); + + test('getPosition 은 현재 포지션의 값을 반환해야 한다.', () => { + expect(car.getPosition()).toBe(position); + }); + + test.each([ + { distance: 1 }, + { distance: 2 }, + { distance: 3 }, + { distance: 4 }, + ])( + 'move 는 현재 포지션에 ($distance) 값을 추가한 만큼 position 을 증가시켜야 한다.', + ({ distance }) => { + const prevPosition = car.getPosition(); + + expect(car.move(distance)).toBe(prevPosition + distance); + } + ); }); diff --git a/src/__tests__/main.test.js b/src/__tests__/main.test.js index cf18ebf..f8ef0f5 100644 --- a/src/__tests__/main.test.js +++ b/src/__tests__/main.test.js @@ -14,14 +14,15 @@ describe('Main 클래스에 대한 단위 테스트 진행', () => { new Main(rounds); }).toThrow(); } - ), - test('setRounds 에 올바른 값을 넣으면 해당 값을 다시 반환한다.', () => { - const rounds = [3, 4, 5]; - const main = new Main([5]); - const nextRounds = main.setRounds(rounds); + ); - expect(nextRounds.every((value, index) => value === rounds[index])).toBe( - true - ); - }); + test('setRounds 에 올바른 값을 넣으면 해당 값을 다시 반환한다.', () => { + const rounds = [3, 4, 5]; + const main = new Main([5]); + const nextRounds = main.setRounds(rounds); + + expect(nextRounds.every((value, index) => value === rounds[index])).toBe( + true + ); + }); }); diff --git a/src/__tests__/racing.test.js b/src/__tests__/racing.test.js index 00f6fc0..8933f7d 100644 --- a/src/__tests__/racing.test.js +++ b/src/__tests__/racing.test.js @@ -8,28 +8,32 @@ describe('Racing 클래스에 대한 단위 테스트', () => { test('setRound 에 올바른 값을 할당하면 round 가 해당 값으로 바뀐다.', () => { expect(racing.getRound()).toBe(5); - }), - test('setCars 에 올바른 값을 할당하면 Car 인스턴스가 담긴다.', () => { - const isCarArray = racing.getCars().every((car) => car instanceof Car); - - expect(isCarArray).toBe(true); - }), - test('startRacing 메서드는 자동차 이름을 포함한 문자열 배열을 반환한다.', () => { - const winners = racing.startRacing(); - const isRightWinner = winners.every((winner) => carSet.has(winner)); - - expect(isRightWinner).toBe(true); - }), - test('dice 메서드는 0~9 사이의 값을 반환한다.', () => { - const diceResult = [...new Array(10)].fill(Racing.dice()); - const isCollectNum = diceResult.every((dice) => dice <= 9 && dice >= 0); - - expect(isCollectNum).toBe(true); - }), - test('getWinners 는 자동차 이름을 포함한 문자열 배열을 반환한다.', () => { - const winners = racing.getWinners(); - const isRightWinner = winners.every((winner) => carSet.has(winner)); - - expect(isRightWinner).toBe(true); - }); + }); + + test('setCars 에 올바른 값을 할당하면 Car 인스턴스가 담긴다.', () => { + const isCarArray = racing.getCars().every((car) => car instanceof Car); + + expect(isCarArray).toBe(true); + }); + + test('startRacing 메서드는 자동차 이름을 포함한 문자열 배열을 반환한다.', () => { + const winners = racing.startRacing(); + const isRightWinner = winners.every((winner) => carSet.has(winner)); + + expect(isRightWinner).toBe(true); + }); + + test('dice 메서드는 0~9 사이의 값을 반환한다.', () => { + const diceResult = [...new Array(10)].fill(Racing.dice()); + const isCollectNum = diceResult.every((dice) => dice <= 9 && dice >= 0); + + expect(isCollectNum).toBe(true); + }); + + test('getWinners 는 자동차 이름을 포함한 문자열 배열을 반환한다.', () => { + const winners = racing.getWinners(); + const isRightWinner = winners.every((winner) => carSet.has(winner)); + + expect(isRightWinner).toBe(true); + }); }); diff --git a/src/__tests__/validation.test.js b/src/__tests__/validation.test.js index 47e086c..9fabc51 100644 --- a/src/__tests__/validation.test.js +++ b/src/__tests__/validation.test.js @@ -20,17 +20,19 @@ describe('라운드 유효성 검사', () => { expect(() => { validation.isValidateRound('1'); }).toThrow(); - }), - test('라운드가 1 미만이면 에러가 발생한다.', () => { - expect(() => { - validation.isValidateRound(0); - }).toThrow(); - }), - test('라운드가 50을 초과하면 에러가 발생한다.', () => { - expect(() => { - validation.isValidateRound(51); - }).toThrow(); - }); + }); + + test('라운드가 1 미만이면 에러가 발생한다.', () => { + expect(() => { + validation.isValidateRound(0); + }).toThrow(); + }); + + test('라운드가 50을 초과하면 에러가 발생한다.', () => { + expect(() => { + validation.isValidateRound(51); + }).toThrow(); + }); test.each([ { round: 1, @@ -49,48 +51,53 @@ describe('자동차 배열 유효성 검사', () => { expect(() => { validation.isValidateCars(['1', ' 1 ']); }).toThrow(); - }), - test('자동차 이름이 2개 미만이면 에러가 발생한다.', () => { - expect(() => { - validation.isValidateCars(['suyeon']); - }).toThrow(); - }), - test('자동차 이름이 10개 초과면 에러가 발생한다.', () => { - expect(() => { - validation.isValidateCars([ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - ]); - }).toThrow(); - }), - test('자동차 이름($)이 중복되지 않고, 2개 이상 10개여야 한다.', () => { - expect(validation.isValidateCars(['suyeon', 'hodu'])).toBe(true); - }); + }); + + test('자동차 이름이 2개 미만이면 에러가 발생한다.', () => { + expect(() => { + validation.isValidateCars(['suyeon']); + }).toThrow(); + }); + + test('자동차 이름이 10개 초과면 에러가 발생한다.', () => { + expect(() => { + validation.isValidateCars([ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + ]); + }).toThrow(); + }); + + test('자동차 이름($)이 중복되지 않고, 2개 이상 10개여야 한다.', () => { + expect(validation.isValidateCars(['suyeon', 'hodu'])).toBe(true); + }); }); describe('자동차 이름 유효성 검사', () => { test('자동차 이름은 앞 뒤 공백을 제외하고 한 글자 이상, 열글자 이하여야 한다.', () => { expect(validation.isValidateCarName(' 123 ')).toBe(true); - }), - test('자동차 이름이 앞 뒤 공백을 제외하고 한 글자 미만이면 에러를 발생한다.', () => { - expect(() => { - validation.isValidateCarName(' ').toThrow(); - }); - }), - test('자동차 이름이 앞 뒤 공백을 제외하고 10글자를 초과하면 에러를 발생한다.', () => { - expect(() => { - validation.isValidateCarName('10글자를초과하는완전긴자동차이름!!'); - }).toThrow(); + }); + + test('자동차 이름이 앞 뒤 공백을 제외하고 한 글자 미만이면 에러를 발생한다.', () => { + expect(() => { + validation.isValidateCarName(' ').toThrow(); }); + }); + + test('자동차 이름이 앞 뒤 공백을 제외하고 10글자를 초과하면 에러를 발생한다.', () => { + expect(() => { + validation.isValidateCarName('10글자를초과하는완전긴자동차이름!!'); + }).toThrow(); + }); }); describe('자동차 포지션 유효성 검사', () => { @@ -98,17 +105,18 @@ describe('자동차 포지션 유효성 검사', () => { expect(() => { validation.isValidatePosition(-1); }).toThrow(); - }), - test.each([ - { - position: 'position', - }, - { position: {} }, - { position: [] }, - { position: null }, - ])('포지션이($position) 숫자가 아니면 에러를 발생한다.', ({ position }) => { - expect(() => { - validation.isValidatePosition(position); - }).toThrow(); - }); + }); + + test.each([ + { + position: 'position', + }, + { position: {} }, + { position: [] }, + { position: null }, + ])('포지션이($position) 숫자가 아니면 에러를 발생한다.', ({ position }) => { + expect(() => { + validation.isValidatePosition(position); + }).toThrow(); + }); }); From 0da0544c37625dcb36a90df7c9516e1e5d4012a3 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Tue, 25 Jun 2024 14:34:46 +0900 Subject: [PATCH 02/34] =?UTF-8?q?refactoring:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EB=AA=85=EC=8B=9C?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=9E=91=EC=84=B1=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/car.test.js | 4 ++-- src/__tests__/main.test.js | 4 ++-- src/__tests__/racing.test.js | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/__tests__/car.test.js b/src/__tests__/car.test.js index b4489b6..844ecc2 100644 --- a/src/__tests__/car.test.js +++ b/src/__tests__/car.test.js @@ -5,7 +5,7 @@ describe('car 클래스에 대한 단위 테스트.', () => { const car = new Car('meca'); const position = 4; - test('setName 에 올바른 문자열을 할당하면 해당 문자열을 반환한다.', () => { + test('setName 에 1~10자 사이의 문자열을 할당하면 해당 문자열을 반환한다.', () => { const nextName = car.setName('Dva'); expect(nextName).toBe('Dva'); @@ -37,7 +37,7 @@ describe('car 클래스에 대한 단위 테스트.', () => { expect(currName).toBe('Dva'); }); - test('setPosition 에 올바른 숫자를 할당하면 해당 숫자를 반환한다.', () => { + test('setPosition 에 0 이상의 숫자를 할당하면 해당 숫자를 반환한다.', () => { const nextPosition = car.setPosition(position); expect(nextPosition).toBe(position); diff --git a/src/__tests__/main.test.js b/src/__tests__/main.test.js index f8ef0f5..384a3b5 100644 --- a/src/__tests__/main.test.js +++ b/src/__tests__/main.test.js @@ -8,7 +8,7 @@ describe('Main 클래스에 대한 단위 테스트 진행', () => { { rounds: null }, { rounds: ['12', '23'] }, ])( - 'Main 클래스 초기화 시 전달하는 ($rounds) 는 문자열 배열이 아니면 에러를 반환한다.', + 'Main 클래스 초기화 시 전달하는 ($rounds) 는 숫자 배열이 아니면 에러를 반환한다.', ({ rounds }) => { expect(() => { new Main(rounds); @@ -16,7 +16,7 @@ describe('Main 클래스에 대한 단위 테스트 진행', () => { } ); - test('setRounds 에 올바른 값을 넣으면 해당 값을 다시 반환한다.', () => { + test('setRounds 에 1 이상 50 이하의 숫자로 이뤄진 배열을 넣으면 해당 값을 다시 반환한다.', () => { const rounds = [3, 4, 5]; const main = new Main([5]); const nextRounds = main.setRounds(rounds); diff --git a/src/__tests__/racing.test.js b/src/__tests__/racing.test.js index 8933f7d..58100a5 100644 --- a/src/__tests__/racing.test.js +++ b/src/__tests__/racing.test.js @@ -6,11 +6,11 @@ describe('Racing 클래스에 대한 단위 테스트', () => { const carSet = new Set(['jjanggu', 'hodu']); const racing = new Racing(5, ['jjanggu', 'hodu']); - test('setRound 에 올바른 값을 할당하면 round 가 해당 값으로 바뀐다.', () => { + test('setRound 에 1 ~ 50 사이의 숫자를 할당하면 round 가 해당 값으로 바뀐다.', () => { expect(racing.getRound()).toBe(5); }); - test('setCars 에 올바른 값을 할당하면 Car 인스턴스가 담긴다.', () => { + test('setCars 에 1 ~ 10 자로 이뤄진 문자열 배열을 할당하면 Car 인스턴스가 담긴다.', () => { const isCarArray = racing.getCars().every((car) => car instanceof Car); expect(isCarArray).toBe(true); @@ -23,7 +23,7 @@ describe('Racing 클래스에 대한 단위 테스트', () => { expect(isRightWinner).toBe(true); }); - test('dice 메서드는 0~9 사이의 값을 반환한다.', () => { + test('dice 메서드는 0 ~ 9 사이의 값을 반환한다.', () => { const diceResult = [...new Array(10)].fill(Racing.dice()); const isCollectNum = diceResult.every((dice) => dice <= 9 && dice >= 0); From 4231bd6158f7bc2fc23c8cb1c4ab714fdfb601c2 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Tue, 25 Jun 2024 14:34:54 +0900 Subject: [PATCH 03/34] =?UTF-8?q?refactoring:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/validation.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/utils/validation.js b/src/utils/validation.js index 677f780..134184c 100644 --- a/src/utils/validation.js +++ b/src/utils/validation.js @@ -63,11 +63,6 @@ const validation = { } name = name.trim(); - if (name.length === 0) { - throw new Error( - '자동차의 이름은 공백을 제외한 한 글자 이상이어야 합니다.' - ); - } if (name.length < 1 || name.length > 10) { throw new Error('자동차 이름은 한 글자 이상 열글자 이하여야 합니다.'); } From d227960a00a8a0e1efd0ac58c7b7eee4edee9d9e Mon Sep 17 00:00:00 2001 From: Suyeon Date: Tue, 25 Jun 2024 14:36:39 +0900 Subject: [PATCH 04/34] =?UTF-8?q?refactoring:=20=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EB=B2=94=EC=9C=84=20=EC=A7=81=EA=B4=80=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9E=91=EC=84=B1=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/racing.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/racing.test.js b/src/__tests__/racing.test.js index 58100a5..d634201 100644 --- a/src/__tests__/racing.test.js +++ b/src/__tests__/racing.test.js @@ -25,7 +25,7 @@ describe('Racing 클래스에 대한 단위 테스트', () => { test('dice 메서드는 0 ~ 9 사이의 값을 반환한다.', () => { const diceResult = [...new Array(10)].fill(Racing.dice()); - const isCollectNum = diceResult.every((dice) => dice <= 9 && dice >= 0); + const isCollectNum = diceResult.every((dice) => 0 <= dice && dice <= 9); expect(isCollectNum).toBe(true); }); From b7a06deab08d6a5a3713aac417cec52832c4617f Mon Sep 17 00:00:00 2001 From: Suyeon Date: Tue, 25 Jun 2024 14:43:29 +0900 Subject: [PATCH 05/34] =?UTF-8?q?refactoring:=20dice=20=EB=A5=BC=20util=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=A1=9C=20=EB=B6=84=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Racing.js | 7 ++----- src/utils/gameSupport.js | 3 +++ 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 src/utils/gameSupport.js diff --git a/src/model/Racing.js b/src/model/Racing.js index 7b81e85..6075634 100644 --- a/src/model/Racing.js +++ b/src/model/Racing.js @@ -1,4 +1,5 @@ import { GAME } from '../constants/index.js'; +import { dice } from '../utils/gameSupport.js'; import validation from '../utils/validation.js'; import Car from './Car.js'; @@ -53,14 +54,10 @@ class Racing { proceedRound() { this.#cars.forEach((car) => { - Racing.dice() >= 4 ? car.move(1) : car.move(0); + dice(9, 0) >= 4 ? car.move(1) : car.move(0); }); } - static dice() { - return Math.floor(Math.random() * 10); - } - getWinners() { const maxPosition = Math.max(...this.#cars.map((car) => car.getPosition())); diff --git a/src/utils/gameSupport.js b/src/utils/gameSupport.js new file mode 100644 index 0000000..1be1f7d --- /dev/null +++ b/src/utils/gameSupport.js @@ -0,0 +1,3 @@ +export function dice(min, max) { + return Math.floor(Math.random() * (max + 1) + min); +} From 4d6d2005cb031bfbab877561fb64b00e22d1d09c Mon Sep 17 00:00:00 2001 From: Suyeon Date: Thu, 27 Jun 2024 15:00:12 +0900 Subject: [PATCH 06/34] =?UTF-8?q?refactoring:=20=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/gameSupport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/gameSupport.js b/src/utils/gameSupport.js index 1be1f7d..7fc3752 100644 --- a/src/utils/gameSupport.js +++ b/src/utils/gameSupport.js @@ -1,3 +1,3 @@ export function dice(min, max) { - return Math.floor(Math.random() * (max + 1) + min); + return Math.floor(Math.random() * (max - min) + min); } From fec7e436b06d74c065a9b6fcf3fbec48843b1964 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Thu, 27 Jun 2024 15:01:50 +0900 Subject: [PATCH 07/34] =?UTF-8?q?refactoring:=20View=20=EB=A5=BC=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EC=9D=98=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=A7=8C=EB=93=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/CarView.js | 21 +++++++++++++++++++++ src/view/InputView.js | 11 ----------- src/view/OutputView.js | 7 ------- 3 files changed, 21 insertions(+), 18 deletions(-) create mode 100644 src/view/CarView.js delete mode 100644 src/view/InputView.js delete mode 100644 src/view/OutputView.js diff --git a/src/view/CarView.js b/src/view/CarView.js new file mode 100644 index 0000000..de4a96e --- /dev/null +++ b/src/view/CarView.js @@ -0,0 +1,21 @@ +import readLineAsync from '../utils/readLine.js'; + +export default class CarView { + constructor() { + this.rl = readLineAsync; + } + + async inputCarName() { + return await this.rl( + '🏎️ 자동차 이름을 쉼표로 구분지어 입력해주세요! 🏎️ > ' + ); + } + + printMovedCar(carName) { + console.log(`${carName} 이 전진합니다!\n`); + } + + printWinners(winners) { + console.log(`✨우승자 ✨ > ${winners}`); + } +} diff --git a/src/view/InputView.js b/src/view/InputView.js deleted file mode 100644 index d97b140..0000000 --- a/src/view/InputView.js +++ /dev/null @@ -1,11 +0,0 @@ -import readLineAsync from '../utils/readLine.js'; - -const InputView = { - getCarName: async () => { - return await readLineAsync( - '🏎️ 자동차 이름을 쉼표로 구분지어 입력해주세요! 🏎️ > ' - ); - }, -}; - -export default InputView; diff --git a/src/view/OutputView.js b/src/view/OutputView.js deleted file mode 100644 index 7bfff25..0000000 --- a/src/view/OutputView.js +++ /dev/null @@ -1,7 +0,0 @@ -const OutputView = { - printWinners: (winners) => { - console.log(`✨우승자 ✨ > ${winners}`); - }, -}; - -export default OutputView; From 550b26293254f826e9a9d03faad9db452f03e80c Mon Sep 17 00:00:00 2001 From: Suyeon Date: Thu, 27 Jun 2024 15:07:28 +0900 Subject: [PATCH 08/34] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/car.test.js | 77 ------------------- src/__tests__/main.test.js | 28 ------- src/__tests__/racing.test.js | 39 ---------- src/__tests__/validation.test.js | 122 ------------------------------- src/model/Racing.js | 70 ------------------ src/utils/validation.js | 74 ------------------- 6 files changed, 410 deletions(-) delete mode 100644 src/__tests__/car.test.js delete mode 100644 src/__tests__/main.test.js delete mode 100644 src/__tests__/racing.test.js delete mode 100644 src/__tests__/validation.test.js delete mode 100644 src/model/Racing.js delete mode 100644 src/utils/validation.js diff --git a/src/__tests__/car.test.js b/src/__tests__/car.test.js deleted file mode 100644 index 844ecc2..0000000 --- a/src/__tests__/car.test.js +++ /dev/null @@ -1,77 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import Car from '../model/Car.js'; - -describe('car 클래스에 대한 단위 테스트.', () => { - const car = new Car('meca'); - const position = 4; - - test('setName 에 1~10자 사이의 문자열을 할당하면 해당 문자열을 반환한다.', () => { - const nextName = car.setName('Dva'); - - expect(nextName).toBe('Dva'); - }); - - test.each([ - { - name: ['hello'], - }, - { - name: '', - }, - { - name: { name: 'hello' }, - }, - { name: undefined }, - ])( - 'setName 에 문자열이 아닌 값($name)을 할당하면 에러를 반환한다.', - ({ name }) => { - expect(() => { - car.setName(name); - }).toThrow(); - } - ); - - test('getName 은 현재 car 인스턴스의 이름을 반환한다.', () => { - const currName = car.getName(); - - expect(currName).toBe('Dva'); - }); - - test('setPosition 에 0 이상의 숫자를 할당하면 해당 숫자를 반환한다.', () => { - const nextPosition = car.setPosition(position); - - expect(nextPosition).toBe(position); - }); - - test.each([ - { position: '1' }, - { position: [] }, - { position: {} }, - { position: undefined }, - ])( - 'setPosition 에 숫자가 아닌 값($position)을 할당하면 에러를 반환한다.', - ({ position }) => { - expect(() => { - car.setPosition(position); - }).toThrow(); - } - ); - - test('getPosition 은 현재 포지션의 값을 반환해야 한다.', () => { - expect(car.getPosition()).toBe(position); - }); - - test.each([ - { distance: 1 }, - { distance: 2 }, - { distance: 3 }, - { distance: 4 }, - ])( - 'move 는 현재 포지션에 ($distance) 값을 추가한 만큼 position 을 증가시켜야 한다.', - ({ distance }) => { - const prevPosition = car.getPosition(); - - expect(car.move(distance)).toBe(prevPosition + distance); - } - ); -}); diff --git a/src/__tests__/main.test.js b/src/__tests__/main.test.js deleted file mode 100644 index 384a3b5..0000000 --- a/src/__tests__/main.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import Main from '../main.js'; - -describe('Main 클래스에 대한 단위 테스트 진행', () => { - test.each([ - { rounds: '' }, - { rounds: function () {} }, - { rounds: null }, - { rounds: ['12', '23'] }, - ])( - 'Main 클래스 초기화 시 전달하는 ($rounds) 는 숫자 배열이 아니면 에러를 반환한다.', - ({ rounds }) => { - expect(() => { - new Main(rounds); - }).toThrow(); - } - ); - - test('setRounds 에 1 이상 50 이하의 숫자로 이뤄진 배열을 넣으면 해당 값을 다시 반환한다.', () => { - const rounds = [3, 4, 5]; - const main = new Main([5]); - const nextRounds = main.setRounds(rounds); - - expect(nextRounds.every((value, index) => value === rounds[index])).toBe( - true - ); - }); -}); diff --git a/src/__tests__/racing.test.js b/src/__tests__/racing.test.js deleted file mode 100644 index d634201..0000000 --- a/src/__tests__/racing.test.js +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import Racing from '../model/Racing.js'; -import Car from '../model/Car'; - -describe('Racing 클래스에 대한 단위 테스트', () => { - const carSet = new Set(['jjanggu', 'hodu']); - const racing = new Racing(5, ['jjanggu', 'hodu']); - - test('setRound 에 1 ~ 50 사이의 숫자를 할당하면 round 가 해당 값으로 바뀐다.', () => { - expect(racing.getRound()).toBe(5); - }); - - test('setCars 에 1 ~ 10 자로 이뤄진 문자열 배열을 할당하면 Car 인스턴스가 담긴다.', () => { - const isCarArray = racing.getCars().every((car) => car instanceof Car); - - expect(isCarArray).toBe(true); - }); - - test('startRacing 메서드는 자동차 이름을 포함한 문자열 배열을 반환한다.', () => { - const winners = racing.startRacing(); - const isRightWinner = winners.every((winner) => carSet.has(winner)); - - expect(isRightWinner).toBe(true); - }); - - test('dice 메서드는 0 ~ 9 사이의 값을 반환한다.', () => { - const diceResult = [...new Array(10)].fill(Racing.dice()); - const isCollectNum = diceResult.every((dice) => 0 <= dice && dice <= 9); - - expect(isCollectNum).toBe(true); - }); - - test('getWinners 는 자동차 이름을 포함한 문자열 배열을 반환한다.', () => { - const winners = racing.getWinners(); - const isRightWinner = winners.every((winner) => carSet.has(winner)); - - expect(isRightWinner).toBe(true); - }); -}); diff --git a/src/__tests__/validation.test.js b/src/__tests__/validation.test.js deleted file mode 100644 index 9fabc51..0000000 --- a/src/__tests__/validation.test.js +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import validation from '../utils/validation'; - -describe('라운드 배열 유효성 검사', () => { - test.each([ - { - rounds: 1, - }, - { rounds: function () {} }, - { rounds: null }, - ])('라운드 배열($rounds)이 배열이 아니면 오류가 발생한다.', ({ rounds }) => { - expect(() => { - validation.isValidateRounds(rounds); - }); - }); -}); - -describe('라운드 유효성 검사', () => { - test('라운드가 숫자 형태가 아니면 오류가 발생한다.', () => { - expect(() => { - validation.isValidateRound('1'); - }).toThrow(); - }); - - test('라운드가 1 미만이면 에러가 발생한다.', () => { - expect(() => { - validation.isValidateRound(0); - }).toThrow(); - }); - - test('라운드가 50을 초과하면 에러가 발생한다.', () => { - expect(() => { - validation.isValidateRound(51); - }).toThrow(); - }); - test.each([ - { - round: 1, - }, - { round: 50 }, - ])( - '라운드($round)가 1 이상 50 이하의 숫자면 에러를 발생하지 않는다.', - ({ round }) => { - expect(validation.isValidateRound(round)).toBe(true); - } - ); -}); - -describe('자동차 배열 유효성 검사', () => { - test('중복되는 이름이 있으면 에러가 발생한다.', () => { - expect(() => { - validation.isValidateCars(['1', ' 1 ']); - }).toThrow(); - }); - - test('자동차 이름이 2개 미만이면 에러가 발생한다.', () => { - expect(() => { - validation.isValidateCars(['suyeon']); - }).toThrow(); - }); - - test('자동차 이름이 10개 초과면 에러가 발생한다.', () => { - expect(() => { - validation.isValidateCars([ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - ]); - }).toThrow(); - }); - - test('자동차 이름($)이 중복되지 않고, 2개 이상 10개여야 한다.', () => { - expect(validation.isValidateCars(['suyeon', 'hodu'])).toBe(true); - }); -}); - -describe('자동차 이름 유효성 검사', () => { - test('자동차 이름은 앞 뒤 공백을 제외하고 한 글자 이상, 열글자 이하여야 한다.', () => { - expect(validation.isValidateCarName(' 123 ')).toBe(true); - }); - - test('자동차 이름이 앞 뒤 공백을 제외하고 한 글자 미만이면 에러를 발생한다.', () => { - expect(() => { - validation.isValidateCarName(' ').toThrow(); - }); - }); - - test('자동차 이름이 앞 뒤 공백을 제외하고 10글자를 초과하면 에러를 발생한다.', () => { - expect(() => { - validation.isValidateCarName('10글자를초과하는완전긴자동차이름!!'); - }).toThrow(); - }); -}); - -describe('자동차 포지션 유효성 검사', () => { - test('포지션이 0 미만인 경우 에러를 발생한다', () => { - expect(() => { - validation.isValidatePosition(-1); - }).toThrow(); - }); - - test.each([ - { - position: 'position', - }, - { position: {} }, - { position: [] }, - { position: null }, - ])('포지션이($position) 숫자가 아니면 에러를 발생한다.', ({ position }) => { - expect(() => { - validation.isValidatePosition(position); - }).toThrow(); - }); -}); diff --git a/src/model/Racing.js b/src/model/Racing.js deleted file mode 100644 index 6075634..0000000 --- a/src/model/Racing.js +++ /dev/null @@ -1,70 +0,0 @@ -import { GAME } from '../constants/index.js'; -import { dice } from '../utils/gameSupport.js'; -import validation from '../utils/validation.js'; -import Car from './Car.js'; - -class Racing { - #round; - #cars; - - constructor(round = GAME.ROUND, cars = []) { - this.#round = this.setRound(round); - this.#cars = this.setCars(cars); - } - - getRound() { - return this.#round; - } - - getCars() { - return this.#cars; - } - - setRound(round) { - if (!validation.isValidateRound(round)) { - return 0; - } - - this.round = round; - return this.round; - } - - setCars(cars) { - cars = typeof cars === 'string' ? cars.split(',') : cars; - - if (!validation.isValidateCars(cars)) { - return []; - } - - this.#cars = cars.map((car) => new Car(car)); - return this.#cars; - } - - startRacing() { - if (!validation.isValidateRacing(this.#cars, this.#round)) { - return []; - } - - for (let currRound = 0; currRound < this.#round; currRound++) { - this.proceedRound(); - } - - return this.getWinners(); - } - - proceedRound() { - this.#cars.forEach((car) => { - dice(9, 0) >= 4 ? car.move(1) : car.move(0); - }); - } - - getWinners() { - const maxPosition = Math.max(...this.#cars.map((car) => car.getPosition())); - - return this.#cars - .filter((car) => car.getPosition() === maxPosition) - .map((car) => car.getName()); - } -} - -export default Racing; diff --git a/src/utils/validation.js b/src/utils/validation.js deleted file mode 100644 index 134184c..0000000 --- a/src/utils/validation.js +++ /dev/null @@ -1,74 +0,0 @@ -import Car from '../model/Car.js'; - -const validation = { - isValidateRounds: (rounds) => { - if (Array.isArray(rounds) === false) { - throw new Error('rounds 는 배열이어야 합니다.'); - } - if (rounds.some((round) => typeof round !== 'number')) { - throw new Error('rounds 는 숫자 배열이어야 합니다.'); - } - - return true; - }, - isValidateRacing: (cars, round) => { - return ( - cars.every((car) => car instanceof Car) && - validation.isValidateRound(round) - ); - }, - isValidateRound: (round) => { - if (isNaN(round) === true || typeof round !== 'number') { - throw new Error('라운드는 숫자형태여야 합니다.'); - } - if (round < 1 || round > 50) { - throw new Error('라운드 횟수는 1회 이상 50회 이하여야 합니다.'); - } - - return true; - }, - isValidateCars: (cars) => { - if (Array.isArray(cars) === false) { - throw new Error('cars 는 배열이어야 합니다'); - } - if (cars.some((car) => typeof car !== 'string')) { - throw new Error('cars 는 문자열로 이뤄져야 합니다'); - } - - cars = cars.map((car) => car.trim()); - const carSet = new Set(cars); - - if (carSet.size !== cars.length) { - throw new Error('중복되는 이름이 있습니다.'); - } - if (carSet.size < 2 || carSet.size > 10) { - throw new Error('자동차는 2개 이상 10개 이하여야 합니다.'); - } - - return true; - }, - isValidatePosition: (position) => { - if (isNaN(position) || typeof position !== 'number') { - throw new Error('포지션은 숫자여야 합니다'); - } - if (position < 0) { - throw new Error('거리는 최소 0부터 시작해야 합니다.'); - } - - return true; - }, - isValidateCarName: (name) => { - if (typeof name !== 'string') { - throw new Error('자동차의 이름은 문자열 형태여야 합니다.'); - } - name = name.trim(); - - if (name.length < 1 || name.length > 10) { - throw new Error('자동차 이름은 한 글자 이상 열글자 이하여야 합니다.'); - } - - return true; - }, -}; - -export default validation; From 2035dd6fabe0b6ffd72d79e78669762b8aae90ce Mon Sep 17 00:00:00 2001 From: Suyeon Date: Thu, 27 Jun 2024 15:07:40 +0900 Subject: [PATCH 09/34] =?UTF-8?q?refactoring:=20MVC=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=97=AD=ED=95=A0=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/GameController.js | 64 ++++++++++++++++++++++++++++++ src/main.js | 37 ++++-------------- src/model/Car.js | 67 +++++++++++++++++++++----------- 3 files changed, 115 insertions(+), 53 deletions(-) create mode 100644 src/controller/GameController.js diff --git a/src/controller/GameController.js b/src/controller/GameController.js new file mode 100644 index 0000000..41bfea0 --- /dev/null +++ b/src/controller/GameController.js @@ -0,0 +1,64 @@ +import Car from '../model/Car.js'; +import { dice } from '../utils/gameSupport.js'; +import CarView from '../view/CarView.js'; + +export default class GameController { + #cars; + #carView; + + constructor(round) { + GameController.validateRound(round); + + this.round = round; + this.#cars = []; + this.#carView = new CarView(); + } + + async startGame() { + const carNames = await this.#carView.inputCarName(); + this.#cars = carNames.split(',').map((name) => new Car(name)); + + for (let currRound = 0; currRound < this.round; currRound++) { + const movedCars = this.#proceedRound(); + movedCars.forEach((movedCar) => this.#carView.printMovedCar(movedCar)); + } + + const winnersName = this.#getWinnersName().join(', '); + this.#carView.printWinners(winnersName); + + return winnersName; + } + + #proceedRound() { + const movedCars = this.#cars.filter((car) => { + return ( + car.move(1, [ + () => { + const result = dice(10, 0); + return result > 4 ? true : false; + }, + ]) === true + ); + }); + + return movedCars.map((car) => car.name); + } + + #getWinnersName() { + const maxPosition = Math.max(...this.#cars.map((car) => car.position)); + + return this.#cars + .filter((car) => car.position === maxPosition) + .map((car) => car.name); + } + + static validateRound(round) { + if (typeof round !== 'number') { + throw new Error('라운드 횟수는 숫자 타입이어야 합니다.'); + } + + if (round < 1 && 10 < round) { + throw new Error('라운드 횟수는 1 ~ 10 사이의 숫자여야 합니다.'); + } + } +} diff --git a/src/main.js b/src/main.js index 10a5ee2..30a1196 100644 --- a/src/main.js +++ b/src/main.js @@ -1,39 +1,16 @@ -import Racing from './model/Racing.js'; -import { GAME } from './constants/index.js'; -import InputView from './view/InputView.js'; -import OutputView from './view/OutputView.js'; -import validation from './utils/validation.js'; +import GameController from './controller/GameController.js'; class Main { - #rounds; - constructor(rounds) { - this.#rounds = this.setRounds(rounds); + constructor(round) { + this.gameController = new GameController(round); } - setRounds(rounds) { - if (!validation.isValidateRounds(rounds)) { - return []; - } - - this.#rounds = rounds; - return this.#rounds; - } - - async play() { - const result = this.#rounds.map(async (round) => { - const cars = await InputView.getCarName(); - const racing = new Racing(round, cars); - const winners = racing.startRacing(cars); - - OutputView.printWinners(winners); - return winners; - }); - - return result; + play() { + this.gameController.startGame(); } } -const main = new Main([GAME.ROUND]); +const main = new Main(5); main.play(); -export default Main; +export default main; diff --git a/src/model/Car.js b/src/model/Car.js index b53afbb..57ea85e 100644 --- a/src/model/Car.js +++ b/src/model/Car.js @@ -1,40 +1,61 @@ -import validation from '../utils/validation.js'; - class Car { - #name; - #position; - constructor(name) { - this.#name = this.setName(name); - this.#position = 0; - } + name = name.trim(); - setName(name) { - if (!validation.isValidateCarName(name)) return ''; + this._name = name; + this._position = 0; + } - this.#name = name.trim(); - return this.#name; + /** + * @param {string} name + */ + set name(name) { + Car.validateName(name); + this._name = name; } - getName() { - return this.#name; + /** + * @param {number} position + */ + set position(position) { + Car.validatePosition(position); + this._position = position; } - setPosition(position) { - if (!validation.isValidatePosition(position)) return this.#position; + get name() { + return this._name; + } - this.#position = position; - return this.#position; + get position() { + return this._position; } - getPosition() { - return this.#position; + move(distance = 1, rules) { + if (rules && Array.isArray(rules)) { + if (rules.some((rule) => rule() === false)) { + return false; + } + } + + this.position = this.position + distance; + + return true; } - move(distance = 1) { - this.setPosition(this.#position + distance); + static validateName(name) { + if (typeof name !== 'string') { + throw new Error('자동자 이름은 문자열이어야 합니다.'); + } + + if (name.length < 1 || name.length > 5) { + throw new Error('자동차 이름은 1 ~ 5 글자로 이뤄져야 합니다.'); + } + } - return this.#position; + static validatePosition(position) { + if (typeof position !== 'number') { + throw new Error('자동차의 위치는 숫자 형태여야 합니다.'); + } } } From c0aadea2077c53c7053ef126b284cf97e3825bcf Mon Sep 17 00:00:00 2001 From: Suyeon Date: Thu, 27 Jun 2024 16:31:17 +0900 Subject: [PATCH 10/34] =?UTF-8?q?chore:=20ESLint=20&=20Prettier=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc | 3 ++ .prettierrc | 11 +++++ eslint.config.js | 11 ++--- package-lock.json | 112 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 ++ 5 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 .eslintrc create mode 100644 .prettierrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..62f705b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["plugin:prettier/recommended"], +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6923972 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "endOfLine": "lf", + "tabWidth": 2, + "printWidth": 80, + "singleQuote": true, + "bracketSameLine": false, + "bracketSpacing": true, + "arrowParens": "always", + "singleAttributePerLine": true +} diff --git a/eslint.config.js b/eslint.config.js index 1aa8477..a027e87 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,8 +1,9 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; - +import globals from 'globals'; +import pluginJs from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; export default [ - {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, pluginJs.configs.recommended, -]; \ No newline at end of file + eslintPluginPrettierRecommended, +]; diff --git a/package-lock.json b/package-lock.json index 21b2a01..e16d6a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,10 @@ "devDependencies": { "@eslint/js": "^9.5.0", "eslint": "^9.5.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "globals": "^15.6.0", + "prettier": "3.3.2", "vitest": "^1.6.0" } }, @@ -565,6 +568,18 @@ "node": ">= 8" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -1223,6 +1238,48 @@ "url": "https://eslint.org/donate" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", @@ -1348,6 +1405,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1975,6 +2038,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -2221,6 +2311,22 @@ "node": ">=8" } }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -2251,6 +2357,12 @@ "node": ">=14.0.0" } }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index e0cf22f..f5b6623 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,10 @@ "devDependencies": { "@eslint/js": "^9.5.0", "eslint": "^9.5.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "globals": "^15.6.0", + "prettier": "3.3.2", "vitest": "^1.6.0" } } From c887561efaace85b6742b61a63c9299c421d602f Mon Sep 17 00:00:00 2001 From: Suyeon Date: Thu, 27 Jun 2024 16:48:09 +0900 Subject: [PATCH 11/34] =?UTF-8?q?fix:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 라운드 마다 '-' 를 사용해서 진행 척도 표시하기. --- src/controller/GameController.js | 18 ++++++++++-------- src/view/CarView.js | 11 +++++++---- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/controller/GameController.js b/src/controller/GameController.js index 41bfea0..8a6b84c 100644 --- a/src/controller/GameController.js +++ b/src/controller/GameController.js @@ -19,18 +19,22 @@ export default class GameController { this.#cars = carNames.split(',').map((name) => new Car(name)); for (let currRound = 0; currRound < this.round; currRound++) { - const movedCars = this.#proceedRound(); - movedCars.forEach((movedCar) => this.#carView.printMovedCar(movedCar)); + this.#proceedRound(); + const carInfo = this.#cars.map((car) => ({ + name: car.name, + position: car.position, + })); + this.#carView.printMovedCar(carInfo); } - const winnersName = this.#getWinnersName().join(', '); - this.#carView.printWinners(winnersName); + const winners = this.#getWinnersName(); + this.#carView.printWinners(winners); - return winnersName; + return winners; } #proceedRound() { - const movedCars = this.#cars.filter((car) => { + this.#cars.filter((car) => { return ( car.move(1, [ () => { @@ -40,8 +44,6 @@ export default class GameController { ]) === true ); }); - - return movedCars.map((car) => car.name); } #getWinnersName() { diff --git a/src/view/CarView.js b/src/view/CarView.js index de4a96e..491efe0 100644 --- a/src/view/CarView.js +++ b/src/view/CarView.js @@ -7,15 +7,18 @@ export default class CarView { async inputCarName() { return await this.rl( - '🏎️ 자동차 이름을 쉼표로 구분지어 입력해주세요! 🏎️ > ' + '🏎️ 자동차 이름을 쉼표로 구분지어 입력해주세요! 🏎️ > ', ); } - printMovedCar(carName) { - console.log(`${carName} 이 전진합니다!\n`); + printMovedCar(carInfo) { + carInfo.forEach(({ name, position }) => { + console.log(`${name}: ${'-'.repeat(position)}`); + }); + console.log('\n'); } printWinners(winners) { - console.log(`✨우승자 ✨ > ${winners}`); + console.log(`✨우승자 ✨: ${winners.join(', ')}`); } } From bf700be185f99cc96559fdf7f9beedb256c92a47 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Thu, 27 Jun 2024 17:17:04 +0900 Subject: [PATCH 12/34] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9A=B4=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/GameController.js | 63 +++++++++++++++++++++++++++----- src/main.js | 8 ++-- src/view/CarView.js | 6 +++ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/controller/GameController.js b/src/controller/GameController.js index 8a6b84c..fda3123 100644 --- a/src/controller/GameController.js +++ b/src/controller/GameController.js @@ -3,22 +3,37 @@ import { dice } from '../utils/gameSupport.js'; import CarView from '../view/CarView.js'; export default class GameController { + static DEFAULT_ROUND; #cars; #carView; - constructor(round) { - GameController.validateRound(round); - - this.round = round; + constructor() { + this._round = GameController.DEFAULT_ROUND; this.#cars = []; this.#carView = new CarView(); } - async startGame() { - const carNames = await this.#carView.inputCarName(); + getGameRules() { + return { + round: this._round, + carNames: this.#cars.map((car) => car.name), + }; + } + + setGameRules({ round, carNames }) { + GameController.validateRound(round); + GameController.ValidateCarNames(carNames); + + this._round = Number(round); this.#cars = carNames.split(',').map((name) => new Car(name)); + } - for (let currRound = 0; currRound < this.round; currRound++) { + async start() { + const round = await this.#carView.inputRound(); + const carNames = await this.#carView.inputCarName(); + this.setGameRules({ round, carNames }); + + for (let currRound = 0; currRound < this._round; currRound++) { this.#proceedRound(); const carInfo = this.#cars.map((car) => ({ name: car.name, @@ -27,7 +42,7 @@ export default class GameController { this.#carView.printMovedCar(carInfo); } - const winners = this.#getWinnersName(); + const winners = this.getWinnersName(); this.#carView.printWinners(winners); return winners; @@ -46,7 +61,7 @@ export default class GameController { }); } - #getWinnersName() { + getWinnersName() { const maxPosition = Math.max(...this.#cars.map((car) => car.position)); return this.#cars @@ -55,7 +70,9 @@ export default class GameController { } static validateRound(round) { - if (typeof round !== 'number') { + round = Number(round); + + if (isNaN(round) || typeof round !== 'number') { throw new Error('라운드 횟수는 숫자 타입이어야 합니다.'); } @@ -63,4 +80,30 @@ export default class GameController { throw new Error('라운드 횟수는 1 ~ 10 사이의 숫자여야 합니다.'); } } + + static ValidateCarNames(carNames) { + if (typeof carNames !== 'string') { + throw new Error( + '자동차 이름은 , 로 분리된 문자열 형태로 입력해야 합니다.', + ); + } + + const carNamesArr = carNames.split(','); + + if (carNamesArr.length < 1 || carNamesArr > 10) { + throw new Error('자동차는 최소 1개에서 10 개 이상이어야 합니다'); + } + + if ( + carNamesArr.some((carName) => { + carName = carName.trim(); + + return carName.length < 1 || carName.length > 10 ? true : false; + }) + ) { + throw new Error( + '자동차 이름은 1 글자 이상, 10글자 미만의 문자열이어야 합니다.', + ); + } + } } diff --git a/src/main.js b/src/main.js index 30a1196..407f88a 100644 --- a/src/main.js +++ b/src/main.js @@ -1,16 +1,16 @@ import GameController from './controller/GameController.js'; class Main { - constructor(round) { - this.gameController = new GameController(round); + constructor() { + this.gameController = new GameController(); } play() { - this.gameController.startGame(); + this.gameController.start(); } } -const main = new Main(5); +const main = new Main(); main.play(); export default main; diff --git a/src/view/CarView.js b/src/view/CarView.js index 491efe0..c5a75f6 100644 --- a/src/view/CarView.js +++ b/src/view/CarView.js @@ -5,6 +5,12 @@ export default class CarView { this.rl = readLineAsync; } + async inputRound() { + return await this.rl( + '몇번의 라운드를 진행할지 1 ~ 10 사이의 숫자를 입력해주세요! > ', + ); + } + async inputCarName() { return await this.rl( '🏎️ 자동차 이름을 쉼표로 구분지어 입력해주세요! 🏎️ > ', From 687fc97136b3cb7457798ad219c9c432f2b544a4 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Fri, 28 Jun 2024 16:59:18 +0900 Subject: [PATCH 13/34] =?UTF-8?q?refactoring:=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=EC=9D=98=20=EC=97=AD=ED=95=A0=EC=9D=84=20mai?= =?UTF-8?q?n=20=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/GameController.js | 109 ------------------------------- src/main.js | 55 +++++++++++++--- 2 files changed, 47 insertions(+), 117 deletions(-) delete mode 100644 src/controller/GameController.js diff --git a/src/controller/GameController.js b/src/controller/GameController.js deleted file mode 100644 index fda3123..0000000 --- a/src/controller/GameController.js +++ /dev/null @@ -1,109 +0,0 @@ -import Car from '../model/Car.js'; -import { dice } from '../utils/gameSupport.js'; -import CarView from '../view/CarView.js'; - -export default class GameController { - static DEFAULT_ROUND; - #cars; - #carView; - - constructor() { - this._round = GameController.DEFAULT_ROUND; - this.#cars = []; - this.#carView = new CarView(); - } - - getGameRules() { - return { - round: this._round, - carNames: this.#cars.map((car) => car.name), - }; - } - - setGameRules({ round, carNames }) { - GameController.validateRound(round); - GameController.ValidateCarNames(carNames); - - this._round = Number(round); - this.#cars = carNames.split(',').map((name) => new Car(name)); - } - - async start() { - const round = await this.#carView.inputRound(); - const carNames = await this.#carView.inputCarName(); - this.setGameRules({ round, carNames }); - - for (let currRound = 0; currRound < this._round; currRound++) { - this.#proceedRound(); - const carInfo = this.#cars.map((car) => ({ - name: car.name, - position: car.position, - })); - this.#carView.printMovedCar(carInfo); - } - - const winners = this.getWinnersName(); - this.#carView.printWinners(winners); - - return winners; - } - - #proceedRound() { - this.#cars.filter((car) => { - return ( - car.move(1, [ - () => { - const result = dice(10, 0); - return result > 4 ? true : false; - }, - ]) === true - ); - }); - } - - getWinnersName() { - const maxPosition = Math.max(...this.#cars.map((car) => car.position)); - - return this.#cars - .filter((car) => car.position === maxPosition) - .map((car) => car.name); - } - - static validateRound(round) { - round = Number(round); - - if (isNaN(round) || typeof round !== 'number') { - throw new Error('라운드 횟수는 숫자 타입이어야 합니다.'); - } - - if (round < 1 && 10 < round) { - throw new Error('라운드 횟수는 1 ~ 10 사이의 숫자여야 합니다.'); - } - } - - static ValidateCarNames(carNames) { - if (typeof carNames !== 'string') { - throw new Error( - '자동차 이름은 , 로 분리된 문자열 형태로 입력해야 합니다.', - ); - } - - const carNamesArr = carNames.split(','); - - if (carNamesArr.length < 1 || carNamesArr > 10) { - throw new Error('자동차는 최소 1개에서 10 개 이상이어야 합니다'); - } - - if ( - carNamesArr.some((carName) => { - carName = carName.trim(); - - return carName.length < 1 || carName.length > 10 ? true : false; - }) - ) { - throw new Error( - '자동차 이름은 1 글자 이상, 10글자 미만의 문자열이어야 합니다.', - ); - } - } -} diff --git a/src/main.js b/src/main.js index 407f88a..db50c17 100644 --- a/src/main.js +++ b/src/main.js @@ -1,16 +1,55 @@ -import GameController from './controller/GameController.js'; +import { GAME } from './constants/game.js'; +import Race from './model/Race.js'; +import Car from './model/Car.js'; +import GameView from './view/RaceView.js'; +import gameSupport from './utils/gameSupport.js'; -class Main { - constructor() { - this.gameController = new GameController(); +async function main() { + const view = new GameView(); + let roundInput = undefined; + let namesInput = undefined; + + while (true) { + roundInput = await view.inputRound(); + const error = Race.getRoundError(roundInput); + + if (error === undefined) { + break; + } + view.printError(error); } - play() { - this.gameController.start(); + while (true) { + namesInput = await view.inputRacerNames(); + const error = Race.getNamesError(namesInput); + + if (error === undefined) { + break; + } + view.printError(error); } + + const race = new Race({ + Racer: Car, + round: Number(roundInput), + names: namesInput, + rules: { + diceRule: () => { + return ( + gameSupport.dice(GAME.RULE.MAX_DICE, GAME.RULE.MIN_DICE) >= + GAME.RULE.DICE_CONDITION + ); + }, + }, + onProceed: () => { + view.printMovedCar(race.getRacerStatus()); + }, + }); + + const winners = race.startRace(); + view.printWinners(winners); } -const main = new Main(); -main.play(); +main(); export default main; From a67c5735cd3812a956835b879607fa74a6bc07d4 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Fri, 28 Jun 2024 16:59:46 +0900 Subject: [PATCH 14/34] =?UTF-8?q?refactoring:=20CarView=20->=20RaceView?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view/{CarView.js => RaceView.js} | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) rename src/view/{CarView.js => RaceView.js} (83%) diff --git a/src/view/CarView.js b/src/view/RaceView.js similarity index 83% rename from src/view/CarView.js rename to src/view/RaceView.js index c5a75f6..5f5c767 100644 --- a/src/view/CarView.js +++ b/src/view/RaceView.js @@ -1,6 +1,6 @@ import readLineAsync from '../utils/readLine.js'; -export default class CarView { +export default class RaceView { constructor() { this.rl = readLineAsync; } @@ -11,7 +11,7 @@ export default class CarView { ); } - async inputCarName() { + async inputRacerNames() { return await this.rl( '🏎️ 자동차 이름을 쉼표로 구분지어 입력해주세요! 🏎️ > ', ); @@ -27,4 +27,8 @@ export default class CarView { printWinners(winners) { console.log(`✨우승자 ✨: ${winners.join(', ')}`); } + + printError(message) { + console.log(`🚨 ${message} 🚨`); + } } From 1ebcfbb8ce384a05a8b04965c7dd248f1a43f380 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Fri, 28 Jun 2024 17:00:20 +0900 Subject: [PATCH 15/34] =?UTF-8?q?refactoring:=20racing=EA=B3=BC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=EB=90=9C=20=EC=83=81=EC=88=98=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/constants/game.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/constants/game.js diff --git a/src/constants/game.js b/src/constants/game.js new file mode 100644 index 0000000..1afc6e5 --- /dev/null +++ b/src/constants/game.js @@ -0,0 +1,11 @@ +export const GAME = { + RULE: { + MAX_ROUND: 10, + MIN_ROUND: 1, + MAX_NAME_LENGTH: 10, + MIN_NAME_LENGTH: 1, + MAX_DICE: 10, + MIN_DICE: 0, + DICE_CONDITION: 4, + }, +}; From 1b7e7bb7c805fa2c31b183da34fd57f47e625be5 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Fri, 28 Jun 2024 17:00:33 +0900 Subject: [PATCH 16/34] =?UTF-8?q?chore:=20index.js=20->=20game.js=EB=A1=9C?= =?UTF-8?q?=20=EC=83=81=EC=88=98=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/index.js | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 src/constants/index.js diff --git a/src/constants/index.js b/src/constants/index.js deleted file mode 100644 index 19507e2..0000000 --- a/src/constants/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export const GAME = { - ROUND: 5, -}; From ab76a7947506b17b0e6ae689eaf2b8fa5c66399d Mon Sep 17 00:00:00 2001 From: Suyeon Date: Fri, 28 Jun 2024 17:01:01 +0900 Subject: [PATCH 17/34] =?UTF-8?q?refactoring:=20Car=20=EC=97=90=EC=84=9C?= =?UTF-8?q?=20Racer=20=EC=97=90=20=ED=95=B4=EB=8B=B9=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=97=AD=ED=95=A0=20=EB=B6=84=EB=A6=AC=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Car.js | 61 +++------------------------------------- src/model/Racer.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 57 deletions(-) create mode 100644 src/model/Racer.js diff --git a/src/model/Car.js b/src/model/Car.js index 57ea85e..6738e90 100644 --- a/src/model/Car.js +++ b/src/model/Car.js @@ -1,61 +1,8 @@ -class Car { - constructor(name) { - name = name.trim(); +import Racer from './Racer.js'; - this._name = name; - this._position = 0; - } - - /** - * @param {string} name - */ - set name(name) { - Car.validateName(name); - this._name = name; - } - - /** - * @param {number} position - */ - set position(position) { - Car.validatePosition(position); - this._position = position; - } - - get name() { - return this._name; - } - - get position() { - return this._position; - } - - move(distance = 1, rules) { - if (rules && Array.isArray(rules)) { - if (rules.some((rule) => rule() === false)) { - return false; - } - } - - this.position = this.position + distance; - - return true; - } - - static validateName(name) { - if (typeof name !== 'string') { - throw new Error('자동자 이름은 문자열이어야 합니다.'); - } - - if (name.length < 1 || name.length > 5) { - throw new Error('자동차 이름은 1 ~ 5 글자로 이뤄져야 합니다.'); - } - } - - static validatePosition(position) { - if (typeof position !== 'number') { - throw new Error('자동차의 위치는 숫자 형태여야 합니다.'); - } +class Car extends Racer { + constructor({ name, position = 0 }) { + super({ name, position }); } } diff --git a/src/model/Racer.js b/src/model/Racer.js new file mode 100644 index 0000000..d132f74 --- /dev/null +++ b/src/model/Racer.js @@ -0,0 +1,69 @@ +export default class Racer { + _name; + _position; + + constructor({ name, position }) { + name = name.trim(); + + this.name = name; + this.position = position; + } + + set name(name) { + const error = Racer.getNameError(name); + + if (error !== undefined) { + throw new Error(error); + } + + this._name = name; + } + + set position(position) { + const error = Racer.getPositionError(position); + + if (error !== undefined) { + throw new Error(error); + } + + this._position = position; + } + + get name() { + return this._name; + } + + get position() { + return this._position; + } + + init() { + this._position = 0; + } + + move(distance = 1) { + this.position = this.position + distance; + + return true; + } + + static getNameError(name) { + if (typeof name !== 'string') { + return '레이서들의 이름은 문자열이어야 합니다.'; + } + + if (name.length < 1 || name.length > 5) { + return '레이서들의 이름은 1 ~ 5 글자로 이뤄져야 합니다.'; + } + + return undefined; + } + + static getPositionError(position) { + if (typeof position !== 'number') { + return '레이서들의 위치는 숫자 형태여야 합니다.'; + } + + return undefined; + } +} From 3d679c298845e0b8dde9db16d11d72703fb9d2a9 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Fri, 28 Jun 2024 17:01:14 +0900 Subject: [PATCH 18/34] =?UTF-8?q?feat:=20=EC=84=9C=EB=B8=8C=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=ED=8C=90=EB=B3=84=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/isSubClass.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/utils/isSubClass.js diff --git a/src/utils/isSubClass.js b/src/utils/isSubClass.js new file mode 100644 index 0000000..ee3e58a --- /dev/null +++ b/src/utils/isSubClass.js @@ -0,0 +1,15 @@ +function isSubClass(child, parent) { + let prototype = Object.getPrototypeOf(child); + + while (prototype) { + if (prototype === parent) { + return true; + } + + prototype = Object.getPrototypeOf(prototype); + } + + return false; +} + +export default isSubClass; From f36a9946daf034177c0e2f841ef0d62d82efc729 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Fri, 28 Jun 2024 17:01:44 +0900 Subject: [PATCH 19/34] =?UTF-8?q?refactoring:=20=EC=9C=A0=ED=8B=B8=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=A5=BC=20default=20=EA=B0=92=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=AC=B6=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/gameSupport.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/gameSupport.js b/src/utils/gameSupport.js index 7fc3752..6773041 100644 --- a/src/utils/gameSupport.js +++ b/src/utils/gameSupport.js @@ -1,3 +1,7 @@ -export function dice(min, max) { - return Math.floor(Math.random() * (max - min) + min); -} +const gameSupport = { + dice(min, max) { + return Math.floor(Math.random() * (max - min) + min); + }, +}; + +export default gameSupport; From dc8f9c76d58443e750eac1b3ef862b08803f63bd Mon Sep 17 00:00:00 2001 From: Suyeon Date: Fri, 28 Jun 2024 17:01:57 +0900 Subject: [PATCH 20/34] =?UTF-8?q?feat:=20Rule=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=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Rule.js | 131 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/model/Rule.js diff --git a/src/model/Rule.js b/src/model/Rule.js new file mode 100644 index 0000000..b6fa6f1 --- /dev/null +++ b/src/model/Rule.js @@ -0,0 +1,131 @@ +export default class Rule { + #rules; + + constructor(rules) { + this.#initRules(rules); + } + + #initRules(rules) { + if (rules === undefined) { + this.#rules = {}; + return; + } + + const ruleKeys = Object.keys(rules); + + for (const key of ruleKeys) { + let _value = rules[key]; + + Object.defineProperty(rules, key, { + get() { + return _value; + }, + set(value) { + const keyValidation = Rule.getKeyError(key); + const valueValidation = Rule.getValueError(value); + + if (keyValidation.isValid === false) { + throw new Error(keyValidation.message); + } + + if (valueValidation.isValid === false) { + throw new Error(valueValidation.message); + } + + _value = value; + }, + }); + } + + this.#rules = rules; + } + + run() { + const rules = Object.values(this.#rules); + + return rules.every((rule) => rule() === true); + } + + change(key, value) { + if (this.isExist(key) === false) { + throw new Error('해당 이름의 룰이 존재하지 않습니다.'); + } + + this.rules[key] = value; + } + + add(key, value) { + if (this.isExist(key)) { + throw new Error('이미 같은 이름의 룰이 존재합니다.'); + } + + this.rules[key] = value; + } + + remove(removeKey) { + const ruleKeys = Object.keys(this.#rules); + const nextRules = {}; + + for (const key of ruleKeys) { + if (removeKey === key) { + continue; + } + + nextRules[key] = this.#rules[key]; + } + + this.rules = nextRules; + } + + removeAll() { + this.rules = {}; + } + + isExist(key) { + return this.#rules[key] === undefined ? false : true; + } + + static getRulesError(rules) { + if (typeof rules !== 'object') { + return '룰은 객체여야 합니다.'; + } + + const keys = Object.keys(rules); + const values = Object.values(rules); + const length = keys.length; + + if (length < 1 || 10 < length) { + return '룰은 1 ~ 10 개여야 합니다.'; + } + + if (keys.some((key) => Rule.getKeyError(key).isValid === false)) { + return '룰의 키는 문자열이어야 합니다.'; + } + + if (values.some((value) => Rule.getValueError(value).isValid === false)) { + return '룰의 값은 불리언을 반환하는 함수여야 합니다.'; + } + + return undefined; + } + + static getKeyError(key) { + if (typeof key !== 'string') { + return '룰의 키는 문자열이어야 합니다.'; + } + + return undefined; + } + + static getValueError(value) { + if (typeof value !== 'function') { + return '룰의 값은 함수 타입이어야 합니다.'; + } + + if (typeof value() !== 'boolean') { + return '룰의 반환값은 불리언 타입이어야 합니다.'; + } + + return undefined; + } +} From 5dc5e87b0d888ebf896e3e22864efbe9ab750fd6 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Fri, 28 Jun 2024 17:02:11 +0900 Subject: [PATCH 21/34] =?UTF-8?q?refactoring:=20Race=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Race.js | 140 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 src/model/Race.js diff --git a/src/model/Race.js b/src/model/Race.js new file mode 100644 index 0000000..baefe80 --- /dev/null +++ b/src/model/Race.js @@ -0,0 +1,140 @@ +import isSubClass from '../utils/isSubClass.js'; +import Rule from './Rule.js'; +import RacerClass from './Racer.js'; + +export default class Race { + #rule; + #racers; + + constructor({ Racer, rules, names, round, onProceed }) { + this._Racer = Racer; + this._round = round; + this.#rule = new Rule(rules); + this.#racers = this.#makeRacers(names); + this.onProceed = onProceed; + } + + #makeRacers(names) { + const error = Race.getNamesError(names); + + if (error !== undefined) { + throw new Error(error); + } + + return (names = names + .split(',') + .map((name) => name.trim()) + .map((name) => new this.Racer({ name }))); + } + + get round() { + return this._round; + } + + get Racer() { + return this._Racer; + } + + set round(round) { + const error = Race.getRoundError(round); + + if (error !== undefined) { + throw new Error(error); + } + + this._round = round; + } + + set Racer(Racer) { + const error = Race.getRacerError(Racer); + + if (error !== undefined) { + throw new Error(error); + } + + this._Racer = Racer; + } + + getRacerStatus(name) { + if (name === undefined) { + return this.#racers.map((racer) => ({ + name: racer.name, + position: racer.position, + })); + } + + const targetRacer = this.#racers.find((racer) => racer.name === name); + + return { + name: targetRacer.name, + position: targetRacer.position, + }; + } + + startRace() { + for (let currentRound = 0; currentRound < this.round; currentRound++) { + this.#proceedRound(); + if (this.onProceed && typeof this.onProceed === 'function') { + this.onProceed(); + } + } + + const racers = this.getRacerStatus(); + const maxPosition = Math.max(...racers.map((racer) => racer.position)); + + return racers + .filter((racer) => racer.position === maxPosition) + .map((racer) => racer.name); + } + + #proceedRound() { + this.#racers.forEach((racer) => { + this.#rule.run() && racer.move(); + }); + } + + static getRacerError(Racer) { + if (isSubClass(Racer, RacerClass) === false) { + return '인자로받는 Racer 프로퍼티는 Racer 클래스의 서브 클래스여야 합니다.'; + } + return undefined; + } + + static getRoundError(round) { + round = Number(round); + + if (isNaN(round) === true) { + return '라운드가 숫자 형태가 아닙니다.'; + } + + if (round < 1 || round > 10) { + return '라운드는 1 ~ 10 사이여야 합니다.'; + } + + return undefined; + } + + static getNamesError(names) { + if (typeof names !== 'string') { + return '이름은 , 로 분리된 문자열 형태로 입력해야 합니다.'; + } + + const namesArr = names.split(','); + + if (namesArr.length < 1 || namesArr > 10) { + return '레이서의 개수는 1 ~ 10 개여야 합니다'; + } + + if ( + namesArr.some((carName) => { + carName = carName.trim(); + + return carName.length < 1 || carName.length > 10 ? true : false; + }) + ) { + return '이름은 1 글자 이상, 10글자 미만의 문자열이어야 합니다.'; + } + + return undefined; + } +} From 208ca45ac940fefb944fd395f06bd9a899f48273 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 15:15:13 +0900 Subject: [PATCH 22/34] =?UTF-8?q?feat:=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/util.test.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/__tests__/util.test.js diff --git a/src/__tests__/util.test.js b/src/__tests__/util.test.js new file mode 100644 index 0000000..96654cf --- /dev/null +++ b/src/__tests__/util.test.js @@ -0,0 +1,32 @@ +import { describe, expect, test } from 'vitest'; +import gameSupport from '../utils/gameSupport'; +import isSubClass from '../utils/isSubClass'; +import Racer from '../model/Racer'; +import Car from '../model/Car'; +import Race from '../model/Race'; + +describe('gameSupport 유틸 함수 테스트', () => { + test('다이스에 최대값과 최소값을 전달하면 최소값과 최대값 사이의 값을 반환해야 한다.', () => { + const min = 0; + const max = 10; + const results = [...new Array(10)].map(() => gameSupport.dice(min, max)); + + expect(results.every((result) => result >= min && result < max)).toBe(true); + }); +}); + +describe('isSubclass 유틸 함수 테스트', () => { + test('어떤 클래스를 상속받은 클래스와 원본 클래스의 경우 true 를 반환한다.', () => { + const subClass = Car; + const superClass = Racer; + + expect(isSubClass(subClass, superClass)).toBe(true); + }); + + test('상속관계가 아닌 클래스는 false 를 반환한다.', () => { + const subClass = Car; + const etcClass = Race; + + expect(isSubClass(subClass, etcClass)).toBe(false); + }); +}); From 4b2e862b58803326f98cefa01e02c885ceffbed2 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 15:15:37 +0900 Subject: [PATCH 23/34] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/{util.test.js => utils.test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/__tests__/{util.test.js => utils.test.js} (100%) diff --git a/src/__tests__/util.test.js b/src/__tests__/utils.test.js similarity index 100% rename from src/__tests__/util.test.js rename to src/__tests__/utils.test.js From 3b4428895ddaec182a0d3b2b0deceba180eec988 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 16:05:16 +0900 Subject: [PATCH 24/34] =?UTF-8?q?fix:=20=EC=97=90=EB=9F=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=9E=98=EB=AA=BB=ED=95=98=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이전에 isValid 프로퍼티로 에러를 판별하던 걸 메세지로 판별하도록 변경 --- src/model/Rule.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/model/Rule.js b/src/model/Rule.js index b6fa6f1..29ba82f 100644 --- a/src/model/Rule.js +++ b/src/model/Rule.js @@ -21,15 +21,15 @@ export default class Rule { return _value; }, set(value) { - const keyValidation = Rule.getKeyError(key); - const valueValidation = Rule.getValueError(value); + const keyError = Rule.getKeyError(key); + const valueError = Rule.getValueError(value); - if (keyValidation.isValid === false) { - throw new Error(keyValidation.message); + if (keyError !== undefined) { + throw new Error(keyError); } - if (valueValidation.isValid === false) { - throw new Error(valueValidation.message); + if (valueError !== undefined) { + throw new Error(valueError); } _value = value; @@ -51,7 +51,7 @@ export default class Rule { throw new Error('해당 이름의 룰이 존재하지 않습니다.'); } - this.rules[key] = value; + this.#rules[key] = value; } add(key, value) { @@ -59,7 +59,7 @@ export default class Rule { throw new Error('이미 같은 이름의 룰이 존재합니다.'); } - this.rules[key] = value; + this.#rules[key] = value; } remove(removeKey) { @@ -74,11 +74,11 @@ export default class Rule { nextRules[key] = this.#rules[key]; } - this.rules = nextRules; + this.#rules = nextRules; } removeAll() { - this.rules = {}; + this.#rules = {}; } isExist(key) { @@ -98,11 +98,11 @@ export default class Rule { return '룰은 1 ~ 10 개여야 합니다.'; } - if (keys.some((key) => Rule.getKeyError(key).isValid === false)) { + if (keys.some((key) => Rule.getKeyError(key) !== undefined)) { return '룰의 키는 문자열이어야 합니다.'; } - if (values.some((value) => Rule.getValueError(value).isValid === false)) { + if (values.some((value) => Rule.getValueError(value) !== undefined)) { return '룰의 값은 불리언을 반환하는 함수여야 합니다.'; } From 0f3cca65ef4f453241c811d939fe4962a811aeb6 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 16:40:20 +0900 Subject: [PATCH 25/34] =?UTF-8?q?fix:=20=EB=A3=B0=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=88=98=EC=A0=95=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 룰 프로퍼티를 바꿀 때 #initRules 메서드를 사용해서 초기화하기 - 초기화 및 프로퍼티 get, set 에서 유효성 검사하기 --- src/model/Rule.js | 56 ++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/model/Rule.js b/src/model/Rule.js index 29ba82f..071e813 100644 --- a/src/model/Rule.js +++ b/src/model/Rule.js @@ -14,32 +14,39 @@ export default class Rule { const ruleKeys = Object.keys(rules); for (const key of ruleKeys) { - let _value = rules[key]; - - Object.defineProperty(rules, key, { - get() { - return _value; - }, - set(value) { - const keyError = Rule.getKeyError(key); - const valueError = Rule.getValueError(value); - - if (keyError !== undefined) { - throw new Error(keyError); - } - - if (valueError !== undefined) { - throw new Error(valueError); - } - - _value = value; - }, - }); + const keyError = Rule.getKeyError(key); + const valueError = Rule.getValueError(rules[key]); + + if (keyError !== undefined || valueError !== undefined) { + throw new Error(keyError && valueError); + } + + this.#definePropertyRule(rules, key); } this.#rules = rules; } + #definePropertyRule(obj, key) { + let _value = obj[key]; + + Object.defineProperty(obj, key, { + get() { + return _value; + }, + set(value) { + const keyError = Rule.getKeyError(key); + const valueError = Rule.getValueError(value); + + if (keyError !== undefined || valueError !== undefined) { + throw new Error(keyError && valueError); + } + + _value = value; + }, + }); + } + run() { const rules = Object.values(this.#rules); @@ -59,7 +66,10 @@ export default class Rule { throw new Error('이미 같은 이름의 룰이 존재합니다.'); } - this.#rules[key] = value; + const nextRule = { ...this.#rules }; + nextRule[key] = value; + + this.#initRules(nextRule); } remove(removeKey) { @@ -74,7 +84,7 @@ export default class Rule { nextRules[key] = this.#rules[key]; } - this.#rules = nextRules; + this.#initRules(nextRules); } removeAll() { From 75520ab305712b3a10b2619f537e9d985a27d777 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 16:51:00 +0900 Subject: [PATCH 26/34] =?UTF-8?q?feat:=20=EB=A3=B0=EC=9D=98=20=ED=82=A4?= =?UTF-8?q?=EA=B0=92=EB=93=A4=EC=9D=84=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Rule.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/model/Rule.js b/src/model/Rule.js index 071e813..cfac339 100644 --- a/src/model/Rule.js +++ b/src/model/Rule.js @@ -47,6 +47,10 @@ export default class Rule { }); } + getKeys() { + return Object.keys(this.#rules); + } + run() { const rules = Object.values(this.#rules); From 0102bc95de70e59390759a5d0de0fe42765c51ad Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 17:03:25 +0900 Subject: [PATCH 27/34] =?UTF-8?q?feat:=20Rule=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/rule.test.js | 164 +++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 src/__tests__/rule.test.js diff --git a/src/__tests__/rule.test.js b/src/__tests__/rule.test.js new file mode 100644 index 0000000..aa761fc --- /dev/null +++ b/src/__tests__/rule.test.js @@ -0,0 +1,164 @@ +import { describe, expect, test } from 'vitest'; +import Rule from '../model/Rule'; +import gameSupport from '../utils/gameSupport'; + +describe('Rule 클래스에 대한 단위 테스트', () => { + const truthyRules = { + rule1: () => { + return true; + }, + rule2: () => { + return true; + }, + }; + const halfTruthyRules = { + rule1: () => { + return true; + }, + rule2: () => { + return false; + }, + }; + test('Rule 의 생성자 매개변수론 boolean 값을 반환하는 함수를 값으로 가지는 객체를 전달해야 한다.', () => { + expect(() => { + createCorrectRule(); + }).not.toThrow(); + }); + + test('Rule 의 생성자 생성자 매개변수로 boolean 값을 반환하는 함수 값을 가진 객체 외의 값을 전달하는 경우 에러를 반환한다.', () => { + expect(() => { + createUncorrectRule(); + }).toThrow(); + }); + + test('getKeys 메서드는 현재 룰의 모든 키값을 반환한다.', () => { + const rule = new Rule({ ...truthyRules }); + const keys = Object.keys(truthyRules); + const isSameKey = rule.getKeys().every((key) => keys.includes(key)); + + expect(isSameKey).toBeTruthy(); + }); + + test('run 메서드는 모든 규칙이 true 를 반환하는 경우 true 를 반환한다.', () => { + const rule = new Rule({ ...truthyRules }); + + expect(rule.run()).toBeTruthy(); + }); + + test('run 메서드는 하나라도 규칙 실행의 결과가 true 가 아닐 경우 false 를 반환한다.', () => { + const rule = new Rule({ ...halfTruthyRules }); + + expect(rule.run()).toBeFalsy(); + }); + + test('change 메서드는 룰에 없는 키 값을 바꾸려고 하는 경우 에러를 반환한다.', () => { + const rule = new Rule({ ...halfTruthyRules }); + + expect(() => { + rule.change('rule3', () => { + return false; + }); + }).toThrow(); + }); + + test('change 메서드는 룰에 있는 키 값을 전달할 경우 전달한 value 값으로 객체 값을 바꾼다.', () => { + const rule = new Rule({ ...halfTruthyRules }); + rule.change('rule2', () => { + return true; + }); + + expect(rule.run()).toBe(true); + }); + + test('change 메서드에 boolean 값을 반환하는 함수가 아닌, 다른 값을 전달하면 에러를 반환한다.', () => { + const rule = new Rule({ ...halfTruthyRules }); + + expect(() => { + rule.change('rule2', () => { + return 3; + }); + }).toThrow(); + }); + + test('add 메서드에 이미 존재하는 키를 추가하려고 하면 에러를 반환한다.', () => { + const rule = new Rule({ ...truthyRules }); + + expect(() => { + rule.add('rule1', () => { + return true; + }); + }).toThrow(); + }); + + test('add 메서드로 룰 객체에 없는 키를 추가하면 해당 프로퍼티를 추가한다.', () => { + const rule = new Rule({ ...truthyRules }); + rule.add('rule3', () => { + return true; + }); + + expect(rule.run()).toBeTruthy(); + }); + + test('add 메서드에 불리언 값을 반환하는 함수가 아닌 다른 타입을 전달하면 에러를 반환한다.', () => { + const rule = new Rule({ ...truthyRules }); + + expect(() => { + rule.add('rule3', () => { + return undefined; + }); + }).toThrow(); + }); + + test('remove 메서드는 매개변수로 전달한 key 값을 없앤다.', () => { + const rule = new Rule({ ...truthyRules }); + rule.remove('rule1'); + + expect(rule.isExist('rule')).toBeFalsy(); + }); + + test('removeAll 메서드는 rules 프로퍼티를 모두 삭제한다.', () => { + const rule = new Rule({ ...truthyRules }); + const keys = rule.getKeys(); + rule.removeAll(); + + expect(keys.every((key) => rule.isExist(key) === false)).toBeTruthy(); + }); + + test('isExistKey 메서드에 이미 존재하는 키를 매개변수로 전달하면 true를 반환한다.', () => { + const rule = new Rule({ ...truthyRules }); + + expect(rule.isExist('rule1')).toBeTruthy(); + }); + + test('isExistKey 메서드에 존재하지 않는 키를 매개변수로 전달하면 false를 반환한다.', () => { + const rule = new Rule({ ...truthyRules }); + + expect(rule.isExist('rule3')).toBeFalsy(); + }); +}); + +function createCorrectRule() { + const rule = new Rule({ + dice: () => { + return gameSupport.dice(0, 10) >= 4; + }, + }); + + return rule; +} + +function createUncorrectRule() { + const uncorrectRules = [ + undefined, + null, + 1, + { + a: () => { + return 1; + }, + }, + ]; + const rules = uncorrectRules.map((rule) => new Rule(rule)); + + return rules; +} From e752de143207fb1d0a1e1ba3c529f153c4dc954c Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 17:38:07 +0900 Subject: [PATCH 28/34] =?UTF-8?q?feat:=20move=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EA=B0=80=20=ED=98=84=EC=9E=AC=20=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=EC=85=98=20=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Racer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/Racer.js b/src/model/Racer.js index d132f74..99c2a14 100644 --- a/src/model/Racer.js +++ b/src/model/Racer.js @@ -44,7 +44,7 @@ export default class Racer { move(distance = 1) { this.position = this.position + distance; - return true; + return this.position; } static getNameError(name) { From 54dbb6b33ad4419c138cde52bdc247152d9023d9 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 17:38:21 +0900 Subject: [PATCH 29/34] =?UTF-8?q?feat:=20Racer=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/racer.test.js | 114 ++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/__tests__/racer.test.js diff --git a/src/__tests__/racer.test.js b/src/__tests__/racer.test.js new file mode 100644 index 0000000..ec28f47 --- /dev/null +++ b/src/__tests__/racer.test.js @@ -0,0 +1,114 @@ +import { describe, expect, test } from 'vitest'; +import Racer from '../model/Racer'; + +describe('Racer 클래스에 대한 단위 테스트', () => { + test('Racer 를 초기화 할 때 name 프로퍼티에 앞 뒤 공백을 제외한 1~5자 사이의 문자열을 입력하면 아무 에러를 반환하지 않는다.', () => { + expect(() => { + makeRacerMockData({ name: '수연' }); + }).not.toThrow(); + }); + + test('Racer 를 초기화할 때 position 프로퍼티에 숫자 타입을 입력하면 아무 에러를 반환하지 않는다.', () => { + expect(() => { + makeRacerMockData({ name: '수연', position: 0 }); + }).not.toThrow(); + }); + + test.each([ + { name: undefined }, + { name: 1 }, + { name: null }, + { name: function name() {} }, + { name: {} }, + ])( + 'name 프로퍼티에 할당하는 값($name) 문자열 타입이 아니면 에러를 발생한다.', + ({ name }) => { + expect(() => { + makeRacerMockData({ name }, false); + }).toThrow(); + }, + ); + + test.each([{ name: '' }, { name: '123456' }])( + 'name 프로퍼티에 해당하는 값($name)이 1글자 미만 5글자 초과인 경우 에러를 발생한다', + ({ name }) => { + expect(() => { + makeRacerMockData({ name }, false); + }).toThrow(); + }, + ); + + test.each([ + { position: undefined }, + { position: '1' }, + { position: null }, + { position: function num() {} }, + { position: {} }, + ])( + 'position 프로퍼티에 해당하는 값($position)이 숫자 타입이 아닌 경우 에러를 발생한다.', + ({ position }) => { + expect(() => { + makeRacerMockData({ position }, false); + }).toThrow(); + }, + ); + + test('position 프로퍼티를 가져올 수 있다.', () => { + const racer = makeRacerMockData({ position: 5 }); + + expect(racer.position).toBe(5); + }); + + test('position 프로퍼티를 가져올 수 있다.', () => { + const racer = makeRacerMockData({ position: 5 }); + + expect(racer.position).toBe(5); + }); + + test('init 메서드를 사용하면 postion 이 0 이 된다.', () => { + const racer = makeRacerMockData({ position: 5 }); + racer.init(); + + expect(racer.position).toBe(0); + }); + + test('init 함수를 사용하면 postion 이 0 이 된다.', () => { + const racer = makeRacerMockData({ position: 5 }); + racer.init(); + + expect(racer.position).toBe(0); + }); + + test('move 메서드에 숫자 타입을 전달하면 변경된 position 을 반환한다.', () => { + const racer = makeRacerMockData({ position: 0 }); + const nextPosition = racer.move(5); + + expect(nextPosition).toBe(5); + }); + + test.each([ + { distance: '1' }, + { distance: function num() {} }, + { distance: {} }, + ])( + 'move 메서드에 빈값 (undefined, null)이나 숫자가 아닌 값($distance)을 전달하면 에러를 발생한다.', + ({ distance }) => { + const racer = makeRacerMockData(); + + expect(() => { + racer.move(distance); + }).toThrow(); + }, + ); +}); + +function makeRacerMockData(initializer, useDefault = true) { + const { name, position } = initializer ? initializer : {}; + + const racer = new Racer({ + name: useDefault && name === undefined ? '수연' : name, + position: useDefault && position === undefined ? 0 : position, + }); + + return racer; +} From 3366cba5d0a6bb8e9a9df22d42b770b90bdb4853 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 20:27:39 +0900 Subject: [PATCH 30/34] =?UTF-8?q?feat:=20=EB=AA=A9=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/racer.test.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/__tests__/racer.test.js b/src/__tests__/racer.test.js index ec28f47..a982835 100644 --- a/src/__tests__/racer.test.js +++ b/src/__tests__/racer.test.js @@ -102,12 +102,11 @@ describe('Racer 클래스에 대한 단위 테스트', () => { ); }); -function makeRacerMockData(initializer, useDefault = true) { - const { name, position } = initializer ? initializer : {}; - +function makeRacerMockData(raceProperty) { const racer = new Racer({ - name: useDefault && name === undefined ? '수연' : name, - position: useDefault && position === undefined ? 0 : position, + name: '수연', + position: 0, + ...raceProperty, }); return racer; From 837047e53e7ec4d860f739a1341842f61d049cbb Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 20:53:47 +0900 Subject: [PATCH 31/34] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20Animal=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Animal.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/model/Animal.js diff --git a/src/model/Animal.js b/src/model/Animal.js new file mode 100644 index 0000000..f71da23 --- /dev/null +++ b/src/model/Animal.js @@ -0,0 +1,32 @@ +import Racer from './Racer'; + +export default class Animal extends Racer { + _sound; + + constructor({ name, position = 0, sound }) { + super({ name, position }); + this.sound = sound; + } + + get sound() { + return this._sound; + } + + set sound(sound) { + const soundError = Animal.getSoundError(sound); + + if (soundError !== undefined) { + throw new Error(soundError); + } + + this._sound = sound; + } + + static getSoundError(sound) { + if (typeof sound !== 'string') { + return 'sound 는 문자열 타입이어야 합니다.'; + } + + return undefined; + } +} From 71db771c77eb9cc3361eab4ccbc3dc0b0194d51e Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 20:54:03 +0900 Subject: [PATCH 32/34] =?UTF-8?q?feat:=20Racer=20=EC=99=80=20round=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=EB=A5=BC=20=EC=9D=BD?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=84=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Race.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/model/Race.js b/src/model/Race.js index baefe80..abb8ef5 100644 --- a/src/model/Race.js +++ b/src/model/Race.js @@ -3,12 +3,14 @@ import Rule from './Rule.js'; import RacerClass from './Racer.js'; export default class Race { + _Racer; + _round; #rule; #racers; constructor({ Racer, rules, names, round, onProceed }) { - this._Racer = Racer; - this._round = round; + this.Racer = Racer; + this.round = round; this.#rule = new Rule(rules); this.#racers = this.#makeRacers(names); this.onProceed = onProceed; @@ -121,7 +123,7 @@ export default class Race { const namesArr = names.split(','); - if (namesArr.length < 1 || namesArr > 10) { + if (namesArr.length < 1 || namesArr.length > 10) { return '레이서의 개수는 1 ~ 10 개여야 합니다'; } From fed37f97d123bb80746bea81838eb05445424809 Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 20:54:31 +0900 Subject: [PATCH 33/34] =?UTF-8?q?fix:=20rules=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=8D=BC=ED=8B=B0=20=EC=97=90=EB=9F=AC=EA=B2=80=EC=82=AC?= =?UTF-8?q?=EB=A5=BC=20=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/Rule.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/model/Rule.js b/src/model/Rule.js index cfac339..48c708d 100644 --- a/src/model/Rule.js +++ b/src/model/Rule.js @@ -6,9 +6,10 @@ export default class Rule { } #initRules(rules) { - if (rules === undefined) { - this.#rules = {}; - return; + const rulesError = Rule.getRulesError(rules); + + if (rulesError !== undefined) { + throw new Error(rulesError); } const ruleKeys = Object.keys(rules); @@ -100,7 +101,7 @@ export default class Rule { } static getRulesError(rules) { - if (typeof rules !== 'object') { + if (rules !== undefined && typeof rules !== 'object') { return '룰은 객체여야 합니다.'; } From 2dcc0c247b8f991ab57c3abf27293e44f68d061c Mon Sep 17 00:00:00 2001 From: Suyeon Date: Mon, 1 Jul 2024 21:00:29 +0900 Subject: [PATCH 34/34] =?UTF-8?q?feat:=20Race=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/race.test.js | 136 +++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/__tests__/race.test.js diff --git a/src/__tests__/race.test.js b/src/__tests__/race.test.js new file mode 100644 index 0000000..1ea4691 --- /dev/null +++ b/src/__tests__/race.test.js @@ -0,0 +1,136 @@ +import { describe, expect, test } from 'vitest'; +import Race from '../model/Race'; +import Car from '../model/Car'; +import gameSupport from '../utils/gameSupport'; +import Rule from '../model/Rule'; +import Animal from '../model/Animal'; + +describe('Race 클래스에 대한 단위 테스트 작성', () => { + test('Race 를 초기화 할때 Racer 의 서브클래스가 아닌 클래스를 Racer 프로퍼티에 전달하면 에러를 발생한다', () => { + expect(() => { + makeRaceMockData({ Racer: Rule }); + }).toThrow(); + }); + + test.each([ + { round: '으아' }, + { round: {} }, + { round: null }, + { round: undefined }, + { round: function round() {} }, + ])( + 'Race 를 초기화할 때 round 에 숫자 형태로 바뀔 수 잇는 값($round)이 아닌 값을 전달하면 에러를 발생한다.', + ({ round }) => { + expect(() => { + makeRaceMockData({ round }); + }).toThrow(); + }, + ); + + test.each([{ round: 0 }, { round: 11 }])( + 'Race 를 초기화할 때 round 에 1미만 10초과의 숫자($round)를 전달하면 에러가 난다.', + ({ round }) => { + expect(() => { + makeRaceMockData({ round }); + }).toThrow(); + }, + ); + + test.each([ + { names: undefined }, + { names: null }, + { names: {} }, + { names: function name() {} }, + { names: 123 }, + { names: [123, 124] }, + ])( + 'Race 초기화 시 names 에 문자열이 아닌 값($names)이 들어가면 에러가 발생한다.', + ({ names }) => { + expect(() => { + makeRaceMockData({ names }); + }).toThrow(); + }, + ); + + test.each([{ names: '' }, { names: '1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11' }])( + 'Race 초기화 시 names($names)을 콤마로 나눠 1명 미만 10명 초과의 레이서를 가지게 되면 에러가 발생한다.', + ({ names }) => { + expect(() => { + makeRaceMockData({ names }); + }).toThrow(); + }, + ); + + test.each([ + { rules: null }, + { rules: '' }, + { rules: { diceRule: () => 9 } }, + { rules: [] }, + { rules: function diceRule() {} }, + ])( + 'Race 초기화 시 rules 프로퍼티에 불리언값을 반환하는 함수를 값으로 갖는 객체($rules)로 이루어져있지 않다면 에러를 발생한다.', + ({ rules }) => { + expect(() => { + makeRaceMockData({ rules }); + }).toThrow(); + }, + ); + + test('round 프로퍼티를 통해 round 를 가져올 수 있다.', () => { + const race = makeRaceMockData({ round: 10 }); + + expect(race.round).toBe(10); + }); + + test('round 프로퍼티를 통해 round 를 설정할 수 있다.', () => { + const race = makeRaceMockData({ round: 10 }); + race.round = 6; + + expect(race.round).toBe(6); + }); + + test('Racer 프로퍼티를 통해 현재 Racer 에 해당하는 클래스를 가져올 수 있다.', () => { + const race = makeRaceMockData({ Racer: Car }); + + expect(race.Racer).toBe(Car); + }); + + test('Racer 프로퍼티를 통해 현재 Racer 에 해당하는 클래스를 가져올 수 있다.', () => { + const race = makeRaceMockData({ Racer: Car }); + race.Racer = Animal; + + expect(race.Racer).toBe(Animal); + }); + + test('getRacerState 메서드는 현재 레이서들의 이름과 포지션을 받아올 수 있다.', () => { + const names = ['a', 'b']; + const race = makeRaceMockData({ names: 'a, b' }); + const state = race.getRacerStatus(); + + expect( + state.every( + ({ name, position }) => names.includes(name) && position === 0, + ), + ).toBeTruthy(); + }); + + test('startRace 메서드는 레이스를 진행하고 우승자 이름 목록을 반환한다.', () => { + const race = makeRaceMockData({ names: 'a', rules: { win: () => true } }); + + expect(race.startRace()[0]).toBe('a'); + }); +}); + +function makeRaceMockData(raceProperty) { + const race = new Race({ + Racer: Car, + round: 5, + rules: { + diceRule: () => gameSupport.dice(0, 10) >= 4, + }, + names: '1, 2, 3', + ...raceProperty, + }); + + return race; +}