diff --git a/README.md b/README.md index b168a180..fcb68492 100644 --- a/README.md +++ b/README.md @@ -1 +1,86 @@ # javascript-planetlotto-precourse + +## ๐Ÿ“‹ ์š”๊ตฌ์‚ฌํ•ญ ๋ช…์„ธ์„œ + +- ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹œ `"๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."` ๋ฉ”์„ธ์ง€๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค. +- ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ `๋กœ๋˜ ๊ตฌ์ž… ๊ธˆ์•ก`์„ ์ž…๋ ฅ๋ฐ›๋Š”๋‹ค. `๋‹จ์œ„๋Š” 1,000์›`์ด๋ฉฐ ๋‚˜๋ˆ„์–ด ๋–จ์–ด์ง€์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ์˜ˆ์™ธ์ฒ˜๋ฆฌํ•œ๋‹ค. +- 500์› ๋‹จ์œ„๋กœ ์ž๋ฅธ ๊ฐœ์ˆ˜์ธ n์— ๋Œ€ํ•ด `"n๊ฐœ๋ฅผ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค."` ๋ฉ”์„ธ์ง€๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค. +- ์ดํ›„ `"๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."` ๋ฉ”์„ธ์ง€๋ฅผ ์ถœ๋ ฅํ•œ ํ›„, ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ `๋‹น์ฒจ ๋ฒˆํ˜ธ`๋ฅผ ์ž…๋ ฅ๋ฐ›๋Š”๋‹ค. ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” `์‰ผํ‘œ(,)`๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ๋ถ„ํ•œ๋‹ค. +- ์ดํ›„ `"๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."` ๋ฉ”์„ธ์ง€๋ฅผ ์ถœ๋ ฅํ•œ ํ›„, ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ `๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ`๋ฅผ ์ž…๋ ฅ๋ฐ›๋Š”๋‹ค. +- ์ž…๋ ฅ์ด ์™„๋ฃŒ๋˜๋ฉด `"๋‹น์ฒจํ†ต๊ณ„"` ๋ฉ”์„ธ์ง€์™€ `"---"`๋ฅผ ์ถœ๋ ฅํ•œ ํ›„, `๋‹น์ฒจ ๋‚ด์—ญ`์„ ์ถœ๋ ฅํ•œ๋‹ค. +- ๋‹น์ฒจ ๋‚ด์—ญ์€ 2๊ฐœ~5๊ฐœ ์ผ์น˜ ํ•ญ๋ชฉ์„ ์ˆœ์„œ๋Œ€๋กœ ๋‹ค์Œ ํ˜•์‹์— ๋งž์ถฐ ์ถœ๋ ฅํ•œ๋‹ค. (n์€ ์‹ค์ œ ๋‹น์ฒจ๋œ ๊ฐœ์ˆ˜) + - 5๊ฐœ ๋ฒˆํ˜ธ ์ผ์น˜ (100,000,000์›) - n๊ฐœ + - 4๊ฐœ ๋ฒˆํ˜ธ, ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ์ผ์น˜ (10,000,000์›) - n๊ฐœ + - 4๊ฐœ ๋ฒˆํ˜ธ ์ผ์น˜ (1,500,000์›) - n๊ฐœ + - 3๊ฐœ ๋ฒˆํ˜ธ ์ผ์น˜, ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ์ผ์น˜ (500,000์›) - n๊ฐœ + - 2๊ฐœ ๋ฒˆํ˜ธ ์ผ์น˜, ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ์ผ์น˜ (5,000์›) - n๊ฐœ + - 0๊ฐœ ๋ฒˆํ˜ธ ์ผ์น˜ (0์›) - n๊ฐœ +- ์˜ˆ์™ธ ์ƒํ™ฉ ์‹œ `[ERROR]`๋กœ ์‹œ์ž‘ํ•˜๋Š” ์—๋Ÿฌ ๋ฌธ๊ตฌ๋ฅผ ์ถœ๋ ฅํ•œ ํ›„, ํ•ด๋‹น ์ง€์ ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ž…๋ ฅ์„ ๋ฐ›๋Š”๋‹ค. + +## โš™๏ธ ๊ตฌํ˜„ํ•  ๊ธฐ๋Šฅ ๋ชฉ๋ก + +### LottoGameController.js + +#### 1๏ธโƒฃ ํ–‰์„ฑ๋กœ๋˜ ๊ฒŒ์ž„ ์ง„ํ–‰ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ + +- [x] ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•˜๋Š” ๊ฐ’๋“ค์€ ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋‹ค์‹œ ๋ฐ˜๋ณตํ•ด์„œ ์ž…๋ ฅ๋ฐ›๋Š”๋‹ค. + +### Lotto.js + +#### 1๏ธโƒฃ ๋กœ๋˜ ๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ธฐ๋Šฅ + +- [x] ๋กœ๋˜ ๋ฒˆํ˜ธ ๊ฐœ์ˆ˜๊ฐ€ 5๊ฐœ๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. +- [x] ๋กœ๋˜ ๋ฒˆํ˜ธ๊ฐ€ ์ •์ˆ˜๊ฐ€ ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. +- [x] ๋กœ๋˜ ๋ฒˆํ˜ธ๊ฐ€ ์ค‘๋ณต๋  ์‹œ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. +- [x] ๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” 1~30 ์‚ฌ์ด์˜ ์ˆซ์ž๊ฐ€ ์•„๋‹ ์‹œ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. + +#### 2๏ธโƒฃ ๋กœ๋˜ ๋ฒˆํ˜ธ ๋ฐ ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜ ๊ธฐ๋Šฅ + +- [x] ๋กœ๋˜ ๋ฒˆํ˜ธ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +- [x] ๋กœ๋˜ ๋ฒˆํ˜ธ๋ฅผ ์ •ํ•ด์ง„ ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### LottoGame.js + +#### 1๏ธโƒฃ ๋กœ๋˜ ๋ฐฐ์—ด ์ƒ์„ฑ ๊ธฐ๋Šฅ + +- [x] ๋กœ๋˜ ๊ตฌ์ž… ๊ธˆ์•ก์— ๋”ฐ๋ฅธ ๋กœ๋˜ ๊ฐœ์ˆ˜๋งŒํผ ๋กœ๋˜ ๊ฐ์ฒด์„ ๋งŒ๋“ค์–ด ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +#### 2๏ธโƒฃ ๋กœ๋˜ ๊ฐ์ฒด ์ƒ์„ฑ ๊ธฐ๋Šฅ + +- [x] ํ•˜๋‚˜์˜ ๋กœ๋˜ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +#### 3๏ธโƒฃ ํ†ต๊ณ„ ์ƒ์„ฑ ๊ธฐ๋Šฅ + +- [x] ๋กœ๋˜ ๋ฐฐ์—ด์˜ ๋ชจ๋“  ๋กœ๋˜์— ๋Œ€ํ•ด ๋“ฑ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ด์„œ ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +#### 4๏ธโƒฃ ๋กœ๋˜ ๋ฐฐ์—ด ๋ฐ˜ํ™˜ ๊ธฐ๋Šฅ + +- [x] ๋กœ๋˜ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### WinningLotto.js + +#### 1๏ธโƒฃ ๋“ฑ์ˆ˜ ํ™•์ธ ๊ธฐ๋Šฅ + +- [x] ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ๋น„๊ตํ•˜์—ฌ ๋“ฑ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ด์„œ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +#### 2๏ธโƒฃ ๋“ฑ์ˆ˜ ์ถ”์ถœ ๊ธฐ๋Šฅ + +- [x] ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ๊ฒน์น˜๋Š” ๋ฒˆํ˜ธ ์ˆ˜์™€ ๋ณด๋„ˆ์Šค ์—ฌ๋ถ€๋ฅผ ๋ฐ›์•„ ๋“ฑ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + +### ๐Ÿšซ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ + +- [x] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•  ๋•Œ ์ฝค๋งˆ(,) ์‚ฌ์ด์˜ ๊ณต๋ฐฑ์€ ๋ฌด์‹œํ•œ๋‹ค. +- [x] ์˜ˆ์™ธ ์ƒํ™ฉ ์‹œ `[ERROR]`๋กœ ์‹œ์ž‘ํ•˜๋Š” ์—๋Ÿฌ ๋ฌธ๊ตฌ๋ฅผ ์ถœ๋ ฅํ•œ ํ›„, ํ•ด๋‹น ์ง€์ ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ž…๋ ฅ์„ ๋ฐ›๋Š”๋‹ค. + +#### โŒ [ERROR] ์ฒ˜๋ฆฌ + +- [x] ๊ตฌ์ž… ๊ธˆ์•ก์€ 500์› ๋‹จ์œ„์˜ ์ˆซ์ž์—ฌ์•ผ ํ•œ๋‹ค. + +- [x] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” 5๊ฐœ์—ฌ์•ผ ํ•œ๋‹ค. +- [x] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ๋ชจ๋‘ ์ •์ˆ˜์—ฌ์•ผ ํ•œ๋‹ค. +- [x] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ์ค‘๋ณต๋  ์ˆ˜ ์—†๋‹ค. +- [x] ๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ๋ชจ๋‘ 1~30 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•œ๋‹ค. + +- [x] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ์ •์ˆ˜์—ฌ์•ผ ํ•œ๋‹ค. +- [x] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” 1~30 ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•œ๋‹ค. +- [x] ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋  ์ˆ˜ ์—†๋‹ค. diff --git a/__tests__/model/LottoGameTest.js b/__tests__/model/LottoGameTest.js new file mode 100644 index 00000000..62250fe9 --- /dev/null +++ b/__tests__/model/LottoGameTest.js @@ -0,0 +1,57 @@ +import { MissionUtils } from '@woowacourse/mission-utils'; + +import LottoGame from '../../src/model/LottoGame.js'; +import WinningLotto from '../../src/model/WinningLotto.js'; +import Lotto from '../../src/model/Lotto.js'; +import { RANK } from '../../src/constants/lottoConstants.js'; + +const mockRandoms = (numbers) => { + MissionUtils.Random.pickUniqueNumbersInRange = jest.fn(); + numbers.reduce((acc, number) => { + return acc.mockReturnValueOnce(number); + }, MissionUtils.Random.pickUniqueNumbersInRange); +}; + +describe('LottoGame ํด๋ž˜์Šค ํ…Œ์ŠคํŠธ', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + test('๊ตฌ๋งค ๊ธˆ์•ก์— ํ•ด๋‹นํ•˜๋Š” ๊ฐœ์ˆ˜๋งŒํผ ๋กœ๋˜๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•œ๋‹ค.', () => { + const purchaseAmount = 3000; + const game = new LottoGame(purchaseAmount); + const lottos = game.getLottos(); + + expect(lottos.length).toBe(6); + }); + + test('createWinningStatistics๋Š” ์˜ฌ๋ฐ”๋ฅธ ๋‹น์ฒจ ํ†ต๊ณ„๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•œ๋‹ค.', () => { + // given + const mockNumbers = [ + [1, 2, 3, 4, 5], // 1๋“ฑ (5๊ฐœ ์ผ์น˜) + [1, 2, 3, 4, 6], // 2๋“ฑ (4๊ฐœ ์ผ์น˜ + ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ์ผ์น˜ O) + [1, 2, 3, 4, 7], // 3๋“ฑ (4๊ฐœ ์ผ์น˜ + ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ์ผ์น˜ X) + ]; + mockRandoms(mockNumbers); + + const purchaseAmount = 1500; + const lottoGame = new LottoGame(purchaseAmount); + + const winningNumbers = [1, 2, 3, 4, 5]; + const bonusNumber = 6; + const winningLotto = new WinningLotto( + new Lotto(winningNumbers), + bonusNumber, + ); + + // when + const winningStatistics = lottoGame.createWinningStatistics(winningLotto); + + // then + expect(winningStatistics[RANK.FIRST]).toBe(1); + expect(winningStatistics[RANK.SECOND]).toBe(1); + expect(winningStatistics[RANK.THIRD]).toBe(1); + expect(winningStatistics[RANK.FOURTH]).toBe(0); + expect(winningStatistics[RANK.FIFTH]).toBe(0); + }); +}); diff --git a/__tests__/model/LottoTest.js b/__tests__/model/LottoTest.js new file mode 100644 index 00000000..d274cd20 --- /dev/null +++ b/__tests__/model/LottoTest.js @@ -0,0 +1,61 @@ +import ERROR_MESSAGES from '../../src/constants/errorMessages.js'; +import Lotto from '../../src/model/Lotto.js'; + +describe('Lotto ํด๋ž˜์Šค ํ…Œ์ŠคํŠธ', () => { + describe('๋กœ๋˜ ๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ', () => { + test.each([ + { + description: '๋ฒˆํ˜ธ๊ฐ€ 5๊ฐœ๊ฐ€ ์•„๋‹ ๋•Œ (6๊ฐœ)', + input: [1, 2, 3, 4, 5, 6], + expectedError: ERROR_MESSAGES.INVALID_NUMBERS_COUNT, + }, + { + description: '๋ฒˆํ˜ธ๊ฐ€ 5๊ฐœ๊ฐ€ ์•„๋‹ ๋•Œ (4๊ฐœ)', + input: [1, 2, 3, 4], + expectedError: ERROR_MESSAGES.INVALID_NUMBERS_COUNT, + }, + { + description: '๋ฒˆํ˜ธ์— ์ค‘๋ณต๋œ ์ˆซ์ž๊ฐ€ ์žˆ์„ ๋•Œ', + input: [1, 2, 3, 4, 4], + expectedError: ERROR_MESSAGES.DUPLICATE_NUMBERS, + }, + { + description: '๋ฒˆํ˜ธ์— ๋ฌธ์ž๊ฐ€ ์žˆ์„ ๋•Œ', + input: [1, 2, 3, 4, 'a'], + expectedError: ERROR_MESSAGES.NUMBERS_MUST_BE_INTEGER, + }, + { + description: '๋ฒˆํ˜ธ์— ์‹ค์ˆ˜๊ฐ€ ์žˆ์„ ๋•Œ', + input: [1, 2, 3, 4, 5.5], + expectedError: ERROR_MESSAGES.NUMBERS_MUST_BE_INTEGER, + }, + { + description: '๋ฒˆํ˜ธ๊ฐ€ 1~30 ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚  ๋•Œ (0)', + input: [0, 1, 2, 3, 4], + expectedError: ERROR_MESSAGES.OUT_OF_RANGE_NUMBERS, + }, + + [ + '๋ฒˆํ˜ธ๊ฐ€ 1~45 ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚  ๋•Œ (31)', + [1, 2, 3, 4, 31], + ERROR_MESSAGES.OUT_OF_RANGE_NUMBERS, + ], + ])('%s, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•œ๋‹ค.', ({ input, expectedError }) => { + expect(() => new Lotto(input)).toThrow(expectedError); + }); + }); + + describe('๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ', () => { + test('Lotto ๊ฐ์ฒด๋Š” ๋ฒˆํ˜ธ๋ฅผ ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜์—ฌ ์ €์žฅํ•œ๋‹ค.', () => { + const numbers = [5, 4, 3, 2, 1]; + const lotto = new Lotto(numbers); + expect(lotto.getNumbers()).toEqual([1, 2, 3, 4, 5]); + }); + + test("toString ๋ฉ”์„œ๋“œ๋Š” '[1, 2, 3, 4, 5]' ํ˜•์‹์˜ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.", () => { + const numbers = [1, 2, 3, 4, 5]; + const lotto = new Lotto(numbers); + expect(lotto.toString()).toEqual('[1, 2, 3, 4, 5]'); + }); + }); +}); diff --git a/__tests__/model/WinningLottoTest.js b/__tests__/model/WinningLottoTest.js new file mode 100644 index 00000000..7b60ac83 --- /dev/null +++ b/__tests__/model/WinningLottoTest.js @@ -0,0 +1,24 @@ +import Lotto from '../../src/model/Lotto.js'; +import WinningLotto from '../../src/model/WinningLotto.js'; +import { RANK } from '../../src/constants/lottoConstants.js'; + +describe('WinningLotto ํด๋ž˜์Šค ํ…Œ์ŠคํŠธ', () => { + const winningNumbers = [1, 2, 3, 4, 5]; + const bonusNumber = 6; + const winningLotto = new WinningLotto(new Lotto(winningNumbers), bonusNumber); + + test.each([ + ['1๋“ฑ(5๊ฐœ ์ผ์น˜)', new Lotto([1, 2, 3, 4, 5]), RANK.FIRST], + ['2๋“ฑ(4๊ฐœ + ๋ณด๋„ˆ์Šค ์ผ์น˜)', new Lotto([1, 2, 3, 4, 6]), RANK.SECOND], + ['3๋“ฑ(4๊ฐœ ์ผ์น˜)', new Lotto([1, 2, 3, 4, 7]), RANK.THIRD], + ['4๋“ฑ(3๊ฐœ + ๋ณด๋„ˆ์Šค ์ผ์น˜)', new Lotto([1, 2, 3, 6, 7]), RANK.FOURTH], + ['5๋“ฑ(2๊ฐœ + ๋ณด๋„ˆ์Šค ์ผ์น˜)', new Lotto([1, 2, 6, 7, 8]), RANK.FIFTH], + ['๊ฝ(0๊ฐœ ์ผ์น˜)', new Lotto([7, 8, 9, 10, 11]), RANK.ZERO], + ])( + '%s์ผ๋•Œ, ์˜ฌ๋ฐ”๋ฅธ ๋“ฑ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค.', + (description, userLotto, expectedRank) => { + const rank = winningLotto.match(userLotto); + expect(rank).toBe(expectedRank); + }, + ); +}); diff --git a/__tests__/utils/validateTest.js b/__tests__/utils/validateTest.js new file mode 100644 index 00000000..da51e130 --- /dev/null +++ b/__tests__/utils/validateTest.js @@ -0,0 +1,59 @@ +import Validate from '../../src/utils/validate.js'; +import Lotto from '../../src/model/Lotto.js'; +import ERROR_MESSAGES from '../../src/constants/errorMessages.js'; + +describe('Validate ํ…Œ์ŠคํŠธ', () => { + describe('validatePurchaseAmount ํ…Œ์ŠคํŠธ', () => { + test.each([ + ['์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ(abc)', 'abc'], + ['500์› ๋‹จ์œ„๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ(750์›)', 750], + ['์ˆซ์ž์™€ ๋ฌธ์ž๊ฐ€ ์„ž์—ฌ์žˆ๋Š” ๊ฒฝ์šฐ(500j)', '500j'], + ])('%s, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•œ๋‹ค.', (description, amount) => { + expect(() => Validate.validatePurchaseAmount(amount)).toThrow( + ERROR_MESSAGES.INVALID_PURCHASE_AMOUNT, + ); + }); + + test('์œ ํšจํ•œ ๊ตฌ๋งค ๊ธˆ์•ก์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.', () => { + expect(() => Validate.validatePurchaseAmount(3000)).not.toThrow(); + }); + }); + + describe('validateBonusNumber ํ…Œ์ŠคํŠธ', () => { + test.each([ + ['์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ', 'a', ERROR_MESSAGES.BONUS_NUMBER_MUST_BE_INTEGER], + ['๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚œ ๊ฒฝ์šฐ (0)', 0, ERROR_MESSAGES.OUT_OF_RANGE_BONUS_NUMBER], + ['๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚œ ๊ฒฝ์šฐ (31)', 31, ERROR_MESSAGES.OUT_OF_RANGE_BONUS_NUMBER], + ])('%s, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•œ๋‹ค.', (description, number, expectedError) => { + expect(() => Validate.validateBonusNumber(number)).toThrow(expectedError); + }); + + test('์œ ํšจํ•œ ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ์ธ ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.', () => { + expect(() => Validate.validateBonusNumber(6)).not.toThrow(); + }); + }); + + describe('validateWinningNumbersAndBonusNumber ํ…Œ์ŠคํŠธ', () => { + test('๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๊ฐ€ ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋  ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•œ๋‹ค.', () => { + const winningNumberLotto = new Lotto([1, 2, 3, 4, 5]); + const bonusNumber = 5; + expect(() => + Validate.validateWinningNumbersAndBonusNumber( + winningNumberLotto, + bonusNumber, + ), + ).toThrow(ERROR_MESSAGES.DUPLICATE_WINNING_NUMBERS_AND_BONUS_NUMBER); + }); + + test('๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๊ฐ€ ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.', () => { + const winningNumberLotto = new Lotto([1, 2, 3, 4, 5]); + const bonusNumber = 6; + expect(() => + Validate.validateWinningNumbersAndBonusNumber( + winningNumberLotto, + bonusNumber, + ), + ).not.toThrow(); + }); + }); +}); diff --git a/src/App.js b/src/App.js index 091aa0a5..7cbc317b 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,10 @@ +import LottoGameController from './controller/LottoGameController.js'; + class App { - async run() {} + async run() { + const lottoGameController = new LottoGameController(); + await lottoGameController.play(); + } } export default App; diff --git a/src/constants/errorMessages.js b/src/constants/errorMessages.js new file mode 100644 index 00000000..5eebe2b0 --- /dev/null +++ b/src/constants/errorMessages.js @@ -0,0 +1,23 @@ +import { LOTTO_RULES } from './lottoConstants.js'; + +const ERROR_MESSAGES = Object.freeze({ + DUPLICATE_WINNING_NUMBERS_AND_BONUS_NUMBER: + '๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.', + + INVALID_PURCHASE_AMOUNT: `๊ตฌ์ž… ๊ธˆ์•ก์€ ${LOTTO_RULES.PRICE}์› ๋‹จ์œ„์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + + INVALID_NUMBERS_COUNT: `๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ${LOTTO_RULES.NUMBER_COUNT}๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + NUMBERS_MUST_BE_INTEGER: '๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ๋ชจ๋‘ ์ •์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค', + DUPLICATE_NUMBERS: '๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.', + OUT_OF_RANGE_NUMBERS: `๋กœ๋˜ ๋ฒˆํ˜ธ๋Š” ${LOTTO_RULES.MIN_NUMBER}๋ถ€ํ„ฐ ${LOTTO_RULES.MAX_NUMBER} ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + + INVALID_WINNING_NUMBERS_COUNT: `๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ${LOTTO_RULES.NUMBER_COUNT}๊ฐœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + WINNING_NUMBERS_MUST_BE_INTEGER: '๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ๋ชจ๋‘ ์ •์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค', + DUPLICATE_WINNING_NUMBERS: '๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ์ค‘๋ณต๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.', + OUT_OF_RANGE_WINNING_NUMBERS: `๋‹น์ฒจ ๋ฒˆํ˜ธ๋Š” ${LOTTO_RULES.MIN_NUMBER}๋ถ€ํ„ฐ ${LOTTO_RULES.MAX_NUMBER} ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, + + BONUS_NUMBER_MUST_BE_INTEGER: '๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ์ •์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.', + OUT_OF_RANGE_BONUS_NUMBER: `๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ${LOTTO_RULES.MIN_NUMBER}๋ถ€ํ„ฐ ${LOTTO_RULES.MAX_NUMBER} ์‚ฌ์ด์˜ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.`, +}); + +export default ERROR_MESSAGES; diff --git a/src/constants/lottoConstants.js b/src/constants/lottoConstants.js new file mode 100644 index 00000000..9da0410f --- /dev/null +++ b/src/constants/lottoConstants.js @@ -0,0 +1,15 @@ +export const RANK = Object.freeze({ + FIRST: 1, + SECOND: 2, + THIRD: 3, + FOURTH: 4, + FIFTH: 5, + ZERO: 0, +}); + +export const LOTTO_RULES = Object.freeze({ + MIN_NUMBER: 1, + MAX_NUMBER: 30, + NUMBER_COUNT: 5, + PRICE: 500, +}); diff --git a/src/controller/LottoGameController.js b/src/controller/LottoGameController.js new file mode 100644 index 00000000..5bf29507 --- /dev/null +++ b/src/controller/LottoGameController.js @@ -0,0 +1,65 @@ +import LottoGame from '../model/LottoGame.js'; +import { InputView, OutputView } from './../view.js'; +import Lotto from '../model/Lotto.js'; +import WinningLotto from '../model/WinningLotto.js'; +import Validate from '../utils/validate.js'; + +class LottoGameController { + async play() { + let purchaseAmount; + let winningNumberLotto; + let bonusNumber; + + while (true) { + try { + purchaseAmount = await InputView.askAmount(); + + break; + } catch (error) { + OutputView.printErrorMessage(error); + } + } + // ๊ตฌ์ž… ๊ธˆ์•ก๋งŒํผ ๋กœ๋˜๋ฅผ ์ƒ์„ฑํ•œ LottoGame ๊ฐ์ฒด ์ƒ์„ฑ + const lottoGame = new LottoGame(purchaseAmount); + // ๊ตฌ์ž…ํ•œ ๋กœ๋˜๋“ค ๋ชจ๋‘ ์ถœ๋ ฅ + OutputView.printPurchasedLottos(lottoGame.getLottoNumbers()); + + while (true) { + try { + let winningNumbers = await InputView.askWinningLotto(); + // ๋‹น์ฒจ ๋ฒˆํ˜ธ์ธ Lotto ๊ฐ์ฒด ํ•˜๋‚˜ ์ƒ์„ฑ + winningNumberLotto = new Lotto(winningNumbers); + + break; + } catch (error) { + OutputView.printErrorMessage(error); + } + } + + while (true) { + try { + bonusNumber = await InputView.askBonusNumber(); + + // ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ถ”๊ฐ€ + Validate.validateWinningNumbersAndBonusNumber( + winningNumberLotto, + bonusNumber, + ); + + break; + } catch (error) { + OutputView.printErrorMessage(error); + } + } + + // ๋‹น์ฒจ ๋ฒˆํ˜ธ์™€ ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋กœ WinningLotto ๊ฐ์ฒด ์ƒ์„ฑ + const winningLotto = new WinningLotto(winningNumberLotto, bonusNumber); + // ๋“ฑ์ˆ˜ ํ†ต๊ณ„ ๊ฐ์ฒด + const rankCounts = lottoGame.createWinningStatistics(winningLotto); + + // ์ตœ์ข… ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ์ถœ๋ ฅ + await OutputView.printResult(lottoGame.createPriceMap(rankCounts)); + } +} + +export default LottoGameController; diff --git a/src/model/Lotto.js b/src/model/Lotto.js new file mode 100644 index 00000000..830d6d15 --- /dev/null +++ b/src/model/Lotto.js @@ -0,0 +1,43 @@ +import ERROR_MESSAGES from '../constants/errorMessages.js'; +import { LOTTO_RULES } from '../constants/lottoConstants.js'; + +class Lotto { + #numbers; // ๋กœ๋˜ ํ•œ ์žฅ์€ 5๊ฐœ์˜ ๋ฒˆํ˜ธ ๋ฐฐ์—ด์„ ๊ฐ€์ง + + // ๋กœ๋˜๋ฅผ ์ƒ์„ฑํ•  ๋•Œ๋Š” ์ž๋™์œผ๋กœ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ & ์ •๋ ฌ ์ˆ˜ํ–‰ + constructor(numbers) { + this.#validate(numbers); + numbers.sort((a, b) => a - b); + this.#numbers = numbers; + } + + #validate(numbers) { + if (numbers.length !== LOTTO_RULES.NUMBER_COUNT) { + throw new Error(ERROR_MESSAGES.INVALID_NUMBERS_COUNT); + } + if (numbers.some((number) => !Number.isInteger(number))) { + throw new Error(ERROR_MESSAGES.NUMBERS_MUST_BE_INTEGER); + } + if (new Set(numbers).size !== LOTTO_RULES.NUMBER_COUNT) { + throw new Error(ERROR_MESSAGES.DUPLICATE_NUMBERS); + } + if ( + numbers.some( + (n) => n < LOTTO_RULES.MIN_NUMBER || n > LOTTO_RULES.MAX_NUMBER, + ) + ) { + throw new Error(ERROR_MESSAGES.OUT_OF_RANGE_NUMBERS); + } + } + + // ์ถœ๋ ฅ์„ ์œ„ํ•œ ๋ฉ”์„œ๋“œ + toString() { + return `[${this.#numbers.join(', ')}]`; + } + // ๋ฒˆํ˜ธ๋ฅผ ๋ฐฐ์—ด๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ + getNumbers() { + return [...this.#numbers]; + } +} + +export default Lotto; diff --git a/src/model/LottoGame.js b/src/model/LottoGame.js new file mode 100644 index 00000000..0e36010f --- /dev/null +++ b/src/model/LottoGame.js @@ -0,0 +1,76 @@ +import { Random } from '@woowacourse/mission-utils'; + +import Lotto from './Lotto.js'; +import { LOTTO_RULES, RANK } from '../constants/lottoConstants.js'; + +class LottoGame { + #purchaseAmount; // ๊ตฌ์ž… ๊ธˆ์•ก + #lottos; // ๋กœ๋˜ ๋ฐฐ์—ด + + constructor(purchaseAmount) { + this.#purchaseAmount = purchaseAmount; + this.#lottos = this.#createLottos(); + } + + // ๊ตฌ์ž… ๊ธˆ์•ก๋งŒํผ ๋กœ๋˜๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + #createLottos() { + const gameCount = this.#purchaseAmount / LOTTO_RULES.PRICE; + const lottos = []; + for (let i = 0; i < gameCount; i++) { + lottos.push(this.#createOneLotto()); + } + return lottos; + } + + // ๋กœ๋˜ ํ•œ ์žฅ์„ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + #createOneLotto() { + // 1~30 ์‚ฌ์ด์˜ ๊ฐ’ ์ค‘ 5๊ฐœ๋ฅผ ๋ฌด์ž‘์œ„๋กœ ์ƒ์„ฑ + const numbers = Random.pickUniqueNumbersInRange( + LOTTO_RULES.MIN_NUMBER, + LOTTO_RULES.MAX_NUMBER, + LOTTO_RULES.NUMBER_COUNT, + ); + numbers.sort((a, b) => a - b); // ์ •๋ ฌ + return new Lotto(numbers); // ์ •๋ ฌ๋œ ์ˆซ์ž ๋ฐฐ์—ด๋กœ ์ƒˆ ๋กœ๋˜ ์ƒ์„ฑ + } + + createWinningStatistics(winningLotto) { + const rankCounts = { + [RANK.FIRST]: 0, + [RANK.SECOND]: 0, + [RANK.THIRD]: 0, + [RANK.FOURTH]: 0, + [RANK.FIFTH]: 0, + [RANK.ZERO]: 0, + }; + + this.#lottos.forEach((lotto) => { + const rank = winningLotto.match(lotto); + rankCounts[rank] += 1; + }); + + return rankCounts; + } + + createPriceMap(rankCounts) { + const priceMap = new Map(); + for (const [key, value] of Object.entries(rankCounts)) { + priceMap.set(Number(key), value); + } + + return priceMap; + } + + getLottos() { + return [...this.#lottos]; + } + getLottoNumbers() { + let line = []; + this.#lottos.forEach((lotto) => { + line.push(lotto.getNumbers()); + }); + return line; + } +} + +export default LottoGame; diff --git a/src/model/WinningLotto.js b/src/model/WinningLotto.js new file mode 100644 index 00000000..e7570395 --- /dev/null +++ b/src/model/WinningLotto.js @@ -0,0 +1,40 @@ +import { RANK } from '../constants/lottoConstants.js'; + +class WinningLotto { + // ๋‹น์ฒจ ๋ฒˆํ˜ธ ๋กœ๋˜ + #winningNumberLotto; + // ๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ + #bonusNumber; + + constructor(winningNumberLotto, bonusNumber) { + this.#winningNumberLotto = winningNumberLotto; + this.#bonusNumber = bonusNumber; + } + + // ๋กœ๋˜ ํ•œ ์žฅ์„ ์ธ์ž๋กœ ๋ฐ›์•„ ๋‹น์ฒจ ๋กœ๋˜์™€ ๋น„๊ตํ•˜์—ฌ rank๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฉ”์„œ๋“œ + match(lotto) { + const lottoNumbers = lotto.getNumbers(); + const winningNumbersSet = new Set(this.#winningNumberLotto.getNumbers()); + + // 1. ๋‹น์ฒจ ๋ฒˆํ˜ธ set์ด ์ธ์ž๋กœ ๋ฐ›์€ ๋กœ๋˜ ๋ฒˆํ˜ธ๋ฅผ ๋น„๊ตํ•œ ๋ฐฐ์—ด์˜ ๊ธธ์ด = ๋งค์น˜๋œ ๊ฐœ์ˆ˜ + const matchCount = lottoNumbers.filter((number) => + winningNumbersSet.has(number), + ).length; + // 2. ๋ณด๋„ˆ์Šค ์—ฌ๋ถ€ + const hasBonus = lottoNumbers.includes(this.#bonusNumber); + + return this.#extractRank(matchCount, hasBonus); + } + + // ๋งค์น˜๋œ ๊ฐœ์ˆ˜์™€ ๋ณด๋„ˆ์Šค ์—ฌ๋ถ€๋ฅผ ์ธ์ž๋กœ ๋ฐ›์•„ rank๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฉ”์„œ๋“œ + #extractRank(matchCount, hasBonus) { + if (matchCount === 5) return RANK.FIRST; + if (matchCount === 4 && hasBonus) return RANK.SECOND; + if (matchCount === 4) return RANK.THIRD; + if (matchCount === 3 && hasBonus) return RANK.FOURTH; + if (matchCount === 2 && hasBonus) return RANK.FIFTH; + return RANK.ZERO; + } +} + +export default WinningLotto; diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 00000000..d6259f44 --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,39 @@ +import ERROR_MESSAGES from '../constants/errorMessages.js'; + +const Validate = { + validatePurchaseAmount(amount) { + if (isNaN(amount) || amount % 500 !== 0 || amount === 0) { + throw new Error(ERROR_MESSAGES.INVALID_PURCHASE_AMOUNT); + } + }, + validateWinningNumbers(numbers) { + if (numbers.length !== 5) { + throw new Error(ERROR_MESSAGES.INVALID_WINNING_NUMBERS_COUNT); + } + if (numbers.some((number) => !Number.isInteger(number))) { + throw new Error(ERROR_MESSAGES.WINNING_NUMBERS_MUST_BE_INTEGER); + } + if (new Set(numbers).size !== 5) { + throw new Error(ERROR_MESSAGES.DUPLICATE_WINNING_NUMBERS); + } + if (numbers.some((n) => n < 1 || n > 30)) { + throw new Error(ERROR_MESSAGES.OUT_OF_RANGE_WINNING_NUMBERS); + } + }, + validateBonusNumber(number) { + if (isNaN(number) || !Number.isInteger(Number(number))) + throw new Error(ERROR_MESSAGES.BONUS_NUMBER_MUST_BE_INTEGER); + if (number < 1 || number > 30) { + throw new Error(ERROR_MESSAGES.OUT_OF_RANGE_BONUS_NUMBER); + } + }, + + validateWinningNumbersAndBonusNumber(winningNumberLotto, bonusNumber) { + if (winningNumberLotto.getNumbers().includes(bonusNumber)) + throw new Error( + ERROR_MESSAGES.DUPLICATE_WINNING_NUMBERS_AND_BONUS_NUMBER, + ); + }, +}; + +export default Validate; diff --git a/src/view.js b/src/view.js index ae6afd9c..c89c4ce3 100644 --- a/src/view.js +++ b/src/view.js @@ -1,15 +1,22 @@ -import { MissionUtils } from "@woowacourse/mission-utils"; +import { MissionUtils } from '@woowacourse/mission-utils'; +import Validate from './utils/validate.js'; const InputView = { /** * @returns {number} */ async askAmount() { - const input = await MissionUtils.Console.readLineAsync('๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n'); + const input = await MissionUtils.Console.readLineAsync( + '๊ตฌ์ž…๊ธˆ์•ก์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n', + ); + + Validate.validatePurchaseAmount(input); // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ถ”๊ฐ€ + const num = parseInt(input, 10); if (Number.isNaN(num)) { throw new Error('๊ตฌ๋งค๊ธˆ์•ก์€ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.'); } + return num; }, @@ -17,7 +24,12 @@ const InputView = { * @returns {number[]} */ async askWinningLotto() { - const input = await MissionUtils.Console.readLineAsync('์ง€๋‚œ ์ฃผ ๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n'); + const input = await MissionUtils.Console.readLineAsync( + '์ง€๋‚œ ์ฃผ ๋‹น์ฒจ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n', + ); + + const numbers = input.split(',').map(Number); + Validate.validateWinningNumbers(numbers); // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ถ”๊ฐ€ return input .replaceAll(' ', '') @@ -35,7 +47,12 @@ const InputView = { * @returns {number} */ async askBonusNumber() { - const input = await MissionUtils.Console.readLineAsync('๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n'); + const input = await MissionUtils.Console.readLineAsync( + '๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.\n', + ); + + Validate.validateBonusNumber(input); // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ถ”๊ฐ€ + const num = parseInt(input, 10); if (Number.isNaN(num)) { throw new Error('๋ณด๋„ˆ์Šค ๋ฒˆํ˜ธ๋Š” ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.'); @@ -51,7 +68,7 @@ const OutputView = { printPurchasedLottos(lottos) { const lines = [ `${lottos.length}๊ฐœ๋ฅผ ๊ตฌ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค.`, - ...lottos.map(lotto => `[${lotto.join(', ')}]`), + ...lottos.map((lotto) => `[${lotto.join(', ')}]`), ]; MissionUtils.Console.print(lines.join('\n')); },