diff --git a/__tests__/domain/lotto-operator.test.js b/__tests__/domain/lotto-operator.test.js new file mode 100644 index 000000000..99304d1f8 --- /dev/null +++ b/__tests__/domain/lotto-operator.test.js @@ -0,0 +1,66 @@ +import { Lotto } from '../../src/domain/lotto'; +import { LottoOperator } from '../../src/domain/lotto-operator'; +import { LottoWinnerNumber } from '../../src/domain/lotto-winner-number'; +import { LottoWinningStat } from '../../src/domain/lotto-winning-stat'; + +describe('로또 사업자', () => { + + test('요구된 종류와 갯수의 로또를 발행한다.', () => { + // given + const lottoOperator = new LottoOperator(); + + // when + const requestedCount = 2; + const lottos = lottoOperator.publish(requestedCount); + + // then + expect(lottos.length).toEqual(requestedCount); + lottos.forEach(l => expect(l instanceof Lotto).toBeTruthy()); + }) + + // TODO : mocking 방법 알아낸 후 구현 + + // test('로또 당첨 번호를 뽑는다.', async () => { + // // given + // const lottoOperator = new LottoOperator(); + // const rl = readline.createInterface({ + // input: process.stdin, + // output: process.stdout, + // }) + // mock() + + // // when + // const lottoWinnerNumber = await lottoOperator.DrawWinningNumber(rl); + + // // then + // expect(lottoWinnerNumber instanceof LottoWinnerNumber).toBeTruthy(); + // }) + + test('당첨 통계를 계산한다.', () => { + // given + const lottoOperator = new LottoOperator(); + const lottos = [ + new Lotto([8, 21, 23, 41, 42, 43]), + new Lotto([3, 5, 11, 16, 32, 38]), + new Lotto([7, 11, 16, 35, 36, 44]), + new Lotto([1, 8, 11, 31, 41, 42]), + new Lotto([13, 14, 16, 38, 42, 45]), + new Lotto([7, 11, 30, 40, 42, 43]), + new Lotto([2, 13, 22, 32, 38, 45]), + new Lotto([1, 3, 5, 14, 22, 45]) + ] + const lottoWinnerNumber = new LottoWinnerNumber([1, 2, 3, 4, 5, 6], 7); + + // when + + const lottoWinningStat = lottoOperator.calculateLottoWinningStat(lottos, lottoWinnerNumber); + + // then + expect(lottoWinningStat instanceof LottoWinningStat).toBeTruthy(); + expect(lottoWinningStat.matchedCount(3)).toEqual(1); + expect(lottoWinningStat.matchedCount(4)).toEqual(0); + expect(lottoWinningStat.matchedCount(5)).toEqual(0); + expect(lottoWinningStat.matchedFiveAndBonusCount).toEqual(0); + expect(lottoWinningStat.matchedCount(6)).toEqual(0); + }) +}) \ No newline at end of file diff --git a/__tests__/domain/lotto-seller.test.js b/__tests__/domain/lotto-seller.test.js new file mode 100644 index 000000000..79dc7ca3f --- /dev/null +++ b/__tests__/domain/lotto-seller.test.js @@ -0,0 +1,19 @@ +import { LottoOperator } from '../../src/domain/lotto-operator'; +import { LottoSeller } from '../../src/domain/lotto-seller' + +describe('로또 판매자는', () => { + describe('판매 시', () => { + test('받은 금액으로 최대한 살 수 있는 로또들을 반환한다.', () => { + // given + const lottoOperator = new LottoOperator(); + const lottoSeller = new LottoSeller(lottoOperator); + + // when + const money = 3000; + const lottos = lottoSeller.sell(money); + + // then + expect(lottos.length).toEqual(3); + }) + }) +}) diff --git a/__tests__/domain/lotto-winning-stat.test.js b/__tests__/domain/lotto-winning-stat.test.js new file mode 100644 index 000000000..1f0629b29 --- /dev/null +++ b/__tests__/domain/lotto-winning-stat.test.js @@ -0,0 +1,34 @@ +import { Lotto } from '../../src/domain/lotto'; +import { LottoOperator } from '../../src/domain/lotto-operator'; +import { LottoWinnerNumber } from '../../src/domain/lotto-winner-number'; + +describe('로또 당첨 통계', () => { + test.each([ + [3, [new Lotto([1, 2, 3, 4, 5, 6])], new LottoWinnerNumber([1, 2, 3, 11, 12, 13], 14)], + [4, [new Lotto([1, 2, 3, 4, 5, 6])], new LottoWinnerNumber([1, 2, 3, 4, 12, 13], 14)], + [5, [new Lotto([1, 2, 3, 4, 5, 6])], new LottoWinnerNumber([1, 2, 3, 4, 5, 13], 14)], + [6, [new Lotto([1, 2, 3, 4, 5, 6])], new LottoWinnerNumber([1, 2, 3, 4, 5, 6], 14)] + ])("%s개 일치를 계산할 수 있다", (matchedCountTarget, lottos, lottoWinnerNumber) => { + // given + const lottoOperator = new LottoOperator(); + + // when + const lottoWinningStat = lottoOperator.calculateLottoWinningStat(lottos, lottoWinnerNumber); + + // then + expect(lottoWinningStat.MatchedCount(matchedCountTarget)).toEqual(1); + }) + + test("5개 일치와 보너스 번호 일치를 계산할 수 있다", () => { + // given + const lottoOperator = new LottoOperator(); + const lottos = [new Lotto([1, 2, 3, 4, 5, 6])]; + const lottoWinnerNumber = new LottoWinnerNumber([1, 2, 3, 4, 5, 11], 6) + + // when + const lottoWinningStat = lottoOperator.calculateLottoWinningStat(lottos, lottoWinnerNumber); + + // then + expect(lottoWinningStat.matchedFiveAndBonusCount).toEqual(1); + }) +}) \ No newline at end of file diff --git a/__tests__/domain/lotto.test.js b/__tests__/domain/lotto.test.js new file mode 100644 index 000000000..94304e90c --- /dev/null +++ b/__tests__/domain/lotto.test.js @@ -0,0 +1,10 @@ +import { Lotto } from '../../src/domain/lotto' + +describe('로또', () => { + describe('번호는', () => { + test('1부터 99까지다.', () => { + // when, then + expect(() => new Lotto([1, 10, 20, 30, 40, 99])).not.toThrow(); + }) + }) +}) \ No newline at end of file diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md new file mode 100644 index 000000000..07961f564 --- /dev/null +++ b/docs/REQUIREMENTS.md @@ -0,0 +1,79 @@ +# 기능적 요구 사항 +## Original version +로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다. +로또 1장의 가격은 1,000원이다. +당첨 번호와 보너스 번호를 입력받는다. +사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력한다. +당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다. +1등: 6개 번호 일치 / 2,000,000,000원 +2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 +3등: 5개 번호 일치 / 1,500,000원 +4등: 4개 번호 일치 / 50,000원 +5등: 3개 번호 일치 / 5,000원 + +## 재해석 version +- 로또 객체가 있다. + - 변수 : 6자리 번호 + - 번호의 범위는 1부터 99까지다. +- 로또 당첨 번호 객체 + - 변수 : 6자리 당첨 번호, 보너스 번호 +- 로또 판매점 객체가 있다. + - 메소드 + - sell() : 로또를 판매한다. + - 1장에 1000원에 판매한다. + - 로또 구입 가격을 입력하면 로또 사업자에 연락하여 금액에 해당하는 만큼 로또를 받아 사용자에게 돌려준다. + - 가격이 맞아 떨어지는 경우 + - 가격이 맞아 떨어지지 않는 경우 + - 가격이 부족한 경우 +- 로또 사업자 객체가 있다. + - 메소드 + - publish() : 로또를 발급한다. + - createWinningNumber() : 로또 당첨 번호 객체를 발급한다. + - calculateLottoWinningStat() : 고객의 로또가 얼마나 맞는지 통계 정보를 발급한다. + +## 유스케이스 +1. 구입 금액 입력 받기 +2. 로또 판매점 객체는 금액에 맞게 로또를 발행함. + - 로또 판매점은 받은 돈을 로또 사업자에게 넘겨 금액에 해당하는 만큼 로또를 받는다. +3. 발행한 로또를 출력함. +4. 당첨 번호와 보너스 번호를 입력 받음. + 1. 로또 사업자 객체가 입력 받은 번호를 이용하여 당첨 번호 객체를 만듬. +5. 로또 사업자에게 당첨 번호와 당첨 번호를 제출하여 로또 통계를 획득함. + 1. 구매자는 자신의 로또를 로또 사업자에게 제출함. + 2. 로또 사업자는 당첨 통계를 계산하여 통계 내역 객체를 만듬. + 3. 당첨 통계를 출력함. + +## 주의할 것 +모듈화와 객체 간에 로직을 재사용하는 방법에 대해 고민한다. + +로또 번호와 당첨 로또 번호의 유효성 검사시 발생하는 중복 코드를 제거해야 한다. +클래스(또는 객체)를 사용하는 경우, 프로퍼티를 외부에서 직접 꺼내지 않는다. 객체에 메시지를 보내도록 한다. +getter를 금지하는 것이 아니라 말 그대로 프로퍼티 자체를 그대로 꺼내서 객체 바깥에서 직접 조작하는 등의 작업을 지양하자는 의미입니다 :) 객체 내부에서 알아서 할 수 있는 일은 객체가 스스로 할 수 있게 맡겨주세요. +클래스를 사용하는 경우, 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다. + +## 예시 +``` +> 구입금액을 입력해 주세요.8000 +8개를 구매했습니다. +[8, 21, 23, 41, 42, 43] +[3, 5, 11, 16, 32, 38] +[7, 11, 16, 35, 36, 44] +[1, 8, 11, 31, 41, 42] +[13, 14, 16, 38, 42, 45] +[7, 11, 30, 40, 42, 43] +[2, 13, 22, 32, 38, 45] +[1, 3, 5, 14, 22, 45] + +> 당첨 번호를 입력해 주세요. 1,2,3,4,5,6 + +> 보너스 번호를 입력해 주세요. 7 + +당첨 통계 +-------------------- +3개 일치 (5,000원) - 1개 +4개 일치 (50,000원) - 0개 +5개 일치 (1,500,000원) - 0개 +5개 일치, 보너스 볼 일치 (30,000,000원) - 0개 +6개 일치 (2,000,000,000원) - 0개 +총 수익률은 62.5%입니다. +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ba0406849..6cec68a11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "javascript-lotto", "version": "0.0.0", + "dependencies": { + "readline-sync": "^1.4.10" + }, "devDependencies": { "@babel/cli": "^7.26.4", "@babel/core": "^7.26.7", @@ -14,6 +17,7 @@ "@types/jest": "^29.5.14", "babel-jest": "^29.7.0", "jest": "^29.6.0", + "ts-mockito": "^2.6.1", "vite": "^6.0.5" }, "engines": { @@ -5116,6 +5120,13 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -5580,6 +5591,15 @@ "node": ">=8.10.0" } }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -6015,6 +6035,16 @@ "node": ">=8.0" } }, + "node_modules/ts-mockito": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", + "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.5" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", diff --git a/package.json b/package.json index 281e4bfc6..411be6567 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/jest": "^29.5.14", "babel-jest": "^29.7.0", "jest": "^29.6.0", + "ts-mockito": "^2.6.1", "vite": "^6.0.5" }, "jest": { @@ -32,5 +33,8 @@ "engines": { "npm": ">=10.8.2", "node": ">=20.17.0" + }, + "dependencies": { + "readline-sync": "^1.4.10" } } diff --git a/src/domain/lotto-operator.js b/src/domain/lotto-operator.js new file mode 100644 index 000000000..7364c4b48 --- /dev/null +++ b/src/domain/lotto-operator.js @@ -0,0 +1,69 @@ +import { Lotto } from './lotto.js'; +import { LottoWinnerNumber } from './lotto-winner-number.js'; +import { LottoWinningStat } from './lotto-winning-stat.js'; + +// 로또 사업자 +export class LottoOperator { + + publish(count) { + const results = Array(); + for (let i = 0; i < count; i++) { + const lotto = this.#createLotto(); + + results.push(lotto); + } + return results; + } + + async createWinningNumber(winnerNumber, bonusNumber) { + return new LottoWinnerNumber(winnerNumber, bonusNumber); + } + + calculateLottoWinningStat(lottos, lottoWinnerNumber) { + const lottoWinningStat = new LottoWinningStat(); + for (const lotto of lottos) { + const matchedCount = this.#caculateMatchedCount(lotto.expectedNumbers, lottoWinnerNumber.numbers); + const bonusNumberMatched = lotto.expectedNumbers.includes(lottoWinnerNumber.bonusNumber) + lottoWinningStat.add(matchedCount, bonusNumberMatched); + } + + return lottoWinningStat; + } + + #createLotto() { + const min = 1; + const max = 99; + const numbers = this.#generateRandomLottoNumber(6, min, max); + return new Lotto(numbers); + } + + #generateRandomLottoNumber(count, min, max) { + const numbers = Array(count); + for (let i = 0; i < count; i++) { + const number = Math.ceil(Math.random() * (max - min)) + min; + numbers.push(number); + } + + return numbers; + } + + #caculateMatchedCount(expectedNumbers, winningNumbers) { + let result = 0; + let eIdx = 0; + let wIdx = 0; + while (eIdx < expectedNumbers.length && wIdx < winningNumbers.length) { + const expectedNumber = expectedNumbers[eIdx]; + const winningNumber = winningNumbers[wIdx]; + if (expectedNumber === winningNumber) { + result++; + eIdx++; + wIdx++; + } else if (expectedNumber > winningNumbers) { + wIdx++; + } else { + eIdx++; + } + } + return result; + } +} diff --git a/src/domain/lotto-seller.js b/src/domain/lotto-seller.js new file mode 100644 index 000000000..f3f7f1623 --- /dev/null +++ b/src/domain/lotto-seller.js @@ -0,0 +1,12 @@ +export class LottoSeller { + + constructor(lottoOperator) { + this.lottoOperator = lottoOperator; + } + + sell(money) { + const lottoPrice = 1000; + const toBuyCount = money / lottoPrice; + return this.lottoOperator.publish(toBuyCount); + } +} diff --git a/src/domain/lotto-winner-number.js b/src/domain/lotto-winner-number.js new file mode 100644 index 000000000..30c8d730c --- /dev/null +++ b/src/domain/lotto-winner-number.js @@ -0,0 +1,19 @@ +export class LottoWinnerNumber { + + #numbers; + #bonusNumber; + + constructor(numbers, bonusNumber) { + numbers.sort(); + this.#numbers = numbers; + this.#bonusNumber = bonusNumber; + } + + get numbers() { + return this.#numbers; + } + + get bonusNumber() { + return this.#bonusNumber; + } +} diff --git a/src/domain/lotto-winning-stat.js b/src/domain/lotto-winning-stat.js new file mode 100644 index 000000000..0be6165f2 --- /dev/null +++ b/src/domain/lotto-winning-stat.js @@ -0,0 +1,73 @@ +export class LottoWinningStat { + + #matchedCountInfo = new Map(); + #matchedFiveAndBonusCount = 0; + + add(matchedCount, bonusNumberMatched) { + if (matchedCount === 5 && bonusNumberMatched) { + this.#matchedFiveAndBonusCount += 1; + return; + } + + const matchedCountValue = this.#matchedCountInfo.get(matchedCount) || 0; + this.#matchedCountInfo.set(matchedCount, matchedCountValue + 1); + } + + matchedCount(count) { + return this.#matchedCountInfo.get(count) || 0; + } + + get matchedFiveAndBonusCount() { + return this.#matchedFiveAndBonusCount; + } + + print() { + console.log('당첨 통계') + console.log("--------------------") + this.printMatchedInfo(3); + this.printMatchedInfo(4); + this.printMatchedInfo(5); + this.printBonusMatchedInfo() + this.printMatchedInfo(6); + } + + printMatchedInfo(matchedCountTarget) { + const matchesCount = this.MatchedCount(matchedCountTarget) + const reward = this.getReward(matchedCountTarget, false) + console.log(`${matchedCountTarget}개 일치 (${reward}원) - ${matchesCount}개`) + } + + printBonusMatchedInfo() { + const reward = this.getReward(5, true); + console.log(`$5개 일치, 보너스 볼 일치 (${reward}원) - ${this.#matchedFiveAndBonusCount}개`) + } + + getReward(matchesCount, bonusNumberMatched) { + + if (matchesCount === 5 && bonusNumberMatched) { + return 30000000; + } + + let result; + + switch (matchesCount) { + case 3: + result = 5000; + break; + case 4: + result = 50000; + break; + case 5: + result = 1500000; + break; + case 6: + result = 2000000000; + break; + default: + result = 0; + break; + } + + return result; + } +} \ No newline at end of file diff --git a/src/domain/lotto.js b/src/domain/lotto.js new file mode 100644 index 000000000..e36d68b05 --- /dev/null +++ b/src/domain/lotto.js @@ -0,0 +1,19 @@ +export class Lotto { + #expectedNumbers; + + constructor(expectedNumbers) { + // todo : 번호의 범위는 1부터 99까지 + // todo : 6개의 숫자인지 확인 + // -> LottoValidator 필요. + expectedNumbers.sort(); + this.#expectedNumbers = expectedNumbers; + } + + get expectedNumbers() { + return this.#expectedNumbers; + } + + print() { + console.log(this.expectedNumbers) + } +} \ No newline at end of file diff --git a/src/step1-index.js b/src/step1-index.js index 44313b450..d5ecf1a57 100644 --- a/src/step1-index.js +++ b/src/step1-index.js @@ -1,4 +1,30 @@ +import { LottoOperator } from './domain/lotto-operator.js'; +import { LottoSeller } from './domain/lotto-seller.js' + +import readline from "node:readline/promises"; + /** * step 1의 시작점이 되는 파일입니다. * 브라우저 환경에서 사용하는 css 파일 등을 불러올 경우 정상적으로 빌드할 수 없습니다. */ + +const lottoOperator = new LottoOperator(); +const lottoSeller = new LottoSeller(lottoOperator); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}) + +let moneyReceived = Number(await rl.question('> 구입금액을 입력해 주세요.')); + +const lottos = lottoSeller.Sell(moneyReceived); +console.log(`${lottos.length}개를 구매했습니다.`) + +lottos.forEach((lotto) => lotto.Print()); +console.log() + +const lottoWinnerNumber = await lottoOperator.DrawWinningNumbers(rl); + +const lottoWinningStat = lottoOperator.CalculateLottoWinningStat(lottos, lottoWinnerNumber); +lottoWinningStat.Print(); \ No newline at end of file