diff --git a/.eslintrc.js b/.eslintrc.js index 6682b83093..96400bec4a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,7 +14,7 @@ module.exports = { rules: { 'no-var': 'error', 'max-depth': ['error', 2], - 'max-lines-per-function': ['error', 15], + 'max-lines-per-function': ['error', 20], 'no-console': 'warn', 'no-param-reassign': 'error', 'padding-line-between-statements': 0, @@ -22,5 +22,6 @@ module.exports = { 'no-undefined': 0, 'no-constant-condition': 0, 'no-unused-private-class-members': 0, + 'lines-between-class-members': 0, }, }; diff --git a/docs/todo.md b/docs/todo.md index b3f3764c5c..dbadc1d0e5 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -19,3 +19,5 @@ - [✅] 로또 게임 모델에 금액이 정상적으로 입력되면, 구매할 수 있는 로또의 수를 반환할 수 있어야 한다. - [✅] 금액은 1000이상의 숫자여야한다. - [✅] 로또 번호 배열들을 입력하여 로또 모델을 생성하고 관리할 수 있어야 한다. +- [✅] 당첨 결과를 이용하여 당첨 통계와 수익률을 반환할 수 있어야 한다. +- [✅] 게임 초기화가 가능해야 한다. diff --git a/images/modal_close_button.png b/images/modal_close_button.png new file mode 100644 index 0000000000..8afcd0dda9 Binary files /dev/null and b/images/modal_close_button.png differ diff --git a/index.html b/index.html index 295fc452b2..a517a43b63 100644 --- a/index.html +++ b/index.html @@ -11,14 +11,14 @@

🎱 행운의 로또

금액을 입력하는 섹션입니다.

-

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

+

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

-
+

구매한 로또를 확인하는 섹션입니다.

@@ -35,24 +35,30 @@

구매한 로또를 확인하는 섹션입

-
+

당첨 번호 입력 섹션

-

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

+

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

당첨 번호

- - - - - - + + + + + +

보너스 번호

- +
@@ -60,6 +66,49 @@

당첨 번호 입력 섹션

+ +
+
+ +
+
diff --git a/src/css/index.css b/src/css/index.css index daca1a1306..626613cdae 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -4,26 +4,44 @@ input[type='number']::-webkit-inner-spin-button { margin: 0; } +input:focus { + border: 1px solid #00bcd4; + outline: 1px solid #00bcd4; +} + +.invalid { + background-color: #ffb0cd; + border: 1px solid #ff6464 !important; + outline: 1px solid #ff6464 !important; +} + input { - outline: none; + background-color: #ffffff; + outline: 1px solid #aaaaaa; + border: 1px solid #dddddd; +} + +label { + cursor: pointer; } body { - width: 98vw; - height: 90vh; - margin: auto; + width: 100%; + min-height: 100vh; + margin: 0; background-color: rgba(0, 0, 0, 0.07); font-family: 'NanumBarunGothic', sans-serif; - + font-size: 1.1rem; display: flex; justify-content: center; align-items: center; + overflow: overlay; } #app { width: 25vw; min-width: 414px; - padding: 50px; + padding: 20px; background-color: #ffffff; border: 1px solid rgba(0, 0, 0, 0.12); @@ -42,25 +60,42 @@ body { #charge-input { flex: 1; margin-right: 20px; - border: 1px solid #b4b4b4; border-radius: 4px; } +#charge-button { + width: 15%; +} + button { border-radius: 4px; background-color: #00bcd4; color: #ffffff; border: 0; min-height: 36px; + font-weight: bold; +} + +button:hover { + cursor: pointer; + background-color: #26daf1; } section { margin-bottom: 20px; } -#lotto-section { +#lotto-section[data-visible-state='false'] { + display: flex; + visibility: hidden; + height: 0vh; +} + +#lotto-section[data-visible-state='true'] { display: flex; flex-direction: row; + visibility: visible; + transition: height 0.5s; } .lotto-wrapper { @@ -77,10 +112,12 @@ section { #lotto-container { display: flex; + flex-wrap: wrap; } + #lotto-container[data-visible-state='false'] { flex-direction: row; - gap: 10px; + gap: 3px; } #lotto-container[data-visible-state='false'] .number { @@ -97,26 +134,49 @@ section { gap: 10px; } +#win-number-input-section[data-visible-state='false'] { + visibility: hidden; + opacity: 0; + height: 0; +} + +#win-number-input-section[data-visible-state='true'] { + margin-top: 40px; + opacity: 1; + transition: opacity 1s; +} + +.win-number-input-notice { + margin-bottom: 0; +} + .win-number-input-wrapper { display: flex; gap: 10px; justify-content: space-between; } +.win-number-input-wrapper > div > input { + text-align: center; +} + #result-button { width: 100%; margin-top: 10px; + padding: 13px; } .win-number-input-wrapper input { width: 30px; height: 30px; + font-size: 1.3rem; } .bonus-number-wrapper { display: flex; align-items: flex-end; flex-direction: column; + margin-bottom: 20px; } #align-converter-container { diff --git a/src/css/modal.css b/src/css/modal.css new file mode 100644 index 0000000000..09e7b94495 --- /dev/null +++ b/src/css/modal.css @@ -0,0 +1,70 @@ +#result-modal-area[data-visible-state='false'] { + display: none; +} + +#result-modal-area[data-visible-state='true'] { + width: 100vw; + height: 100vh; + position: fixed; + left: 0; + top: 0; +} + +.background-area { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); +} + +.modal { + background: #ffffff; + width: 25vw; + border-radius: 4px; + margin: auto; + text-align: center; + padding: 1rem; +} + +.winning-statistics-table { + padding: 1rem; +} + +.winning-statistics-table > li { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 10px; + border-bottom: 1px solid #dcdcdc; +} + +.table-head { + border-top: 1px solid #dcdcdc; + font-weight: bold; +} + +#modal-close-button { + width: 14px; + min-height: 14px; + background: no-repeat; + background-image: url('../../images/modal_close_button.png'); + position: relative; + right: -49%; + top: 0; +} + +.modal-title { + margin-top: 0; +} + +#earning-rate-notice { + font-weight: bold; + font-size: 1.2rem; +} + +#replay-button { + font-size: 1rem; + margin: 1rem 0 2rem 0; + padding: 0.8rem 2.5rem; +} diff --git a/src/index.js b/src/index.js index fddec6c039..57d0d77574 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ import './css/index.css'; import './css/converter.css'; +import './css/modal.css'; import './css/nanumbarungothic.css'; -import './js/utils/customPrototypeMethod'; import RacingGameManager from './js/app'; export default new RacingGameManager().init(); diff --git a/src/js/__tests__/lotto.test.js b/src/js/__tests__/lotto.test.js index 3e74f9abd7..0092aebcd9 100644 --- a/src/js/__tests__/lotto.test.js +++ b/src/js/__tests__/lotto.test.js @@ -1,4 +1,8 @@ +/* eslint-disable max-lines-per-function */ +/* eslint-disable no-undef */ + import { ERROR_MESSAGE } from '../constants/errorMessage'; +import { NUMBER } from '../constants/number'; import Lotto from '../models/Lotto'; describe('로또 모델 테스트', () => { @@ -15,4 +19,16 @@ describe('로또 모델 테스트', () => { expect(message).toEqual(ERROR_MESSAGE.LOTTO_NUMBER_IS_INVALIDATE); } }); + + it('로또 모델의 번호와 당첨 번호를 비교하여 등수를 반환할 수 있어야 한다.', () => { + const lottoNumbers = [1, 2, 3, 4, 5, 6]; + const winningFirstNumbers = [1, 2, 3, 4, 5, 6, 7]; + const winningSecondNumbers = [1, 2, 3, 4, 5, 7, 6]; + const notWinningNumbers = [7, 8, 9, 10, 11, 12, 13]; + + const lotto = Lotto.create(lottoNumbers); + expect(lotto.result(winningFirstNumbers)).toBe(NUMBER.FIRST_GRADE_INDEX); + expect(lotto.result(winningSecondNumbers)).toBe(NUMBER.SECOND_GRADE_INDEX); + expect(lotto.result(notWinningNumbers)).toBe(NUMBER.NOT_WINNING_INDEX); + }); }); diff --git a/src/js/__tests__/lottoGame.test.js b/src/js/__tests__/lottoGame.test.js index 52ed9d55c9..bfe1325e93 100644 --- a/src/js/__tests__/lottoGame.test.js +++ b/src/js/__tests__/lottoGame.test.js @@ -1,44 +1,87 @@ -import '../utils/customPrototypeMethod'; +/* eslint-disable max-lines-per-function */ +/* eslint-disable no-undef */ + import { ERROR_MESSAGE } from '../constants/errorMessage'; -import LottoGame from '../models/LottoGame'; +import LottoRound from '../models/LottoRound'; +import Lotto from '../models/Lotto'; + +const charge = 5000; describe('로또 게임 모델 테스트', () => { it('로또 게임 모델에 금액이 정상적으로 입력되면, 구매할 수 있는 로또의 수를 반환할 수 있어야 한다.', () => { - const lottoGame = new LottoGame(); - const charge = 5000; + const lottoRound = new LottoRound(); const expectedAvailableLottoAmount = 5; - const availableLottoAmount = lottoGame.exchangeChargeToLottoAmount(charge); + const availableLottoAmount = lottoRound.exchangeChargeToLottoAmount(charge); expect(availableLottoAmount).toBe(expectedAvailableLottoAmount); }); it('금액은 1000이상의 숫자여야한다.', () => { - const lottoGame = new LottoGame(); + const lottoRound = new LottoRound(); const lessThanLottoPriceCharge = 500; + try { - lottoGame.exchangeChargeToLottoAmount(lessThanLottoPriceCharge); + lottoRound.exchangeChargeToLottoAmount(lessThanLottoPriceCharge); } catch ({ message }) { - expect(message).toEqual(ERROR_MESSAGE.CHARGE_IS_INVALIDATE); + expect(message).toEqual(ERROR_MESSAGE.CHARGE_IS_NOT_ENOUGH); + } + }); + + it('금액은 1000으로 나누어 떨어지는 숫자여야한다.', () => { + const lottoRound = new LottoRound(); + const notDivisibleCharge = 1500; + + try { + lottoRound.exchangeChargeToLottoAmount(notDivisibleCharge); + } catch ({ message }) { + expect(message).toEqual(ERROR_MESSAGE.CHARGE_IS_NOT_DIVISIBLE); } }); it('로또 번호 배열들을 입력하여 로또 모델을 생성하고 관리할 수 있어야 한다.', () => { - const lottoGame = new LottoGame(); - const charge = 5000; - const availableLottoAmount = lottoGame.exchangeChargeToLottoAmount(charge); + const lottoRound = new LottoRound(); + const availableLottoAmount = lottoRound.exchangeChargeToLottoAmount(charge); + + lottoRound.createLottoList(charge); + + expect(lottoRound.lottoList.length).toBe(availableLottoAmount); + }); + + it('당첨 결과를 이용하여 당첨 통계와 수익률을 반환할 수 있어야 한다.', () => { + const lottoRound = new LottoRound(); + const lottoList = []; + const winningNumbers = [1, 2, 3, 4, 5, 6, 10]; + const firstGradeCount = 1; + const secondGradeCount = 0; + const thirdGradeCount = 1; + const fourthGradeCount = 1; + const fifthGradeCount = 0; + const earningRate = 66718233; + const notWinningCount = 0; + const expectResult = [ + firstGradeCount, + secondGradeCount, + thirdGradeCount, + fourthGradeCount, + fifthGradeCount, + earningRate, + notWinningCount, + ]; - lottoGame.createLottoList(charge); + lottoList.push(Lotto.create([1, 2, 3, 4, 5, 6])); + lottoList.push(Lotto.create([1, 2, 3, 4, 5, 7])); + lottoList.push(Lotto.create([1, 2, 3, 4, 7, 8])); - expect(lottoGame.lottoList.length).toBe(availableLottoAmount); + lottoRound.lottoList = lottoList; + expect(lottoRound.getRoundResult(winningNumbers)).toEqual(expectResult); }); - /** 이 부분이 lottoGame의 테스트인지, 유틸 함수에 대한 테스트인지 궁금하다. */ - it('lottoList의 getter는 깊게 복사된 값을 반환한다.', () => { - const lottoGame = new LottoGame(); - const charge = 5000; + it('게임 초기화가 가능해야 한다.', () => { + const lottoRound = new LottoRound(); + const initializedLottoRound = new LottoRound(); - lottoGame.createLottoList(charge); + lottoRound.createLottoList(charge); + lottoRound.initialize(); - const lottoListFromGetterFunc = lottoGame.getLottoList(); - expect(lottoListFromGetterFunc).toEqual(lottoGame.lottoList); + expect(lottoRound).toEqual(initializedLottoRound); }); }); diff --git a/src/js/app.js b/src/js/app.js index b7e254c30c..6648bcac9f 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -1,19 +1,26 @@ -import LottoGameModel from './models/LottoGame'; +import LottoRoundModel from './models/LottoRound'; import { SELECTOR } from './constants/selector'; -import LottoGameView from './views'; +import LottoRoundView from './views'; import { findElement } from './utils/elementSelector'; +import { isNotValidNumber } from './utils/validator'; -class LottoGameManager { - init() { - this.lottoGameModel = new LottoGameModel(); - this.lottoGameView = new LottoGameView(); - - this.$chargeForm = findElement(SELECTOR.CHARGE_INPUT_FORM); - this.$chargeInput = findElement(SELECTOR.CHARGE_INPUT); - this.$alignConverter = findElement(SELECTOR.ALIGN_CONVERTER); +class LottoRoundManager { + lottoRoundModel = new LottoRoundModel(); + lottoRoundView = new LottoRoundView(); + $chargeForm = findElement(SELECTOR.CHARGE_INPUT_FORM); + $chargeInput = findElement(SELECTOR.CHARGE_INPUT); + $alignConverter = findElement(SELECTOR.ALIGN_CONVERTER); + $winNumberForm = findElement(SELECTOR.WIN_NUMBER_INPUT_FORM); + $modalCloseButton = findElement(SELECTOR.MODAL_CLOSE_BUTTON); + $replayButton = findElement(SELECTOR.REPLAY_BUTTON); + init() { this.$chargeForm.addEventListener('submit', this.onSubmitChargeInputForm); this.$alignConverter.addEventListener('change', this.onChangeAlignState); + this.$winNumberForm.addEventListener('submit', this.onSubmitWinNumberInputForm); + this.$winNumberForm.addEventListener('input', this.onInputWinNumberForm); + this.$replayButton.addEventListener('click', this.onClickReplayButton); + this.$modalCloseButton.addEventListener('click', this.onClickCloseModalButton); } onSubmitChargeInputForm = (e) => { @@ -21,23 +28,74 @@ class LottoGameManager { try { const { value: chargeInputStr } = this.$chargeInput; const chargeInput = Number(chargeInputStr); - this.triggerChargeInputAction(chargeInput); - } catch ({ message }) { + this.lottoRoundModel.createLottoList(chargeInput); + + const lottoList = this.lottoRoundModel.getLottoList(); + this.lottoRoundView.renderLottoSection(lottoList); + this.lottoRoundView.renderWinNumberInputSection(true); + } catch (message) { alert(message); } }; - triggerChargeInputAction(chargeInput) { - // mutate model - this.lottoGameModel.createLottoList(chargeInput); - // mutate view by new model state - const lottoList = this.lottoGameModel.getLottoList(); - this.lottoGameView.renderLottoSection(lottoList); - } - onChangeAlignState = (e) => { const { checked: alignState } = e.target; - this.lottoGameView.renderAlignState(alignState); + this.lottoRoundView.renderAlignState(alignState, this.lottoRoundModel.getLottoList().length); + }; + + onInputWinNumberForm = (e) => { + const targetElement = e.target; + if (isNotValidNumber(Number(targetElement.value))) { + this.lottoRoundView.setInvalidInputState(targetElement); + e.target.value = ''; + return; + } + this.lottoRoundView.setValidInputState(targetElement); + if (targetElement.value.length !== 2) { + return; + } + if (targetElement.nextElementSibling !== null) { + targetElement.nextElementSibling.focus(); + return; + } + if (targetElement === findElement(SELECTOR.WIN_NUMBER_INPUT_6)) { + findElement(SELECTOR.BONUS_NUMBER_INPUT).focus(); + } + }; + + onSubmitWinNumberInputForm = (e) => { + e.preventDefault(); + try { + const inputWinNumber = this.getInputWinNumber(); + const roundResult = this.lottoRoundModel.getRoundResult(inputWinNumber); + this.lottoRoundView.openResultModal(roundResult); + } catch (message) { + alert(message); + } + }; + + getInputWinNumber() { + return [ + Number(findElement(SELECTOR.WIN_NUMBER_INPUT_1).value), + Number(findElement(SELECTOR.WIN_NUMBER_INPUT_2).value), + Number(findElement(SELECTOR.WIN_NUMBER_INPUT_3).value), + Number(findElement(SELECTOR.WIN_NUMBER_INPUT_4).value), + Number(findElement(SELECTOR.WIN_NUMBER_INPUT_5).value), + Number(findElement(SELECTOR.WIN_NUMBER_INPUT_6).value), + Number(findElement(SELECTOR.BONUS_NUMBER_INPUT).value), + ]; + } + + onClickReplayButton = () => { + this.lottoRoundModel.initialize(); + this.lottoRoundView.initialize(); + this.$chargeForm.reset(); + this.$winNumberForm.reset(); + this.lottoRoundView.closeResultModal(); + }; + + onClickCloseModalButton = () => { + this.lottoRoundView.closeResultModal(); }; } -export default LottoGameManager; +export default LottoRoundManager; diff --git a/src/js/constants/elementProperty.js b/src/js/constants/elementProperty.js new file mode 100644 index 0000000000..95bdb32234 --- /dev/null +++ b/src/js/constants/elementProperty.js @@ -0,0 +1,4 @@ +export const ELEMENT_PROPERTY = { + HEIGHT_OF_ONE_LOTTO_ICON_LINE: 46, + GAP_OF_LOTTO_ITEM: 3, +}; diff --git a/src/js/constants/errorMessage.js b/src/js/constants/errorMessage.js index f12977bb92..8e3bfd7e01 100644 --- a/src/js/constants/errorMessage.js +++ b/src/js/constants/errorMessage.js @@ -1,4 +1,7 @@ export const ERROR_MESSAGE = { - CHARGE_IS_INVALIDATE: '금액은 1000원 이상이어야합니다. 금액을 다시 입력해주세요.', + CHARGE_IS_NOT_ENOUGH: '금액은 1000원 이상이어야합니다. 금액을 다시 입력해주세요.', + CHARGE_IS_NOT_DIVISIBLE: '금액은 1000원으로 나누어 떨어져야합니다. 금액을 다시 입력해주세요.', LOTTO_NUMBER_IS_INVALIDATE: '로또 숫자들의 값이 부정확합니다. 금액을 다시 입력해주세요.', + WIN_NUMBER_IS_INVALIDATE: '당첨 숫자의 범위는 1에서 45사이 입니다. 다시 입력해주세요.', + DUPLICATE_NUMBER_IS_EXIST: '중복된 숫자가 포함되어 있습니다. 다시 입력해주세요.', }; diff --git a/src/js/constants/number.js b/src/js/constants/number.js index eb3fde7d7d..0561b189c4 100644 --- a/src/js/constants/number.js +++ b/src/js/constants/number.js @@ -3,4 +3,21 @@ export const NUMBER = { LOTTO_MIN_NUMBER: 1, LOTTO_MAX_NUMBER: 45, LOTTO_PRICE: 1000, + BONUS_NUMBER: 6, + FIRST_GRADE_PRIZE: 2000000000, + SECOND_GRADE_PRIZE: 30000000, + THIRD_GRADE_PRIZE: 1500000, + FOURTH_GRADE_PRIZE: 50000, + FIFTH_GRADE_PRIZE: 5000, + FIRST_GRADE_INDEX: 0, + SECOND_GRADE_INDEX: 1, + THIRD_GRADE_INDEX: 2, + FOURTH_GRADE_INDEX: 3, + FIFTH_GRADE_INDEX: 4, + EARNING_RATE_INDEX: 5, + NOT_WINNING_INDEX: 6, + LOTTO_ELEMENT_PER_LINE: 7, + LOTTO_SECTIONS_DEFALUT_CAPACITY_IN_ICON: 7, + LOTTO_SECTIONS_DEFALUT_CAPACITY_IN_DETAIL: 1, + ANIMATION_TIME: 500, }; diff --git a/src/js/constants/selector.js b/src/js/constants/selector.js index e12365e7aa..14051812c0 100644 --- a/src/js/constants/selector.js +++ b/src/js/constants/selector.js @@ -1,8 +1,29 @@ export const SELECTOR = { CHARGE_INPUT_FORM: '#charge-input-form', CHARGE_INPUT: '#charge-input', + WIN_NUMBER_INPUT_FORM: '#win-number-input-form', + WIN_NUMBER_INPUT_1: '#win-number-1', + WIN_NUMBER_INPUT_2: '#win-number-2', + WIN_NUMBER_INPUT_3: '#win-number-3', + WIN_NUMBER_INPUT_4: '#win-number-4', + WIN_NUMBER_INPUT_5: '#win-number-5', + WIN_NUMBER_INPUT_6: '#win-number-6', + BONUS_NUMBER_INPUT: '#bonus-number', ALIGN_CONVERTER: '#align-converter', PURCHASED_MESSAGE: '#purchased-message', + LOTTO_SECTION: '#lotto-section', LOTTO_CONTAINER: '#lotto-container', + WIN_NUMBER_INPUT_SECTION: '#win-number-input-section', + + RESULT_MODAL: '#result-modal-area', + EARNING_RATE_NOTICE: '#earning-rate-notice', + MODAL_CLOSE_BUTTON: '#modal-close-button', + REPLAY_BUTTON: '#replay-button', + + FIRST_GRADE_AMOUNT: '#first-grade-amount', + SECOND_GRADE_AMOUNT: '#second-grade-amount', + THIRD_GRADE_AMOUNT: '#third-grade-amount', + FOURTH_GRADE_AMOUNT: '#fourth-grade-amount', + FIFTH_GRADE_AMOUNT: '#fifth-grade-amount', }; diff --git a/src/js/models/Lotto.js b/src/js/models/Lotto.js index df43855bfa..2023f218cd 100644 --- a/src/js/models/Lotto.js +++ b/src/js/models/Lotto.js @@ -1,5 +1,6 @@ import { isValidNumber, isValidLength } from '../utils/validator'; import { ERROR_MESSAGE } from '../constants/errorMessage'; +import { NUMBER } from '../constants/number'; class Lotto { constructor(lottoNumbers) { @@ -12,6 +13,36 @@ class Lotto { } throw new Error(ERROR_MESSAGE.LOTTO_NUMBER_IS_INVALIDATE); } + + result(winningNumbers) { + let countMatchNumber = 0; + + this.lottoNumbers.forEach((number) => { + if (winningNumbers.includes(number)) { + countMatchNumber += 1; + } + }); + + return this.getLottoRank(countMatchNumber, winningNumbers[NUMBER.BONUS_NUMBER]); + } + + getLottoRank(countMatchNumber, bonusNumber) { + switch (countMatchNumber) { + case 3: + return NUMBER.FIFTH_GRADE_INDEX; + case 4: + return NUMBER.FOURTH_GRADE_INDEX; + case 5: + return NUMBER.THIRD_GRADE_INDEX; + case 6: + if (this.lottoNumbers.some((number) => number === bonusNumber)) { + return NUMBER.SECOND_GRADE_INDEX; + } + return NUMBER.FIRST_GRADE_INDEX; + default: + return NUMBER.NOT_WINNING_INDEX; + } + } } export default Lotto; diff --git a/src/js/models/LottoGame.js b/src/js/models/LottoGame.js deleted file mode 100644 index e0c18005a1..0000000000 --- a/src/js/models/LottoGame.js +++ /dev/null @@ -1,48 +0,0 @@ -import Lotto from './Lotto'; -import { isValidCharge, getRandomNumber } from '../utils/validator'; -import { ERROR_MESSAGE } from '../constants/errorMessage'; -import { NUMBER } from '../constants/number'; - -class LottoGameModel { - constructor() { - this.lottoList = []; - } - - getLottoList() { - /** getter로 가져간 lottoList를 변경하여도 lottoList의 멤버에겐 영향이 없다. */ - return this.lottoList.deepCopy(); - } - - createLottoList(chargeInput) { - /** 정상적이지 않은 로또가 하나라도 존재한다면, 멤버는 빈 값이고 사용자는 금액을 다시 입력하여야 한다. */ - try { - const availableLottoAmount = this.exchangeChargeToLottoAmount(chargeInput); - const newLottoList = new Array(availableLottoAmount).fill().map(() => { - const lottoNumbers = this.createLottoNumbers(); - return Lotto.create(lottoNumbers); - }); - this.lottoList = newLottoList; - } catch ({ message }) { - alert(message); - } - } - - createLottoNumbers() { - const lottoArray = new Set(); - - while (lottoArray.size < NUMBER.LOTTO_NUMBER_AMOUNT) { - lottoArray.add(getRandomNumber(lottoArray)); - } - - return [...lottoArray]; - } - - exchangeChargeToLottoAmount(charge) { - if (isValidCharge(charge)) { - return Math.floor(charge / NUMBER.LOTTO_PRICE); - } - throw new Error(ERROR_MESSAGE.CHARGE_IS_INVALIDATE); - } -} - -export default LottoGameModel; diff --git a/src/js/models/LottoRound.js b/src/js/models/LottoRound.js new file mode 100644 index 0000000000..9a91ca8a3f --- /dev/null +++ b/src/js/models/LottoRound.js @@ -0,0 +1,93 @@ +import Lotto from './Lotto'; +import { + isValidNumber, + isEnoughCharge, + isDivisibleCharge, + getRandomNumber, + hasUniqueElement, +} from '../utils/validator'; +import { ERROR_MESSAGE } from '../constants/errorMessage'; +import { NUMBER } from '../constants/number'; + +class LottoRoundModel { + constructor() { + this.initialize(); + } + + initialize() { + this.lottoList = []; + this.winningResult = [0, 0, 0, 0, 0, 0, 0]; + } + + getLottoList() { + return this.lottoList; + } + + createLottoList(chargeInput) { + try { + const availableLottoAmount = this.exchangeChargeToLottoAmount(chargeInput); + const newLottoList = new Array(availableLottoAmount).fill().map(() => { + const lottoNumbers = this.createLottoNumbers(); + return Lotto.create(lottoNumbers); + }); + this.lottoList = newLottoList; + } catch ({ message }) { + throw message; + } + } + + createLottoNumbers() { + const lottoArray = new Set(); + + while (lottoArray.size < NUMBER.LOTTO_NUMBER_AMOUNT) { + lottoArray.add(getRandomNumber(lottoArray)); + } + + return [...lottoArray]; + } + + exchangeChargeToLottoAmount(charge) { + if (!isEnoughCharge(charge)) { + throw new Error(ERROR_MESSAGE.CHARGE_IS_NOT_ENOUGH); + } + + if (!isDivisibleCharge(charge)) { + throw new Error(ERROR_MESSAGE.CHARGE_IS_NOT_DIVISIBLE); + } + return charge / NUMBER.LOTTO_PRICE; + } + + getRoundResult(winningNumbers) { + if (!isValidNumber(winningNumbers)) { + throw new Error(ERROR_MESSAGE.WIN_NUMBER_IS_INVALIDATE); + } + if (!hasUniqueElement(winningNumbers)) { + throw new Error(ERROR_MESSAGE.DUPLICATE_NUMBER_IS_EXIST); + } + this.winningResult = [0, 0, 0, 0, 0, 0, 0]; + this.lottoList.forEach((lotto) => { + this.updateLottoRankResult(lotto, winningNumbers); + }); + this.updateLottoEarningRate(); + return this.winningResult; + } + + updateLottoRankResult(lotto, winningNumbers) { + this.winningResult[lotto.result(winningNumbers)] += 1; + } + + updateLottoEarningRate() { + const totalCharge = this.lottoList.length * NUMBER.LOTTO_PRICE; + const totalWinningMoney = + NUMBER.FIRST_GRADE_PRIZE * this.winningResult[NUMBER.FIRST_GRADE_INDEX] + + NUMBER.SECOND_GRADE_PRIZE * this.winningResult[NUMBER.SECOND_GRADE_INDEX] + + NUMBER.THIRD_GRADE_PRIZE * this.winningResult[NUMBER.THIRD_GRADE_INDEX] + + NUMBER.FOURTH_GRADE_PRIZE * this.winningResult[NUMBER.FOURTH_GRADE_INDEX] + + NUMBER.FIFTH_GRADE_PRIZE * this.winningResult[NUMBER.FIFTH_GRADE_INDEX]; + this.winningResult[NUMBER.EARNING_RATE_INDEX] = Math.floor( + ((totalWinningMoney - totalCharge) / totalCharge) * 100 + ); + } +} + +export default LottoRoundModel; diff --git a/src/js/utils/customPrototypeMethod.js b/src/js/utils/customPrototypeMethod.js deleted file mode 100644 index 9e983f42ac..0000000000 --- a/src/js/utils/customPrototypeMethod.js +++ /dev/null @@ -1,4 +0,0 @@ -/* eslint no-extend-native:0 */ -Array.prototype.deepCopy = function () { - return JSON.parse(JSON.stringify(this)); -}; diff --git a/src/js/utils/validator.js b/src/js/utils/validator.js index 60038cbd19..27171761d6 100644 --- a/src/js/utils/validator.js +++ b/src/js/utils/validator.js @@ -13,8 +13,16 @@ export function isValidNumber(lottoNumbers) { ); } -export function isValidCharge(charge) { - return Number.isInteger(charge) && charge >= NUMBER.LOTTO_PRICE; +export function isNotValidNumber(number) { + return number < 1 || number > 45; +} + +export function isEnoughCharge(charge) { + return charge >= NUMBER.LOTTO_PRICE; +} + +export function isDivisibleCharge(charge) { + return charge % NUMBER.LOTTO_PRICE === 0; } export function getRandomNumber(array) { @@ -27,3 +35,8 @@ export function getRandomNumber(array) { return randomNumber; } + +export function hasUniqueElement(element) { + const uniqueSet = new Set(element); + return uniqueSet.size === element.length; +} diff --git a/src/js/views/index.js b/src/js/views/index.js index 7591335071..da764bf0b2 100644 --- a/src/js/views/index.js +++ b/src/js/views/index.js @@ -1,17 +1,41 @@ import { SELECTOR } from '../constants/selector'; import { findElement } from '../utils/elementSelector'; +import { ELEMENT_PROPERTY } from '../constants/elementProperty'; +import { NUMBER } from '../constants/number'; -class LottoGameView { +class LottoRoundView { constructor() { this.$purchasedMessage = findElement(SELECTOR.PURCHASED_MESSAGE); this.$lottoContainer = findElement(SELECTOR.LOTTO_CONTAINER); + this.$lottoSection = findElement(SELECTOR.LOTTO_SECTION); + this.$winNumberInputSection = findElement(SELECTOR.WIN_NUMBER_INPUT_SECTION); + this.$resultModal = findElement(SELECTOR.RESULT_MODAL); + this.$earningRateNotice = findElement(SELECTOR.EARNING_RATE_NOTICE); + this.$firstGradeAmount = findElement(SELECTOR.FIRST_GRADE_AMOUNT); + this.$secondGradeAmount = findElement(SELECTOR.SECOND_GRADE_AMOUNT); + this.$thirdGradeAmount = findElement(SELECTOR.THIRD_GRADE_AMOUNT); + this.$fourthGradeAmount = findElement(SELECTOR.FOURTH_GRADE_AMOUNT); + this.$fifthGradeAmount = findElement(SELECTOR.FIFTH_GRADE_AMOUNT); + this.$alignConverter = findElement(SELECTOR.ALIGN_CONVERTER); + } + + initialize() { + this.$purchasedMessage.innerText = ''; + this.$lottoContainer.innerHTML = ''; + this.$alignConverter.checked = false; + this.$lottoSection.setAttribute('data-visible-state', false); + this.renderWinNumberInputSection(false); + this.renderAlignState(false); + this.$lottoSection.style.height = 0; } renderLottoSection(lottoList) { + this.$lottoSection.setAttribute('data-visible-state', true); this.$purchasedMessage.innerText = `총 ${lottoList.length}개를 구매하였습니다.`; this.$lottoContainer.innerHTML = lottoList .map((lotto) => this.generateLottoTemplate(lotto)) .join(''); + this.$lottoSection.style.height = this.#calculateInvisibleLottoSectionHeight(lottoList.length); } generateLottoTemplate({ lottoNumbers }) { @@ -21,8 +45,64 @@ class LottoGameView { `; } - renderAlignState(visibleState) { + renderAlignState(visibleState, lottoAmount = 0) { + if (visibleState) { + this.$lottoSection.style.height = this.#calculateVisibleLottoSectionHeight(lottoAmount); + this.$alignConverter.setAttribute('disabled', true); + setTimeout(() => { + this.$lottoContainer.setAttribute('data-visible-state', visibleState); + this.$alignConverter.removeAttribute('disabled'); + }, NUMBER.ANIMATION_TIME); + return; + } this.$lottoContainer.setAttribute('data-visible-state', visibleState); + this.$lottoSection.style.height = this.#calculateInvisibleLottoSectionHeight(lottoAmount); + } + + #calculateVisibleLottoSectionHeight(lottoAmount) { + return `${lottoAmount * ELEMENT_PROPERTY.HEIGHT_OF_ONE_LOTTO_ICON_LINE}px`; + } + + #calculateInvisibleLottoSectionHeight(lottoAmount) { + if (lottoAmount > NUMBER.LOTTO_SECTIONS_DEFALUT_CAPACITY_IN_ICON) { + const linesOfLottoIcon = Math.ceil( + (lottoAmount - NUMBER.LOTTO_SECTIONS_DEFALUT_CAPACITY_IN_ICON) / + NUMBER.LOTTO_ELEMENT_PER_LINE + ); + return `${ + linesOfLottoIcon * + (ELEMENT_PROPERTY.HEIGHT_OF_ONE_LOTTO_ICON_LINE + ELEMENT_PROPERTY.GAP_OF_LOTTO_ITEM) + + ELEMENT_PROPERTY.HEIGHT_OF_ONE_LOTTO_ICON_LINE + }px`; + } + return `${ELEMENT_PROPERTY.HEIGHT_OF_ONE_LOTTO_ICON_LINE}px`; + } + + renderWinNumberInputSection(visibleState) { + this.$winNumberInputSection.setAttribute('data-visible-state', visibleState); + } + + setInvalidInputState(target) { + target.classList.add('invalid'); + } + + setValidInputState(target) { + target.classList.remove('invalid'); + } + + openResultModal(resultArray) { + this.$firstGradeAmount.innerText = `${resultArray[0]}개`; + this.$secondGradeAmount.innerText = `${resultArray[1]}개`; + this.$thirdGradeAmount.innerText = `${resultArray[2]}개`; + this.$fourthGradeAmount.innerText = `${resultArray[3]}개`; + this.$fifthGradeAmount.innerText = `${resultArray[4]}개`; + this.$earningRateNotice.innerText = `당신의 총 수익률은 ${resultArray[5]}%입니다.`; + + this.$resultModal.setAttribute('data-visible-state', true); + } + + closeResultModal() { + this.$resultModal.setAttribute('data-visible-state', false); } } -export default LottoGameView; +export default LottoRoundView;