diff --git a/__tests__/Lotto.test.js b/__tests__/Lotto.test.js index e34f651a43..4b338cc726 100644 --- a/__tests__/Lotto.test.js +++ b/__tests__/Lotto.test.js @@ -10,7 +10,7 @@ describe('Lotto 클래스 입니다.', () => { [7, 6, 5, 3, 2, 1], [1, 2, 3, 5, 6, 7], ], - ])('로또 배열을 정렬한다.', (numbers, expected) => { + ])('생성할 때 인자로 받은 로또 배열을 정렬한다.', (numbers, expected) => { const lotto = new Lotto(numbers); expect(lotto.getNumbers()).toEqual(expected); }); diff --git a/__tests__/LottoMachine.test.js b/__tests__/LottoMachine.test.js new file mode 100644 index 0000000000..a826a5e69d --- /dev/null +++ b/__tests__/LottoMachine.test.js @@ -0,0 +1,15 @@ +import LottoMachine from '../src/domain/LottoMachine'; + +describe('LottoMachine 클래스입니다.', () => { + const lottoMachine = new LottoMachine(1000); + test.each([ + [1000, 1], + [2000, 2], + [3000, 3], + ])('금액을 받으면 구매해야 할 올바른 수량 반환한다.', (money, expected) => { + expect(lottoMachine.calcLottoAmount(money)).toEqual(expected); + }); + test.each([1001, 10015, 34678])('잘못된 금액을 받으면 에러를 반환한다.', (money) => { + expect(() => lottoMachine.calcLottoAmount(money)).toThrow(); + }); +}); diff --git a/__tests__/LottoRank.test.js b/__tests__/LottoRank.test.js new file mode 100644 index 0000000000..9f48153402 --- /dev/null +++ b/__tests__/LottoRank.test.js @@ -0,0 +1,42 @@ +/* eslint-disable max-params */ +import LottoRank from '../src/domain/LottoRank'; + +describe('LottoRank 객체입니다.', () => { + const winRankByMatchCount = (matchCount, hasBonus, expected) => { + expect(LottoRank.getWinRank(matchCount, hasBonus)).toEqual(expected); + }; + + test.each([ + [0, true, LottoRank.PRIZE.MISS.RANK], + [0, false, LottoRank.PRIZE.MISS.RANK], + [1, true, LottoRank.PRIZE.MISS.RANK], + [1, false, LottoRank.PRIZE.MISS.RANK], + [2, true, LottoRank.PRIZE.MISS.RANK], + [2, false, LottoRank.PRIZE.MISS.RANK], + ])('로또등수가 낙첨인지 판단', winRankByMatchCount); + + test.each([ + [3, true, LottoRank.PRIZE.FIFTH.RANK], + [3, false, LottoRank.PRIZE.FIFTH.RANK], + ])('로또 등수가 5등인지 판단(3개 일치)', winRankByMatchCount); + + test.each([ + [4, true, LottoRank.PRIZE.FOURTH.RANK], + [4, false, LottoRank.PRIZE.FOURTH.RANK], + ])('로또 등수가 4등인지 판단(4개 일치)', winRankByMatchCount); + + test.each([[5, false, LottoRank.PRIZE.THIRD.RANK]])( + '로또 등수가 3등인지 판단(5개 일치)', + winRankByMatchCount, + ); + + test.each([[5, true, LottoRank.PRIZE.SECOND.RANK]])( + '로또 등수가 2등인지 판단(5개 일치 + 보너스볼 일치)', + winRankByMatchCount, + ); + + test.each([[6, false, LottoRank.PRIZE.FIRST.RANK]])( + '로또 등수가 1등인지 판단(6개 일치)', + winRankByMatchCount, + ); +}); diff --git a/__tests__/validation.test.js b/__tests__/validation.test.js index 6858b06cc4..b00073098c 100644 --- a/__tests__/validation.test.js +++ b/__tests__/validation.test.js @@ -1,18 +1,42 @@ -import { isPositiveInteger, isValidRestartCommand } from '../src/validation'; +import { + isPositiveInteger, + isValidRestartCommand, + isValidLottoNumber, + isDuplicateNumbers, +} from '../src/validation'; describe('유효성 검증 테스트입니다.', () => { - test.each([1.11, null, undefined, 'string', {}])( - '양의 정수가 아니면 false를 반환한다.', - (value) => { - expect(isPositiveInteger(Number(value))).toBeFalsy(); - }, - ); + test.each([1.11, -1, 2.3, 3.1])('양의 정수가 아니면 false를 반환한다.', (value) => { + expect(isPositiveInteger(Number(value))).toBeFalsy(); + }); test.each(['y', 'n'])('재시작 입력에서 y와 n를 받으면 true를 반환한다.', (input) => { expect(isValidRestartCommand(input)).toBeTruthy(); }); - test.each([1, '1'])('재시작 입력에서 y와 n인 아닌 입력을 받으면 false를 반환한다.', (input) => { - expect(isValidRestartCommand(input)).toBeFalsy(); + test.each(['df', 's', 'w', 'a', 'gg'])( + '재시작 입력에서 y와 n인 아닌 입력을 받으면 false를 반환한다.', + (input) => { + expect(isValidRestartCommand(input)).toBeFalsy(); + }, + ); + + test.each([1, 2, 3, 4, 34, 44, 45])('유효한 로또 숫자이면 true를 반환한다.', (number) => { + expect(isValidLottoNumber(number)).toBeTruthy(); + }); + + test.each([-1, 0, 46, 47])('유효한 로또 숫자가 아니면 false를 반환한다.', (number) => { + expect(isValidLottoNumber(number)).toBeFalsy(); + }); + + test.each([ + [[2, 1, 2, 3, 4, 5]], + [[1, 1, 2, 3, 4, 5, 6]], + [[5, 6, 7, 8, 44, 44]], + [[1, 44, 45, 42, 43, 44]], + [[1, 13, 8, 16, 4, 12, 15, 16]], + [[10, 11, 12, 13, 14]], + ])('로또 번호가 중복되었는지와 길이(갯수)가 유효한지 검사한다.', (numbers) => { + expect(isDuplicateNumbers(numbers)).toBeTruthy(); }); }); diff --git a/index.html b/index.html index e083bc1cbb..3f6b03a0d7 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,89 @@ + 🎱 행운의 로또 - Document -
-

🎱 행운의 로또

+ - +
🎱 행운의 로또
+
+
+

🎱 내 번호 당첨 확인 🎱

+
+

구입할 금액을 입력해주세요.

+ + +
+
+

지난 주 당첨번호 6개와 보너스 번호 1개를 입력해주세요.

+
+ 당첨번호 + 보너스 번호 +
+
+
+ + + + + + +
+
+ +
+
+ +
+
+
+ - + + + \ No newline at end of file diff --git a/package.json b/package.json index 9543cc455b..db709dda0a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "test": "jest --watch --no-cache", "build-step1": "webpack --config step1.config.js", "start-step1": "npm run build-step1 && node dist/step1-bundle.js", - "start-step2": "webpack serve --open --config step2.config.js" + "start-step2": "webpack serve --open --config step2.config.js", + "build-step2": "webpack --mode production --config step2.config.js" }, "devDependencies": { "@babel/cli": "^7.20.7", diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000000..794dc60068 --- /dev/null +++ b/public/style.css @@ -0,0 +1,196 @@ +:root { + --color-primary: #4E5BA6; + --color-gray-scale-1: #FFFFFF; + --color-gray-scale-2: #FCFDFD; + --color-gray-scale-3: #B4B4B4; + --color-gray-scale-4: #8B8B8B; + --color-gray-scale-5: #000000; + --color-border: rgba(0, 0, 0, 0.12); + --button-height: 36px; + --extra-bold: 800; + --bold: 700; + --semi-bold: 600; + --medium: 500; +} + +body, h3, p { + margin: 0; +} + +body { + width: 100%; + height: 100%; +} + +header { + background: var(--color-primary); + color: var(--color-gray-scale-1); + font: var(--extra-bold) 24px system-ui; + padding: 14px 30px; +} + +th, td { + padding: 6px 0; + border-bottom: 1px solid var(--color-border); +} + +main { + display: flex; + justify-content: center; + align-items: center; + flex: 1; +} + +button { + border: 0; + color: var(--color-gray-scale-1); + background: var(--color-primary); + border-radius: 4px; + cursor: pointer; +} + +#app { + display: flex; + flex-direction: column; + width: 414px; + height: 727px; + padding: 0 16px; + border: 1px solid var(--color-border); + border-radius: 10px; + margin: 40px 0; +} + +.lotto-subtitle { + margin: 36px 0; + font: var(--bold) 24px system-ui; + text-align: center; +} + +.payments-input { + width: 310px; + height: 30px; + border: 1px solid var(--color-border); + border-radius: 4px; + margin: 10px 0; +} + +.payments-btn { + height: var(--button-height); + width: 56px; + margin: 0 0 0 16px; +} + +.payments-btn:disabled { + background-color: var(--color-gray-scale-4); + cursor:not-allowed; +} + +.lottos-container { + margin: 10px 0; + max-height: 300px; + overflow-y: scroll; +} + +.lotto-numbers{ + margin: 7px 0; +} + +.winning-lotto-container { + visibility: hidden; +} + +.winning-lotto-notice { + margin: 10px 0; +} + +.winning-lotto-title-container { + display: flex; + justify-content: space-between; + margin: 10px 0; +} + +.winning-numbers-input-container { + display: flex; + justify-content: space-between; +} + +.winning-number-input, .bonus-number-input { + height: var(--button-height); + width: 34px; + font: var(--medium) 15px system-ui; + text-align: center; + border: 1px solid var(--color-border); + border-radius: 4px; +} + +.modal { + display: flex; + visibility: hidden; + position: absolute; + flex: 1; + justify-content: center; + align-items: center; + width: inherit; + height: inherit; + background: var(--color-border); +} + +.modal-body { + display: flex; + flex-direction: column; + align-items: center; +} + +.result-table { + width: 319px; + text-align: center; + border-collapse: collapse; + border-top: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); +} + +.profit-rate { + margin: 37px 0; + font: var(--semi-bold) 16px system-ui; +} + +.modal-close-btn { + margin: 12px 0 0 360px; + background: var(--color-gray-scale-1); + color: var(--color-gray-scale-5); + font: var(--medium) 24px system-ui; + cursor: pointer; +} + +.win-result-container { + background: var(--color-gray-scale-1); + border-radius: 10px; + width: 400px; + height: 500px; +} + +.modal-restart-btn { + width: 318px; + height: var(--button-height); +} + +footer { + display: flex; + justify-content: center; + align-items: center; + flex: 1; + border-top: 1px solid var(--color-border); + color: var(--color-primary); + font: bold 14px system-ui; + padding: 14px 0; +} + +.result-btn { + width: 414px; + height: var(--button-height); + margin: 50px 0 0 0; +} +.result-btn:disabled { + background-color: var(--color-gray-scale-4); + cursor:not-allowed; +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 66a0020712..d872d357f5 100644 --- a/src/App.js +++ b/src/App.js @@ -11,7 +11,7 @@ class App { } async play() { - await this.#lottoController.readBuyMoney(); + await this.#lottoController.readPayments(); this.#lottoController.printBuyLottos(); await this.#lottoController.readWinNumbers(); await this.#lottoController.readBonusNumber(); diff --git a/src/LottoController.js b/src/LottoController.js deleted file mode 100644 index 2667d45372..0000000000 --- a/src/LottoController.js +++ /dev/null @@ -1,48 +0,0 @@ -import LottoMachine from './domain/LottoMachine'; -import InputView from './view/console/InputView'; -import OutputView from './view/console/OutputView'; - -class LottoController { - #lottoMachine; - - async readBuyMoney() { - try { - const money = await InputView.readBuyMoney(); - this.#lottoMachine = new LottoMachine(money); - } catch (error) { - OutputView.printErrorMsg(error.message); - await this.readBuyMoney(); - } - } - - printBuyLottos() { - const lottoNumbers = this.#lottoMachine.getLottoNumbers(); - OutputView.printBuyLottos(lottoNumbers); - } - - async readWinNumbers() { - try { - const winNumbers = await InputView.readWinNumbers(); - this.#lottoMachine.generateWinningLotto(winNumbers); - } catch (error) { - OutputView.printErrorMsg(error.message); - await this.readWinNumbers(); - } - } - - async readBonusNumber() { - try { - const bonusNumber = await InputView.readBonusNumber(); - this.#lottoMachine.setBonusNumber(bonusNumber); - } catch (error) { - OutputView.printErrorMsg(error.message); - await this.readBonusNumber(); - } - } - - printWinStatistics() { - OutputView.printWinStatistics(this.#lottoMachine.calcStatstics()); - } -} - -export default LottoController; diff --git a/src/LottoMeditator.js b/src/LottoMeditator.js new file mode 100644 index 0000000000..cee72c2da6 --- /dev/null +++ b/src/LottoMeditator.js @@ -0,0 +1,27 @@ +import LottoMachine from './domain/LottoMachine'; + +class LottoMeditator { + #lottoMachine; + + receivePaymentsInput(payments) { + this.#lottoMachine = new LottoMachine(payments); + } + + receiveWinningLottoNumbersInput(winningNumbers) { + this.#lottoMachine.generateWinningLotto(winningNumbers); + } + + receiveBonusNumberInput(bonusNumber) { + this.#lottoMachine.setBonusNumber(bonusNumber); + } + + sendLottoNumbers() { + return this.#lottoMachine.getLottoNumbers(); + } + + sendStatstics() { + return this.#lottoMachine.calcStatstics(); + } +} + +export default LottoMeditator; diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index 66730cba1c..1393f70c77 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -1,38 +1,15 @@ -import { isPositiveInteger } from '../validation'; -import { - LOTTO_NUMBER_SIZE, - LOTTO_NUMBER_RANGE_MIN, - LOTTO_NUMBER_RANGE_MAX, -} from '../util/constants'; +import { isValidLottoNumbers, isDuplicateNumbers } from '../validation'; +import { ERROR_INVALID, ERROR_DUPLICATE } from '../util/constants'; class Lotto { - static ERROR_INVALID = '잘못된 입력입니다.'; - static ERROR_DUPLICATE = '중복된 입력입니다.'; - - static isValidLottoNumber(number) { - return isPositiveInteger(number) || this.isValidLottoNumberRange(number); - } - - static isValidLottoNumberRange(number) { - return number >= LOTTO_NUMBER_RANGE_MIN && number <= LOTTO_NUMBER_RANGE_MAX; - } - - static isDuplicateNumbers(numbers) { - return new Set(numbers).size !== LOTTO_NUMBER_SIZE || numbers.length !== LOTTO_NUMBER_SIZE; - } - - static isValidLottoNumbers(numbers) { - return numbers.every(Lotto.isValidLottoNumber); - } - #numbers = []; constructor(numbers) { - if (!Lotto.isValidLottoNumbers(numbers)) { - throw new Error(Lotto.ERROR_INVALID); + if (!isValidLottoNumbers(numbers)) { + throw new Error(ERROR_INVALID); } - if (Lotto.isDuplicateNumbers(numbers)) { - throw new Error(Lotto.ERROR_DUPLICATE); + if (isDuplicateNumbers(numbers)) { + throw new Error(ERROR_DUPLICATE); } this.#numbers = numbers.sort((a, b) => a - b); diff --git a/src/domain/LottoMachine.js b/src/domain/LottoMachine.js index 7878208005..aacc374f7e 100644 --- a/src/domain/LottoMachine.js +++ b/src/domain/LottoMachine.js @@ -1,22 +1,31 @@ -import RandomGenerator from '../RandomGenerator'; -import { LOTTO_NUMBER_RANGE_MAX, LOTTO_NUMBER_SIZE } from '../util/constants'; +import RandomGenerator from '../util/RandomGenerator'; +import { + LOTTO_COST, + WIN_PRIZE_MONEY, + ERROR_INVALID_AMOUNT, + LOTTO_NUMBER_RANGE_MAX, + LOTTO_NUMBER_SIZE, + MISS, + FIRST, + SECOND, + THIRD, + FOURTH, + FIFTH, +} from '../util/constants'; import { isPositiveInteger } from '../validation'; import Lotto from './Lotto'; import WinningLotto from './WinningLotto'; class LottoMachine { - static LOTTO_COST = 1000; - static WIN_PRIZE_MONEY = { 0: 0, 1: 2000000000, 2: 30000000, 3: 1500000, 4: 50000, 5: 5000 }; - static INVALID_AMOUNT_ERROR = '유효하지 않은 금액입니다.'; #lottos; #winningLotto; #winCount = { - 0: 0, - 1: 0, - 2: 0, - 3: 0, - 4: 0, - 5: 0, + [MISS]: 0, + [FIRST]: 0, + [SECOND]: 0, + [THIRD]: 0, + [FOURTH]: 0, + [FIFTH]: 0, }; constructor(money) { @@ -24,8 +33,8 @@ class LottoMachine { } calcLottoAmount(money) { - const lottoAmount = money / LottoMachine.LOTTO_COST; - if (!isPositiveInteger(lottoAmount)) throw new Error(LottoMachine.INVALID_AMOUNT_ERROR); + const lottoAmount = money / LOTTO_COST; + if (!isPositiveInteger(lottoAmount)) throw new Error(ERROR_INVALID_AMOUNT); return lottoAmount; } @@ -68,13 +77,13 @@ class LottoMachine { return { winCount: this.#winCount, profitRate, - winPrizeMoney: LottoMachine.WIN_PRIZE_MONEY, + winPrizeMoney: WIN_PRIZE_MONEY, }; } calcProfitRate(prizes) { - const totalWinMoney = prizes.reduce((acc, cur) => acc + LottoMachine.WIN_PRIZE_MONEY[cur], 0); - return totalWinMoney / (this.#lottos.length * LottoMachine.LOTTO_COST); + const totalWinMoney = prizes.reduce((acc, cur) => acc + WIN_PRIZE_MONEY[cur], 0); + return totalWinMoney / (this.#lottos.length * LOTTO_COST); } } diff --git a/src/domain/WinningLotto.js b/src/domain/WinningLotto.js index bd9a3d7a89..3d742df389 100644 --- a/src/domain/WinningLotto.js +++ b/src/domain/WinningLotto.js @@ -1,10 +1,8 @@ -import Lotto from './Lotto'; import LottoRank from './LottoRank'; +import { ERROR_INVALID, ERROR_BONUS_DUPLICATE } from '../util/constants'; +import { isValidLottoNumber } from '../validation'; class WinningLotto { - static ERROR_INVALID = '잘못된 입력입니다.'; - static ERROR_DUPLICATE = '보너스 번호는 당첨 번호와 중복될 수 없습니다.'; - #lotto; #bonusNumber; @@ -13,11 +11,11 @@ class WinningLotto { } setBonusNumber(bonusNumber) { - if (!Lotto.isValidLottoNumber(bonusNumber)) { - throw new Error(WinningLotto.ERROR_INVALID); + if (!isValidLottoNumber(bonusNumber)) { + throw new Error(ERROR_INVALID); } if (this.isDuplicateBonus(bonusNumber)) { - throw new Error(WinningLotto.ERROR_DUPLICATE); + throw new Error(ERROR_BONUS_DUPLICATE); } this.#bonusNumber = bonusNumber; } diff --git a/src/step2-index.js b/src/step2-index.js index f1527d9448..97c6c8fbe3 100644 --- a/src/step2-index.js +++ b/src/step2-index.js @@ -1,4 +1,79 @@ -/** - * step 2의 시작점이 되는 파일입니다. - * 노드 환경에서 사용하는 readline 등을 불러올 경우 정상적으로 빌드할 수 없습니다. - */ +/* eslint-disable no-undef */ +import { renderLottosContainer, renderResultTable } from './view/render'; +import '../public/style.css'; +import LottoMeditator from './LottoMeditator'; + +const modal = document.querySelector('.modal'); +const paymentsContainer = document.querySelector('.payments-container'); +const winningLottoContainer = document.querySelector('.winning-lotto-container'); + +const paymentsBtn = document.querySelector('.payments-btn'); +const resultBtn = document.querySelector('.result-btn'); +const modalRestartBtn = document.querySelector('.modal-restart-btn'); +const webController = new LottoMeditator(); + +const handlePayments = () => { + const paymentsInput = document.querySelector('.payments-input'); + const payments = Number(paymentsInput.value); + + webController.receivePaymentsInput(payments); +}; + +const changeCSSByPaymentsEvent = () => { + paymentsBtn.disabled = true; + resultBtn.disabled = false; + winningLottoContainer.style.visibility = 'visible'; +}; + +const resetPaymentsInput = () => { + const paymentsInput = document.querySelector('.payments-input'); + paymentsInput.value = ''; +}; + +const handleWinningLottos = () => { + const winningNumberInputs = document.querySelectorAll('.winning-number-input'); + const bonusNumberInput = document.querySelector('.bonus-number-input'); + + const winningNumbers = Array.from(winningNumberInputs, (input) => Number(input.value)); + webController.receiveWinningLottoNumbersInput(winningNumbers); + + const bonusNumber = Number(bonusNumberInput.value); + webController.receiveBonusNumberInput(bonusNumber); +}; + +const changeCSSByResultBtnEvent = () => { + resultBtn.disabled = true; + modal.style.visibility = 'visible'; +}; + +paymentsContainer.addEventListener('submit', (e) => { + e.preventDefault(); + try { + handlePayments(); + renderLottosContainer(webController.sendLottoNumbers()); + changeCSSByPaymentsEvent(); + } catch (error) { + window.alert(error.message); + resetPaymentsInput(); + } +}); + +winningLottoContainer.addEventListener('submit', (e) => { + e.preventDefault(); + try { + handleWinningLottos(); + renderResultTable(webController.sendStatstics()); + changeCSSByResultBtnEvent(); + } catch (error) { + window.alert(error.message); + } +}); + +const modalCloseBtn = modal.querySelector('.modal-close-btn'); +modalCloseBtn.addEventListener('click', () => { + modal.style.visibility = 'hidden'; +}); + +modalRestartBtn.addEventListener('click', () => { + window.location.reload(); +}); diff --git a/src/RandomGenerator.js b/src/util/RandomGenerator.js similarity index 100% rename from src/RandomGenerator.js rename to src/util/RandomGenerator.js diff --git a/src/util/constants.js b/src/util/constants.js index b7fc5d7a8d..0bf7fa0ff6 100644 --- a/src/util/constants.js +++ b/src/util/constants.js @@ -3,3 +3,22 @@ export const LOTTO_NUMBER_RANGE_MIN = 1; export const LOTTO_NUMBER_RANGE_MAX = 45; export const RESTART_COMMAND = 'y'; export const EXIT_COMMAND = 'n'; +export const ERROR_INVALID = '잘못된 입력입니다.'; +export const ERROR_DUPLICATE = '중복된 입력입니다.'; +export const ERROR_BONUS_DUPLICATE = '보너스 번호는 당첨 번호와 중복될 수 없습니다.'; +export const ERROR_INVALID_AMOUNT = '유효하지 않은 금액입니다.'; +export const MISS = 0; +export const FIRST = 1; +export const SECOND = 2; +export const THIRD = 3; +export const FOURTH = 4; +export const FIFTH = 5; +export const LOTTO_COST = 1000; +export const WIN_PRIZE_MONEY = { + [MISS]: 0, + [FIRST]: 2000000000, + [SECOND]: 30000000, + [THIRD]: 1500000, + [FOURTH]: 50000, + [FIFTH]: 5000, +}; diff --git a/src/validation.js b/src/validation.js index 2b40f9c4b0..3b13b9c1c1 100644 --- a/src/validation.js +++ b/src/validation.js @@ -1,6 +1,29 @@ -const isInteger = (value) => Number.isInteger(value); +import { + LOTTO_NUMBER_RANGE_MIN, + LOTTO_NUMBER_RANGE_MAX, + LOTTO_NUMBER_SIZE, + RESTART_COMMAND, + EXIT_COMMAND, +} from './util/constants'; + const isPositiveNumber = (number) => number > 0; +const isValidLottoNumberRange = (number) => { + return number >= LOTTO_NUMBER_RANGE_MIN && number <= LOTTO_NUMBER_RANGE_MAX; +}; + +export const isPositiveInteger = (value) => Number.isInteger(value) && isPositiveNumber(value); + +export const isValidRestartCommand = (command) => + command === RESTART_COMMAND || command === EXIT_COMMAND; + +export const isValidLottoNumber = (number) => { + return isPositiveInteger(number) && isValidLottoNumberRange(number); +}; -export const isPositiveInteger = (value) => isInteger(value) && isPositiveNumber(value); +export const isDuplicateNumbers = (numbers) => { + return new Set(numbers).size !== LOTTO_NUMBER_SIZE || numbers.length !== LOTTO_NUMBER_SIZE; +}; -export const isValidRestartCommand = (command) => command === 'y' || command === 'n'; +export const isValidLottoNumbers = (numbers) => { + return numbers.every(isValidLottoNumber); +}; diff --git a/src/view/console/Console.js b/src/view/console/Console.js deleted file mode 100644 index cd6b8f16e2..0000000000 --- a/src/view/console/Console.js +++ /dev/null @@ -1,25 +0,0 @@ -import * as readline from 'node:readline/promises'; -import { stdin as input, stdout as output } from 'node:process'; - -const rl = readline.createInterface({ input, output }); - -const Console = { - async read(query) { - const answer = await rl.question(`> ${query}`); - return answer; - }, - - print(text) { - console.log(text); - }, - - printError(message) { - console.error(message); - }, - - close() { - rl.close(); - }, -}; - -export default Console; diff --git a/src/view/console/InputView.js b/src/view/console/InputView.js deleted file mode 100644 index c7ecc6de33..0000000000 --- a/src/view/console/InputView.js +++ /dev/null @@ -1,40 +0,0 @@ -import { isValidRestartCommand } from '../../validation'; -import Console from './Console'; -import OutputView from './OutputView'; - -const InputView = { - BUY_MONEY_QUERY: '구입금액을 입력해 주세요.', - WIN_NUMBERS_QUERY: '당첨 번호를 입력해 주세요.', - BONUS_QUERY: '보너스 번호를 입력해 주세요.', - RESTART_QUERY: '다시 시작 하겠습니까? (y/n)', - INVALID_COMMAND_ERROR: '잘못된 명령어입니다.', - - async readBuyMoney() { - const buyMoney = await Console.read(InputView.BUY_MONEY_QUERY); - return Number(buyMoney); - }, - - async readWinNumbers() { - const winNumbers = await Console.read(InputView.WIN_NUMBERS_QUERY); - return winNumbers.split(',').map(Number); - }, - - async readBonusNumber() { - const bonusNumber = await Console.read(InputView.BONUS_QUERY); - return Number(bonusNumber); - }, - - async readRestartCommand() { - const command = await Console.read(InputView.RESTART_QUERY); - try { - if (!isValidRestartCommand(command)) { - throw new Error(this.INVALID_COMMAND_ERROR); - } - } catch (error) { - OutputView.printErrorMsg(error.message); - } - return command; - }, -}; - -export default InputView; diff --git a/src/view/console/OutputView.js b/src/view/console/OutputView.js deleted file mode 100644 index 5c85dd5f8f..0000000000 --- a/src/view/console/OutputView.js +++ /dev/null @@ -1,66 +0,0 @@ -import Console from './Console'; - -const OutputView = { - WIN_TITLE: '당첨 통계\n--------------------', - WIN_CONDITION: { - 1: '6개 일치', - 2: '5개 일치, 보너스 볼 일치', - 3: '5개 일치', - 4: '4개 일치', - 5: '3개 일치', - }, - - generateNumbersMessage(lotto) { - return `[${lotto.join(', ')}]`; - }, - - generateBuyAmountMessage(amount) { - return `${amount}개를 구매했습니다.`; - }, - - generateProfitRateMessage(profitRate) { - return `총 수익률은 ${profitRate.toFixed(2)}%입니다.`; - }, - // to-do : 메서드명 나중에 변경하기 - printBuyLottos(lottos) { - Console.print(this.generateBuyAmountMessage(lottos.length)); - lottos.forEach((lotto) => { - Console.print(OutputView.generateNumbersMessage(lotto)); - }); - }, - - printWinTitle() { - Console.print(OutputView.WIN_TITLE); - }, - - generateWinPrizeMoneyMessage(winPrizeMoney, rank) { - return `(${winPrizeMoney[rank].toLocaleString('ko-KR')}원)`; - }, - - generateWinCountMessage(winCount, rank) { - return `${winCount[rank]}개`; - }, - - printWinStatistics({ winCount, winPrizeMoney, profitRate }) { - this.printWinTitle(); - const rankLength = 5; - const results = Array.from({ length: rankLength }, (_, i) => { - return `${this.WIN_CONDITION[rankLength - i]} ${this.generateWinPrizeMoneyMessage( - winPrizeMoney, - rankLength - i, - )} - ${this.generateWinCountMessage(winCount, rankLength - i)}`; - }); - results.forEach((result) => Console.print(result)); - this.printProfitRate(profitRate); - }, - - printProfitRate(profitRate) { - Console.print(this.generateProfitRateMessage(profitRate)); - }, - - printErrorMsg(message) { - console.error(`[ERROR]:${message}`); - }, -}; - -export default OutputView; diff --git a/src/view/render.js b/src/view/render.js new file mode 100644 index 0000000000..25d968fe71 --- /dev/null +++ b/src/view/render.js @@ -0,0 +1,59 @@ +/* eslint-disable no-undef */ +const paymentsContainer = document.querySelector('.payments-container'); + +const renderLottoListTitle = (amount) => { + const title = document.createElement('p'); + title.innerText = `총 ${amount}개를 구매했습니다.`; + + paymentsContainer.append(title); +}; + +const renderLottoList = (lottoNumbers) => { + const lottosContainer = document.createElement('section'); + lottosContainer.classList.add('lottos-container'); + + lottoNumbers.forEach((lottoNumber) => { + const lottoElement = document.createElement('div'); + const lottoNumberElement = document.createElement('p'); + + lottoElement.classList.add('lotto-numbers'); + lottoNumberElement.innerText = `🎟️ ${lottoNumber.join(', ')}`; + + lottoElement.append(lottoNumberElement); + lottosContainer.append(lottoElement); + }); + paymentsContainer.after(lottosContainer); +}; + +export const renderLottosContainer = (lottoNumbers) => { + renderLottoListTitle(lottoNumbers.length); + renderLottoList(lottoNumbers); +}; + +const renderHitLottoCount = (winCount) => { + const resultTableBody = document.querySelector('.result-table-body'); + const tableRows = resultTableBody.querySelectorAll('tr'); + + tableRows.forEach((tr, index) => { + const td = document.createElement('td'); + const rank = 5 - index; + + td.innerText = `${winCount[rank]}개`; + tr.append(td); + }); +}; + +const renderProfitRate = (profitRate) => { + const resultTable = document.querySelector('.result-table'); + const resultProfitRate = document.createElement('p'); + + resultProfitRate.innerText = `당신의 총 수익률은 ${profitRate.toFixed(2)}% 입니다.`; + resultProfitRate.classList.add('profit-rate'); + + resultTable.after(resultProfitRate); +}; + +export const renderResultTable = ({ winCount, profitRate }) => { + renderHitLottoCount(winCount); + renderProfitRate(profitRate); +};