From a2b83daf7b80b4caa8156ef99d6e3934df161d48 Mon Sep 17 00:00:00 2001 From: Sejin Cha <62418379+chasj0326@users.noreply.github.com> Date: Sat, 6 Jul 2024 14:38:07 +0900 Subject: [PATCH 01/22] docs: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bab3552..d33becb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 자동차 경주 미션 +# 로또 미션 ## 참고링크 및 저장소 From 3dc2712063a42d705e23f1ff5f72c0fc6032245e Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 7 Jul 2024 01:06:05 +0900 Subject: [PATCH 02/22] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=A0=80=EC=9E=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lottoNumbers.test.js | 66 ++++++++++++++++++++++++++++++ src/domain/Lotto.js | 17 ++++++++ src/domain/createLottoNumbers.js | 11 +++++ src/main.js | 10 ++++- src/service/ConsolePrinter.js | 31 ++++++++++++++ src/service/ConsoleReader.js | 39 ++++++++++++++++++ src/utils/createReadline.js | 15 +++++++ src/utils/getRandomNumber.js | 3 ++ src/validations/createValidator.js | 14 +++++++ src/validations/lotto.js | 40 ++++++++++++++++++ vitest.config.js | 7 ++++ 11 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/lottoNumbers.test.js create mode 100644 src/domain/Lotto.js create mode 100644 src/domain/createLottoNumbers.js create mode 100644 src/service/ConsolePrinter.js create mode 100644 src/service/ConsoleReader.js create mode 100644 src/utils/createReadline.js create mode 100644 src/utils/getRandomNumber.js create mode 100644 src/validations/createValidator.js create mode 100644 src/validations/lotto.js create mode 100644 vitest.config.js diff --git a/src/__tests__/lottoNumbers.test.js b/src/__tests__/lottoNumbers.test.js new file mode 100644 index 0000000..c5f9ce3 --- /dev/null +++ b/src/__tests__/lottoNumbers.test.js @@ -0,0 +1,66 @@ +import Lotto from "../domain/Lotto.js"; +import {lottoValidations} from "../validations/lotto.js"; + +describe('로또 테스트', () => { + test('로또 번호는 [ticket, winnig] 두가지 유형을 가질 수 있다.', () => { + expect(() => new Lotto({ + type: 'otherType', + numbers: [1,2,3,4,5,6], + })).toThrow(lottoValidations.lottoType.errorMessage) + }) + + test('로또 번호는 6개여야 한다.', () => { + expect(() => new Lotto({ + type: 'ticket', + numbers: [1,2,3,4,5] + })).toThrow(lottoValidations.lottoNumbersLength.errorMessage) + }) + + test('당첨 로또 번호는 보너스 번호를 가져야 한다.', () => { + expect(() => new Lotto({ + type: 'winning', + numbers: [1,2,3,4,5,6] + })).toThrow(lottoValidations.winningLottoHasBonus.errorMessage) + }) + + test('티켓 로또 번호는 보너스 번호가 없어야 한다.', () => { + expect(() => new Lotto({ + type: 'ticket', + numbers: [1,2,3,4,5,6], + bonusNumber: 7 + })).toThrow(lottoValidations.ticketLottoBonusNull.errorMessage) + }) + + test('로또 번호는 각각 달라야 한다.', () => { + expect(() => new Lotto({ + type: 'winning', + numbers: [1, 2, 3, 4, 5, 6], + bonusNumber: 6 + })).toThrow(lottoValidations.lottoEachUnique.errorMessage) + + expect(() => new Lotto({ + type: 'ticket', + numbers: [1, 2, 3, 3, 5, 6], + })).toThrow(lottoValidations.lottoEachUnique.errorMessage) + }) + + test('모든 로또 번호는 정수여야 한다.', () => { + expect(() => new Lotto({ + type: 'ticket', + numbers: [1, 2, 3, 4, 5, '6'] + })).toThrow(lottoValidations.lottoInteger.errorMessage) + }) + + test('모든 로또 번호는 1 이상 45 이하여야 한다.', () => { + expect(() => new Lotto({ + type: 'winning', + numbers: [1, 2, 3, 4, 5, 45], + bonusNumber: 46 + })).toThrow(lottoValidations.lottoRange.errorMessage) + + expect(() => new Lotto({ + type: 'ticket', + numbers: [0, 1, 2, 3, 4, 5], + })).toThrow(lottoValidations.lottoRange.errorMessage) + }) +}) \ No newline at end of file diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js new file mode 100644 index 0000000..5c5dea2 --- /dev/null +++ b/src/domain/Lotto.js @@ -0,0 +1,17 @@ +import { validateLotto } from "../validations/lotto.js"; + +export default class Lotto{ + constructor({ type, numbers, bonusNumber = null }) { + Lotto.#validate({ type, numbers, bonusNumber }) + + this.type = type + this.numbers = numbers + this.bonusNumber = bonusNumber + } + + static #validate(lottoProps) { + validateLotto({ + target: lottoProps + }) + } +} \ No newline at end of file diff --git a/src/domain/createLottoNumbers.js b/src/domain/createLottoNumbers.js new file mode 100644 index 0000000..1cc2ca3 --- /dev/null +++ b/src/domain/createLottoNumbers.js @@ -0,0 +1,11 @@ +import {getRandomNumber} from "../utils/getRandomNumber.js"; + +const createLottoNumbers = () => { + const lottoNumbers = new Set() + while (lottoNumbers.size < 6) { + lottoNumbers.add(getRandomNumber(1, 45)) + } + return [...lottoNumbers] +} + +export default createLottoNumbers() \ No newline at end of file diff --git a/src/main.js b/src/main.js index 96bab59..010bea6 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,11 @@ -function main() { - console.log('main의 내용을 채워주세요'); +import Lotto from "./domain/Lotto.js"; +async function main() { + new Lotto({ + type: 'ticket', + numbers: [1,2,3,4,5,6] + }) } main(); + +// 로또Q:q \ No newline at end of file diff --git a/src/service/ConsolePrinter.js b/src/service/ConsolePrinter.js new file mode 100644 index 0000000..7693ae3 --- /dev/null +++ b/src/service/ConsolePrinter.js @@ -0,0 +1,31 @@ +export default class ConsolePrinter { + constructor(template) { + this.template = template; + } + + format(templateKey, messages) { + let result = this.template[templateKey]; + if (messages && messages.length > 0) { + messages.forEach((message, index) => { + result = result.replaceAll(`%{${index + 1}}`, message); + }); + } + return result; + } + + print(...messages) { + console.log(...messages); + } + + printWithTemplate(templateKey, messages) { + if (this.template.hasOwnProperty(templateKey)) { + console.log(this.format(templateKey, messages)); + } else { + console.log(...messages); + } + } + + lineBreak() { + console.log(''); + } +} \ No newline at end of file diff --git a/src/service/ConsoleReader.js b/src/service/ConsoleReader.js new file mode 100644 index 0000000..e6947ab --- /dev/null +++ b/src/service/ConsoleReader.js @@ -0,0 +1,39 @@ +import createReadline from "../utils/createReadline.js"; + +class ConsoleReader { + constructor() { + this.readLine = createReadline() + } + + askQuestion(message) { + return new Promise(resolve => { + this.readLine.question(message, input => { + resolve(input) + }) + }) + } + + findInputError(input, validations = []) { + const { errorMessage } = validations.find(({ check }) => !check(input)) ?? {} + return errorMessage + } + + read(message, validations = []) { + const processInput = async() => { + const input = await this.askQuestion(message) + const errorMessage = this.findInputError(input, validations) + + if (errorMessage) { + return processInput() + } + this.readLine.close() + return input + } + + return processInput() + } +} + +const consoleReader = new ConsoleReader() + +export default consoleReader \ No newline at end of file diff --git a/src/utils/createReadline.js b/src/utils/createReadline.js new file mode 100644 index 0000000..30c92fc --- /dev/null +++ b/src/utils/createReadline.js @@ -0,0 +1,15 @@ +import readline from 'readline'; + +const createReadline = () => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + rl.isOpened = true; + rl.on('close', () => { + rl.isOpened = false; + }); + return rl; +}; + +export default createReadline; \ No newline at end of file diff --git a/src/utils/getRandomNumber.js b/src/utils/getRandomNumber.js new file mode 100644 index 0000000..23eff6f --- /dev/null +++ b/src/utils/getRandomNumber.js @@ -0,0 +1,3 @@ +export const getRandomNumber = (min, max) => { + return Math.floor(Math.random() * (max - min + 1)) + min; +}; \ No newline at end of file diff --git a/src/validations/createValidator.js b/src/validations/createValidator.js new file mode 100644 index 0000000..5185b6b --- /dev/null +++ b/src/validations/createValidator.js @@ -0,0 +1,14 @@ +const createValidator = (validations) => { + return ({ target, validationKeys }) => { + (validationKeys ?? Object.keys(validations)).forEach(key => { + if (!validations.hasOwnProperty(key)) { + throw new Error('올바른 검사 키가 아닙니다.') + } + if (!validations[key].check(target)) { + throw new Error(validations[key].errorMessage) + } + }) + } +} + +export default createValidator \ No newline at end of file diff --git a/src/validations/lotto.js b/src/validations/lotto.js new file mode 100644 index 0000000..834eadc --- /dev/null +++ b/src/validations/lotto.js @@ -0,0 +1,40 @@ +import createValidator from "./createValidator.js"; + +export const lottoValidations = { + lottoType: { + check: ({ type }) => type === 'winning' || type === 'ticket', + errorMessage: '로또는 ticket, winning 두 가지 유형이어야 합니다.' + }, + lottoNumbersLength: { + check: ({ numbers }) => numbers.length === 6, + errorMessage: '로또 번호는 6개여야 합니다.' + }, + winningLottoHasBonus: { + check: ({ type, bonusNumber }) => type === 'winning' ? Boolean(bonusNumber) : true, + errorMessage: '당첨 로또 번호는 보너스 번호를 가져야 합니다.' + }, + ticketLottoBonusNull: { + check: ({ type, bonusNumber }) => type === 'ticket' ? bonusNumber === null : true, + errorMessage: '구매한 로또 번호는 보너스 번호가 없어야 합니다.' + }, + lottoEachUnique: { + check: ({ numbers, bonusNumber }) => new Set(numbers).size === numbers.length && !numbers.includes(bonusNumber), + errorMessage: '로또 번호는 각각 달라야 합니다.' + }, + lottoInteger: { + check: ({ numbers, bonusNumber }) => { + const lottoNumbers = bonusNumber === null? numbers : [...numbers, bonusNumber] + return lottoNumbers.every(number => Number.isInteger(number)) + }, + errorMessage: '모든 로또 번호는 정수여야 합니다.' + }, + lottoRange: { + check: ({ numbers, bonusNumber }) => { + const lottoNumbers = bonusNumber === null ? numbers : [...numbers, bonusNumber] + return lottoNumbers.every(number => 1 <= number && number <= 45) + }, + errorMessage: '모든 로또 번호는 1 이상 45 이하여야 합니다.' + } +} + +export const validateLotto = createValidator(lottoValidations) \ No newline at end of file diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..b758f89 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + }, +}) \ No newline at end of file From a3ee751a4b968a83ad95028061c2b9fd2c65437a Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 7 Jul 2024 02:02:19 +0900 Subject: [PATCH 03/22] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lottoPayment.test.js | 26 ++++++++++++++++++++++++++ src/domain/Lotto.js | 3 ++- src/domain/LottoPayment.js | 30 ++++++++++++++++++++++++++++++ src/domain/LottoSystem.js | 7 +++++++ src/domain/createLottoNumbers.js | 2 +- src/validations/lottoPayment.js | 14 ++++++++++++++ 6 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/lottoPayment.test.js create mode 100644 src/domain/LottoPayment.js create mode 100644 src/domain/LottoSystem.js create mode 100644 src/validations/lottoPayment.js diff --git a/src/__tests__/lottoPayment.test.js b/src/__tests__/lottoPayment.test.js new file mode 100644 index 0000000..bd54f94 --- /dev/null +++ b/src/__tests__/lottoPayment.test.js @@ -0,0 +1,26 @@ +import LottoPayment from "../domain/LottoPayment.js"; +import { lottoPaymentValidations } from "../validations/lottoPayment.js"; + +describe('로또 결제 테스트', () => { + test('로또 결제 금액은 정수여야 한다.', () => { + expect(() => new LottoPayment({ + payAmount: '1000' + })).toThrow(lottoPaymentValidations.payAmountInteger.errorMessage) + }) + + test('로또 결제는 1000원 단위로만 가능하다.', () => { + expect(() => new LottoPayment(({ + payAmount: 1100 + }))).toThrow(lottoPaymentValidations.payAmountUnit1000.errorMessage) + }) + + test('1000원 당 1장의 로또 티켓을 발행해야 한다.', () => { + const lottoPayment = new LottoPayment({ + payAmount: 8000 + }) + lottoPayment.createLottoTickets() + + expect(lottoPayment.ticketCount).toBe(8) + expect(lottoPayment.paidTickets.length).toBe(8) + }) +}) \ No newline at end of file diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index 5c5dea2..074f4cf 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -14,4 +14,5 @@ export default class Lotto{ target: lottoProps }) } -} \ No newline at end of file +} + diff --git a/src/domain/LottoPayment.js b/src/domain/LottoPayment.js new file mode 100644 index 0000000..ea1196e --- /dev/null +++ b/src/domain/LottoPayment.js @@ -0,0 +1,30 @@ +import { validateLottoPayment } from "../validations/lottoPayment.js"; +import createLottoNumbers from "./createLottoNumbers.js"; +import Lotto from "./Lotto.js"; + +export default class LottoPayment { + #payAmount; + constructor({ payAmount }) { + LottoPayment.#validate({ payAmount }) + + this.#payAmount = payAmount + this.paidTickets = this.createLottoTickets() + } + + static #validate(lottoPaymentProps) { + validateLottoPayment({ + target: lottoPaymentProps + }) + } + + get ticketCount() { + return this.#payAmount / 1000 + } + + createLottoTickets() { + return Array.from({ length: this.ticketCount }, () => new Lotto({ + type: 'ticket', + numbers: createLottoNumbers() + })) + } +} \ No newline at end of file diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js new file mode 100644 index 0000000..cc49fd9 --- /dev/null +++ b/src/domain/LottoSystem.js @@ -0,0 +1,7 @@ +// LottoSystem +// paid, lottoList +// paid, count(get), lottoResult, winningLotto, profitRate(get) +export default class LottoSystem{ + constructor({ paid, lottoTickets }) { + } +} \ No newline at end of file diff --git a/src/domain/createLottoNumbers.js b/src/domain/createLottoNumbers.js index 1cc2ca3..676a126 100644 --- a/src/domain/createLottoNumbers.js +++ b/src/domain/createLottoNumbers.js @@ -8,4 +8,4 @@ const createLottoNumbers = () => { return [...lottoNumbers] } -export default createLottoNumbers() \ No newline at end of file +export default createLottoNumbers \ No newline at end of file diff --git a/src/validations/lottoPayment.js b/src/validations/lottoPayment.js new file mode 100644 index 0000000..9005f0c --- /dev/null +++ b/src/validations/lottoPayment.js @@ -0,0 +1,14 @@ +import createValidator from "./createValidator.js"; + +export const lottoPaymentValidations = { + payAmountInteger: { + check: ({ payAmount }) => Number.isInteger(payAmount), + errorMessage: '로또 결제 금액은 숫자(정수)여야 합니다.' + }, + payAmountUnit1000: { + check: ({ payAmount }) => payAmount % 1000 === 0, + errorMessage: '로또 결제는 1000원 단위로만 가능합니다.' + } +} + +export const validateLottoPayment = createValidator(lottoPaymentValidations) \ No newline at end of file From b466e196922016e949fb78268076e8f7fd1d50ed Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 7 Jul 2024 15:45:40 +0900 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EA=B3=84=EC=82=B0=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A1=9C=EB=98=90=20=ED=83=80=EC=9E=85=20=EC=83=81=EC=88=98?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{lottoNumbers.test.js => lotto.test.js} | 19 +++--- src/__tests__/lottoMatcher.test.js | 66 +++++++++++++++++++ src/constants/lottoType.js | 6 ++ src/domain/Lotto.js | 10 +++ src/domain/LottoMatcher.js | 31 +++++++++ src/domain/LottoPayment.js | 7 +- src/domain/LottoSystem.js | 8 ++- src/main.js | 2 +- src/validations/lotto.js | 7 +- src/validations/lottoMatcher.js | 16 +++++ 10 files changed, 153 insertions(+), 19 deletions(-) rename src/__tests__/{lottoNumbers.test.js => lotto.test.js} (83%) create mode 100644 src/__tests__/lottoMatcher.test.js create mode 100644 src/constants/lottoType.js create mode 100644 src/domain/LottoMatcher.js create mode 100644 src/validations/lottoMatcher.js diff --git a/src/__tests__/lottoNumbers.test.js b/src/__tests__/lotto.test.js similarity index 83% rename from src/__tests__/lottoNumbers.test.js rename to src/__tests__/lotto.test.js index c5f9ce3..7410d26 100644 --- a/src/__tests__/lottoNumbers.test.js +++ b/src/__tests__/lotto.test.js @@ -1,8 +1,9 @@ import Lotto from "../domain/Lotto.js"; import {lottoValidations} from "../validations/lotto.js"; +import LOTTO_TYPE from "../constants/lottoType.js"; describe('로또 테스트', () => { - test('로또 번호는 [ticket, winnig] 두가지 유형을 가질 수 있다.', () => { + test('로또 번호는 [당첨, 티켓] 두가지 유형을 가질 수 있다.', () => { expect(() => new Lotto({ type: 'otherType', numbers: [1,2,3,4,5,6], @@ -11,21 +12,21 @@ describe('로또 테스트', () => { test('로또 번호는 6개여야 한다.', () => { expect(() => new Lotto({ - type: 'ticket', + type: LOTTO_TYPE.TICKET, numbers: [1,2,3,4,5] })).toThrow(lottoValidations.lottoNumbersLength.errorMessage) }) test('당첨 로또 번호는 보너스 번호를 가져야 한다.', () => { expect(() => new Lotto({ - type: 'winning', + type: LOTTO_TYPE.WINNING, numbers: [1,2,3,4,5,6] })).toThrow(lottoValidations.winningLottoHasBonus.errorMessage) }) test('티켓 로또 번호는 보너스 번호가 없어야 한다.', () => { expect(() => new Lotto({ - type: 'ticket', + type: LOTTO_TYPE.TICKET, numbers: [1,2,3,4,5,6], bonusNumber: 7 })).toThrow(lottoValidations.ticketLottoBonusNull.errorMessage) @@ -33,33 +34,33 @@ describe('로또 테스트', () => { test('로또 번호는 각각 달라야 한다.', () => { expect(() => new Lotto({ - type: 'winning', + type: LOTTO_TYPE.WINNING, numbers: [1, 2, 3, 4, 5, 6], bonusNumber: 6 })).toThrow(lottoValidations.lottoEachUnique.errorMessage) expect(() => new Lotto({ - type: 'ticket', + type: LOTTO_TYPE.TICKET, numbers: [1, 2, 3, 3, 5, 6], })).toThrow(lottoValidations.lottoEachUnique.errorMessage) }) test('모든 로또 번호는 정수여야 한다.', () => { expect(() => new Lotto({ - type: 'ticket', + type: LOTTO_TYPE.TICKET, numbers: [1, 2, 3, 4, 5, '6'] })).toThrow(lottoValidations.lottoInteger.errorMessage) }) test('모든 로또 번호는 1 이상 45 이하여야 한다.', () => { expect(() => new Lotto({ - type: 'winning', + type: LOTTO_TYPE.WINNING, numbers: [1, 2, 3, 4, 5, 45], bonusNumber: 46 })).toThrow(lottoValidations.lottoRange.errorMessage) expect(() => new Lotto({ - type: 'ticket', + type: LOTTO_TYPE.TICKET, numbers: [0, 1, 2, 3, 4, 5], })).toThrow(lottoValidations.lottoRange.errorMessage) }) diff --git a/src/__tests__/lottoMatcher.test.js b/src/__tests__/lottoMatcher.test.js new file mode 100644 index 0000000..c84d524 --- /dev/null +++ b/src/__tests__/lottoMatcher.test.js @@ -0,0 +1,66 @@ +import {createLottoTicket, createWinningLotto} from "../domain/Lotto.js"; +import LottoMatcher from "../domain/LottoMatcher.js"; +import {lottoMatcherValidations} from "../validations/lottoMatcher.js"; +import LOTTO_TYPE from "../constants/lottoType.js"; + +describe('로또 번호 일치여부 계산 테스트', () => { + + test('유효한 당첨 로또로만 계산할 수 있다.', () => { + const validLottoTicket = createLottoTicket([11,12,13,14,15,16]) + + expect(() => new LottoMatcher({ + winningLotto: { type: LOTTO_TYPE.WINNING, numbers: [1,2,3,4,5,6], bonusNumber: 8 }, + lottoTickets: [ validLottoTicket ] + })).toThrow(lottoMatcherValidations.validWinningLotto.errorMessage) + + expect(() => new LottoMatcher({ + winningLotto: validLottoTicket, + lottoTickets: [ validLottoTicket ] + })).toThrow(lottoMatcherValidations.validWinningLotto.errorMessage) + }) + + test('유효한 로또 티켓들만 계산할 수 있다.', () => { + const validWinningLotto = createWinningLotto( + [1,2,3,4,5,6], 7 + ) + + expect(() => new LottoMatcher({ + winningLotto: validWinningLotto, + lottoTickets: [ { type: LOTTO_TYPE.TICKET, numbers: [1,2,3,4,5,6] } ] + })) + + expect(() => new LottoMatcher({ + winningLotto: validWinningLotto, + lottoTickets: [ validWinningLotto ] + })).toThrow(lottoMatcherValidations.validLottoTickets.errorMessage) + }) + + test('로또 티켓들의 [당첨 번호 일치 개수, 보너스 번호 일치 여부]를 반환한다.', () => { + const winningLotto = createWinningLotto([1,2,3,4,5,6], 7) + const lottoTicket1 = createLottoTicket([1,2,3,14,15,16]) // 3, false + const lottoTicket2 = createLottoTicket([11,12,13,14,15,16]) // 0, false + const lottoTicket3 = createLottoTicket([1,2,3,4,7,8]) // 4, true + + const lottoMatcher = new LottoMatcher({ + winningLotto, + lottoTickets: [lottoTicket1, lottoTicket2, lottoTicket3] + }) + expect(lottoMatcher.lottoMatchResult).toEqual([ + { + lotto: lottoTicket1, + matchCount: 3, + bonusMatch: false + }, + { + lotto: lottoTicket2, + matchCount: 0, + bonusMatch: false + }, + { + lotto: lottoTicket3, + matchCount: 4, + bonusMatch: true + } + ]) + }) +}) \ No newline at end of file diff --git a/src/constants/lottoType.js b/src/constants/lottoType.js new file mode 100644 index 0000000..72087f7 --- /dev/null +++ b/src/constants/lottoType.js @@ -0,0 +1,6 @@ +const LOTTO_TYPE = { + TICKET: Symbol('lottoTicket'), + WINNING: Symbol('winningLotto') +} + +export default LOTTO_TYPE \ No newline at end of file diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index 074f4cf..cc2e7e1 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -1,4 +1,5 @@ import { validateLotto } from "../validations/lotto.js"; +import LOTTO_TYPE from "../constants/lottoType.js"; export default class Lotto{ constructor({ type, numbers, bonusNumber = null }) { @@ -16,3 +17,12 @@ export default class Lotto{ } } +export const createWinningLotto = (numbers, bonusNumber) => new Lotto({ + type: LOTTO_TYPE.WINNING, + numbers, bonusNumber +}) + +export const createLottoTicket = (numbers) => new Lotto({ + type: LOTTO_TYPE.TICKET, + numbers +}) \ No newline at end of file diff --git a/src/domain/LottoMatcher.js b/src/domain/LottoMatcher.js new file mode 100644 index 0000000..a5aeba5 --- /dev/null +++ b/src/domain/LottoMatcher.js @@ -0,0 +1,31 @@ +import {validateLottoMatcher} from "../validations/lottoMatcher.js"; + +export default class LottoMatcher{ + constructor({ winningLotto, lottoTickets }) { + LottoMatcher.#validate({ winningLotto, lottoTickets }) + this.winningLotto = winningLotto + this.lottoTickets = lottoTickets + } + + get lottoMatchResult() { + return this.lottoTickets.map(lottoTicket => ({ + lotto: lottoTicket, + matchCount: this.#getMatchCount(lottoTicket.numbers), + bonusMatch: this.#getBonusMatch(lottoTicket.numbers) + })) + } + + static #validate(lottoMatcherProps) { + validateLottoMatcher({ + target: lottoMatcherProps + }) + } + + #getMatchCount(ticketNumbers) { + return ticketNumbers.filter(ticketNumber => this.winningLotto.numbers.includes(ticketNumber)).length + } + + #getBonusMatch(ticketNumbers) { + return ticketNumbers.includes(this.winningLotto.bonusNumber) + } +} \ No newline at end of file diff --git a/src/domain/LottoPayment.js b/src/domain/LottoPayment.js index ea1196e..d63366a 100644 --- a/src/domain/LottoPayment.js +++ b/src/domain/LottoPayment.js @@ -1,6 +1,6 @@ import { validateLottoPayment } from "../validations/lottoPayment.js"; import createLottoNumbers from "./createLottoNumbers.js"; -import Lotto from "./Lotto.js"; +import {createLottoTicket} from "./Lotto.js"; export default class LottoPayment { #payAmount; @@ -22,9 +22,6 @@ export default class LottoPayment { } createLottoTickets() { - return Array.from({ length: this.ticketCount }, () => new Lotto({ - type: 'ticket', - numbers: createLottoNumbers() - })) + return Array.from({ length: this.ticketCount }, () => createLottoTicket(createLottoNumbers())) } } \ No newline at end of file diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index cc49fd9..4a5552e 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -2,6 +2,12 @@ // paid, lottoList // paid, count(get), lottoResult, winningLotto, profitRate(get) export default class LottoSystem{ - constructor({ paid, lottoTickets }) { + constructor() { } + + // 로또 결제 + + // 로또 생성 + + // 로또 매칭 결과 계산 } \ No newline at end of file diff --git a/src/main.js b/src/main.js index 010bea6..756fad3 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,7 @@ import Lotto from "./domain/Lotto.js"; async function main() { new Lotto({ - type: 'ticket', + type: LOTTO_TYPE.TICKET, numbers: [1,2,3,4,5,6] }) } diff --git a/src/validations/lotto.js b/src/validations/lotto.js index 834eadc..160e3dc 100644 --- a/src/validations/lotto.js +++ b/src/validations/lotto.js @@ -1,8 +1,9 @@ import createValidator from "./createValidator.js"; +import LOTTO_TYPE from "../constants/lottoType.js"; export const lottoValidations = { lottoType: { - check: ({ type }) => type === 'winning' || type === 'ticket', + check: ({ type }) => type === LOTTO_TYPE.WINNING || type === LOTTO_TYPE.TICKET, errorMessage: '로또는 ticket, winning 두 가지 유형이어야 합니다.' }, lottoNumbersLength: { @@ -10,11 +11,11 @@ export const lottoValidations = { errorMessage: '로또 번호는 6개여야 합니다.' }, winningLottoHasBonus: { - check: ({ type, bonusNumber }) => type === 'winning' ? Boolean(bonusNumber) : true, + check: ({ type, bonusNumber }) => type === LOTTO_TYPE.WINNING ? Boolean(bonusNumber) : true, errorMessage: '당첨 로또 번호는 보너스 번호를 가져야 합니다.' }, ticketLottoBonusNull: { - check: ({ type, bonusNumber }) => type === 'ticket' ? bonusNumber === null : true, + check: ({ type, bonusNumber }) => type === LOTTO_TYPE.TICKET ? bonusNumber === null : true, errorMessage: '구매한 로또 번호는 보너스 번호가 없어야 합니다.' }, lottoEachUnique: { diff --git a/src/validations/lottoMatcher.js b/src/validations/lottoMatcher.js new file mode 100644 index 0000000..72bb24c --- /dev/null +++ b/src/validations/lottoMatcher.js @@ -0,0 +1,16 @@ +import Lotto from "../domain/Lotto.js"; +import createValidator from "./createValidator.js"; +import LOTTO_TYPE from "../constants/lottoType.js"; + +export const lottoMatcherValidations = { + validWinningLotto: { + check: ({ winningLotto }) => winningLotto instanceof Lotto && winningLotto.type === LOTTO_TYPE.WINNING, + errorMessage: '유효한 로또 당첨 정보가 아닙니다.' + }, + validLottoTickets: { + check: ({ lottoTickets }) => lottoTickets.every(lottoTicket => lottoTicket instanceof Lotto && lottoTicket.type === LOTTO_TYPE.TICKET), + errorMessage: '유효하지 않은 로또 티켓이 존재합니다.' + } +} + +export const validateLottoMatcher = createValidator(lottoMatcherValidations) \ No newline at end of file From e95ccea147c99a8b27b88561e9a59d92f8477170 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 7 Jul 2024 20:46:37 +0900 Subject: [PATCH 05/22] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lottoSystem.test.js | 117 ++++++++++++++++++++++++++++++ src/constants/lottoRankingData.js | 28 +++++++ src/domain/Lotto.js | 1 - src/domain/LottoMatcher.js | 1 + src/domain/LottoPayment.js | 8 +- src/domain/LottoSystem.js | 65 ++++++++++++++--- src/domain/createLottoNumbers.js | 2 +- src/validations/lottoSystem.js | 29 ++++++++ 8 files changed, 237 insertions(+), 14 deletions(-) create mode 100644 src/__tests__/lottoSystem.test.js create mode 100644 src/constants/lottoRankingData.js create mode 100644 src/validations/lottoSystem.js diff --git a/src/__tests__/lottoSystem.test.js b/src/__tests__/lottoSystem.test.js new file mode 100644 index 0000000..3932c5c --- /dev/null +++ b/src/__tests__/lottoSystem.test.js @@ -0,0 +1,117 @@ +import LottoSystem from "../domain/LottoSystem.js"; +import {lottoSystemValidations} from "../validations/lottoSystem.js"; +import createLottoNumbers from "../domain/createLottoNumbers.js"; + + +vi.mock('../domain/createLottoNumbers'); + +describe('로또 시스템 테스트', async() => { + const originalCreateLottoNumbers = await vi.importActual('../domain/createLottoNumbers').then(module => module.default); + beforeEach(() => { + createLottoNumbers.mockReset(); + createLottoNumbers.mockImplementation(originalCreateLottoNumbers) + }); + + test('로또 시스템은 순위가 올바르게 매겨진 랭킹 데이터만 사용한다.', () => { + const rankingDataHasInvalidRank1 = [{ + matchCount: 3, + bonusMatch: false, + profit: 1000, + rank: -1, + }] + expect(() => new LottoSystem({ + rankingData: rankingDataHasInvalidRank1 + })).toThrow(lottoSystemValidations.validRanks.errorMessage) + + const rankingDataHasInvalidRank2 = [{ + matchCount: 3, + bonusMatch: false, + profit: 1000, + rank: 3 + }] + expect(() => new LottoSystem({ + rankingData: rankingDataHasInvalidRank2 + })).toThrow(lottoSystemValidations.hasAllRanks.errorMessage) + }) + + test('로또 시스템은 유효한 matchCount, bonus, profit 이 포함된 랭킹 데이터만 사용한다.', () => { + const rankingDataHasInvalidMatchCount = [{ + bonusMatch: true, + profit: 1000, + rank: 1, + matchCount: 100 + }] + expect(() => new LottoSystem({ + rankingData: rankingDataHasInvalidMatchCount + })).toThrow(lottoSystemValidations.validMatchCounts.errorMessage) + + const rankingDataHasInvalidBonusMatch = [{ + profit: 1000, + rank: 1, + matchCount: 3 + }] + expect(() => new LottoSystem({ + rankingData: rankingDataHasInvalidBonusMatch + })).toThrow(lottoSystemValidations.validBonusMatches.errorMessage) + + const rankingDataHasInvalidProfit = [{ + bonusMatch: true, + profit: '1000', + rank: 1, + matchCount: 3 + }] + expect(() => new LottoSystem({ + rankingData: rankingDataHasInvalidProfit + })).toThrow(lottoSystemValidations.validProfits.errorMessage) + }) + + test('로또 시스템은 정확한 개수의 로또를 구매한다.', () => { + const lottoSystem = new LottoSystem() + lottoSystem.payLottoTicket(10000) + + expect(lottoSystem.ticketCount).toBe(10) + expect(lottoSystem.payAmount).toBe(10000) + }) + + test('로또 시스템은 발행한 로또들을 등수 별로 분류할 수 있다.', () => { + createLottoNumbers + .mockReturnValueOnce([1, 2, 3, 4, 5, 6]) // 1등 + .mockReturnValueOnce([1, 2, 3, 4, 5, 7]) // 2등 + .mockReturnValueOnce([1, 2, 3, 4, 5, 7]) // 2등 + .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 + .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 + .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 + + const lottoSystem = new LottoSystem() + lottoSystem.setWinningLotto([1,2,3,4,5,6], 7) + lottoSystem.payLottoTicket(6000) + + expect(lottoSystem.getTicketCountByRank(1)).toBe(1) + expect(lottoSystem.getTicketCountByRank(2)).toBe(2) + expect(lottoSystem.getTicketCountByRank(4)).toBe(3) + }) + + test('로또 시스템은 총 수익을 계산할 수 있다.', () => { + createLottoNumbers + .mockReturnValueOnce([1, 2, 3, 4, 5, 6]) // 1등 + .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 + + const lottoSystem = new LottoSystem() + lottoSystem.setWinningLotto([1,2,3,4,5,6], 7) + lottoSystem.payLottoTicket(2000) + + expect(lottoSystem.profitAmount).toBe(2000050000) + }) + + test('로또 시스템은 수익율을 계산할 수 있다.', () => { + createLottoNumbers + .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 = 50000 + .mockImplementation(() => [11, 12, 13, 14, 15, 16]) + + const lottoSystem = new LottoSystem() + lottoSystem.setWinningLotto([1,2,3,4,5,6], 7) + lottoSystem.payLottoTicket(100000) + + expect(lottoSystem.profitRatio).toBe(50) + }) +}) \ No newline at end of file diff --git a/src/constants/lottoRankingData.js b/src/constants/lottoRankingData.js new file mode 100644 index 0000000..43fd7fd --- /dev/null +++ b/src/constants/lottoRankingData.js @@ -0,0 +1,28 @@ +const LOTTO_RANKING_DATA = [{ + rank: 5, + matchCount: 3, + bonusMatch: false, + profit: 5000 +}, { + rank: 4, + matchCount: 4, + bonusMatch: false, + profit: 50000 +}, { + rank: 3, + matchCount: 5, + bonusMatch: false, + profit: 1500000 +}, { + rank: 2, + matchCount: 5, + bonusMatch: true, + profit: 30000000 +}, { + rank: 1, + matchCount: 6, + bonusMatch: false, + profit: 2000000000 +}] + +export default LOTTO_RANKING_DATA \ No newline at end of file diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index cc2e7e1..3dd8994 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -4,7 +4,6 @@ import LOTTO_TYPE from "../constants/lottoType.js"; export default class Lotto{ constructor({ type, numbers, bonusNumber = null }) { Lotto.#validate({ type, numbers, bonusNumber }) - this.type = type this.numbers = numbers this.bonusNumber = bonusNumber diff --git a/src/domain/LottoMatcher.js b/src/domain/LottoMatcher.js index a5aeba5..bd5b7b2 100644 --- a/src/domain/LottoMatcher.js +++ b/src/domain/LottoMatcher.js @@ -3,6 +3,7 @@ import {validateLottoMatcher} from "../validations/lottoMatcher.js"; export default class LottoMatcher{ constructor({ winningLotto, lottoTickets }) { LottoMatcher.#validate({ winningLotto, lottoTickets }) + this.winningLotto = winningLotto this.lottoTickets = lottoTickets } diff --git a/src/domain/LottoPayment.js b/src/domain/LottoPayment.js index d63366a..21fbd55 100644 --- a/src/domain/LottoPayment.js +++ b/src/domain/LottoPayment.js @@ -8,7 +8,6 @@ export default class LottoPayment { LottoPayment.#validate({ payAmount }) this.#payAmount = payAmount - this.paidTickets = this.createLottoTickets() } static #validate(lottoPaymentProps) { @@ -18,10 +17,13 @@ export default class LottoPayment { } get ticketCount() { - return this.#payAmount / 1000 + return Math.floor(this.#payAmount / 1000) } createLottoTickets() { - return Array.from({ length: this.ticketCount }, () => createLottoTicket(createLottoNumbers())) + return Array.from({ length: this.ticketCount }, () => { + const numbers = createLottoNumbers() + return createLottoTicket(numbers) + }) } } \ No newline at end of file diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index 4a5552e..f3e0586 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -1,13 +1,60 @@ -// LottoSystem -// paid, lottoList -// paid, count(get), lottoResult, winningLotto, profitRate(get) -export default class LottoSystem{ - constructor() { +import { createWinningLotto } from "./Lotto.js"; +import LottoPayment from "./LottoPayment.js"; +import LottoMatcher from "./LottoMatcher.js"; +import LOTTO_RANKING_DATA from "../constants/lottoRankingData.js"; +import {validateLottoSystem} from "../validations/lottoSystem.js"; + +export default class LottoSystem { + constructor({ rankingData = LOTTO_RANKING_DATA } = {}) { + LottoSystem.#validate({ rankingData }) + + this.rankingData = rankingData; + this.lottoData = { + winningLotto: null, + lottoTickets: [] + }; + this.payAmount = 0 + } + + static #validate(lottoSystemProps) { + validateLottoSystem({ + target: lottoSystemProps + }) + } + + setWinningLotto(winningNumbers, bonusNumber) { + this.lottoData.winningLotto = createWinningLotto(winningNumbers, bonusNumber); + } + + payLottoTicket(payAmount) { + this.lottoData.lottoTickets = new LottoPayment({ payAmount }).createLottoTickets(); + this.payAmount = payAmount } - // 로또 결제 + get ticketCount() { + return this.lottoData.lottoTickets.length + } + + get lottoRankingResult() { + const { lottoMatchResult } = new LottoMatcher(this.lottoData); + + return this.rankingData.map((data) => ({ + ...data, + rankedList: lottoMatchResult + .filter(({ matchCount, bonusMatch }) => matchCount === data.matchCount && bonusMatch === data.bonusMatch) + .map(({ lotto }) => lotto) + })); + } - // 로또 생성 + getTicketCountByRank(rank) { + return this.lottoRankingResult.find(result => result.rank === rank).rankedList.length + } + + get profitAmount() { + return this.lottoRankingResult.reduce((sum, { rankedList, profit }) => sum + profit * rankedList.length, 0); + } - // 로또 매칭 결과 계산 -} \ No newline at end of file + get profitRatio() { + return parseFloat((this.profitAmount / this.payAmount * 100).toFixed(2)); + } +} diff --git a/src/domain/createLottoNumbers.js b/src/domain/createLottoNumbers.js index 676a126..cc5c1d6 100644 --- a/src/domain/createLottoNumbers.js +++ b/src/domain/createLottoNumbers.js @@ -1,4 +1,4 @@ -import {getRandomNumber} from "../utils/getRandomNumber.js"; +import { getRandomNumber } from "../utils/getRandomNumber.js"; const createLottoNumbers = () => { const lottoNumbers = new Set() diff --git a/src/validations/lottoSystem.js b/src/validations/lottoSystem.js new file mode 100644 index 0000000..4532238 --- /dev/null +++ b/src/validations/lottoSystem.js @@ -0,0 +1,29 @@ +import createValidator from "./createValidator.js"; + +export const lottoSystemValidations = { + validRanks: { + check: ({ rankingData }) => rankingData.every(({ rank }) => Number.isInteger(rank) && rank > 0), + errorMessage: '랭킹 데이터에 유효하지 않은 값이(rank) 포함되어 있습니다.' + }, + validMatchCounts: { + check: ({ rankingData }) => rankingData.every(({ matchCount }) => Number.isInteger(matchCount) && 0 < matchCount && matchCount <= 6), + errorMessage: '랭킹 데이터에 유효하지 않은 값이(matchCount) 포함되어 있습니다.' + }, + validBonusMatches: { + check: ({ rankingData }) => rankingData.every(({ bonusMatch }) => typeof bonusMatch === 'boolean'), + errorMessage: '랭킹 데이터에 유효하지 않은 값이(bonusMatch) 포함되어 있습니다.' + }, + validProfits: { + check: ({ rankingData }) => rankingData.every(({ profit }) => Number.isInteger(profit) && profit > 0), + errorMessage: '랭킹 데이터에 유효하지 않은 값이(profit) 포함되어 있습니다.' + }, + hasAllRanks: { + check: ({ rankingData }) => { + const ranks = rankingData.map(data => data.rank).sort((a, b) => a - b); + return rankingData.length > 0 && ranks.every((rank, index) => rank === index + 1); + }, + errorMessage: '랭킹 데이터에 모든 등수가 빠짐없이 있어야 합니다.' + } +}; + +export const validateLottoSystem = createValidator(lottoSystemValidations) \ No newline at end of file From f693fe0e0f48934a2d3325768cf1589c9a1c45a5 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 7 Jul 2024 21:41:25 +0900 Subject: [PATCH 06/22] =?UTF-8?q?feat:=20viewer=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20Matcher,=20Payment=20static=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lottoMatcher.test.js | 5 ++- src/__tests__/lottoPayment.test.js | 14 +++----- src/__tests__/lottoSystem.test.js | 6 ++-- src/domain/LottoMatcher.js | 17 +++++----- src/domain/LottoPayment.js | 17 +++------- src/domain/LottoSystem.js | 15 +++------ src/domain/LottoSystemController.js | 3 ++ src/domain/LottoSystemViewer.js | 51 +++++++++++++++++++++++++++++ src/service/ConsoleReader.js | 4 +-- 9 files changed, 83 insertions(+), 49 deletions(-) create mode 100644 src/domain/LottoSystemController.js create mode 100644 src/domain/LottoSystemViewer.js diff --git a/src/__tests__/lottoMatcher.test.js b/src/__tests__/lottoMatcher.test.js index c84d524..8f234c5 100644 --- a/src/__tests__/lottoMatcher.test.js +++ b/src/__tests__/lottoMatcher.test.js @@ -41,11 +41,10 @@ describe('로또 번호 일치여부 계산 테스트', () => { const lottoTicket2 = createLottoTicket([11,12,13,14,15,16]) // 0, false const lottoTicket3 = createLottoTicket([1,2,3,4,7,8]) // 4, true - const lottoMatcher = new LottoMatcher({ + expect(LottoMatcher.matchLotto({ winningLotto, lottoTickets: [lottoTicket1, lottoTicket2, lottoTicket3] - }) - expect(lottoMatcher.lottoMatchResult).toEqual([ + })).toEqual([ { lotto: lottoTicket1, matchCount: 3, diff --git a/src/__tests__/lottoPayment.test.js b/src/__tests__/lottoPayment.test.js index bd54f94..964490d 100644 --- a/src/__tests__/lottoPayment.test.js +++ b/src/__tests__/lottoPayment.test.js @@ -3,24 +3,20 @@ import { lottoPaymentValidations } from "../validations/lottoPayment.js"; describe('로또 결제 테스트', () => { test('로또 결제 금액은 정수여야 한다.', () => { - expect(() => new LottoPayment({ + expect(() => LottoPayment.createLottoTickets({ payAmount: '1000' })).toThrow(lottoPaymentValidations.payAmountInteger.errorMessage) }) test('로또 결제는 1000원 단위로만 가능하다.', () => { - expect(() => new LottoPayment(({ + expect(() => LottoPayment.createLottoTickets({ payAmount: 1100 - }))).toThrow(lottoPaymentValidations.payAmountUnit1000.errorMessage) + })).toThrow(lottoPaymentValidations.payAmountUnit1000.errorMessage) }) test('1000원 당 1장의 로또 티켓을 발행해야 한다.', () => { - const lottoPayment = new LottoPayment({ + expect(LottoPayment.createLottoTickets({ payAmount: 8000 - }) - lottoPayment.createLottoTickets() - - expect(lottoPayment.ticketCount).toBe(8) - expect(lottoPayment.paidTickets.length).toBe(8) + }).length).toBe(8) }) }) \ No newline at end of file diff --git a/src/__tests__/lottoSystem.test.js b/src/__tests__/lottoSystem.test.js index 3932c5c..5806dfb 100644 --- a/src/__tests__/lottoSystem.test.js +++ b/src/__tests__/lottoSystem.test.js @@ -86,9 +86,9 @@ describe('로또 시스템 테스트', async() => { lottoSystem.setWinningLotto([1,2,3,4,5,6], 7) lottoSystem.payLottoTicket(6000) - expect(lottoSystem.getTicketCountByRank(1)).toBe(1) - expect(lottoSystem.getTicketCountByRank(2)).toBe(2) - expect(lottoSystem.getTicketCountByRank(4)).toBe(3) + expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 1).ticketList.length).toBe(1) + expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 2).ticketList.length).toBe(2) + expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 4).ticketList.length).toBe(3) }) test('로또 시스템은 총 수익을 계산할 수 있다.', () => { diff --git a/src/domain/LottoMatcher.js b/src/domain/LottoMatcher.js index bd5b7b2..fffe57b 100644 --- a/src/domain/LottoMatcher.js +++ b/src/domain/LottoMatcher.js @@ -8,11 +8,12 @@ export default class LottoMatcher{ this.lottoTickets = lottoTickets } - get lottoMatchResult() { - return this.lottoTickets.map(lottoTicket => ({ + static matchLotto({ winningLotto, lottoTickets }) { + LottoMatcher.#validate({ winningLotto, lottoTickets }) + return lottoTickets.map(lottoTicket => ({ lotto: lottoTicket, - matchCount: this.#getMatchCount(lottoTicket.numbers), - bonusMatch: this.#getBonusMatch(lottoTicket.numbers) + matchCount: LottoMatcher.#getMatchCount(lottoTicket.numbers, winningLotto), + bonusMatch: LottoMatcher.#getBonusMatch(lottoTicket.numbers, winningLotto) })) } @@ -22,11 +23,11 @@ export default class LottoMatcher{ }) } - #getMatchCount(ticketNumbers) { - return ticketNumbers.filter(ticketNumber => this.winningLotto.numbers.includes(ticketNumber)).length + static #getMatchCount(ticketNumbers, winningLotto) { + return ticketNumbers.filter(ticketNumber => winningLotto.numbers.includes(ticketNumber)).length } - #getBonusMatch(ticketNumbers) { - return ticketNumbers.includes(this.winningLotto.bonusNumber) + static #getBonusMatch(ticketNumbers, winningLotto) { + return ticketNumbers.includes(winningLotto.bonusNumber) } } \ No newline at end of file diff --git a/src/domain/LottoPayment.js b/src/domain/LottoPayment.js index 21fbd55..189448e 100644 --- a/src/domain/LottoPayment.js +++ b/src/domain/LottoPayment.js @@ -3,25 +3,16 @@ import createLottoNumbers from "./createLottoNumbers.js"; import {createLottoTicket} from "./Lotto.js"; export default class LottoPayment { - #payAmount; - constructor({ payAmount }) { - LottoPayment.#validate({ payAmount }) - - this.#payAmount = payAmount - } - static #validate(lottoPaymentProps) { validateLottoPayment({ target: lottoPaymentProps }) } + static createLottoTickets({ payAmount }) { + LottoPayment.#validate({ payAmount }) - get ticketCount() { - return Math.floor(this.#payAmount / 1000) - } - - createLottoTickets() { - return Array.from({ length: this.ticketCount }, () => { + const ticketCount = Math.floor(payAmount / 1000) + return Array.from({ length: ticketCount }, () => { const numbers = createLottoNumbers() return createLottoTicket(numbers) }) diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index f3e0586..e82e952 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -27,7 +27,7 @@ export default class LottoSystem { } payLottoTicket(payAmount) { - this.lottoData.lottoTickets = new LottoPayment({ payAmount }).createLottoTickets(); + this.lottoData.lottoTickets = LottoPayment.createLottoTickets({ payAmount }); this.payAmount = payAmount } @@ -36,22 +36,17 @@ export default class LottoSystem { } get lottoRankingResult() { - const { lottoMatchResult } = new LottoMatcher(this.lottoData); - + const lottoMatchResult = LottoMatcher.matchLotto(this.lottoData); return this.rankingData.map((data) => ({ ...data, - rankedList: lottoMatchResult + ticketList : lottoMatchResult .filter(({ matchCount, bonusMatch }) => matchCount === data.matchCount && bonusMatch === data.bonusMatch) .map(({ lotto }) => lotto) - })); - } - - getTicketCountByRank(rank) { - return this.lottoRankingResult.find(result => result.rank === rank).rankedList.length + })) } get profitAmount() { - return this.lottoRankingResult.reduce((sum, { rankedList, profit }) => sum + profit * rankedList.length, 0); + return this.lottoRankingResult.reduce((sum, { ticketList, profit }) => sum + profit * ticketList.length, 0); } get profitRatio() { diff --git a/src/domain/LottoSystemController.js b/src/domain/LottoSystemController.js new file mode 100644 index 0000000..399c07f --- /dev/null +++ b/src/domain/LottoSystemController.js @@ -0,0 +1,3 @@ +export default class LottoSystemController{ + +} \ No newline at end of file diff --git a/src/domain/LottoSystemViewer.js b/src/domain/LottoSystemViewer.js new file mode 100644 index 0000000..5e4b68b --- /dev/null +++ b/src/domain/LottoSystemViewer.js @@ -0,0 +1,51 @@ +import ConsolePrinter from "../service/ConsolePrinter.js"; +import ConsoleReader from "../service/ConsoleReader.js"; + +export default class LottoSystemViewer{ + constructor() { + this.printer = new ConsolePrinter({ + lottoTicket: '%{1} | %{2} | %{3} | %{4} | %{5} | %{6}', + startShowResult: '\n당첨 통계\n---------------------------------------------', + ranking: '%{1}등 : %{2}개 일치 (%{3}원) - %{4}개', + rankingWithBonus: '%{1}등 : %{2}개 일치, 보너스볼 일치 (%{3}원) - %{4}개', + profitRatio: '총 수익률은 %{1}% 입니다.' + }) + this.reader = new ConsoleReader() + } + + async readPayAmount() { + const answer = await this.reader.read('> 구입 금액을 입력해 주세요. : ') + return Number(answer) + } + + async readWinningNumbers() { + const answer = await this.reader.read('> 당첨 번호를 입력해 주세요. : ') + return answer.split(',').map(Number) + } + + async readBonusNumber() { + const answer = await this.reader.read('> 보너스 번호를 입력해 주세요. : ') + return Number(answer) + } + + displayLottoTickets({ lottoData }) { + lottoData.lottoTickets.forEach(({ numbers }) => { + const numbersForPrint = numbers.map(number => `${number}`.padStart(2, '0')) + this.printer.printWithTemplate('lottoTicket', numbersForPrint) + }) + } + + displayLottoResult({ lottoRankingResult, profitRatio }) { + this.printer.printWithTemplate('startShowResult') + lottoRankingResult.forEach(({ rank, matchCount, bonusMatch, profit, ticketList }) => { + const resultForPrint = [rank, matchCount, profit, ticketList.length] + if (bonusMatch) { + this.printer.printWithTemplate('rankingWithBonus', resultForPrint) + } + else { + this.printer.printWithTemplate('ranking', resultForPrint) + } + }) + this.printer.printWithTemplate('profitRatio', [profitRatio]) + } +} diff --git a/src/service/ConsoleReader.js b/src/service/ConsoleReader.js index e6947ab..ebabacd 100644 --- a/src/service/ConsoleReader.js +++ b/src/service/ConsoleReader.js @@ -1,6 +1,6 @@ import createReadline from "../utils/createReadline.js"; -class ConsoleReader { +export default class ConsoleReader { constructor() { this.readLine = createReadline() } @@ -35,5 +35,3 @@ class ConsoleReader { } const consoleReader = new ConsoleReader() - -export default consoleReader \ No newline at end of file From 2198c35b489f6726fed70eba107b4c5a773d0a29 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 7 Jul 2024 22:19:18 +0900 Subject: [PATCH 07/22] =?UTF-8?q?feat:=20controller=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EB=B0=8F=20main=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lottoMatcher.test.js | 8 +++---- src/domain/LottoMatcher.js | 8 +------ src/domain/LottoSystemController.js | 37 +++++++++++++++++++++++++++++ src/domain/LottoSystemViewer.js | 34 ++++++++++++++++++++++---- src/main.js | 19 ++++++++------- src/service/ConsoleReader.js | 8 +++---- 6 files changed, 87 insertions(+), 27 deletions(-) diff --git a/src/__tests__/lottoMatcher.test.js b/src/__tests__/lottoMatcher.test.js index 8f234c5..18b443c 100644 --- a/src/__tests__/lottoMatcher.test.js +++ b/src/__tests__/lottoMatcher.test.js @@ -8,12 +8,12 @@ describe('로또 번호 일치여부 계산 테스트', () => { test('유효한 당첨 로또로만 계산할 수 있다.', () => { const validLottoTicket = createLottoTicket([11,12,13,14,15,16]) - expect(() => new LottoMatcher({ + expect(() => LottoMatcher.matchLotto({ winningLotto: { type: LOTTO_TYPE.WINNING, numbers: [1,2,3,4,5,6], bonusNumber: 8 }, lottoTickets: [ validLottoTicket ] })).toThrow(lottoMatcherValidations.validWinningLotto.errorMessage) - expect(() => new LottoMatcher({ + expect(() => LottoMatcher.matchLotto({ winningLotto: validLottoTicket, lottoTickets: [ validLottoTicket ] })).toThrow(lottoMatcherValidations.validWinningLotto.errorMessage) @@ -24,12 +24,12 @@ describe('로또 번호 일치여부 계산 테스트', () => { [1,2,3,4,5,6], 7 ) - expect(() => new LottoMatcher({ + expect(() => LottoMatcher.matchLotto({ winningLotto: validWinningLotto, lottoTickets: [ { type: LOTTO_TYPE.TICKET, numbers: [1,2,3,4,5,6] } ] })) - expect(() => new LottoMatcher({ + expect(() => LottoMatcher.matchLotto({ winningLotto: validWinningLotto, lottoTickets: [ validWinningLotto ] })).toThrow(lottoMatcherValidations.validLottoTickets.errorMessage) diff --git a/src/domain/LottoMatcher.js b/src/domain/LottoMatcher.js index fffe57b..35f7a9e 100644 --- a/src/domain/LottoMatcher.js +++ b/src/domain/LottoMatcher.js @@ -1,15 +1,9 @@ import {validateLottoMatcher} from "../validations/lottoMatcher.js"; export default class LottoMatcher{ - constructor({ winningLotto, lottoTickets }) { - LottoMatcher.#validate({ winningLotto, lottoTickets }) - - this.winningLotto = winningLotto - this.lottoTickets = lottoTickets - } - static matchLotto({ winningLotto, lottoTickets }) { LottoMatcher.#validate({ winningLotto, lottoTickets }) + return lottoTickets.map(lottoTicket => ({ lotto: lottoTicket, matchCount: LottoMatcher.#getMatchCount(lottoTicket.numbers, winningLotto), diff --git a/src/domain/LottoSystemController.js b/src/domain/LottoSystemController.js index 399c07f..fd81c41 100644 --- a/src/domain/LottoSystemController.js +++ b/src/domain/LottoSystemController.js @@ -1,3 +1,40 @@ + export default class LottoSystemController{ + constructor({ lottoSystem, viewer }) { + this.lottoSystem = lottoSystem + this.viewer = viewer + } + + async setUpPayAmount () { + try { + const payAmount = await this.viewer.readPayAmount() + this.lottoSystem.payLottoTicket(payAmount) + }catch (error) { + this.viewer.displayError(error) + await this.setUpPayAmount() + } + } + + async setUpWinningLotto() { + try { + const winningNumbers = await this.viewer.readWinningNumbers() + const bonusNumber = await this.viewer.readBonusNumber() + this.lottoSystem.setWinningLotto(winningNumbers, bonusNumber) + }catch (error) { + this.viewer.displayError(error) + await this.setUpWinningLotto() + } + } + + async run() { + await this.setUpPayAmount() + this.viewer.displayPaidCount(this.lottoSystem) + this.viewer.displayLottoTickets(this.lottoSystem) + + await this.setUpWinningLotto() + this.viewer.displayLottoResult(this.lottoSystem) + this.viewer.displayProfitRatio(this.lottoSystem) + this.viewer.finish() + } } \ No newline at end of file diff --git a/src/domain/LottoSystemViewer.js b/src/domain/LottoSystemViewer.js index 5e4b68b..db9b04f 100644 --- a/src/domain/LottoSystemViewer.js +++ b/src/domain/LottoSystemViewer.js @@ -4,11 +4,13 @@ import ConsoleReader from "../service/ConsoleReader.js"; export default class LottoSystemViewer{ constructor() { this.printer = new ConsolePrinter({ + paidCount: '%{1}개를 구매했습니다.', lottoTicket: '%{1} | %{2} | %{3} | %{4} | %{5} | %{6}', - startShowResult: '\n당첨 통계\n---------------------------------------------', + line: '---------------------------------------------', ranking: '%{1}등 : %{2}개 일치 (%{3}원) - %{4}개', rankingWithBonus: '%{1}등 : %{2}개 일치, 보너스볼 일치 (%{3}원) - %{4}개', - profitRatio: '총 수익률은 %{1}% 입니다.' + profitRatio: '총 수익률은 %{1}% 입니다.', + error: '⚠️ %{1}' }) this.reader = new ConsoleReader() } @@ -28,15 +30,24 @@ export default class LottoSystemViewer{ return Number(answer) } + displayPaidCount({ ticketCount }) { + this.printer.lineBreak() + this.printer.printWithTemplate('paidCount', [ticketCount]) + } + displayLottoTickets({ lottoData }) { lottoData.lottoTickets.forEach(({ numbers }) => { const numbersForPrint = numbers.map(number => `${number}`.padStart(2, '0')) this.printer.printWithTemplate('lottoTicket', numbersForPrint) }) + this.printer.lineBreak() } - displayLottoResult({ lottoRankingResult, profitRatio }) { - this.printer.printWithTemplate('startShowResult') + displayLottoResult({ lottoRankingResult }) { + this.printer.lineBreak() + this.printer.print('당첨 통계') + this.printer.printWithTemplate('line') + lottoRankingResult.forEach(({ rank, matchCount, bonusMatch, profit, ticketList }) => { const resultForPrint = [rank, matchCount, profit, ticketList.length] if (bonusMatch) { @@ -46,6 +57,21 @@ export default class LottoSystemViewer{ this.printer.printWithTemplate('ranking', resultForPrint) } }) + } + + displayProfitRatio({ profitRatio }) { + this.printer.printWithTemplate('line') this.printer.printWithTemplate('profitRatio', [profitRatio]) + this.printer.lineBreak() + } + + displayError(error) { + this.printer.lineBreak() + this.printer.printWithTemplate('error',[ error.message]) + this.printer.lineBreak() + } + + finish() { + this.reader.close() } } diff --git a/src/main.js b/src/main.js index 756fad3..0ecc8fa 100644 --- a/src/main.js +++ b/src/main.js @@ -1,11 +1,14 @@ -import Lotto from "./domain/Lotto.js"; +import LottoSystemController from "./domain/LottoSystemController.js"; +import LottoSystem from "./domain/LottoSystem.js"; +import LOTTO_RANKING_DATA from "./constants/lottoRankingData.js"; +import LottoSystemViewer from "./domain/LottoSystemViewer.js"; async function main() { - new Lotto({ - type: LOTTO_TYPE.TICKET, - numbers: [1,2,3,4,5,6] - }) + await new LottoSystemController({ + lottoSystem: new LottoSystem({ + rankingData: LOTTO_RANKING_DATA + }), + viewer: new LottoSystemViewer() + }).run() } -main(); - -// 로또Q:q \ No newline at end of file +main(); \ No newline at end of file diff --git a/src/service/ConsoleReader.js b/src/service/ConsoleReader.js index ebabacd..ce2de56 100644 --- a/src/service/ConsoleReader.js +++ b/src/service/ConsoleReader.js @@ -26,12 +26,12 @@ export default class ConsoleReader { if (errorMessage) { return processInput() } - this.readLine.close() return input } - return processInput() } -} -const consoleReader = new ConsoleReader() + close() { + this.readLine.close() + } +} From 6c1a6814158633cb94d981c3e866d695caba05ec Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 7 Jul 2024 22:29:13 +0900 Subject: [PATCH 08/22] =?UTF-8?q?feat:=20currency=20=EC=9C=A0=ED=8B=B8=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/LottoSystemViewer.js | 5 +++-- src/utils/formatCurrency.js | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 src/utils/formatCurrency.js diff --git a/src/domain/LottoSystemViewer.js b/src/domain/LottoSystemViewer.js index db9b04f..dbb2eb7 100644 --- a/src/domain/LottoSystemViewer.js +++ b/src/domain/LottoSystemViewer.js @@ -1,5 +1,6 @@ import ConsolePrinter from "../service/ConsolePrinter.js"; import ConsoleReader from "../service/ConsoleReader.js"; +import formatCurrency from "../utils/formatCurrency.js"; export default class LottoSystemViewer{ constructor() { @@ -37,7 +38,7 @@ export default class LottoSystemViewer{ displayLottoTickets({ lottoData }) { lottoData.lottoTickets.forEach(({ numbers }) => { - const numbersForPrint = numbers.map(number => `${number}`.padStart(2, '0')) + const numbersForPrint = numbers.map(number => `${number}`.padStart(2, '0')).sort() this.printer.printWithTemplate('lottoTicket', numbersForPrint) }) this.printer.lineBreak() @@ -49,7 +50,7 @@ export default class LottoSystemViewer{ this.printer.printWithTemplate('line') lottoRankingResult.forEach(({ rank, matchCount, bonusMatch, profit, ticketList }) => { - const resultForPrint = [rank, matchCount, profit, ticketList.length] + const resultForPrint = [rank, matchCount, formatCurrency(profit), ticketList.length] if (bonusMatch) { this.printer.printWithTemplate('rankingWithBonus', resultForPrint) } diff --git a/src/utils/formatCurrency.js b/src/utils/formatCurrency.js new file mode 100644 index 0000000..eb92557 --- /dev/null +++ b/src/utils/formatCurrency.js @@ -0,0 +1,5 @@ +const formatCurrency = (number) => { + return new Intl.NumberFormat('ko-KR').format(number); +} + +export default formatCurrency \ No newline at end of file From 9d7e79ed9d91be551c29c8ebcc1b7c99e1773513 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 7 Jul 2024 23:25:52 +0900 Subject: [PATCH 09/22] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lotto.test.js | 127 +++++++++++------- src/__tests__/lottoMatcher.test.js | 91 +++++++------ src/__tests__/lottoPayment.test.js | 36 ++--- src/__tests__/lottoSystem.test.js | 198 ++++++++++++++++------------ src/__tests__/sum.test.js | 11 -- src/constants/lottoRankingData.js | 60 +++++---- src/constants/lottoType.js | 6 +- src/domain/Lotto.js | 37 +++--- src/domain/LottoMatcher.js | 22 ++-- src/domain/LottoPayment.js | 22 ++-- src/domain/LottoSystem.js | 32 ++--- src/domain/LottoSystemController.js | 47 ++++--- src/domain/LottoSystemViewer.js | 69 +++++----- src/domain/createLottoNumbers.js | 12 +- src/main.js | 16 +-- src/service/ConsolePrinter.js | 2 +- src/service/ConsoleReader.js | 34 ++--- src/utils/createReadline.js | 2 +- src/utils/formatCurrency.js | 4 +- src/utils/getRandomNumber.js | 2 +- src/validations/createValidator.js | 14 +- src/validations/lotto.js | 36 ++--- src/validations/lottoMatcher.js | 19 +-- src/validations/lottoPayment.js | 12 +- src/validations/lottoSystem.js | 21 +-- 25 files changed, 498 insertions(+), 434 deletions(-) delete mode 100644 src/__tests__/sum.test.js diff --git a/src/__tests__/lotto.test.js b/src/__tests__/lotto.test.js index 7410d26..4b64e23 100644 --- a/src/__tests__/lotto.test.js +++ b/src/__tests__/lotto.test.js @@ -1,67 +1,94 @@ -import Lotto from "../domain/Lotto.js"; -import {lottoValidations} from "../validations/lotto.js"; -import LOTTO_TYPE from "../constants/lottoType.js"; +import Lotto from '../domain/Lotto.js'; +import { lottoValidations } from '../validations/lotto.js'; +import LOTTO_TYPE from '../constants/lottoType.js'; describe('로또 테스트', () => { test('로또 번호는 [당첨, 티켓] 두가지 유형을 가질 수 있다.', () => { - expect(() => new Lotto({ - type: 'otherType', - numbers: [1,2,3,4,5,6], - })).toThrow(lottoValidations.lottoType.errorMessage) - }) + expect( + () => + new Lotto({ + type: 'otherType', + numbers: [1, 2, 3, 4, 5, 6], + }), + ).toThrow(lottoValidations.lottoType.errorMessage); + }); test('로또 번호는 6개여야 한다.', () => { - expect(() => new Lotto({ - type: LOTTO_TYPE.TICKET, - numbers: [1,2,3,4,5] - })).toThrow(lottoValidations.lottoNumbersLength.errorMessage) - }) + expect( + () => + new Lotto({ + type: LOTTO_TYPE.TICKET, + numbers: [1, 2, 3, 4, 5], + }), + ).toThrow(lottoValidations.lottoNumbersLength.errorMessage); + }); test('당첨 로또 번호는 보너스 번호를 가져야 한다.', () => { - expect(() => new Lotto({ - type: LOTTO_TYPE.WINNING, - numbers: [1,2,3,4,5,6] - })).toThrow(lottoValidations.winningLottoHasBonus.errorMessage) - }) + expect( + () => + new Lotto({ + type: LOTTO_TYPE.WINNING, + numbers: [1, 2, 3, 4, 5, 6], + }), + ).toThrow(lottoValidations.winningLottoHasBonus.errorMessage); + }); test('티켓 로또 번호는 보너스 번호가 없어야 한다.', () => { - expect(() => new Lotto({ - type: LOTTO_TYPE.TICKET, - numbers: [1,2,3,4,5,6], - bonusNumber: 7 - })).toThrow(lottoValidations.ticketLottoBonusNull.errorMessage) - }) + expect( + () => + new Lotto({ + type: LOTTO_TYPE.TICKET, + numbers: [1, 2, 3, 4, 5, 6], + bonusNumber: 7, + }), + ).toThrow(lottoValidations.ticketLottoBonusNull.errorMessage); + }); test('로또 번호는 각각 달라야 한다.', () => { - expect(() => new Lotto({ - type: LOTTO_TYPE.WINNING, - numbers: [1, 2, 3, 4, 5, 6], - bonusNumber: 6 - })).toThrow(lottoValidations.lottoEachUnique.errorMessage) + expect( + () => + new Lotto({ + type: LOTTO_TYPE.WINNING, + numbers: [1, 2, 3, 4, 5, 6], + bonusNumber: 6, + }), + ).toThrow(lottoValidations.lottoEachUnique.errorMessage); - expect(() => new Lotto({ - type: LOTTO_TYPE.TICKET, - numbers: [1, 2, 3, 3, 5, 6], - })).toThrow(lottoValidations.lottoEachUnique.errorMessage) - }) + expect( + () => + new Lotto({ + type: LOTTO_TYPE.TICKET, + numbers: [1, 2, 3, 3, 5, 6], + }), + ).toThrow(lottoValidations.lottoEachUnique.errorMessage); + }); test('모든 로또 번호는 정수여야 한다.', () => { - expect(() => new Lotto({ - type: LOTTO_TYPE.TICKET, - numbers: [1, 2, 3, 4, 5, '6'] - })).toThrow(lottoValidations.lottoInteger.errorMessage) - }) + expect( + () => + new Lotto({ + type: LOTTO_TYPE.TICKET, + numbers: [1, 2, 3, 4, 5, '6'], + }), + ).toThrow(lottoValidations.lottoInteger.errorMessage); + }); test('모든 로또 번호는 1 이상 45 이하여야 한다.', () => { - expect(() => new Lotto({ - type: LOTTO_TYPE.WINNING, - numbers: [1, 2, 3, 4, 5, 45], - bonusNumber: 46 - })).toThrow(lottoValidations.lottoRange.errorMessage) + expect( + () => + new Lotto({ + type: LOTTO_TYPE.WINNING, + numbers: [1, 2, 3, 4, 5, 45], + bonusNumber: 46, + }), + ).toThrow(lottoValidations.lottoRange.errorMessage); - expect(() => new Lotto({ - type: LOTTO_TYPE.TICKET, - numbers: [0, 1, 2, 3, 4, 5], - })).toThrow(lottoValidations.lottoRange.errorMessage) - }) -}) \ No newline at end of file + expect( + () => + new Lotto({ + type: LOTTO_TYPE.TICKET, + numbers: [0, 1, 2, 3, 4, 5], + }), + ).toThrow(lottoValidations.lottoRange.errorMessage); + }); +}); diff --git a/src/__tests__/lottoMatcher.test.js b/src/__tests__/lottoMatcher.test.js index 18b443c..9472666 100644 --- a/src/__tests__/lottoMatcher.test.js +++ b/src/__tests__/lottoMatcher.test.js @@ -1,65 +1,72 @@ -import {createLottoTicket, createWinningLotto} from "../domain/Lotto.js"; -import LottoMatcher from "../domain/LottoMatcher.js"; -import {lottoMatcherValidations} from "../validations/lottoMatcher.js"; -import LOTTO_TYPE from "../constants/lottoType.js"; +import { createLottoTicket, createWinningLotto } from '../domain/Lotto.js'; +import LottoMatcher from '../domain/LottoMatcher.js'; +import { lottoMatcherValidations } from '../validations/lottoMatcher.js'; +import LOTTO_TYPE from '../constants/lottoType.js'; describe('로또 번호 일치여부 계산 테스트', () => { - test('유효한 당첨 로또로만 계산할 수 있다.', () => { - const validLottoTicket = createLottoTicket([11,12,13,14,15,16]) + const validLottoTicket = createLottoTicket([11, 12, 13, 14, 15, 16]); - expect(() => LottoMatcher.matchLotto({ - winningLotto: { type: LOTTO_TYPE.WINNING, numbers: [1,2,3,4,5,6], bonusNumber: 8 }, - lottoTickets: [ validLottoTicket ] - })).toThrow(lottoMatcherValidations.validWinningLotto.errorMessage) + expect(() => + LottoMatcher.matchLotto({ + winningLotto: { type: LOTTO_TYPE.WINNING, numbers: [1, 2, 3, 4, 5, 6], bonusNumber: 8 }, + lottoTickets: [validLottoTicket], + }), + ).toThrow(lottoMatcherValidations.validWinningLotto.errorMessage); - expect(() => LottoMatcher.matchLotto({ - winningLotto: validLottoTicket, - lottoTickets: [ validLottoTicket ] - })).toThrow(lottoMatcherValidations.validWinningLotto.errorMessage) - }) + expect(() => + LottoMatcher.matchLotto({ + winningLotto: validLottoTicket, + lottoTickets: [validLottoTicket], + }), + ).toThrow(lottoMatcherValidations.validWinningLotto.errorMessage); + }); test('유효한 로또 티켓들만 계산할 수 있다.', () => { - const validWinningLotto = createWinningLotto( - [1,2,3,4,5,6], 7 - ) + const validWinningLotto = createWinningLotto([1, 2, 3, 4, 5, 6], 7); - expect(() => LottoMatcher.matchLotto({ - winningLotto: validWinningLotto, - lottoTickets: [ { type: LOTTO_TYPE.TICKET, numbers: [1,2,3,4,5,6] } ] - })) + expect(() => + LottoMatcher.matchLotto({ + winningLotto: validWinningLotto, + lottoTickets: [{ type: LOTTO_TYPE.TICKET, numbers: [1, 2, 3, 4, 5, 6] }], + }), + ); - expect(() => LottoMatcher.matchLotto({ - winningLotto: validWinningLotto, - lottoTickets: [ validWinningLotto ] - })).toThrow(lottoMatcherValidations.validLottoTickets.errorMessage) - }) + expect(() => + LottoMatcher.matchLotto({ + winningLotto: validWinningLotto, + lottoTickets: [validWinningLotto], + }), + ).toThrow(lottoMatcherValidations.validLottoTickets.errorMessage); + }); test('로또 티켓들의 [당첨 번호 일치 개수, 보너스 번호 일치 여부]를 반환한다.', () => { - const winningLotto = createWinningLotto([1,2,3,4,5,6], 7) - const lottoTicket1 = createLottoTicket([1,2,3,14,15,16]) // 3, false - const lottoTicket2 = createLottoTicket([11,12,13,14,15,16]) // 0, false - const lottoTicket3 = createLottoTicket([1,2,3,4,7,8]) // 4, true + const winningLotto = createWinningLotto([1, 2, 3, 4, 5, 6], 7); + const lottoTicket1 = createLottoTicket([1, 2, 3, 14, 15, 16]); // 3, false + const lottoTicket2 = createLottoTicket([11, 12, 13, 14, 15, 16]); // 0, false + const lottoTicket3 = createLottoTicket([1, 2, 3, 4, 7, 8]); // 4, true - expect(LottoMatcher.matchLotto({ - winningLotto, - lottoTickets: [lottoTicket1, lottoTicket2, lottoTicket3] - })).toEqual([ + expect( + LottoMatcher.matchLotto({ + winningLotto, + lottoTickets: [lottoTicket1, lottoTicket2, lottoTicket3], + }), + ).toEqual([ { lotto: lottoTicket1, matchCount: 3, - bonusMatch: false + bonusMatch: false, }, { lotto: lottoTicket2, matchCount: 0, - bonusMatch: false + bonusMatch: false, }, { lotto: lottoTicket3, matchCount: 4, - bonusMatch: true - } - ]) - }) -}) \ No newline at end of file + bonusMatch: true, + }, + ]); + }); +}); diff --git a/src/__tests__/lottoPayment.test.js b/src/__tests__/lottoPayment.test.js index 964490d..a033fb0 100644 --- a/src/__tests__/lottoPayment.test.js +++ b/src/__tests__/lottoPayment.test.js @@ -1,22 +1,28 @@ -import LottoPayment from "../domain/LottoPayment.js"; -import { lottoPaymentValidations } from "../validations/lottoPayment.js"; +import LottoPayment from '../domain/LottoPayment.js'; +import { lottoPaymentValidations } from '../validations/lottoPayment.js'; describe('로또 결제 테스트', () => { test('로또 결제 금액은 정수여야 한다.', () => { - expect(() => LottoPayment.createLottoTickets({ - payAmount: '1000' - })).toThrow(lottoPaymentValidations.payAmountInteger.errorMessage) - }) + expect(() => + LottoPayment.createLottoTickets({ + payAmount: '1000', + }), + ).toThrow(lottoPaymentValidations.payAmountInteger.errorMessage); + }); test('로또 결제는 1000원 단위로만 가능하다.', () => { - expect(() => LottoPayment.createLottoTickets({ - payAmount: 1100 - })).toThrow(lottoPaymentValidations.payAmountUnit1000.errorMessage) - }) + expect(() => + LottoPayment.createLottoTickets({ + payAmount: 1100, + }), + ).toThrow(lottoPaymentValidations.payAmountUnit1000.errorMessage); + }); test('1000원 당 1장의 로또 티켓을 발행해야 한다.', () => { - expect(LottoPayment.createLottoTickets({ - payAmount: 8000 - }).length).toBe(8) - }) -}) \ No newline at end of file + expect( + LottoPayment.createLottoTickets({ + payAmount: 8000, + }).length, + ).toBe(8); + }); +}); diff --git a/src/__tests__/lottoSystem.test.js b/src/__tests__/lottoSystem.test.js index 5806dfb..47f2f3e 100644 --- a/src/__tests__/lottoSystem.test.js +++ b/src/__tests__/lottoSystem.test.js @@ -1,117 +1,143 @@ -import LottoSystem from "../domain/LottoSystem.js"; -import {lottoSystemValidations} from "../validations/lottoSystem.js"; -import createLottoNumbers from "../domain/createLottoNumbers.js"; - +import LottoSystem from '../domain/LottoSystem.js'; +import { lottoSystemValidations } from '../validations/lottoSystem.js'; +import createLottoNumbers from '../domain/createLottoNumbers.js'; vi.mock('../domain/createLottoNumbers'); -describe('로또 시스템 테스트', async() => { - const originalCreateLottoNumbers = await vi.importActual('../domain/createLottoNumbers').then(module => module.default); +describe('로또 시스템 테스트', async () => { + const originalCreateLottoNumbers = await vi + .importActual('../domain/createLottoNumbers') + .then((module) => module.default); beforeEach(() => { createLottoNumbers.mockReset(); - createLottoNumbers.mockImplementation(originalCreateLottoNumbers) + createLottoNumbers.mockImplementation(originalCreateLottoNumbers); }); test('로또 시스템은 순위가 올바르게 매겨진 랭킹 데이터만 사용한다.', () => { - const rankingDataHasInvalidRank1 = [{ - matchCount: 3, - bonusMatch: false, - profit: 1000, - rank: -1, - }] - expect(() => new LottoSystem({ - rankingData: rankingDataHasInvalidRank1 - })).toThrow(lottoSystemValidations.validRanks.errorMessage) - - const rankingDataHasInvalidRank2 = [{ - matchCount: 3, - bonusMatch: false, - profit: 1000, - rank: 3 - }] - expect(() => new LottoSystem({ - rankingData: rankingDataHasInvalidRank2 - })).toThrow(lottoSystemValidations.hasAllRanks.errorMessage) - }) + const rankingDataHasInvalidRank1 = [ + { + matchCount: 3, + bonusMatch: false, + profit: 1000, + rank: -1, + }, + ]; + expect( + () => + new LottoSystem({ + rankingData: rankingDataHasInvalidRank1, + }), + ).toThrow(lottoSystemValidations.validRanks.errorMessage); + + const rankingDataHasInvalidRank2 = [ + { + matchCount: 3, + bonusMatch: false, + profit: 1000, + rank: 3, + }, + ]; + expect( + () => + new LottoSystem({ + rankingData: rankingDataHasInvalidRank2, + }), + ).toThrow(lottoSystemValidations.hasAllRanks.errorMessage); + }); test('로또 시스템은 유효한 matchCount, bonus, profit 이 포함된 랭킹 데이터만 사용한다.', () => { - const rankingDataHasInvalidMatchCount = [{ - bonusMatch: true, - profit: 1000, - rank: 1, - matchCount: 100 - }] - expect(() => new LottoSystem({ - rankingData: rankingDataHasInvalidMatchCount - })).toThrow(lottoSystemValidations.validMatchCounts.errorMessage) - - const rankingDataHasInvalidBonusMatch = [{ - profit: 1000, - rank: 1, - matchCount: 3 - }] - expect(() => new LottoSystem({ - rankingData: rankingDataHasInvalidBonusMatch - })).toThrow(lottoSystemValidations.validBonusMatches.errorMessage) - - const rankingDataHasInvalidProfit = [{ - bonusMatch: true, - profit: '1000', - rank: 1, - matchCount: 3 - }] - expect(() => new LottoSystem({ - rankingData: rankingDataHasInvalidProfit - })).toThrow(lottoSystemValidations.validProfits.errorMessage) - }) + const rankingDataHasInvalidMatchCount = [ + { + bonusMatch: true, + profit: 1000, + rank: 1, + matchCount: 100, + }, + ]; + expect( + () => + new LottoSystem({ + rankingData: rankingDataHasInvalidMatchCount, + }), + ).toThrow(lottoSystemValidations.validMatchCounts.errorMessage); + + const rankingDataHasInvalidBonusMatch = [ + { + profit: 1000, + rank: 1, + matchCount: 3, + }, + ]; + expect( + () => + new LottoSystem({ + rankingData: rankingDataHasInvalidBonusMatch, + }), + ).toThrow(lottoSystemValidations.validBonusMatches.errorMessage); + + const rankingDataHasInvalidProfit = [ + { + bonusMatch: true, + profit: '1000', + rank: 1, + matchCount: 3, + }, + ]; + expect( + () => + new LottoSystem({ + rankingData: rankingDataHasInvalidProfit, + }), + ).toThrow(lottoSystemValidations.validProfits.errorMessage); + }); test('로또 시스템은 정확한 개수의 로또를 구매한다.', () => { - const lottoSystem = new LottoSystem() - lottoSystem.payLottoTicket(10000) + const lottoSystem = new LottoSystem(); + lottoSystem.payLottoTicket(10000); - expect(lottoSystem.ticketCount).toBe(10) - expect(lottoSystem.payAmount).toBe(10000) - }) + expect(lottoSystem.ticketCount).toBe(10); + expect(lottoSystem.payAmount).toBe(10000); + }); test('로또 시스템은 발행한 로또들을 등수 별로 분류할 수 있다.', () => { createLottoNumbers - .mockReturnValueOnce([1, 2, 3, 4, 5, 6]) // 1등 - .mockReturnValueOnce([1, 2, 3, 4, 5, 7]) // 2등 - .mockReturnValueOnce([1, 2, 3, 4, 5, 7]) // 2등 - .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 + .mockReturnValueOnce([1, 2, 3, 4, 5, 6]) // 1등 + .mockReturnValueOnce([1, 2, 3, 4, 5, 7]) // 2등 + .mockReturnValueOnce([1, 2, 3, 4, 5, 7]) // 2등 .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 + .mockReturnValueOnce([1, 2, 3, 4, 15, 16]); // 4등 - const lottoSystem = new LottoSystem() - lottoSystem.setWinningLotto([1,2,3,4,5,6], 7) - lottoSystem.payLottoTicket(6000) + const lottoSystem = new LottoSystem(); + lottoSystem.setWinningLotto([1, 2, 3, 4, 5, 6], 7); + lottoSystem.payLottoTicket(6000); - expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 1).ticketList.length).toBe(1) - expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 2).ticketList.length).toBe(2) - expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 4).ticketList.length).toBe(3) - }) + expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 1).ticketList.length).toBe(1); + expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 2).ticketList.length).toBe(2); + expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 4).ticketList.length).toBe(3); + }); test('로또 시스템은 총 수익을 계산할 수 있다.', () => { createLottoNumbers - .mockReturnValueOnce([1, 2, 3, 4, 5, 6]) // 1등 - .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 + .mockReturnValueOnce([1, 2, 3, 4, 5, 6]) // 1등 + .mockReturnValueOnce([1, 2, 3, 4, 15, 16]); // 4등 - const lottoSystem = new LottoSystem() - lottoSystem.setWinningLotto([1,2,3,4,5,6], 7) - lottoSystem.payLottoTicket(2000) + const lottoSystem = new LottoSystem(); + lottoSystem.setWinningLotto([1, 2, 3, 4, 5, 6], 7); + lottoSystem.payLottoTicket(2000); - expect(lottoSystem.profitAmount).toBe(2000050000) - }) + expect(lottoSystem.profitAmount).toBe(2000050000); + }); test('로또 시스템은 수익율을 계산할 수 있다.', () => { createLottoNumbers .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 = 50000 - .mockImplementation(() => [11, 12, 13, 14, 15, 16]) + .mockImplementation(() => [11, 12, 13, 14, 15, 16]); - const lottoSystem = new LottoSystem() - lottoSystem.setWinningLotto([1,2,3,4,5,6], 7) - lottoSystem.payLottoTicket(100000) + const lottoSystem = new LottoSystem(); + lottoSystem.setWinningLotto([1, 2, 3, 4, 5, 6], 7); + lottoSystem.payLottoTicket(100000); - expect(lottoSystem.profitRatio).toBe(50) - }) -}) \ No newline at end of file + expect(lottoSystem.profitRatio).toBe(50); + }); +}); diff --git a/src/__tests__/sum.test.js b/src/__tests__/sum.test.js deleted file mode 100644 index efc011c..0000000 --- a/src/__tests__/sum.test.js +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, test, expect } from "vitest"; - -function sum(...args) { - return args.reduce((a, b) => a+ b); -} - -describe('예제 테스트입니다.', () => { - test('sum > ', () => { - expect(sum(1,2,3,4,5)).toBe(15); - }) -}) diff --git a/src/constants/lottoRankingData.js b/src/constants/lottoRankingData.js index 43fd7fd..bcd3a2d 100644 --- a/src/constants/lottoRankingData.js +++ b/src/constants/lottoRankingData.js @@ -1,28 +1,34 @@ -const LOTTO_RANKING_DATA = [{ - rank: 5, - matchCount: 3, - bonusMatch: false, - profit: 5000 -}, { - rank: 4, - matchCount: 4, - bonusMatch: false, - profit: 50000 -}, { - rank: 3, - matchCount: 5, - bonusMatch: false, - profit: 1500000 -}, { - rank: 2, - matchCount: 5, - bonusMatch: true, - profit: 30000000 -}, { - rank: 1, - matchCount: 6, - bonusMatch: false, - profit: 2000000000 -}] +const LOTTO_RANKING_DATA = [ + { + rank: 5, + matchCount: 3, + bonusMatch: false, + profit: 5000, + }, + { + rank: 4, + matchCount: 4, + bonusMatch: false, + profit: 50000, + }, + { + rank: 3, + matchCount: 5, + bonusMatch: false, + profit: 1500000, + }, + { + rank: 2, + matchCount: 5, + bonusMatch: true, + profit: 30000000, + }, + { + rank: 1, + matchCount: 6, + bonusMatch: false, + profit: 2000000000, + }, +]; -export default LOTTO_RANKING_DATA \ No newline at end of file +export default LOTTO_RANKING_DATA; diff --git a/src/constants/lottoType.js b/src/constants/lottoType.js index 72087f7..d35802a 100644 --- a/src/constants/lottoType.js +++ b/src/constants/lottoType.js @@ -1,6 +1,6 @@ const LOTTO_TYPE = { TICKET: Symbol('lottoTicket'), - WINNING: Symbol('winningLotto') -} + WINNING: Symbol('winningLotto'), +}; -export default LOTTO_TYPE \ No newline at end of file +export default LOTTO_TYPE; diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index 3dd8994..e4c4732 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -1,27 +1,30 @@ -import { validateLotto } from "../validations/lotto.js"; -import LOTTO_TYPE from "../constants/lottoType.js"; +import { validateLotto } from '../validations/lotto.js'; +import LOTTO_TYPE from '../constants/lottoType.js'; -export default class Lotto{ +export default class Lotto { constructor({ type, numbers, bonusNumber = null }) { - Lotto.#validate({ type, numbers, bonusNumber }) - this.type = type - this.numbers = numbers - this.bonusNumber = bonusNumber + Lotto.#validate({ type, numbers, bonusNumber }); + this.type = type; + this.numbers = numbers; + this.bonusNumber = bonusNumber; } static #validate(lottoProps) { validateLotto({ - target: lottoProps - }) + target: lottoProps, + }); } } -export const createWinningLotto = (numbers, bonusNumber) => new Lotto({ - type: LOTTO_TYPE.WINNING, - numbers, bonusNumber -}) +export const createWinningLotto = (numbers, bonusNumber) => + new Lotto({ + type: LOTTO_TYPE.WINNING, + numbers, + bonusNumber, + }); -export const createLottoTicket = (numbers) => new Lotto({ - type: LOTTO_TYPE.TICKET, - numbers -}) \ No newline at end of file +export const createLottoTicket = (numbers) => + new Lotto({ + type: LOTTO_TYPE.TICKET, + numbers, + }); diff --git a/src/domain/LottoMatcher.js b/src/domain/LottoMatcher.js index 35f7a9e..7b59e6e 100644 --- a/src/domain/LottoMatcher.js +++ b/src/domain/LottoMatcher.js @@ -1,27 +1,27 @@ -import {validateLottoMatcher} from "../validations/lottoMatcher.js"; +import { validateLottoMatcher } from '../validations/lottoMatcher.js'; -export default class LottoMatcher{ +export default class LottoMatcher { static matchLotto({ winningLotto, lottoTickets }) { - LottoMatcher.#validate({ winningLotto, lottoTickets }) + LottoMatcher.#validate({ winningLotto, lottoTickets }); - return lottoTickets.map(lottoTicket => ({ + return lottoTickets.map((lottoTicket) => ({ lotto: lottoTicket, matchCount: LottoMatcher.#getMatchCount(lottoTicket.numbers, winningLotto), - bonusMatch: LottoMatcher.#getBonusMatch(lottoTicket.numbers, winningLotto) - })) + bonusMatch: LottoMatcher.#getBonusMatch(lottoTicket.numbers, winningLotto), + })); } static #validate(lottoMatcherProps) { validateLottoMatcher({ - target: lottoMatcherProps - }) + target: lottoMatcherProps, + }); } static #getMatchCount(ticketNumbers, winningLotto) { - return ticketNumbers.filter(ticketNumber => winningLotto.numbers.includes(ticketNumber)).length + return ticketNumbers.filter((ticketNumber) => winningLotto.numbers.includes(ticketNumber)).length; } static #getBonusMatch(ticketNumbers, winningLotto) { - return ticketNumbers.includes(winningLotto.bonusNumber) + return ticketNumbers.includes(winningLotto.bonusNumber); } -} \ No newline at end of file +} diff --git a/src/domain/LottoPayment.js b/src/domain/LottoPayment.js index 189448e..6cbbdc0 100644 --- a/src/domain/LottoPayment.js +++ b/src/domain/LottoPayment.js @@ -1,20 +1,20 @@ -import { validateLottoPayment } from "../validations/lottoPayment.js"; -import createLottoNumbers from "./createLottoNumbers.js"; -import {createLottoTicket} from "./Lotto.js"; +import { validateLottoPayment } from '../validations/lottoPayment.js'; +import createLottoNumbers from './createLottoNumbers.js'; +import { createLottoTicket } from './Lotto.js'; export default class LottoPayment { static #validate(lottoPaymentProps) { validateLottoPayment({ - target: lottoPaymentProps - }) + target: lottoPaymentProps, + }); } static createLottoTickets({ payAmount }) { - LottoPayment.#validate({ payAmount }) + LottoPayment.#validate({ payAmount }); - const ticketCount = Math.floor(payAmount / 1000) + const ticketCount = Math.floor(payAmount / 1000); return Array.from({ length: ticketCount }, () => { - const numbers = createLottoNumbers() - return createLottoTicket(numbers) - }) + const numbers = createLottoNumbers(); + return createLottoTicket(numbers); + }); } -} \ No newline at end of file +} diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index e82e952..16f1f5b 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -1,25 +1,25 @@ -import { createWinningLotto } from "./Lotto.js"; -import LottoPayment from "./LottoPayment.js"; -import LottoMatcher from "./LottoMatcher.js"; -import LOTTO_RANKING_DATA from "../constants/lottoRankingData.js"; -import {validateLottoSystem} from "../validations/lottoSystem.js"; +import { createWinningLotto } from './Lotto.js'; +import LottoPayment from './LottoPayment.js'; +import LottoMatcher from './LottoMatcher.js'; +import LOTTO_RANKING_DATA from '../constants/lottoRankingData.js'; +import { validateLottoSystem } from '../validations/lottoSystem.js'; export default class LottoSystem { constructor({ rankingData = LOTTO_RANKING_DATA } = {}) { - LottoSystem.#validate({ rankingData }) + LottoSystem.#validate({ rankingData }); this.rankingData = rankingData; this.lottoData = { winningLotto: null, - lottoTickets: [] + lottoTickets: [], }; - this.payAmount = 0 + this.payAmount = 0; } static #validate(lottoSystemProps) { validateLottoSystem({ - target: lottoSystemProps - }) + target: lottoSystemProps, + }); } setWinningLotto(winningNumbers, bonusNumber) { @@ -28,21 +28,21 @@ export default class LottoSystem { payLottoTicket(payAmount) { this.lottoData.lottoTickets = LottoPayment.createLottoTickets({ payAmount }); - this.payAmount = payAmount + this.payAmount = payAmount; } get ticketCount() { - return this.lottoData.lottoTickets.length + return this.lottoData.lottoTickets.length; } get lottoRankingResult() { const lottoMatchResult = LottoMatcher.matchLotto(this.lottoData); return this.rankingData.map((data) => ({ ...data, - ticketList : lottoMatchResult + ticketList: lottoMatchResult .filter(({ matchCount, bonusMatch }) => matchCount === data.matchCount && bonusMatch === data.bonusMatch) - .map(({ lotto }) => lotto) - })) + .map(({ lotto }) => lotto), + })); } get profitAmount() { @@ -50,6 +50,6 @@ export default class LottoSystem { } get profitRatio() { - return parseFloat((this.profitAmount / this.payAmount * 100).toFixed(2)); + return parseFloat(((this.profitAmount / this.payAmount) * 100).toFixed(2)); } } diff --git a/src/domain/LottoSystemController.js b/src/domain/LottoSystemController.js index fd81c41..679772e 100644 --- a/src/domain/LottoSystemController.js +++ b/src/domain/LottoSystemController.js @@ -1,40 +1,39 @@ - -export default class LottoSystemController{ +export default class LottoSystemController { constructor({ lottoSystem, viewer }) { - this.lottoSystem = lottoSystem - this.viewer = viewer + this.lottoSystem = lottoSystem; + this.viewer = viewer; } - async setUpPayAmount () { + async setUpPayAmount() { try { - const payAmount = await this.viewer.readPayAmount() - this.lottoSystem.payLottoTicket(payAmount) - }catch (error) { - this.viewer.displayError(error) - await this.setUpPayAmount() + const payAmount = await this.viewer.readPayAmount(); + this.lottoSystem.payLottoTicket(payAmount); + } catch (error) { + this.viewer.displayError(error); + await this.setUpPayAmount(); } } async setUpWinningLotto() { try { - const winningNumbers = await this.viewer.readWinningNumbers() - const bonusNumber = await this.viewer.readBonusNumber() - this.lottoSystem.setWinningLotto(winningNumbers, bonusNumber) - }catch (error) { - this.viewer.displayError(error) - await this.setUpWinningLotto() + const winningNumbers = await this.viewer.readWinningNumbers(); + const bonusNumber = await this.viewer.readBonusNumber(); + this.lottoSystem.setWinningLotto(winningNumbers, bonusNumber); + } catch (error) { + this.viewer.displayError(error); + await this.setUpWinningLotto(); } } async run() { - await this.setUpPayAmount() - this.viewer.displayPaidCount(this.lottoSystem) - this.viewer.displayLottoTickets(this.lottoSystem) + await this.setUpPayAmount(); + this.viewer.displayPaidCount(this.lottoSystem); + this.viewer.displayLottoTickets(this.lottoSystem); - await this.setUpWinningLotto() - this.viewer.displayLottoResult(this.lottoSystem) - this.viewer.displayProfitRatio(this.lottoSystem) + await this.setUpWinningLotto(); + this.viewer.displayLottoResult(this.lottoSystem); + this.viewer.displayProfitRatio(this.lottoSystem); - this.viewer.finish() + this.viewer.finish(); } -} \ No newline at end of file +} diff --git a/src/domain/LottoSystemViewer.js b/src/domain/LottoSystemViewer.js index dbb2eb7..0e548b8 100644 --- a/src/domain/LottoSystemViewer.js +++ b/src/domain/LottoSystemViewer.js @@ -1,8 +1,8 @@ -import ConsolePrinter from "../service/ConsolePrinter.js"; -import ConsoleReader from "../service/ConsoleReader.js"; -import formatCurrency from "../utils/formatCurrency.js"; +import ConsolePrinter from '../service/ConsolePrinter.js'; +import ConsoleReader from '../service/ConsoleReader.js'; +import formatCurrency from '../utils/formatCurrency.js'; -export default class LottoSystemViewer{ +export default class LottoSystemViewer { constructor() { this.printer = new ConsolePrinter({ paidCount: '%{1}개를 구매했습니다.', @@ -11,68 +11,67 @@ export default class LottoSystemViewer{ ranking: '%{1}등 : %{2}개 일치 (%{3}원) - %{4}개', rankingWithBonus: '%{1}등 : %{2}개 일치, 보너스볼 일치 (%{3}원) - %{4}개', profitRatio: '총 수익률은 %{1}% 입니다.', - error: '⚠️ %{1}' - }) - this.reader = new ConsoleReader() + error: '⚠️ %{1}', + }); + this.reader = new ConsoleReader(); } async readPayAmount() { - const answer = await this.reader.read('> 구입 금액을 입력해 주세요. : ') - return Number(answer) + const answer = await this.reader.read('> 구입 금액을 입력해 주세요. : '); + return Number(answer); } async readWinningNumbers() { - const answer = await this.reader.read('> 당첨 번호를 입력해 주세요. : ') - return answer.split(',').map(Number) + const answer = await this.reader.read('> 당첨 번호를 입력해 주세요. : '); + return answer.split(',').map(Number); } async readBonusNumber() { - const answer = await this.reader.read('> 보너스 번호를 입력해 주세요. : ') - return Number(answer) + const answer = await this.reader.read('> 보너스 번호를 입력해 주세요. : '); + return Number(answer); } displayPaidCount({ ticketCount }) { - this.printer.lineBreak() - this.printer.printWithTemplate('paidCount', [ticketCount]) + this.printer.lineBreak(); + this.printer.printWithTemplate('paidCount', [ticketCount]); } displayLottoTickets({ lottoData }) { lottoData.lottoTickets.forEach(({ numbers }) => { - const numbersForPrint = numbers.map(number => `${number}`.padStart(2, '0')).sort() - this.printer.printWithTemplate('lottoTicket', numbersForPrint) - }) - this.printer.lineBreak() + const numbersForPrint = numbers.map((number) => `${number}`.padStart(2, '0')).sort(); + this.printer.printWithTemplate('lottoTicket', numbersForPrint); + }); + this.printer.lineBreak(); } displayLottoResult({ lottoRankingResult }) { - this.printer.lineBreak() - this.printer.print('당첨 통계') - this.printer.printWithTemplate('line') + this.printer.lineBreak(); + this.printer.print('당첨 통계'); + this.printer.printWithTemplate('line'); lottoRankingResult.forEach(({ rank, matchCount, bonusMatch, profit, ticketList }) => { - const resultForPrint = [rank, matchCount, formatCurrency(profit), ticketList.length] + const resultForPrint = [rank, matchCount, formatCurrency(profit), ticketList.length]; if (bonusMatch) { - this.printer.printWithTemplate('rankingWithBonus', resultForPrint) + this.printer.printWithTemplate('rankingWithBonus', resultForPrint); + } else { + this.printer.printWithTemplate('ranking', resultForPrint); } - else { - this.printer.printWithTemplate('ranking', resultForPrint) - } - }) + }); } displayProfitRatio({ profitRatio }) { - this.printer.printWithTemplate('line') - this.printer.printWithTemplate('profitRatio', [profitRatio]) - this.printer.lineBreak() + this.printer.printWithTemplate('line'); + this.printer.printWithTemplate('profitRatio', [profitRatio]); + this.printer.lineBreak(); } displayError(error) { - this.printer.lineBreak() - this.printer.printWithTemplate('error',[ error.message]) - this.printer.lineBreak() + this.printer.lineBreak(); + this.printer.printWithTemplate('error', [error.message]); + this.printer.lineBreak(); } finish() { - this.reader.close() + this.reader.close(); } } diff --git a/src/domain/createLottoNumbers.js b/src/domain/createLottoNumbers.js index cc5c1d6..e2d1f45 100644 --- a/src/domain/createLottoNumbers.js +++ b/src/domain/createLottoNumbers.js @@ -1,11 +1,11 @@ -import { getRandomNumber } from "../utils/getRandomNumber.js"; +import { getRandomNumber } from '../utils/getRandomNumber.js'; const createLottoNumbers = () => { - const lottoNumbers = new Set() + const lottoNumbers = new Set(); while (lottoNumbers.size < 6) { - lottoNumbers.add(getRandomNumber(1, 45)) + lottoNumbers.add(getRandomNumber(1, 45)); } - return [...lottoNumbers] -} + return [...lottoNumbers]; +}; -export default createLottoNumbers \ No newline at end of file +export default createLottoNumbers; diff --git a/src/main.js b/src/main.js index 0ecc8fa..c740279 100644 --- a/src/main.js +++ b/src/main.js @@ -1,14 +1,14 @@ -import LottoSystemController from "./domain/LottoSystemController.js"; -import LottoSystem from "./domain/LottoSystem.js"; -import LOTTO_RANKING_DATA from "./constants/lottoRankingData.js"; -import LottoSystemViewer from "./domain/LottoSystemViewer.js"; +import LottoSystemController from './domain/LottoSystemController.js'; +import LottoSystem from './domain/LottoSystem.js'; +import LOTTO_RANKING_DATA from './constants/lottoRankingData.js'; +import LottoSystemViewer from './domain/LottoSystemViewer.js'; async function main() { await new LottoSystemController({ lottoSystem: new LottoSystem({ - rankingData: LOTTO_RANKING_DATA + rankingData: LOTTO_RANKING_DATA, }), - viewer: new LottoSystemViewer() - }).run() + viewer: new LottoSystemViewer(), + }).run(); } -main(); \ No newline at end of file +main(); diff --git a/src/service/ConsolePrinter.js b/src/service/ConsolePrinter.js index 7693ae3..41d1ada 100644 --- a/src/service/ConsolePrinter.js +++ b/src/service/ConsolePrinter.js @@ -28,4 +28,4 @@ export default class ConsolePrinter { lineBreak() { console.log(''); } -} \ No newline at end of file +} diff --git a/src/service/ConsoleReader.js b/src/service/ConsoleReader.js index ce2de56..682c2db 100644 --- a/src/service/ConsoleReader.js +++ b/src/service/ConsoleReader.js @@ -1,37 +1,37 @@ -import createReadline from "../utils/createReadline.js"; +import createReadline from '../utils/createReadline.js'; export default class ConsoleReader { constructor() { - this.readLine = createReadline() + this.readLine = createReadline(); } askQuestion(message) { - return new Promise(resolve => { - this.readLine.question(message, input => { - resolve(input) - }) - }) + return new Promise((resolve) => { + this.readLine.question(message, (input) => { + resolve(input); + }); + }); } findInputError(input, validations = []) { - const { errorMessage } = validations.find(({ check }) => !check(input)) ?? {} - return errorMessage + const { errorMessage } = validations.find(({ check }) => !check(input)) ?? {}; + return errorMessage; } read(message, validations = []) { - const processInput = async() => { - const input = await this.askQuestion(message) - const errorMessage = this.findInputError(input, validations) + const processInput = async () => { + const input = await this.askQuestion(message); + const errorMessage = this.findInputError(input, validations); if (errorMessage) { - return processInput() + return processInput(); } - return input - } - return processInput() + return input; + }; + return processInput(); } close() { - this.readLine.close() + this.readLine.close(); } } diff --git a/src/utils/createReadline.js b/src/utils/createReadline.js index 30c92fc..3131ce1 100644 --- a/src/utils/createReadline.js +++ b/src/utils/createReadline.js @@ -12,4 +12,4 @@ const createReadline = () => { return rl; }; -export default createReadline; \ No newline at end of file +export default createReadline; diff --git a/src/utils/formatCurrency.js b/src/utils/formatCurrency.js index eb92557..c8a318b 100644 --- a/src/utils/formatCurrency.js +++ b/src/utils/formatCurrency.js @@ -1,5 +1,5 @@ const formatCurrency = (number) => { return new Intl.NumberFormat('ko-KR').format(number); -} +}; -export default formatCurrency \ No newline at end of file +export default formatCurrency; diff --git a/src/utils/getRandomNumber.js b/src/utils/getRandomNumber.js index 23eff6f..c624e58 100644 --- a/src/utils/getRandomNumber.js +++ b/src/utils/getRandomNumber.js @@ -1,3 +1,3 @@ export const getRandomNumber = (min, max) => { return Math.floor(Math.random() * (max - min + 1)) + min; -}; \ No newline at end of file +}; diff --git a/src/validations/createValidator.js b/src/validations/createValidator.js index 5185b6b..aaef61b 100644 --- a/src/validations/createValidator.js +++ b/src/validations/createValidator.js @@ -1,14 +1,14 @@ const createValidator = (validations) => { return ({ target, validationKeys }) => { - (validationKeys ?? Object.keys(validations)).forEach(key => { + (validationKeys ?? Object.keys(validations)).forEach((key) => { if (!validations.hasOwnProperty(key)) { - throw new Error('올바른 검사 키가 아닙니다.') + throw new Error('올바른 검사 키가 아닙니다.'); } if (!validations[key].check(target)) { - throw new Error(validations[key].errorMessage) + throw new Error(validations[key].errorMessage); } - }) - } -} + }); + }; +}; -export default createValidator \ No newline at end of file +export default createValidator; diff --git a/src/validations/lotto.js b/src/validations/lotto.js index 160e3dc..85eee23 100644 --- a/src/validations/lotto.js +++ b/src/validations/lotto.js @@ -1,41 +1,41 @@ -import createValidator from "./createValidator.js"; -import LOTTO_TYPE from "../constants/lottoType.js"; +import createValidator from './createValidator.js'; +import LOTTO_TYPE from '../constants/lottoType.js'; export const lottoValidations = { lottoType: { check: ({ type }) => type === LOTTO_TYPE.WINNING || type === LOTTO_TYPE.TICKET, - errorMessage: '로또는 ticket, winning 두 가지 유형이어야 합니다.' + errorMessage: '로또는 ticket, winning 두 가지 유형이어야 합니다.', }, lottoNumbersLength: { check: ({ numbers }) => numbers.length === 6, - errorMessage: '로또 번호는 6개여야 합니다.' + errorMessage: '로또 번호는 6개여야 합니다.', }, winningLottoHasBonus: { - check: ({ type, bonusNumber }) => type === LOTTO_TYPE.WINNING ? Boolean(bonusNumber) : true, - errorMessage: '당첨 로또 번호는 보너스 번호를 가져야 합니다.' + check: ({ type, bonusNumber }) => (type === LOTTO_TYPE.WINNING ? Boolean(bonusNumber) : true), + errorMessage: '당첨 로또 번호는 보너스 번호를 가져야 합니다.', }, ticketLottoBonusNull: { - check: ({ type, bonusNumber }) => type === LOTTO_TYPE.TICKET ? bonusNumber === null : true, - errorMessage: '구매한 로또 번호는 보너스 번호가 없어야 합니다.' + check: ({ type, bonusNumber }) => (type === LOTTO_TYPE.TICKET ? bonusNumber === null : true), + errorMessage: '구매한 로또 번호는 보너스 번호가 없어야 합니다.', }, lottoEachUnique: { check: ({ numbers, bonusNumber }) => new Set(numbers).size === numbers.length && !numbers.includes(bonusNumber), - errorMessage: '로또 번호는 각각 달라야 합니다.' + errorMessage: '로또 번호는 각각 달라야 합니다.', }, lottoInteger: { check: ({ numbers, bonusNumber }) => { - const lottoNumbers = bonusNumber === null? numbers : [...numbers, bonusNumber] - return lottoNumbers.every(number => Number.isInteger(number)) + const lottoNumbers = bonusNumber === null ? numbers : [...numbers, bonusNumber]; + return lottoNumbers.every((number) => Number.isInteger(number)); }, - errorMessage: '모든 로또 번호는 정수여야 합니다.' + errorMessage: '모든 로또 번호는 정수여야 합니다.', }, lottoRange: { check: ({ numbers, bonusNumber }) => { - const lottoNumbers = bonusNumber === null ? numbers : [...numbers, bonusNumber] - return lottoNumbers.every(number => 1 <= number && number <= 45) + const lottoNumbers = bonusNumber === null ? numbers : [...numbers, bonusNumber]; + return lottoNumbers.every((number) => 1 <= number && number <= 45); }, - errorMessage: '모든 로또 번호는 1 이상 45 이하여야 합니다.' - } -} + errorMessage: '모든 로또 번호는 1 이상 45 이하여야 합니다.', + }, +}; -export const validateLotto = createValidator(lottoValidations) \ No newline at end of file +export const validateLotto = createValidator(lottoValidations); diff --git a/src/validations/lottoMatcher.js b/src/validations/lottoMatcher.js index 72bb24c..3887a84 100644 --- a/src/validations/lottoMatcher.js +++ b/src/validations/lottoMatcher.js @@ -1,16 +1,17 @@ -import Lotto from "../domain/Lotto.js"; -import createValidator from "./createValidator.js"; -import LOTTO_TYPE from "../constants/lottoType.js"; +import Lotto from '../domain/Lotto.js'; +import createValidator from './createValidator.js'; +import LOTTO_TYPE from '../constants/lottoType.js'; export const lottoMatcherValidations = { validWinningLotto: { check: ({ winningLotto }) => winningLotto instanceof Lotto && winningLotto.type === LOTTO_TYPE.WINNING, - errorMessage: '유효한 로또 당첨 정보가 아닙니다.' + errorMessage: '유효한 로또 당첨 정보가 아닙니다.', }, validLottoTickets: { - check: ({ lottoTickets }) => lottoTickets.every(lottoTicket => lottoTicket instanceof Lotto && lottoTicket.type === LOTTO_TYPE.TICKET), - errorMessage: '유효하지 않은 로또 티켓이 존재합니다.' - } -} + check: ({ lottoTickets }) => + lottoTickets.every((lottoTicket) => lottoTicket instanceof Lotto && lottoTicket.type === LOTTO_TYPE.TICKET), + errorMessage: '유효하지 않은 로또 티켓이 존재합니다.', + }, +}; -export const validateLottoMatcher = createValidator(lottoMatcherValidations) \ No newline at end of file +export const validateLottoMatcher = createValidator(lottoMatcherValidations); diff --git a/src/validations/lottoPayment.js b/src/validations/lottoPayment.js index 9005f0c..0a56a3a 100644 --- a/src/validations/lottoPayment.js +++ b/src/validations/lottoPayment.js @@ -1,14 +1,14 @@ -import createValidator from "./createValidator.js"; +import createValidator from './createValidator.js'; export const lottoPaymentValidations = { payAmountInteger: { check: ({ payAmount }) => Number.isInteger(payAmount), - errorMessage: '로또 결제 금액은 숫자(정수)여야 합니다.' + errorMessage: '로또 결제 금액은 숫자(정수)여야 합니다.', }, payAmountUnit1000: { check: ({ payAmount }) => payAmount % 1000 === 0, - errorMessage: '로또 결제는 1000원 단위로만 가능합니다.' - } -} + errorMessage: '로또 결제는 1000원 단위로만 가능합니다.', + }, +}; -export const validateLottoPayment = createValidator(lottoPaymentValidations) \ No newline at end of file +export const validateLottoPayment = createValidator(lottoPaymentValidations); diff --git a/src/validations/lottoSystem.js b/src/validations/lottoSystem.js index 4532238..e93ae7a 100644 --- a/src/validations/lottoSystem.js +++ b/src/validations/lottoSystem.js @@ -1,29 +1,30 @@ -import createValidator from "./createValidator.js"; +import createValidator from './createValidator.js'; export const lottoSystemValidations = { validRanks: { check: ({ rankingData }) => rankingData.every(({ rank }) => Number.isInteger(rank) && rank > 0), - errorMessage: '랭킹 데이터에 유효하지 않은 값이(rank) 포함되어 있습니다.' + errorMessage: '랭킹 데이터에 유효하지 않은 값이(rank) 포함되어 있습니다.', }, validMatchCounts: { - check: ({ rankingData }) => rankingData.every(({ matchCount }) => Number.isInteger(matchCount) && 0 < matchCount && matchCount <= 6), - errorMessage: '랭킹 데이터에 유효하지 않은 값이(matchCount) 포함되어 있습니다.' + check: ({ rankingData }) => + rankingData.every(({ matchCount }) => Number.isInteger(matchCount) && 0 < matchCount && matchCount <= 6), + errorMessage: '랭킹 데이터에 유효하지 않은 값이(matchCount) 포함되어 있습니다.', }, validBonusMatches: { check: ({ rankingData }) => rankingData.every(({ bonusMatch }) => typeof bonusMatch === 'boolean'), - errorMessage: '랭킹 데이터에 유효하지 않은 값이(bonusMatch) 포함되어 있습니다.' + errorMessage: '랭킹 데이터에 유효하지 않은 값이(bonusMatch) 포함되어 있습니다.', }, validProfits: { check: ({ rankingData }) => rankingData.every(({ profit }) => Number.isInteger(profit) && profit > 0), - errorMessage: '랭킹 데이터에 유효하지 않은 값이(profit) 포함되어 있습니다.' + errorMessage: '랭킹 데이터에 유효하지 않은 값이(profit) 포함되어 있습니다.', }, hasAllRanks: { check: ({ rankingData }) => { - const ranks = rankingData.map(data => data.rank).sort((a, b) => a - b); + const ranks = rankingData.map((data) => data.rank).sort((a, b) => a - b); return rankingData.length > 0 && ranks.every((rank, index) => rank === index + 1); }, - errorMessage: '랭킹 데이터에 모든 등수가 빠짐없이 있어야 합니다.' - } + errorMessage: '랭킹 데이터에 모든 등수가 빠짐없이 있어야 합니다.', + }, }; -export const validateLottoSystem = createValidator(lottoSystemValidations) \ No newline at end of file +export const validateLottoSystem = createValidator(lottoSystemValidations); From 712c577dafd67dbd8aba20bc7a2e21eb57fbf85b Mon Sep 17 00:00:00 2001 From: chasj0326 Date: Mon, 8 Jul 2024 08:31:27 +0900 Subject: [PATCH 10/22] =?UTF-8?q?refactor:=20lottoData=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lottoPayment.test.js | 9 ++++----- src/domain/LottoPayment.js | 20 ++++++++++++-------- src/domain/LottoSystem.js | 19 ++++++++++++++----- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/__tests__/lottoPayment.test.js b/src/__tests__/lottoPayment.test.js index a033fb0..f4f695b 100644 --- a/src/__tests__/lottoPayment.test.js +++ b/src/__tests__/lottoPayment.test.js @@ -19,10 +19,9 @@ describe('로또 결제 테스트', () => { }); test('1000원 당 1장의 로또 티켓을 발행해야 한다.', () => { - expect( - LottoPayment.createLottoTickets({ - payAmount: 8000, - }).length, - ).toBe(8); + const { lottoTickets } = LottoPayment.createLottoTickets({ + payAmount: 8000, + }) + expect(lottoTickets.length).toBe(8); }); }); diff --git a/src/domain/LottoPayment.js b/src/domain/LottoPayment.js index 6cbbdc0..e37909b 100644 --- a/src/domain/LottoPayment.js +++ b/src/domain/LottoPayment.js @@ -3,18 +3,22 @@ import createLottoNumbers from './createLottoNumbers.js'; import { createLottoTicket } from './Lotto.js'; export default class LottoPayment { - static #validate(lottoPaymentProps) { - validateLottoPayment({ - target: lottoPaymentProps, - }); - } static createLottoTickets({ payAmount }) { LottoPayment.#validate({ payAmount }); const ticketCount = Math.floor(payAmount / 1000); - return Array.from({ length: ticketCount }, () => { - const numbers = createLottoNumbers(); - return createLottoTicket(numbers); + return { + lottoTickets: Array.from({ length: ticketCount }, () => { + const numbers = createLottoNumbers(); + return createLottoTicket(numbers); + }), + paidAmount: payAmount, + }; + } + + static #validate(lottoPaymentProps) { + validateLottoPayment({ + target: lottoPaymentProps, }); } } diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index 16f1f5b..a6bda85 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -12,8 +12,8 @@ export default class LottoSystem { this.lottoData = { winningLotto: null, lottoTickets: [], + paidAmount: 0 }; - this.payAmount = 0; } static #validate(lottoSystemProps) { @@ -23,12 +23,21 @@ export default class LottoSystem { } setWinningLotto(winningNumbers, bonusNumber) { - this.lottoData.winningLotto = createWinningLotto(winningNumbers, bonusNumber); + this.#setLottoData({ + winningLotto: createWinningLotto(winningNumbers, bonusNumber) + }) } payLottoTicket(payAmount) { - this.lottoData.lottoTickets = LottoPayment.createLottoTickets({ payAmount }); - this.payAmount = payAmount; + const { lottoTickets, paidAmount } = LottoPayment.createLottoTickets({ payAmount }) + this.#setLottoData({ + lottoTickets, + paidAmount + }) + } + + #setLottoData(newData) { + this.lottoData = { ...this.lottoData, ...newData }; } get ticketCount() { @@ -50,6 +59,6 @@ export default class LottoSystem { } get profitRatio() { - return parseFloat(((this.profitAmount / this.payAmount) * 100).toFixed(2)); + return parseFloat(((this.profitAmount / this.lottoData.paidAmount) * 100).toFixed(2)); } } From 60bc9859afdefc1c4bf18af25afbe77af362835a Mon Sep 17 00:00:00 2001 From: chasj0326 Date: Mon, 8 Jul 2024 08:40:04 +0900 Subject: [PATCH 11/22] =?UTF-8?q?refactor:=20LottoSystem=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=20=EB=B3=80=EC=88=98=20=EC=A0=91=EA=B7=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lottoSystem.test.js | 3 ++- src/domain/LottoSystem.js | 25 ++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/__tests__/lottoSystem.test.js b/src/__tests__/lottoSystem.test.js index 47f2f3e..30c1ca8 100644 --- a/src/__tests__/lottoSystem.test.js +++ b/src/__tests__/lottoSystem.test.js @@ -8,6 +8,7 @@ describe('로또 시스템 테스트', async () => { const originalCreateLottoNumbers = await vi .importActual('../domain/createLottoNumbers') .then((module) => module.default); + beforeEach(() => { createLottoNumbers.mockReset(); createLottoNumbers.mockImplementation(originalCreateLottoNumbers); @@ -96,7 +97,7 @@ describe('로또 시스템 테스트', async () => { lottoSystem.payLottoTicket(10000); expect(lottoSystem.ticketCount).toBe(10); - expect(lottoSystem.payAmount).toBe(10000); + expect(lottoSystem.paidAmount).toBe(10000); }); test('로또 시스템은 발행한 로또들을 등수 별로 분류할 수 있다.', () => { diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index a6bda85..da16572 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -5,11 +5,14 @@ import LOTTO_RANKING_DATA from '../constants/lottoRankingData.js'; import { validateLottoSystem } from '../validations/lottoSystem.js'; export default class LottoSystem { + #rankingData + #lottoData + constructor({ rankingData = LOTTO_RANKING_DATA } = {}) { LottoSystem.#validate({ rankingData }); - this.rankingData = rankingData; - this.lottoData = { + this.#rankingData = rankingData; + this.#lottoData = { winningLotto: null, lottoTickets: [], paidAmount: 0 @@ -37,16 +40,24 @@ export default class LottoSystem { } #setLottoData(newData) { - this.lottoData = { ...this.lottoData, ...newData }; + this.#lottoData = { ...this.#lottoData, ...newData }; + } + + get paidAmount() { + return this.#lottoData.paidAmount; + } + + get lottoTickets() { + return this.#lottoData.lottoTickets } get ticketCount() { - return this.lottoData.lottoTickets.length; + return this.#lottoData.lottoTickets.length; } get lottoRankingResult() { - const lottoMatchResult = LottoMatcher.matchLotto(this.lottoData); - return this.rankingData.map((data) => ({ + const lottoMatchResult = LottoMatcher.matchLotto(this.#lottoData); + return this.#rankingData.map((data) => ({ ...data, ticketList: lottoMatchResult .filter(({ matchCount, bonusMatch }) => matchCount === data.matchCount && bonusMatch === data.bonusMatch) @@ -59,6 +70,6 @@ export default class LottoSystem { } get profitRatio() { - return parseFloat(((this.profitAmount / this.lottoData.paidAmount) * 100).toFixed(2)); + return parseFloat(((this.profitAmount / this.#lottoData.paidAmount) * 100).toFixed(2)); } } From d685e274be3ab4dad35e084f95c0d4e424a180c2 Mon Sep 17 00:00:00 2001 From: chasj0326 Date: Mon, 8 Jul 2024 11:11:17 +0900 Subject: [PATCH 12/22] =?UTF-8?q?fix:=20lottoViewer=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=B3=B4=EB=84=88=EC=8A=A4?= =?UTF-8?q?=EB=B3=BC=20=EB=A1=9C=EC=A7=81=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lottoSystem.test.js | 1 + src/constants/lottoRankingData.js | 4 ---- src/domain/LottoSystem.js | 10 +++++++--- src/domain/LottoSystemViewer.js | 4 ++-- src/utils/cloneDeep.js | 23 +++++++++++++++++++++++ src/validations/lottoSystem.js | 2 +- 6 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 src/utils/cloneDeep.js diff --git a/src/__tests__/lottoSystem.test.js b/src/__tests__/lottoSystem.test.js index 30c1ca8..9759226 100644 --- a/src/__tests__/lottoSystem.test.js +++ b/src/__tests__/lottoSystem.test.js @@ -67,6 +67,7 @@ describe('로또 시스템 테스트', async () => { profit: 1000, rank: 1, matchCount: 3, + bonusMatch: 'false', }, ]; expect( diff --git a/src/constants/lottoRankingData.js b/src/constants/lottoRankingData.js index bcd3a2d..4ec684b 100644 --- a/src/constants/lottoRankingData.js +++ b/src/constants/lottoRankingData.js @@ -2,19 +2,16 @@ const LOTTO_RANKING_DATA = [ { rank: 5, matchCount: 3, - bonusMatch: false, profit: 5000, }, { rank: 4, matchCount: 4, - bonusMatch: false, profit: 50000, }, { rank: 3, matchCount: 5, - bonusMatch: false, profit: 1500000, }, { @@ -26,7 +23,6 @@ const LOTTO_RANKING_DATA = [ { rank: 1, matchCount: 6, - bonusMatch: false, profit: 2000000000, }, ]; diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index da16572..3eada76 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -3,6 +3,7 @@ import LottoPayment from './LottoPayment.js'; import LottoMatcher from './LottoMatcher.js'; import LOTTO_RANKING_DATA from '../constants/lottoRankingData.js'; import { validateLottoSystem } from '../validations/lottoSystem.js'; +import cloneDeep from "../utils/cloneDeep.js"; export default class LottoSystem { #rankingData @@ -48,7 +49,7 @@ export default class LottoSystem { } get lottoTickets() { - return this.#lottoData.lottoTickets + return cloneDeep(this.#lottoData.lottoTickets); } get ticketCount() { @@ -57,10 +58,13 @@ export default class LottoSystem { get lottoRankingResult() { const lottoMatchResult = LottoMatcher.matchLotto(this.#lottoData); + return this.#rankingData.map((data) => ({ ...data, ticketList: lottoMatchResult - .filter(({ matchCount, bonusMatch }) => matchCount === data.matchCount && bonusMatch === data.bonusMatch) + .filter(({ matchCount, bonusMatch }) => + matchCount === data.matchCount + && (data.bonusMatch === undefined || bonusMatch === data.bonusMatch)) .map(({ lotto }) => lotto), })); } @@ -70,6 +74,6 @@ export default class LottoSystem { } get profitRatio() { - return parseFloat(((this.profitAmount / this.#lottoData.paidAmount) * 100).toFixed(2)); + return parseFloat(((this.profitAmount / this.paidAmount) * 100).toFixed(2)); } } diff --git a/src/domain/LottoSystemViewer.js b/src/domain/LottoSystemViewer.js index 0e548b8..9fcbab5 100644 --- a/src/domain/LottoSystemViewer.js +++ b/src/domain/LottoSystemViewer.js @@ -36,8 +36,8 @@ export default class LottoSystemViewer { this.printer.printWithTemplate('paidCount', [ticketCount]); } - displayLottoTickets({ lottoData }) { - lottoData.lottoTickets.forEach(({ numbers }) => { + displayLottoTickets({ lottoTickets }) { + lottoTickets.forEach(({ numbers }) => { const numbersForPrint = numbers.map((number) => `${number}`.padStart(2, '0')).sort(); this.printer.printWithTemplate('lottoTicket', numbersForPrint); }); diff --git a/src/utils/cloneDeep.js b/src/utils/cloneDeep.js new file mode 100644 index 0000000..ed4cbd0 --- /dev/null +++ b/src/utils/cloneDeep.js @@ -0,0 +1,23 @@ +const cloneDeep = (obj) => { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + const clonedArray = []; + obj.forEach(item => { + clonedArray.push(cloneDeep(item)); + }) + return clonedArray; + } + + const clonedObj = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + clonedObj[key] = cloneDeep(obj[key]); + } + } + return clonedObj; +} + +export default cloneDeep; diff --git a/src/validations/lottoSystem.js b/src/validations/lottoSystem.js index e93ae7a..717bd50 100644 --- a/src/validations/lottoSystem.js +++ b/src/validations/lottoSystem.js @@ -11,7 +11,7 @@ export const lottoSystemValidations = { errorMessage: '랭킹 데이터에 유효하지 않은 값이(matchCount) 포함되어 있습니다.', }, validBonusMatches: { - check: ({ rankingData }) => rankingData.every(({ bonusMatch }) => typeof bonusMatch === 'boolean'), + check: ({ rankingData }) => rankingData.every(({ bonusMatch }) => bonusMatch === undefined || typeof bonusMatch === 'boolean'), errorMessage: '랭킹 데이터에 유효하지 않은 값이(bonusMatch) 포함되어 있습니다.', }, validProfits: { From 1235582d56127d1f34878d9c308a838407545f0d Mon Sep 17 00:00:00 2001 From: chasj0326 Date: Tue, 9 Jul 2024 08:57:44 +0900 Subject: [PATCH 13/22] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20bonusMatch=20=EB=B3=B4=EC=99=84=20=EB=B0=8F=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lottoSystem.test.js | 24 ++++++++-------- ...ottoRankingData.js => lottoRankingRule.js} | 28 ++++++++++--------- src/domain/LottoSystem.js | 28 ++++++++++++------- ...temController.js => LottoSystemControl.js} | 2 +- ...ottoSystemViewer.js => LottoSystemView.js} | 2 +- src/main.js | 17 ++++++----- src/validations/lottoSystem.js | 26 ++++++++--------- 7 files changed, 70 insertions(+), 57 deletions(-) rename src/constants/{lottoRankingData.js => lottoRankingRule.js} (94%) rename src/domain/{LottoSystemController.js => LottoSystemControl.js} (95%) rename src/domain/{LottoSystemViewer.js => LottoSystemView.js} (98%) diff --git a/src/__tests__/lottoSystem.test.js b/src/__tests__/lottoSystem.test.js index 9759226..52ec929 100644 --- a/src/__tests__/lottoSystem.test.js +++ b/src/__tests__/lottoSystem.test.js @@ -14,8 +14,8 @@ describe('로또 시스템 테스트', async () => { createLottoNumbers.mockImplementation(originalCreateLottoNumbers); }); - test('로또 시스템은 순위가 올바르게 매겨진 랭킹 데이터만 사용한다.', () => { - const rankingDataHasInvalidRank1 = [ + test('로또 시스템은 순위가 올바르게 매겨진 랭킹 규칙만 사용한다.', () => { + const rankingRuleHasInvalidRank1 = [ { matchCount: 3, bonusMatch: false, @@ -26,11 +26,11 @@ describe('로또 시스템 테스트', async () => { expect( () => new LottoSystem({ - rankingData: rankingDataHasInvalidRank1, + rankingRule: rankingRuleHasInvalidRank1, }), ).toThrow(lottoSystemValidations.validRanks.errorMessage); - const rankingDataHasInvalidRank2 = [ + const rankingRuleHasInvalidRank2 = [ { matchCount: 3, bonusMatch: false, @@ -41,13 +41,13 @@ describe('로또 시스템 테스트', async () => { expect( () => new LottoSystem({ - rankingData: rankingDataHasInvalidRank2, + rankingRule: rankingRuleHasInvalidRank2, }), ).toThrow(lottoSystemValidations.hasAllRanks.errorMessage); }); - test('로또 시스템은 유효한 matchCount, bonus, profit 이 포함된 랭킹 데이터만 사용한다.', () => { - const rankingDataHasInvalidMatchCount = [ + test('로또 시스템은 유효한 matchCount, bonus, profit 이 포함된 랭킹 규칙만 사용한다.', () => { + const rankingRuleHasInvalidMatchCount = [ { bonusMatch: true, profit: 1000, @@ -58,11 +58,11 @@ describe('로또 시스템 테스트', async () => { expect( () => new LottoSystem({ - rankingData: rankingDataHasInvalidMatchCount, + rankingRule: rankingRuleHasInvalidMatchCount, }), ).toThrow(lottoSystemValidations.validMatchCounts.errorMessage); - const rankingDataHasInvalidBonusMatch = [ + const rankingRuleHasInvalidBonusMatch = [ { profit: 1000, rank: 1, @@ -73,11 +73,11 @@ describe('로또 시스템 테스트', async () => { expect( () => new LottoSystem({ - rankingData: rankingDataHasInvalidBonusMatch, + rankingRule: rankingRuleHasInvalidBonusMatch, }), ).toThrow(lottoSystemValidations.validBonusMatches.errorMessage); - const rankingDataHasInvalidProfit = [ + const rankingRuleHasInvalidProfit = [ { bonusMatch: true, profit: '1000', @@ -88,7 +88,7 @@ describe('로또 시스템 테스트', async () => { expect( () => new LottoSystem({ - rankingData: rankingDataHasInvalidProfit, + rankingRule: rankingRuleHasInvalidProfit, }), ).toThrow(lottoSystemValidations.validProfits.errorMessage); }); diff --git a/src/constants/lottoRankingData.js b/src/constants/lottoRankingRule.js similarity index 94% rename from src/constants/lottoRankingData.js rename to src/constants/lottoRankingRule.js index 4ec684b..7772b12 100644 --- a/src/constants/lottoRankingData.js +++ b/src/constants/lottoRankingRule.js @@ -1,30 +1,32 @@ const LOTTO_RANKING_DATA = [ { - rank: 5, - matchCount: 3, - profit: 5000, + rank: 1, + matchCount: 6, + profit: 2000000000, }, { - rank: 4, - matchCount: 4, - profit: 50000, + rank: 2, + matchCount: 5, + bonusMatch: true, + profit: 30000000, }, { rank: 3, matchCount: 5, + bonusMatch: false, profit: 1500000, }, { - rank: 2, - matchCount: 5, - bonusMatch: true, - profit: 30000000, + rank: 4, + matchCount: 4, + profit: 50000, }, { - rank: 1, - matchCount: 6, - profit: 2000000000, + rank: 5, + matchCount: 3, + profit: 5000, }, ]; + export default LOTTO_RANKING_DATA; diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index 3eada76..37f82d5 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -1,18 +1,18 @@ import { createWinningLotto } from './Lotto.js'; import LottoPayment from './LottoPayment.js'; import LottoMatcher from './LottoMatcher.js'; -import LOTTO_RANKING_DATA from '../constants/lottoRankingData.js'; +import LOTTO_RANKING_RULE from '../constants/lottoRankingRule.js'; import { validateLottoSystem } from '../validations/lottoSystem.js'; import cloneDeep from "../utils/cloneDeep.js"; export default class LottoSystem { - #rankingData + #rankingRule #lottoData - constructor({ rankingData = LOTTO_RANKING_DATA } = {}) { - LottoSystem.#validate({ rankingData }); + constructor({ rankingRule = LOTTO_RANKING_RULE } = {}) { + LottoSystem.#validate({ rankingRule }); - this.#rankingData = rankingData; + this.#rankingRule = rankingRule.sort((a, b) => b.rank - a.rank); this.#lottoData = { winningLotto: null, lottoTickets: [], @@ -59,13 +59,13 @@ export default class LottoSystem { get lottoRankingResult() { const lottoMatchResult = LottoMatcher.matchLotto(this.#lottoData); - return this.#rankingData.map((data) => ({ - ...data, + return this.#rankingRule.map((rule) => ({ + ...rule, ticketList: lottoMatchResult .filter(({ matchCount, bonusMatch }) => - matchCount === data.matchCount - && (data.bonusMatch === undefined || bonusMatch === data.bonusMatch)) - .map(({ lotto }) => lotto), + this.#isSameMatchCount(matchCount, rule.matchCount) + && this.#isSameBonusMatch(bonusMatch, rule.bonusMatch)) + .map(({ lotto }) => cloneDeep(lotto)), })); } @@ -76,4 +76,12 @@ export default class LottoSystem { get profitRatio() { return parseFloat(((this.profitAmount / this.paidAmount) * 100).toFixed(2)); } + + #isSameMatchCount(ticketMatchCount, ruleMatchCount) { + return ticketMatchCount === ruleMatchCount; + } + + #isSameBonusMatch(ticketBonusMatch, ruleBonusMatch) { + return ruleBonusMatch === undefined || ruleBonusMatch === ticketBonusMatch; + } } diff --git a/src/domain/LottoSystemController.js b/src/domain/LottoSystemControl.js similarity index 95% rename from src/domain/LottoSystemController.js rename to src/domain/LottoSystemControl.js index 679772e..58aeda6 100644 --- a/src/domain/LottoSystemController.js +++ b/src/domain/LottoSystemControl.js @@ -1,4 +1,4 @@ -export default class LottoSystemController { +export default class LottoSystemControl { constructor({ lottoSystem, viewer }) { this.lottoSystem = lottoSystem; this.viewer = viewer; diff --git a/src/domain/LottoSystemViewer.js b/src/domain/LottoSystemView.js similarity index 98% rename from src/domain/LottoSystemViewer.js rename to src/domain/LottoSystemView.js index 9fcbab5..409adf0 100644 --- a/src/domain/LottoSystemViewer.js +++ b/src/domain/LottoSystemView.js @@ -2,7 +2,7 @@ import ConsolePrinter from '../service/ConsolePrinter.js'; import ConsoleReader from '../service/ConsoleReader.js'; import formatCurrency from '../utils/formatCurrency.js'; -export default class LottoSystemViewer { +export default class LottoSystemView { constructor() { this.printer = new ConsolePrinter({ paidCount: '%{1}개를 구매했습니다.', diff --git a/src/main.js b/src/main.js index c740279..416c9ee 100644 --- a/src/main.js +++ b/src/main.js @@ -1,14 +1,17 @@ -import LottoSystemController from './domain/LottoSystemController.js'; +import LottoSystemControl from './domain/LottoSystemControl.js'; import LottoSystem from './domain/LottoSystem.js'; -import LOTTO_RANKING_DATA from './constants/lottoRankingData.js'; -import LottoSystemViewer from './domain/LottoSystemViewer.js'; +import LOTTO_RANKING_RULE from './constants/lottoRankingRule.js'; +import LottoSystemView from './domain/LottoSystemView.js'; + async function main() { - await new LottoSystemController({ + const lottoSystemControl = new LottoSystemControl({ lottoSystem: new LottoSystem({ - rankingData: LOTTO_RANKING_DATA, + rankingRule: LOTTO_RANKING_RULE }), - viewer: new LottoSystemViewer(), - }).run(); + viewer: new LottoSystemView() + }); + + await lottoSystemControl.run(); } main(); diff --git a/src/validations/lottoSystem.js b/src/validations/lottoSystem.js index 717bd50..eabd178 100644 --- a/src/validations/lottoSystem.js +++ b/src/validations/lottoSystem.js @@ -2,28 +2,28 @@ import createValidator from './createValidator.js'; export const lottoSystemValidations = { validRanks: { - check: ({ rankingData }) => rankingData.every(({ rank }) => Number.isInteger(rank) && rank > 0), - errorMessage: '랭킹 데이터에 유효하지 않은 값이(rank) 포함되어 있습니다.', + check: ({ rankingRule }) => rankingRule.every(({ rank }) => Number.isInteger(rank) && rank > 0), + errorMessage: '로또 랭킹 규칙에 유효하지 않은 값이(rank) 포함되어 있습니다.', }, validMatchCounts: { - check: ({ rankingData }) => - rankingData.every(({ matchCount }) => Number.isInteger(matchCount) && 0 < matchCount && matchCount <= 6), - errorMessage: '랭킹 데이터에 유효하지 않은 값이(matchCount) 포함되어 있습니다.', + check: ({ rankingRule }) => + rankingRule.every(({ matchCount }) => Number.isInteger(matchCount) && 0 < matchCount && matchCount <= 6), + errorMessage: '로또 랭킹 규칙에 유효하지 않은 값이(matchCount) 포함되어 있습니다.', }, validBonusMatches: { - check: ({ rankingData }) => rankingData.every(({ bonusMatch }) => bonusMatch === undefined || typeof bonusMatch === 'boolean'), - errorMessage: '랭킹 데이터에 유효하지 않은 값이(bonusMatch) 포함되어 있습니다.', + check: ({ rankingRule }) => rankingRule.every(({ bonusMatch }) => bonusMatch === undefined || typeof bonusMatch === 'boolean'), + errorMessage: '로또 랭킹 규칙에 유효하지 않은 값이(bonusMatch) 포함되어 있습니다.', }, validProfits: { - check: ({ rankingData }) => rankingData.every(({ profit }) => Number.isInteger(profit) && profit > 0), - errorMessage: '랭킹 데이터에 유효하지 않은 값이(profit) 포함되어 있습니다.', + check: ({ rankingRule }) => rankingRule.every(({ profit }) => Number.isInteger(profit) && profit > 0), + errorMessage: '로또 랭킹 규칙에 유효하지 않은 값이(profit) 포함되어 있습니다.', }, hasAllRanks: { - check: ({ rankingData }) => { - const ranks = rankingData.map((data) => data.rank).sort((a, b) => a - b); - return rankingData.length > 0 && ranks.every((rank, index) => rank === index + 1); + check: ({ rankingRule }) => { + const ranks = rankingRule.map((data) => data.rank).sort((a, b) => a - b); + return rankingRule.length > 0 && ranks.every((rank, index) => rank === index + 1); }, - errorMessage: '랭킹 데이터에 모든 등수가 빠짐없이 있어야 합니다.', + errorMessage: '로또 랭킹 규칙에 모든 등수가 빠짐없이 있어야 합니다.', }, }; From 5a705f0df1baefe2404670b9b3eb02d952874025 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 14 Jul 2024 19:08:47 +0900 Subject: [PATCH 14/22] =?UTF-8?q?feat:=20=EC=88=AB=EC=9E=90=20=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=EB=A1=9C=20=EB=A1=9C=EB=98=90=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lotto.test.js | 6 ++++++ src/domain/Lotto.js | 13 ++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/__tests__/lotto.test.js b/src/__tests__/lotto.test.js index 4b64e23..8fc294b 100644 --- a/src/__tests__/lotto.test.js +++ b/src/__tests__/lotto.test.js @@ -91,4 +91,10 @@ describe('로또 테스트', () => { }), ).toThrow(lottoValidations.lottoRange.errorMessage); }); + + test('숫자 리스트를 통해 로또 티켓을 생성할 수 있다.', () => { + expect(new Lotto([1, 2, 3, 4, 5, 6]).numbers).toEqual([1, 2, 3, 4, 5, 6]); + }); + + test('로또에서 특정 번호가 포함되어있는지 여부를 알 수 있다.', () => {}); }); diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index e4c4732..29a1d3a 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -2,7 +2,18 @@ import { validateLotto } from '../validations/lotto.js'; import LOTTO_TYPE from '../constants/lottoType.js'; export default class Lotto { - constructor({ type, numbers, bonusNumber = null }) { + constructor(lottoProps) { + if (Array.isArray(lottoProps)) { + this.#setLotto({ + type: LOTTO_TYPE.TICKET, + numbers: lottoProps, + }); + } else { + this.#setLotto(lottoProps); + } + } + + #setLotto({ type, numbers, bonusNumber = null }) { Lotto.#validate({ type, numbers, bonusNumber }); this.type = type; this.numbers = numbers; From 809ff2fe21b3757f6e35fb21ca0304a6fd04c717 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 14 Jul 2024 19:41:05 +0900 Subject: [PATCH 15/22] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EC=88=AB?= =?UTF-8?q?=EC=9E=90=20=ED=8F=AC=ED=95=A8=EC=97=AC=EB=B6=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=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/__tests__/lotto.test.js | 12 ++++++++++- src/domain/Lotto.js | 41 ++++++++++++++++++++++++++++--------- src/validations/lotto.js | 5 +++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/__tests__/lotto.test.js b/src/__tests__/lotto.test.js index 8fc294b..684139a 100644 --- a/src/__tests__/lotto.test.js +++ b/src/__tests__/lotto.test.js @@ -96,5 +96,15 @@ describe('로또 테스트', () => { expect(new Lotto([1, 2, 3, 4, 5, 6]).numbers).toEqual([1, 2, 3, 4, 5, 6]); }); - test('로또에서 특정 번호가 포함되어있는지 여부를 알 수 있다.', () => {}); + test('로또에서 특정 번호가 포함되어있는지 여부를 알 수 있다.', () => { + const lotto1 = new Lotto([1, 2, 3, 4, 5, 6]); + const lotto2 = new Lotto({ + type: LOTTO_TYPE.WINNING, + numbers: [1, 2, 3, 4, 5, 6], + bonusNumber: 7, + }); + + expect(lotto1.contain(7)).toBe(false); + expect(lotto2.contain(7)).toBe(true); + }); }); diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index 29a1d3a..4bf38bd 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -3,26 +3,47 @@ import LOTTO_TYPE from '../constants/lottoType.js'; export default class Lotto { constructor(lottoProps) { - if (Array.isArray(lottoProps)) { - this.#setLotto({ - type: LOTTO_TYPE.TICKET, - numbers: lottoProps, - }); - } else { - this.#setLotto(lottoProps); - } + this.#setLotto( + Array.isArray(lottoProps) + ? { + type: LOTTO_TYPE.TICKET, + numbers: lottoProps, + } + : lottoProps, + ); } #setLotto({ type, numbers, bonusNumber = null }) { - Lotto.#validate({ type, numbers, bonusNumber }); + Lotto.#validateLottoProps({ type, numbers, bonusNumber }); this.type = type; this.numbers = numbers; this.bonusNumber = bonusNumber; } - static #validate(lottoProps) { + contain(targetNumber) { + const totalLottoNumbers = this.type === LOTTO_TYPE.TICKET ? this.numbers : [...this.numbers, this.bonusNumber]; + return totalLottoNumbers.includes(targetNumber); + } + + static #validateLottoProps(lottoProps) { validateLotto({ target: lottoProps, + validationKeys: [ + 'lottoType', + 'lottoNumbersLength', + 'winningLottoHasBonus', + 'ticketLottoBonusNull', + 'lottoEachUnique', + 'lottoInteger', + 'lottoRange', + ], + }); + } + + static #validateLotto(lotto) { + validateLotto({ + target: lotto, + validationKeys: ['lottoInstance'], }); } } diff --git a/src/validations/lotto.js b/src/validations/lotto.js index 85eee23..3602da2 100644 --- a/src/validations/lotto.js +++ b/src/validations/lotto.js @@ -1,5 +1,6 @@ import createValidator from './createValidator.js'; import LOTTO_TYPE from '../constants/lottoType.js'; +import Lotto from '../domain/Lotto.js'; export const lottoValidations = { lottoType: { @@ -36,6 +37,10 @@ export const lottoValidations = { }, errorMessage: '모든 로또 번호는 1 이상 45 이하여야 합니다.', }, + lottoInstance: { + check: (lotto) => lotto instanceof Lotto, + errorMessage: '유효한 로또가 아닙니다.', + }, }; export const validateLotto = createValidator(lottoValidations); From cfddd8c414b9768a139ce17b0e583c54da58332b Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 14 Jul 2024 20:17:49 +0900 Subject: [PATCH 16/22] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=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/__tests__/lotto.test.js | 12 ++++++++++++ src/domain/Lotto.js | 22 +++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/__tests__/lotto.test.js b/src/__tests__/lotto.test.js index 684139a..0ed4538 100644 --- a/src/__tests__/lotto.test.js +++ b/src/__tests__/lotto.test.js @@ -107,4 +107,16 @@ describe('로또 테스트', () => { expect(lotto1.contain(7)).toBe(false); expect(lotto2.contain(7)).toBe(true); }); + + test('두 로또를 비교하여 겹치는 숫자 목록을 확인할 수 있다.', () => { + const lotto1 = new Lotto([1, 2, 3, 4, 5, 6]); + const lotto2 = new Lotto({ + type: LOTTO_TYPE.WINNING, + numbers: [3, 4, 5, 6, 7, 8], + bonusNumber: 2, + }); + + expect(new Set(lotto1.match(lotto2))).toEqual(new Set([2, 3, 4, 5, 6])); + expect(new Set(lotto2.match(lotto1))).toEqual(new Set([2, 3, 4, 5, 6])); + }); }); diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index 4bf38bd..a760da3 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -13,16 +13,32 @@ export default class Lotto { ); } + contain(number) { + return this.totalNumbers.includes(number); + } + + match(otherLotto) { + Lotto.#validateLotto(otherLotto); + return this.totalNumbers.filter((number) => otherLotto.contain(number)); + } + + get totalNumbers() { + if (this.type === LOTTO_TYPE.WINNING) { + return [...this.numbers, this.bonusNumber]; + } + return this.numbers; + } + #setLotto({ type, numbers, bonusNumber = null }) { Lotto.#validateLottoProps({ type, numbers, bonusNumber }); this.type = type; this.numbers = numbers; this.bonusNumber = bonusNumber; + this.#sortNumbers(); } - contain(targetNumber) { - const totalLottoNumbers = this.type === LOTTO_TYPE.TICKET ? this.numbers : [...this.numbers, this.bonusNumber]; - return totalLottoNumbers.includes(targetNumber); + #sortNumbers() { + this.numbers.sort((a, b) => a - b); } static #validateLottoProps(lottoProps) { From 0828dd79e1f52da0787bf5a0266cb22a519daf21 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 14 Jul 2024 21:33:44 +0900 Subject: [PATCH 17/22] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lotto.test.js | 4 +- src/__tests__/lottoMatcher.test.js | 72 ------------------------------ src/__tests__/lottoPayment.test.js | 10 +---- src/__tests__/lottoSystem.test.js | 20 +++------ src/domain/Lotto.js | 13 ++---- src/domain/LottoMatcher.js | 27 ----------- src/domain/LottoPayment.js | 12 ++--- src/domain/LottoSystem.js | 66 ++++++++++++++------------- src/utils/cloneDeep.js | 10 ++--- 9 files changed, 57 insertions(+), 177 deletions(-) delete mode 100644 src/__tests__/lottoMatcher.test.js delete mode 100644 src/domain/LottoMatcher.js diff --git a/src/__tests__/lotto.test.js b/src/__tests__/lotto.test.js index 0ed4538..85dcfd2 100644 --- a/src/__tests__/lotto.test.js +++ b/src/__tests__/lotto.test.js @@ -116,7 +116,7 @@ describe('로또 테스트', () => { bonusNumber: 2, }); - expect(new Set(lotto1.match(lotto2))).toEqual(new Set([2, 3, 4, 5, 6])); - expect(new Set(lotto2.match(lotto1))).toEqual(new Set([2, 3, 4, 5, 6])); + expect(lotto1.matchNumbers(lotto2)).toEqual([3, 4, 5, 6]); + expect(lotto2.matchNumbers(lotto1)).toEqual([3, 4, 5, 6]); }); }); diff --git a/src/__tests__/lottoMatcher.test.js b/src/__tests__/lottoMatcher.test.js deleted file mode 100644 index 9472666..0000000 --- a/src/__tests__/lottoMatcher.test.js +++ /dev/null @@ -1,72 +0,0 @@ -import { createLottoTicket, createWinningLotto } from '../domain/Lotto.js'; -import LottoMatcher from '../domain/LottoMatcher.js'; -import { lottoMatcherValidations } from '../validations/lottoMatcher.js'; -import LOTTO_TYPE from '../constants/lottoType.js'; - -describe('로또 번호 일치여부 계산 테스트', () => { - test('유효한 당첨 로또로만 계산할 수 있다.', () => { - const validLottoTicket = createLottoTicket([11, 12, 13, 14, 15, 16]); - - expect(() => - LottoMatcher.matchLotto({ - winningLotto: { type: LOTTO_TYPE.WINNING, numbers: [1, 2, 3, 4, 5, 6], bonusNumber: 8 }, - lottoTickets: [validLottoTicket], - }), - ).toThrow(lottoMatcherValidations.validWinningLotto.errorMessage); - - expect(() => - LottoMatcher.matchLotto({ - winningLotto: validLottoTicket, - lottoTickets: [validLottoTicket], - }), - ).toThrow(lottoMatcherValidations.validWinningLotto.errorMessage); - }); - - test('유효한 로또 티켓들만 계산할 수 있다.', () => { - const validWinningLotto = createWinningLotto([1, 2, 3, 4, 5, 6], 7); - - expect(() => - LottoMatcher.matchLotto({ - winningLotto: validWinningLotto, - lottoTickets: [{ type: LOTTO_TYPE.TICKET, numbers: [1, 2, 3, 4, 5, 6] }], - }), - ); - - expect(() => - LottoMatcher.matchLotto({ - winningLotto: validWinningLotto, - lottoTickets: [validWinningLotto], - }), - ).toThrow(lottoMatcherValidations.validLottoTickets.errorMessage); - }); - - test('로또 티켓들의 [당첨 번호 일치 개수, 보너스 번호 일치 여부]를 반환한다.', () => { - const winningLotto = createWinningLotto([1, 2, 3, 4, 5, 6], 7); - const lottoTicket1 = createLottoTicket([1, 2, 3, 14, 15, 16]); // 3, false - const lottoTicket2 = createLottoTicket([11, 12, 13, 14, 15, 16]); // 0, false - const lottoTicket3 = createLottoTicket([1, 2, 3, 4, 7, 8]); // 4, true - - expect( - LottoMatcher.matchLotto({ - winningLotto, - lottoTickets: [lottoTicket1, lottoTicket2, lottoTicket3], - }), - ).toEqual([ - { - lotto: lottoTicket1, - matchCount: 3, - bonusMatch: false, - }, - { - lotto: lottoTicket2, - matchCount: 0, - bonusMatch: false, - }, - { - lotto: lottoTicket3, - matchCount: 4, - bonusMatch: true, - }, - ]); - }); -}); diff --git a/src/__tests__/lottoPayment.test.js b/src/__tests__/lottoPayment.test.js index f4f695b..f37c6ba 100644 --- a/src/__tests__/lottoPayment.test.js +++ b/src/__tests__/lottoPayment.test.js @@ -11,17 +11,11 @@ describe('로또 결제 테스트', () => { }); test('로또 결제는 1000원 단위로만 가능하다.', () => { - expect(() => - LottoPayment.createLottoTickets({ - payAmount: 1100, - }), - ).toThrow(lottoPaymentValidations.payAmountUnit1000.errorMessage); + expect(() => LottoPayment.createLottoTickets(1100)).toThrow(lottoPaymentValidations.payAmountUnit1000.errorMessage); }); test('1000원 당 1장의 로또 티켓을 발행해야 한다.', () => { - const { lottoTickets } = LottoPayment.createLottoTickets({ - payAmount: 8000, - }) + const lottoTickets = LottoPayment.createLottoTickets(8000); expect(lottoTickets.length).toBe(8); }); }); diff --git a/src/__tests__/lottoSystem.test.js b/src/__tests__/lottoSystem.test.js index 52ec929..15b72d9 100644 --- a/src/__tests__/lottoSystem.test.js +++ b/src/__tests__/lottoSystem.test.js @@ -1,6 +1,7 @@ import LottoSystem from '../domain/LottoSystem.js'; import { lottoSystemValidations } from '../validations/lottoSystem.js'; import createLottoNumbers from '../domain/createLottoNumbers.js'; +import Lotto from '../domain/Lotto.js'; vi.mock('../domain/createLottoNumbers'); @@ -101,22 +102,15 @@ describe('로또 시스템 테스트', async () => { expect(lottoSystem.paidAmount).toBe(10000); }); - test('로또 시스템은 발행한 로또들을 등수 별로 분류할 수 있다.', () => { - createLottoNumbers - .mockReturnValueOnce([1, 2, 3, 4, 5, 6]) // 1등 - .mockReturnValueOnce([1, 2, 3, 4, 5, 7]) // 2등 - .mockReturnValueOnce([1, 2, 3, 4, 5, 7]) // 2등 - .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 - .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 - .mockReturnValueOnce([1, 2, 3, 4, 15, 16]); // 4등 - + test('로또 시스템에서 로또 티켓의 등수를 확인할 수 있다.', () => { const lottoSystem = new LottoSystem(); lottoSystem.setWinningLotto([1, 2, 3, 4, 5, 6], 7); - lottoSystem.payLottoTicket(6000); - expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 1).ticketList.length).toBe(1); - expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 2).ticketList.length).toBe(2); - expect(lottoSystem.lottoRankingResult.find(({ rank }) => rank === 4).ticketList.length).toBe(3); + expect(lottoSystem.getRank(new Lotto([1, 2, 3, 4, 5, 6]))).toBe(1); + expect(lottoSystem.getRank(new Lotto([1, 2, 3, 4, 5, 7]))).toBe(2); + expect(lottoSystem.getRank(new Lotto([1, 2, 3, 4, 5, 8]))).toBe(3); + expect(lottoSystem.getRank(new Lotto([1, 2, 3, 4, 7, 8]))).toBe(4); + expect(lottoSystem.getRank(new Lotto([1, 2, 3, 7, 8, 9]))).toBe(5); }); test('로또 시스템은 총 수익을 계산할 수 있다.', () => { diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index a760da3..0f6b27e 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -14,19 +14,12 @@ export default class Lotto { } contain(number) { - return this.totalNumbers.includes(number); + return [...this.numbers, this.bonusNumber].includes(number); } - match(otherLotto) { + matchNumbers(otherLotto) { Lotto.#validateLotto(otherLotto); - return this.totalNumbers.filter((number) => otherLotto.contain(number)); - } - - get totalNumbers() { - if (this.type === LOTTO_TYPE.WINNING) { - return [...this.numbers, this.bonusNumber]; - } - return this.numbers; + return this.numbers.filter((number) => otherLotto.numbers.includes(number)); } #setLotto({ type, numbers, bonusNumber = null }) { diff --git a/src/domain/LottoMatcher.js b/src/domain/LottoMatcher.js deleted file mode 100644 index 7b59e6e..0000000 --- a/src/domain/LottoMatcher.js +++ /dev/null @@ -1,27 +0,0 @@ -import { validateLottoMatcher } from '../validations/lottoMatcher.js'; - -export default class LottoMatcher { - static matchLotto({ winningLotto, lottoTickets }) { - LottoMatcher.#validate({ winningLotto, lottoTickets }); - - return lottoTickets.map((lottoTicket) => ({ - lotto: lottoTicket, - matchCount: LottoMatcher.#getMatchCount(lottoTicket.numbers, winningLotto), - bonusMatch: LottoMatcher.#getBonusMatch(lottoTicket.numbers, winningLotto), - })); - } - - static #validate(lottoMatcherProps) { - validateLottoMatcher({ - target: lottoMatcherProps, - }); - } - - static #getMatchCount(ticketNumbers, winningLotto) { - return ticketNumbers.filter((ticketNumber) => winningLotto.numbers.includes(ticketNumber)).length; - } - - static #getBonusMatch(ticketNumbers, winningLotto) { - return ticketNumbers.includes(winningLotto.bonusNumber); - } -} diff --git a/src/domain/LottoPayment.js b/src/domain/LottoPayment.js index e37909b..06e2d6c 100644 --- a/src/domain/LottoPayment.js +++ b/src/domain/LottoPayment.js @@ -1,19 +1,13 @@ import { validateLottoPayment } from '../validations/lottoPayment.js'; import createLottoNumbers from './createLottoNumbers.js'; -import { createLottoTicket } from './Lotto.js'; +import Lotto, { createLottoTicket } from './Lotto.js'; export default class LottoPayment { - static createLottoTickets({ payAmount }) { + static createLottoTickets(payAmount) { LottoPayment.#validate({ payAmount }); const ticketCount = Math.floor(payAmount / 1000); - return { - lottoTickets: Array.from({ length: ticketCount }, () => { - const numbers = createLottoNumbers(); - return createLottoTicket(numbers); - }), - paidAmount: payAmount, - }; + return Array.from({ length: ticketCount }, () => new Lotto(createLottoNumbers())); } static #validate(lottoPaymentProps) { diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index 37f82d5..aad1952 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -1,14 +1,13 @@ import { createWinningLotto } from './Lotto.js'; import LottoPayment from './LottoPayment.js'; -import LottoMatcher from './LottoMatcher.js'; import LOTTO_RANKING_RULE from '../constants/lottoRankingRule.js'; import { validateLottoSystem } from '../validations/lottoSystem.js'; -import cloneDeep from "../utils/cloneDeep.js"; +import cloneDeep from '../utils/cloneDeep.js'; export default class LottoSystem { - #rankingRule - #lottoData - + #rankingRule; + #lottoData; + constructor({ rankingRule = LOTTO_RANKING_RULE } = {}) { LottoSystem.#validate({ rankingRule }); @@ -16,7 +15,7 @@ export default class LottoSystem { this.#lottoData = { winningLotto: null, lottoTickets: [], - paidAmount: 0 + paidAmount: 0, }; } @@ -26,22 +25,37 @@ export default class LottoSystem { }); } + #setLottoData(newData) { + this.#lottoData = { ...this.#lottoData, ...newData }; + } + + getRank(lottoTicket) { + const matchedRuleWithBonus = this.#rankingRule + .filter(({ bonusMatch }) => bonusMatch !== undefined) + .find( + ({ matchCount, bonusMatch }) => + lottoTicket.contain(this.winningLotto.bonusNumber) === bonusMatch && + lottoTicket.matchNumbers(this.winningLotto).length === matchCount, + ); + const matchedRuleWithoutBonus = this.#rankingRule + .filter(({ bonusMatch }) => bonusMatch === undefined) + .find(({ matchCount }) => lottoTicket.matchNumbers(this.winningLotto).length === matchCount); + + if (matchedRuleWithBonus) return matchedRuleWithBonus.rank; + return matchedRuleWithoutBonus?.rank; + } + setWinningLotto(winningNumbers, bonusNumber) { this.#setLottoData({ - winningLotto: createWinningLotto(winningNumbers, bonusNumber) - }) + winningLotto: createWinningLotto(winningNumbers, bonusNumber), + }); } payLottoTicket(payAmount) { - const { lottoTickets, paidAmount } = LottoPayment.createLottoTickets({ payAmount }) this.#setLottoData({ - lottoTickets, - paidAmount - }) - } - - #setLottoData(newData) { - this.#lottoData = { ...this.#lottoData, ...newData }; + lottoTickets: LottoPayment.createLottoTickets(payAmount), + paidAmount: payAmount, + }); } get paidAmount() { @@ -52,20 +66,18 @@ export default class LottoSystem { return cloneDeep(this.#lottoData.lottoTickets); } + get winningLotto() { + return cloneDeep(this.#lottoData.winningLotto); + } + get ticketCount() { return this.#lottoData.lottoTickets.length; } get lottoRankingResult() { - const lottoMatchResult = LottoMatcher.matchLotto(this.#lottoData); - return this.#rankingRule.map((rule) => ({ ...rule, - ticketList: lottoMatchResult - .filter(({ matchCount, bonusMatch }) => - this.#isSameMatchCount(matchCount, rule.matchCount) - && this.#isSameBonusMatch(bonusMatch, rule.bonusMatch)) - .map(({ lotto }) => cloneDeep(lotto)), + ticketList: this.lottoTickets.filter((lottoTicket) => this.getRank(lottoTicket) === rule.rank), })); } @@ -76,12 +88,4 @@ export default class LottoSystem { get profitRatio() { return parseFloat(((this.profitAmount / this.paidAmount) * 100).toFixed(2)); } - - #isSameMatchCount(ticketMatchCount, ruleMatchCount) { - return ticketMatchCount === ruleMatchCount; - } - - #isSameBonusMatch(ticketBonusMatch, ruleBonusMatch) { - return ruleBonusMatch === undefined || ruleBonusMatch === ticketBonusMatch; - } } diff --git a/src/utils/cloneDeep.js b/src/utils/cloneDeep.js index ed4cbd0..66ebe5c 100644 --- a/src/utils/cloneDeep.js +++ b/src/utils/cloneDeep.js @@ -5,19 +5,19 @@ const cloneDeep = (obj) => { if (Array.isArray(obj)) { const clonedArray = []; - obj.forEach(item => { + obj.forEach((item) => { clonedArray.push(cloneDeep(item)); - }) + }); return clonedArray; } - const clonedObj = {}; - for (const key in obj) { + const clonedObj = Object.create(Object.getPrototypeOf(obj)); + for (let key in obj) { if (obj.hasOwnProperty(key)) { clonedObj[key] = cloneDeep(obj[key]); } } return clonedObj; -} +}; export default cloneDeep; From 5c8f89bd2d046fa058f1fb89ae6333f762b8f059 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Sun, 14 Jul 2024 21:41:33 +0900 Subject: [PATCH 18/22] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lotto.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/__tests__/lotto.test.js b/src/__tests__/lotto.test.js index 85dcfd2..5e776df 100644 --- a/src/__tests__/lotto.test.js +++ b/src/__tests__/lotto.test.js @@ -108,6 +108,13 @@ describe('로또 테스트', () => { expect(lotto2.contain(7)).toBe(true); }); + test('유효한 로또로만 로또를 비교할 수 있다.', () => { + const lotto1 = new Lotto([1, 2, 3, 4, 5, 6]); + const lotto2 = [1, 2, 3, 4, 5, 6]; + + expect(() => lotto1.matchNumbers(lotto2)).toThrow(lottoValidations.lottoInstance.errorMessage); + }); + test('두 로또를 비교하여 겹치는 숫자 목록을 확인할 수 있다.', () => { const lotto1 = new Lotto([1, 2, 3, 4, 5, 6]); const lotto2 = new Lotto({ From 5cf19902f2949395e5adfce408ae8d426caea516 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Mon, 15 Jul 2024 22:01:32 +0900 Subject: [PATCH 19/22] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EC=88=98?= =?UTF-8?q?=EC=9D=B5=20=EC=A6=9D=EA=B0=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/lottoRankingRule.js | 5 +- src/domain/Lotto.js | 4 +- src/domain/LottoRule.js | 35 +++++++++++++ src/domain/LottoSystem.js | 85 ++++++++++++++++--------------- src/domain/LottoSystemControl.js | 16 +++--- src/domain/LottoSystemView.js | 11 +++- src/validations/lottoMatcher.js | 17 ------- src/validations/lottoRuleSet.js | 44 ++++++++++++++++ 8 files changed, 148 insertions(+), 69 deletions(-) create mode 100644 src/domain/LottoRule.js delete mode 100644 src/validations/lottoMatcher.js create mode 100644 src/validations/lottoRuleSet.js diff --git a/src/constants/lottoRankingRule.js b/src/constants/lottoRankingRule.js index 7772b12..a0c05e8 100644 --- a/src/constants/lottoRankingRule.js +++ b/src/constants/lottoRankingRule.js @@ -3,23 +3,27 @@ const LOTTO_RANKING_DATA = [ rank: 1, matchCount: 6, profit: 2000000000, + distribute: 0.5, }, { rank: 2, matchCount: 5, bonusMatch: true, profit: 30000000, + distribute: 0.3, }, { rank: 3, matchCount: 5, bonusMatch: false, profit: 1500000, + distribute: 0.15, }, { rank: 4, matchCount: 4, profit: 50000, + distribute: 0.05, }, { rank: 5, @@ -28,5 +32,4 @@ const LOTTO_RANKING_DATA = [ }, ]; - export default LOTTO_RANKING_DATA; diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js index 0f6b27e..ff85579 100644 --- a/src/domain/Lotto.js +++ b/src/domain/Lotto.js @@ -18,7 +18,7 @@ export default class Lotto { } matchNumbers(otherLotto) { - Lotto.#validateLotto(otherLotto); + Lotto.validateLotto(otherLotto); return this.numbers.filter((number) => otherLotto.numbers.includes(number)); } @@ -49,7 +49,7 @@ export default class Lotto { }); } - static #validateLotto(lotto) { + static validateLotto(lotto) { validateLotto({ target: lotto, validationKeys: ['lottoInstance'], diff --git a/src/domain/LottoRule.js b/src/domain/LottoRule.js new file mode 100644 index 0000000..2336ff7 --- /dev/null +++ b/src/domain/LottoRule.js @@ -0,0 +1,35 @@ +import { validateLottoRuleSet } from '../validations/lottoRuleSet.js'; +import cloneDeep from '../utils/cloneDeep.js'; + +export default class LottoRuleSet { + #ruleSet; + constructor({ initialRule }) { + LottoRuleSet.#validate({ initialRule }); + this.#ruleSet = initialRule.sort((a, b) => b.rank - a.rank); + } + + get rulesWithBonusMatch() { + return this.#ruleSet.filter(({ bonusMatch }) => bonusMatch !== undefined); + } + + get rulesWithoutBonusMatch() { + return this.#ruleSet.filter(({ bonusMatch }) => bonusMatch === undefined); + } + + get rules() { + return cloneDeep(this.#ruleSet); + } + + increaseRankProfit(leftAmount) { + this.#ruleSet = this.#ruleSet.map(({ profit, distribute, ...rule }) => ({ + ...rule, + profit: distribute ? profit + Math.floor(leftAmount * distribute) : profit, + })); + } + + static #validate(lottoRuleSetProps) { + validateLottoRuleSet({ + target: lottoRuleSetProps, + }); + } +} diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index aad1952..ea5240c 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -1,50 +1,26 @@ import { createWinningLotto } from './Lotto.js'; import LottoPayment from './LottoPayment.js'; import LOTTO_RANKING_RULE from '../constants/lottoRankingRule.js'; -import { validateLottoSystem } from '../validations/lottoSystem.js'; import cloneDeep from '../utils/cloneDeep.js'; +import LottoRuleSet from './LottoRule.js'; export default class LottoSystem { - #rankingRule; + #ruleSet; #lottoData; constructor({ rankingRule = LOTTO_RANKING_RULE } = {}) { - LottoSystem.#validate({ rankingRule }); - - this.#rankingRule = rankingRule.sort((a, b) => b.rank - a.rank); this.#lottoData = { winningLotto: null, lottoTickets: [], paidAmount: 0, }; - } - - static #validate(lottoSystemProps) { - validateLottoSystem({ - target: lottoSystemProps, - }); + this.#ruleSet = new LottoRuleSet({ initialRule: rankingRule }); } #setLottoData(newData) { this.#lottoData = { ...this.#lottoData, ...newData }; } - getRank(lottoTicket) { - const matchedRuleWithBonus = this.#rankingRule - .filter(({ bonusMatch }) => bonusMatch !== undefined) - .find( - ({ matchCount, bonusMatch }) => - lottoTicket.contain(this.winningLotto.bonusNumber) === bonusMatch && - lottoTicket.matchNumbers(this.winningLotto).length === matchCount, - ); - const matchedRuleWithoutBonus = this.#rankingRule - .filter(({ bonusMatch }) => bonusMatch === undefined) - .find(({ matchCount }) => lottoTicket.matchNumbers(this.winningLotto).length === matchCount); - - if (matchedRuleWithBonus) return matchedRuleWithBonus.rank; - return matchedRuleWithoutBonus?.rank; - } - setWinningLotto(winningNumbers, bonusNumber) { this.#setLottoData({ winningLotto: createWinningLotto(winningNumbers, bonusNumber), @@ -58,34 +34,61 @@ export default class LottoSystem { }); } - get paidAmount() { - return this.#lottoData.paidAmount; - } - - get lottoTickets() { - return cloneDeep(this.#lottoData.lottoTickets); - } + getRank(lottoTicket) { + const matchedRuleWithBonus = this.#ruleSet.rulesWithBonusMatch.find( + ({ matchCount, bonusMatch }) => + lottoTicket.contain(this.winningLotto.bonusNumber) === bonusMatch && + lottoTicket.matchNumbers(this.winningLotto).length === matchCount, + ); + const matchedRuleWithoutBonus = this.#ruleSet.rulesWithoutBonusMatch.find( + ({ matchCount }) => lottoTicket.matchNumbers(this.winningLotto).length === matchCount, + ); - get winningLotto() { - return cloneDeep(this.#lottoData.winningLotto); + if (matchedRuleWithBonus) return matchedRuleWithBonus.rank; + return matchedRuleWithoutBonus?.rank; } - get ticketCount() { - return this.#lottoData.lottoTickets.length; + getCountByRank(rank) { + return this.lottoTickets.filter((lottoTicket) => this.getRank(lottoTicket) === rank).length; } get lottoRankingResult() { - return this.#rankingRule.map((rule) => ({ + return this.#ruleSet.rules.map((rule) => ({ ...rule, - ticketList: this.lottoTickets.filter((lottoTicket) => this.getRank(lottoTicket) === rule.rank), + count: this.getCountByRank(rule.rank), })); } get profitAmount() { - return this.lottoRankingResult.reduce((sum, { ticketList, profit }) => sum + profit * ticketList.length, 0); + console.log(this.lottoRankingResult); + return this.lottoRankingResult.reduce((sum, { count, profit }) => sum + profit * count, 0); + } + + get leftPaidAmount() { + return Math.max(0, this.paidAmount - this.profitAmount); + } + + increaseProfit() { + this.#ruleSet.increaseRankProfit(this.leftPaidAmount); } get profitRatio() { return parseFloat(((this.profitAmount / this.paidAmount) * 100).toFixed(2)); } + + get paidAmount() { + return this.#lottoData.paidAmount; + } + + get lottoTickets() { + return cloneDeep(this.#lottoData.lottoTickets); + } + + get winningLotto() { + return cloneDeep(this.#lottoData.winningLotto); + } + + get ticketCount() { + return this.#lottoData.lottoTickets.length; + } } diff --git a/src/domain/LottoSystemControl.js b/src/domain/LottoSystemControl.js index 58aeda6..178e088 100644 --- a/src/domain/LottoSystemControl.js +++ b/src/domain/LottoSystemControl.js @@ -26,13 +26,17 @@ export default class LottoSystemControl { } async run() { - await this.setUpPayAmount(); - this.viewer.displayPaidCount(this.lottoSystem); - this.viewer.displayLottoTickets(this.lottoSystem); + do { + await this.setUpPayAmount(); + this.viewer.displayPaidCount(this.lottoSystem); + this.viewer.displayLottoTickets(this.lottoSystem); + + await this.setUpWinningLotto(); + this.viewer.displayLottoResult(this.lottoSystem); + this.viewer.displayProfitRatio(this.lottoSystem); - await this.setUpWinningLotto(); - this.viewer.displayLottoResult(this.lottoSystem); - this.viewer.displayProfitRatio(this.lottoSystem); + this.lottoSystem.increaseProfit(); + } while (await this.viewer.readKeepGoing(this.lottoSystem)); this.viewer.finish(); } diff --git a/src/domain/LottoSystemView.js b/src/domain/LottoSystemView.js index 409adf0..231f195 100644 --- a/src/domain/LottoSystemView.js +++ b/src/domain/LottoSystemView.js @@ -12,6 +12,7 @@ export default class LottoSystemView { rankingWithBonus: '%{1}등 : %{2}개 일치, 보너스볼 일치 (%{3}원) - %{4}개', profitRatio: '총 수익률은 %{1}% 입니다.', error: '⚠️ %{1}', + replay: '게임을 계속하시겠습니까? 남은 수익 %{1}원은 다음 당첨금으로 이관됩니다.', }); this.reader = new ConsoleReader(); } @@ -31,6 +32,12 @@ export default class LottoSystemView { return Number(answer); } + async readKeepGoing({ leftPaidAmount }) { + this.printer.printWithTemplate('replay', [formatCurrency(leftPaidAmount)]); + const answer = await this.reader.read('다시하기 = S / 그민두기 = Q : '); + return answer.toUpperCase() === 'S'; + } + displayPaidCount({ ticketCount }) { this.printer.lineBreak(); this.printer.printWithTemplate('paidCount', [ticketCount]); @@ -49,8 +56,8 @@ export default class LottoSystemView { this.printer.print('당첨 통계'); this.printer.printWithTemplate('line'); - lottoRankingResult.forEach(({ rank, matchCount, bonusMatch, profit, ticketList }) => { - const resultForPrint = [rank, matchCount, formatCurrency(profit), ticketList.length]; + lottoRankingResult.forEach(({ rank, matchCount, bonusMatch, profit, count }) => { + const resultForPrint = [rank, matchCount, formatCurrency(profit), count]; if (bonusMatch) { this.printer.printWithTemplate('rankingWithBonus', resultForPrint); } else { diff --git a/src/validations/lottoMatcher.js b/src/validations/lottoMatcher.js deleted file mode 100644 index 3887a84..0000000 --- a/src/validations/lottoMatcher.js +++ /dev/null @@ -1,17 +0,0 @@ -import Lotto from '../domain/Lotto.js'; -import createValidator from './createValidator.js'; -import LOTTO_TYPE from '../constants/lottoType.js'; - -export const lottoMatcherValidations = { - validWinningLotto: { - check: ({ winningLotto }) => winningLotto instanceof Lotto && winningLotto.type === LOTTO_TYPE.WINNING, - errorMessage: '유효한 로또 당첨 정보가 아닙니다.', - }, - validLottoTickets: { - check: ({ lottoTickets }) => - lottoTickets.every((lottoTicket) => lottoTicket instanceof Lotto && lottoTicket.type === LOTTO_TYPE.TICKET), - errorMessage: '유효하지 않은 로또 티켓이 존재합니다.', - }, -}; - -export const validateLottoMatcher = createValidator(lottoMatcherValidations); diff --git a/src/validations/lottoRuleSet.js b/src/validations/lottoRuleSet.js new file mode 100644 index 0000000..8b9614e --- /dev/null +++ b/src/validations/lottoRuleSet.js @@ -0,0 +1,44 @@ +import createValidator from './createValidator.js'; + +export const lottoRuleSetValidations = { + validRankList: { + check: ({ initialRule }) => Array.isArray(initialRule), + errorMessage: '로또 랭킹 규칙은 배열 형태여야 합니다.', + }, + validRanks: { + check: ({ initialRule }) => initialRule.every(({ rank }) => Number.isInteger(rank) && rank > 0), + errorMessage: '로또 랭킹 규칙에 유효하지 않은 값이(rank) 포함되어 있습니다.', + }, + validMatchCounts: { + check: ({ initialRule }) => + initialRule.every(({ matchCount }) => Number.isInteger(matchCount) && 0 < matchCount && matchCount <= 6), + errorMessage: '로또 랭킹 규칙에 유효하지 않은 값이(matchCount) 포함되어 있습니다.', + }, + validBonusMatches: { + check: ({ initialRule }) => + initialRule.every(({ bonusMatch }) => bonusMatch === undefined || typeof bonusMatch === 'boolean'), + errorMessage: '로또 랭킹 규칙에 유효하지 않은 값이(bonusMatch) 포함되어 있습니다.', + }, + validProfits: { + check: ({ initialRule }) => initialRule.every(({ profit }) => Number.isInteger(profit) && profit > 0), + errorMessage: '로또 랭킹 규칙에 유효하지 않은 값이(profit) 포함되어 있습니다.', + }, + validDistribute: { + check: ({ initialRule }) => + initialRule.every(({ distribute }) => distribute === undefined || (0 < distribute && distribute < 1)), + errorMessage: '로또 랭킹 규칙에 유효하지 않은 값이(distribute) 포함되어 있습니다.', + }, + distributeSum: { + check: ({ initialRule }) => initialRule.reduce((sum, { distribute }) => sum + (distribute ?? 0), 0) <= 1, + errorMessage: '로또 잔여금 분배율의 합은 1을 초과할 수 없습니다.', + }, + hasAllRanks: { + check: ({ initialRule }) => { + const ranks = initialRule.map((data) => data.rank).sort((a, b) => a - b); + return initialRule.length > 0 && ranks.every((rank, index) => rank === index + 1); + }, + errorMessage: '로또 랭킹 규칙에 모든 등수가 빠짐없이 있어야 합니다.', + }, +}; + +export const validateLottoRuleSet = createValidator(lottoRuleSetValidations); From c1532e3a5f6072cfeade0a3da3091a08b76b1389 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Mon, 15 Jul 2024 23:17:18 +0900 Subject: [PATCH 20/22] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B3=B4=EC=99=84=20=EB=B0=8F=20=EC=BD=98?= =?UTF-8?q?=EC=86=94=20=EC=B6=9C=EB=A0=A5=20=EA=BE=B8=EB=AF=B8=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/lottoRuleSet.test.js | 160 +++++++++++++++++++ src/__tests__/lottoSystem.test.js | 90 ++--------- src/domain/{LottoRule.js => LottoRuleSet.js} | 1 + src/domain/LottoSystem.js | 3 +- src/domain/LottoSystemControl.js | 2 + src/domain/LottoSystemView.js | 9 ++ 6 files changed, 183 insertions(+), 82 deletions(-) create mode 100644 src/__tests__/lottoRuleSet.test.js rename src/domain/{LottoRule.js => LottoRuleSet.js} (98%) diff --git a/src/__tests__/lottoRuleSet.test.js b/src/__tests__/lottoRuleSet.test.js new file mode 100644 index 0000000..097bb76 --- /dev/null +++ b/src/__tests__/lottoRuleSet.test.js @@ -0,0 +1,160 @@ +import LottoRuleSet from '../domain/LottoRuleSet.js'; +import { lottoRuleSetValidations } from '../validations/lottoRuleSet.js'; + +vi.mock('../domain/createLottoNumbers'); + +describe('로또 규칙 테스트', async () => { + test('로또 규칙은 순위가 1위부터 최하위까지 올바르게 매겨져야 한다.', () => { + const rankingRuleHasInvalidRank1 = [ + { + matchCount: 3, + bonusMatch: false, + profit: 1000, + rank: -1, + distribute: 0.5, + }, + ]; + expect( + () => + new LottoRuleSet({ + initialRule: rankingRuleHasInvalidRank1, + }), + ).toThrow(lottoRuleSetValidations.validRanks.errorMessage); + + const rankingRuleHasInvalidRank2 = [ + { + matchCount: 3, + bonusMatch: false, + profit: 1000, + rank: 3, + distribute: 0.5, + }, + ]; + expect( + () => + new LottoRuleSet({ + initialRule: rankingRuleHasInvalidRank2, + }), + ).toThrow(lottoRuleSetValidations.hasAllRanks.errorMessage); + }); + + test('로또 규칙에서 각 순위의 distribute 의 합은 1을 넘으면 안된다.', () => { + const rankingRuleHasInvalidDistributeSum = [ + { + bonusMatch: true, + profit: 1000, + rank: 1, + matchCount: 3, + distribute: 0.5, + }, + { + bonusMatch: true, + profit: 1000, + rank: 2, + matchCount: 2, + distribute: 0.4, + }, + { + bonusMatch: true, + profit: 1000, + rank: 3, + matchCount: 1, + distribute: 0.3, + }, + ]; + expect(() => new LottoRuleSet({ initialRule: rankingRuleHasInvalidDistributeSum })).toThrow( + lottoRuleSetValidations.distributeSum.errorMessage, + ); + }); + + test('로또 규칙에는 유효한 matchCount, bonus, profit, distribute 이 포함되어야 한다.', () => { + const rankingRuleHasInvalidMatchCount = [ + { + bonusMatch: true, + profit: 1000, + rank: 1, + matchCount: 100, + distribute: 0.5, + }, + ]; + expect( + () => + new LottoRuleSet({ + initialRule: rankingRuleHasInvalidMatchCount, + }), + ).toThrow(lottoRuleSetValidations.validMatchCounts.errorMessage); + + const rankingRuleHasInvalidBonusMatch = [ + { + profit: 1000, + rank: 1, + matchCount: 3, + bonusMatch: 'false', + }, + ]; + expect( + () => + new LottoRuleSet({ + initialRule: rankingRuleHasInvalidBonusMatch, + }), + ).toThrow(lottoRuleSetValidations.validBonusMatches.errorMessage); + + const rankingRuleHasInvalidProfit = [ + { + bonusMatch: true, + profit: '1000', + rank: 1, + matchCount: 3, + distribute: 0.5, + }, + ]; + expect( + () => + new LottoRuleSet({ + initialRule: rankingRuleHasInvalidProfit, + }), + ).toThrow(lottoRuleSetValidations.validProfits.errorMessage); + + const rankingRuleHasInvalidDistribute = [ + { + bonusMatch: true, + profit: 1000, + rank: 1, + matchCount: 3, + distribute: -1, + }, + ]; + expect( + () => + new LottoRuleSet({ + initialRule: rankingRuleHasInvalidDistribute, + }), + ).toThrow(lottoRuleSetValidations.validDistribute.errorMessage); + }); + + test('increaseRankProfit은 남은 액수와 distribute 비율을 기반으로 각 순위의 profit을 증가시켜야 한다.', () => { + const initialRule = [ + { + matchCount: 5, + bonusMatch: true, + profit: 1000, + rank: 1, + distribute: 0.6, + }, + { + matchCount: 5, + bonusMatch: false, + profit: 600, + rank: 2, + distribute: 0.4, + }, + ]; + const lottoRuleSet = new LottoRuleSet({ initialRule: initialRule }); + + lottoRuleSet.increaseRankProfit(1000); + const updatedRules = lottoRuleSet.rules; + + expect(updatedRules.find(({ rank }) => rank === 1).profit).toBe(1600); + expect(updatedRules.find(({ rank }) => rank === 2).profit).toBe(1000); + }); +}); diff --git a/src/__tests__/lottoSystem.test.js b/src/__tests__/lottoSystem.test.js index 15b72d9..02bc3b3 100644 --- a/src/__tests__/lottoSystem.test.js +++ b/src/__tests__/lottoSystem.test.js @@ -1,5 +1,4 @@ import LottoSystem from '../domain/LottoSystem.js'; -import { lottoSystemValidations } from '../validations/lottoSystem.js'; import createLottoNumbers from '../domain/createLottoNumbers.js'; import Lotto from '../domain/Lotto.js'; @@ -15,85 +14,6 @@ describe('로또 시스템 테스트', async () => { createLottoNumbers.mockImplementation(originalCreateLottoNumbers); }); - test('로또 시스템은 순위가 올바르게 매겨진 랭킹 규칙만 사용한다.', () => { - const rankingRuleHasInvalidRank1 = [ - { - matchCount: 3, - bonusMatch: false, - profit: 1000, - rank: -1, - }, - ]; - expect( - () => - new LottoSystem({ - rankingRule: rankingRuleHasInvalidRank1, - }), - ).toThrow(lottoSystemValidations.validRanks.errorMessage); - - const rankingRuleHasInvalidRank2 = [ - { - matchCount: 3, - bonusMatch: false, - profit: 1000, - rank: 3, - }, - ]; - expect( - () => - new LottoSystem({ - rankingRule: rankingRuleHasInvalidRank2, - }), - ).toThrow(lottoSystemValidations.hasAllRanks.errorMessage); - }); - - test('로또 시스템은 유효한 matchCount, bonus, profit 이 포함된 랭킹 규칙만 사용한다.', () => { - const rankingRuleHasInvalidMatchCount = [ - { - bonusMatch: true, - profit: 1000, - rank: 1, - matchCount: 100, - }, - ]; - expect( - () => - new LottoSystem({ - rankingRule: rankingRuleHasInvalidMatchCount, - }), - ).toThrow(lottoSystemValidations.validMatchCounts.errorMessage); - - const rankingRuleHasInvalidBonusMatch = [ - { - profit: 1000, - rank: 1, - matchCount: 3, - bonusMatch: 'false', - }, - ]; - expect( - () => - new LottoSystem({ - rankingRule: rankingRuleHasInvalidBonusMatch, - }), - ).toThrow(lottoSystemValidations.validBonusMatches.errorMessage); - - const rankingRuleHasInvalidProfit = [ - { - bonusMatch: true, - profit: '1000', - rank: 1, - matchCount: 3, - }, - ]; - expect( - () => - new LottoSystem({ - rankingRule: rankingRuleHasInvalidProfit, - }), - ).toThrow(lottoSystemValidations.validProfits.errorMessage); - }); - test('로또 시스템은 정확한 개수의 로또를 구매한다.', () => { const lottoSystem = new LottoSystem(); lottoSystem.payLottoTicket(10000); @@ -125,6 +45,16 @@ describe('로또 시스템 테스트', async () => { expect(lottoSystem.profitAmount).toBe(2000050000); }); + test('로또 시스템은 당첨금이 지불된 후의 남은 구매금액을 계산할 수 있다.', () => { + createLottoNumbers.mockReturnValueOnce([1, 2, 3, 4, 5, 6]).mockReturnValue([7, 8, 9, 10, 11, 12]); + + const lottoSystem = new LottoSystem(); + lottoSystem.setWinningLotto([4, 5, 6, 13, 14, 15], 16); + lottoSystem.payLottoTicket(100000); + + expect(lottoSystem.leftPaidAmount).toBe(95000); + }); + test('로또 시스템은 수익율을 계산할 수 있다.', () => { createLottoNumbers .mockReturnValueOnce([1, 2, 3, 4, 15, 16]) // 4등 = 50000 diff --git a/src/domain/LottoRule.js b/src/domain/LottoRuleSet.js similarity index 98% rename from src/domain/LottoRule.js rename to src/domain/LottoRuleSet.js index 2336ff7..9106a27 100644 --- a/src/domain/LottoRule.js +++ b/src/domain/LottoRuleSet.js @@ -23,6 +23,7 @@ export default class LottoRuleSet { increaseRankProfit(leftAmount) { this.#ruleSet = this.#ruleSet.map(({ profit, distribute, ...rule }) => ({ ...rule, + distribute, profit: distribute ? profit + Math.floor(leftAmount * distribute) : profit, })); } diff --git a/src/domain/LottoSystem.js b/src/domain/LottoSystem.js index ea5240c..324362d 100644 --- a/src/domain/LottoSystem.js +++ b/src/domain/LottoSystem.js @@ -2,7 +2,7 @@ import { createWinningLotto } from './Lotto.js'; import LottoPayment from './LottoPayment.js'; import LOTTO_RANKING_RULE from '../constants/lottoRankingRule.js'; import cloneDeep from '../utils/cloneDeep.js'; -import LottoRuleSet from './LottoRule.js'; +import LottoRuleSet from './LottoRuleSet.js'; export default class LottoSystem { #ruleSet; @@ -60,7 +60,6 @@ export default class LottoSystem { } get profitAmount() { - console.log(this.lottoRankingResult); return this.lottoRankingResult.reduce((sum, { count, profit }) => sum + profit * count, 0); } diff --git a/src/domain/LottoSystemControl.js b/src/domain/LottoSystemControl.js index 178e088..23937bd 100644 --- a/src/domain/LottoSystemControl.js +++ b/src/domain/LottoSystemControl.js @@ -26,7 +26,9 @@ export default class LottoSystemControl { } async run() { + let gameCount = 1; do { + this.viewer.displayLottoStart(gameCount++); await this.setUpPayAmount(); this.viewer.displayPaidCount(this.lottoSystem); this.viewer.displayLottoTickets(this.lottoSystem); diff --git a/src/domain/LottoSystemView.js b/src/domain/LottoSystemView.js index 231f195..0bc015e 100644 --- a/src/domain/LottoSystemView.js +++ b/src/domain/LottoSystemView.js @@ -5,9 +5,11 @@ import formatCurrency from '../utils/formatCurrency.js'; export default class LottoSystemView { constructor() { this.printer = new ConsolePrinter({ + start: '🍀 제 %{1}회 로또 복권 추첨 🍀', paidCount: '%{1}개를 구매했습니다.', lottoTicket: '%{1} | %{2} | %{3} | %{4} | %{5} | %{6}', line: '---------------------------------------------', + line2: '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', ranking: '%{1}등 : %{2}개 일치 (%{3}원) - %{4}개', rankingWithBonus: '%{1}등 : %{2}개 일치, 보너스볼 일치 (%{3}원) - %{4}개', profitRatio: '총 수익률은 %{1}% 입니다.', @@ -38,6 +40,13 @@ export default class LottoSystemView { return answer.toUpperCase() === 'S'; } + displayLottoStart(gameCount) { + this.printer.lineBreak(); + this.printer.printWithTemplate('line2'); + this.printer.printWithTemplate('start', [gameCount]); + this.printer.printWithTemplate('line2'); + } + displayPaidCount({ ticketCount }) { this.printer.lineBreak(); this.printer.printWithTemplate('paidCount', [ticketCount]); From afee3850f3786c9ade7317cefeca5ccd17678433 Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Mon, 15 Jul 2024 23:21:59 +0900 Subject: [PATCH 21/22] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84=20=EA=B7=B8?= =?UTF-8?q?=EB=A7=8C=EB=91=90=EA=B8=B0=20=EC=9E=85=EB=A0=A5=20=ED=82=A4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/domain/LottoSystemView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/LottoSystemView.js b/src/domain/LottoSystemView.js index 0bc015e..12ba096 100644 --- a/src/domain/LottoSystemView.js +++ b/src/domain/LottoSystemView.js @@ -36,7 +36,7 @@ export default class LottoSystemView { async readKeepGoing({ leftPaidAmount }) { this.printer.printWithTemplate('replay', [formatCurrency(leftPaidAmount)]); - const answer = await this.reader.read('다시하기 = S / 그민두기 = Q : '); + const answer = await this.reader.read('다시하기 = S / 그민두기 = 아무키 : '); return answer.toUpperCase() === 'S'; } From e74656c32782c3e29a40cf5b8d970299184bdf7a Mon Sep 17 00:00:00 2001 From: Sejin Cha Date: Mon, 15 Jul 2024 23:37:28 +0900 Subject: [PATCH 22/22] =?UTF-8?q?fix:=20config=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A4=84=EB=B0=94=EA=BF=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vitest.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vitest.config.js b/vitest.config.js index b758f89..7382f40 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,7 +1,7 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, }, -}) \ No newline at end of file +});