From 8c1b35776de2536587e6e0e4823c1ec2d32d52f0 Mon Sep 17 00:00:00 2001 From: HongGunWoo Date: Tue, 9 Jul 2024 01:12:38 +0900 Subject: [PATCH 1/8] =?UTF-8?q?chore:=20project=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + package-lock.json | 30 ++++++++++++++++++++++++++++++ package.json | 10 +++++++--- src/__tests__/sum.test.js | 11 ----------- src/{main.js => main.ts} | 0 tsconfig.json | 11 +++++++++++ vitest.config.ts | 7 +++++++ 7 files changed, 56 insertions(+), 14 deletions(-) delete mode 100644 src/__tests__/sum.test.js rename src/{main.js => main.ts} (100%) create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 84fae54..0268847 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea .vscode node_modules +dist \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2e1d661..bc2e460 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { + "@types/node": "^20.14.10", + "typescript": "^5.5.3", "vitest": "^1.6.0" } }, @@ -618,6 +620,15 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@vitest/expect": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", @@ -1353,12 +1364,31 @@ "node": ">=4" } }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/ufo": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", "dev": true }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/vite": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", diff --git a/package.json b/package.json index 1fe342c..a2eeaed 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,20 @@ "version": "1.0.0", "description": "로또 미션을 통해서 학습하는 클린코드", "main": "./src/main.js", - "type": "module", + "type": "module", "scripts": { - "start": "node src/main.js", - "start:watch": "node --watch src/main.js", + "start": "node dist/main.js", + "start:watch": "node --watch dist/main.js", + "build": "tsc", + "clean": "tsc --build --clean", "test": "vitest" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { + "@types/node": "^20.14.10", + "typescript": "^5.5.3", "vitest": "^1.6.0" } } 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/main.js b/src/main.ts similarity index 100% rename from src/main.js rename to src/main.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c766f66 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "strict": true, + "outDir": "dist", + "types": ["vitest/globals"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..7382f40 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + }, +}); From a4087195908780b8e1872c6cc81522a746b5016a Mon Sep 17 00:00:00 2001 From: HongGunWoo Date: Tue, 9 Jul 2024 01:16:09 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/Lotto.test.ts | 32 ++++++++++++++++++++++++++++++++ src/model/Lotto.ts | 20 ++++++++++++++++++++ src/model/WinningLotto.ts | 9 +++++++++ 3 files changed, 61 insertions(+) create mode 100644 src/__tests__/Lotto.test.ts create mode 100644 src/model/Lotto.ts create mode 100644 src/model/WinningLotto.ts diff --git a/src/__tests__/Lotto.test.ts b/src/__tests__/Lotto.test.ts new file mode 100644 index 0000000..25990e7 --- /dev/null +++ b/src/__tests__/Lotto.test.ts @@ -0,0 +1,32 @@ +import { Lotto } from '../model/Lotto.js'; + +describe('Lotto 클래스', () => { + describe('matchNumbers 메서드', () => { + const lotto = new Lotto([1, 2, 3, 4, 5, 6]); + const winningNumbers = [2, 3, 4, 5, 6, 7]; + + it('맞춘 숫자 개수와 보너스 번호 포함 여부를 정확하게 반환해야 한다', () => { + const bonusNumber = 8; + + const { matchedCount, includedBonusNumber } = lotto.matchNumbers( + winningNumbers, + bonusNumber + ); + + expect(matchedCount).toBe(5); + expect(includedBonusNumber).toBe(false); + }); + + it('보너스 번호가 포함된 경우 맞춘 숫자 개수와 보너스 번호 포함 여부를 정확하게 반환해야 한다', () => { + const bonusNumber = 6; + + const { matchedCount, includedBonusNumber } = lotto.matchNumbers( + winningNumbers, + bonusNumber + ); + + expect(matchedCount).toBe(5); + expect(includedBonusNumber).toBe(true); + }); + }); +}); diff --git a/src/model/Lotto.ts b/src/model/Lotto.ts new file mode 100644 index 0000000..190f3a0 --- /dev/null +++ b/src/model/Lotto.ts @@ -0,0 +1,20 @@ +export class Lotto { + public readonly numbers: number[] = []; + + constructor(numbers: number[]) { + this.numbers = numbers; + } + + public matchNumbers(winningNumbers: number[], bonusNumber: number) { + const matchedCount = this.numbers.filter((number) => + winningNumbers.includes(number) + ).length; + + const includedBonusNumber = this.numbers.includes(bonusNumber); + + return { + matchedCount, + includedBonusNumber, + }; + } +} diff --git a/src/model/WinningLotto.ts b/src/model/WinningLotto.ts new file mode 100644 index 0000000..c2ba692 --- /dev/null +++ b/src/model/WinningLotto.ts @@ -0,0 +1,9 @@ +export class WinningLotto { + public readonly numbers: number[] = []; + public readonly bonusNumber: number; + + constructor(numbers: number[], bonusNumber: number) { + this.numbers = numbers; + this.bonusNumber = bonusNumber; + } +} From e93251a530151f06ca35967e96fa87fde5ec2faf Mon Sep 17 00:00:00 2001 From: HongGunWoo Date: Tue, 9 Jul 2024 01:16:25 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20util=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/getRandomNumber.test.ts | 29 ++++++++++++++++++++++ src/__tests__/hasDuplicatesInArray.test.ts | 13 ++++++++++ src/__tests__/isArrayLengthValid.test.ts | 15 +++++++++++ src/__tests__/isNumberInRange.test.ts | 25 +++++++++++++++++++ src/util/getRandomNumber.ts | 7 ++++++ src/util/hasDuplicatesInArray.ts | 3 +++ src/util/isArrayLengthValid.ts | 3 +++ src/util/isNumberInRange.ts | 15 +++++++++++ 8 files changed, 110 insertions(+) create mode 100644 src/__tests__/getRandomNumber.test.ts create mode 100644 src/__tests__/hasDuplicatesInArray.test.ts create mode 100644 src/__tests__/isArrayLengthValid.test.ts create mode 100644 src/__tests__/isNumberInRange.test.ts create mode 100644 src/util/getRandomNumber.ts create mode 100644 src/util/hasDuplicatesInArray.ts create mode 100644 src/util/isArrayLengthValid.ts create mode 100644 src/util/isNumberInRange.ts diff --git a/src/__tests__/getRandomNumber.test.ts b/src/__tests__/getRandomNumber.test.ts new file mode 100644 index 0000000..64057c0 --- /dev/null +++ b/src/__tests__/getRandomNumber.test.ts @@ -0,0 +1,29 @@ +import { getRandomNumber } from '../util/getRandomNumber.js'; + +describe('getRandomNumber', () => { + it('지정된 최소값과 최대값 사이의 랜덤 숫자를 반환한다.', () => { + const min = 1; + const max = 10; + const randomNumber = getRandomNumber(min, max); + + expect(randomNumber).toBeGreaterThanOrEqual(min); + expect(randomNumber).toBeLessThanOrEqual(max); + }); + + it('최소값과 최대값이 동일할 때 최소값을 반환한다.', () => { + const min = 5; + const max = 5; + const randomNumber = getRandomNumber(min, max); + + expect(randomNumber).toBe(min); + }); + + it('최소값이 최대값보다 클 때 값을 교환하여 랜덤 값을 반환한다.', () => { + const min = 4; + const max = 1; + const randomNumber = getRandomNumber(min, max); + + expect(randomNumber).toBeGreaterThanOrEqual(max); + expect(randomNumber).toBeLessThanOrEqual(min); + }); +}); diff --git a/src/__tests__/hasDuplicatesInArray.test.ts b/src/__tests__/hasDuplicatesInArray.test.ts new file mode 100644 index 0000000..2afac1d --- /dev/null +++ b/src/__tests__/hasDuplicatesInArray.test.ts @@ -0,0 +1,13 @@ +import { hasDuplicatesInArray } from '../util/hasDuplicatesInArray.js'; + +describe('hasDuplicatesInArray', () => { + it('중복된 값이 있는 경우 true를 반환한다.', () => { + const arrayWithDuplicates = [1, 2, 3, 4, 2]; + expect(hasDuplicatesInArray(arrayWithDuplicates)).toBe(true); + }); + + it('중복된 값이 없을 경우 false를 반환한다.', () => { + const arrayWithNoDuplicates = [1, 2, 3, 4, 5]; + expect(hasDuplicatesInArray(arrayWithNoDuplicates)).toBe(false); + }); +}); diff --git a/src/__tests__/isArrayLengthValid.test.ts b/src/__tests__/isArrayLengthValid.test.ts new file mode 100644 index 0000000..f7c9f9b --- /dev/null +++ b/src/__tests__/isArrayLengthValid.test.ts @@ -0,0 +1,15 @@ +import { isArrayLengthValid } from '../util/isArrayLengthValid.js'; + +describe('isArrayLengthValid', () => { + it('배열의 길이가 주어진 길이와 일치하면 true를 반환한다.', () => { + const array = [1, 2, 3]; + const length = 3; + expect(isArrayLengthValid(array, length)).toBe(true); + }); + + it('배열의 길이가 주어진 길이와 일치하지 않으면 false를 반환한다.', () => { + const array = [1, 2, 3, 4]; + const length = 3; + expect(isArrayLengthValid(array, length)).toBe(false); + }); +}); diff --git a/src/__tests__/isNumberInRange.test.ts b/src/__tests__/isNumberInRange.test.ts new file mode 100644 index 0000000..7065bf9 --- /dev/null +++ b/src/__tests__/isNumberInRange.test.ts @@ -0,0 +1,25 @@ +import { isNumberInRange } from '../util/isNumberInRange.js'; + +describe('isNumberInRange', () => { + it('숫자가 최소 범위 미만인 경우 false를 반환한다.', () => { + expect(isNumberInRange({ number: 0, minRange: 1, maxRange: 10 })).toBe( + false + ); + }); + + it('숫자가 최대 범위 초과인 경우 false를 반환한다.', () => { + expect(isNumberInRange({ number: 11, minRange: 1, maxRange: 10 })).toBe( + false + ); + }); + + it('숫자가 유효한 범위 내에 있는 경우 true를 반환한다.', () => { + expect(isNumberInRange({ number: 5, minRange: 1, maxRange: 10 })).toBe( + true + ); + }); + + it('최대 범위가 지정되지 않은 경우에도 유효한 범위 내에 있는 경우 true를 반환한다.', () => { + expect(isNumberInRange({ number: 5, minRange: 1 })).toBe(true); + }); +}); diff --git a/src/util/getRandomNumber.ts b/src/util/getRandomNumber.ts new file mode 100644 index 0000000..6efa5fb --- /dev/null +++ b/src/util/getRandomNumber.ts @@ -0,0 +1,7 @@ +export function getRandomNumber(min: number, max: number): number { + if (min > max) { + [min, max] = [max, min]; + } + + return Math.floor(Math.random() * (max - min + 1) + min); +} diff --git a/src/util/hasDuplicatesInArray.ts b/src/util/hasDuplicatesInArray.ts new file mode 100644 index 0000000..5877f32 --- /dev/null +++ b/src/util/hasDuplicatesInArray.ts @@ -0,0 +1,3 @@ +export function hasDuplicatesInArray(array: T[]): boolean { + return new Set(array).size !== array.length; +} diff --git a/src/util/isArrayLengthValid.ts b/src/util/isArrayLengthValid.ts new file mode 100644 index 0000000..658c532 --- /dev/null +++ b/src/util/isArrayLengthValid.ts @@ -0,0 +1,3 @@ +export function isArrayLengthValid(array: T[], length: number): boolean { + return array.length === length; +} diff --git a/src/util/isNumberInRange.ts b/src/util/isNumberInRange.ts new file mode 100644 index 0000000..069a395 --- /dev/null +++ b/src/util/isNumberInRange.ts @@ -0,0 +1,15 @@ +export function isNumberInRange({ + number, + minRange, + maxRange, +}: { + number: number; + minRange: number; + maxRange?: number; +}): boolean { + if (number < minRange) return false; + + if (maxRange && number > maxRange) return false; + + return true; +} From 0e79c4c94492ea6a259a94b55426236dbb72240c Mon Sep 17 00:00:00 2001 From: HongGunWoo Date: Tue, 9 Jul 2024 01:17:19 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20lotto=20rule=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/LottoRule.test.ts | 11 +++ src/constants/lottoType.ts | 125 ++++++++++++++++++++++++++++++++ src/model/LottoRule.ts | 19 +++++ 3 files changed, 155 insertions(+) create mode 100644 src/__tests__/LottoRule.test.ts create mode 100644 src/constants/lottoType.ts create mode 100644 src/model/LottoRule.ts diff --git a/src/__tests__/LottoRule.test.ts b/src/__tests__/LottoRule.test.ts new file mode 100644 index 0000000..ad50f0b --- /dev/null +++ b/src/__tests__/LottoRule.test.ts @@ -0,0 +1,11 @@ +import { LottoRule } from '../model/LottoRule.js'; + +describe('LottoRule', () => { + it('가격이 0보다 작으면 에러가 발생한다.', () => { + expect(() => new LottoRule(-1000, '1')).toThrowError(); + }); + + it('로또 타입이 올바르지 않으면 에러가 발생한다.', () => { + expect(() => new LottoRule(1000, 'INVALID_TYPE')).toThrowError(); + }); +}); diff --git a/src/constants/lottoType.ts b/src/constants/lottoType.ts new file mode 100644 index 0000000..43ead1e --- /dev/null +++ b/src/constants/lottoType.ts @@ -0,0 +1,125 @@ +export interface LottoType { + selectedCount: number; + minNumber: number; + maxNumber: number; + winningInfo: { + matchCount: number; + reward: number; + includeBonus?: boolean; + }[]; +} + +export const LOTTO_TYPE: { [key: string]: LottoType } = { + '1': { + selectedCount: 6, + minNumber: 1, + maxNumber: 45, + winningInfo: [ + { + matchCount: 3, + reward: 5000, + }, + { + matchCount: 4, + reward: 50000, + }, + { + matchCount: 5, + reward: 1500000, + }, + { + matchCount: 5, + includeBonus: true, + reward: 30000000, + }, + { + matchCount: 6, + reward: 2000000000, + }, + ], + }, + '2': { + selectedCount: 6, + minNumber: 1, + maxNumber: 54, + winningInfo: [ + { + matchCount: 4, + reward: 5000, + }, + { + matchCount: 5, + reward: 50000, + }, + { + matchCount: 5, + reward: 1500000, + }, + { + matchCount: 5, + includeBonus: true, + reward: 30000000, + }, + { + matchCount: 6, + reward: 2000000000, + }, + ], + }, + '3': { + selectedCount: 4, + minNumber: 1, + maxNumber: 40, + winningInfo: [ + { + matchCount: 4, + reward: 5000, + }, + { + matchCount: 5, + reward: 50000, + }, + { + matchCount: 5, + reward: 1500000, + }, + { + matchCount: 5, + includeBonus: true, + reward: 30000000, + }, + { + matchCount: 6, + reward: 2000000000, + }, + ], + }, + '4': { + selectedCount: 7, + minNumber: 1, + maxNumber: 69, + winningInfo: [ + { + matchCount: 4, + reward: 5000, + }, + { + matchCount: 5, + reward: 50000, + }, + { + matchCount: 5, + reward: 1500000, + }, + { + matchCount: 5, + includeBonus: true, + reward: 30000000, + }, + { + matchCount: 6, + reward: 2000000000, + }, + ], + }, +}; diff --git a/src/model/LottoRule.ts b/src/model/LottoRule.ts new file mode 100644 index 0000000..2c0097d --- /dev/null +++ b/src/model/LottoRule.ts @@ -0,0 +1,19 @@ +import { LOTTO_TYPE, LottoType } from '../constants/lottoType.js'; +import { isNumberInRange } from '../util/isNumberInRange.js'; + +export class LottoRule { + public readonly price: number; + public readonly lottoType: LottoType; + + constructor(price: number, type: string) { + if (!isNumberInRange({ number: price, minRange: 0 })) { + throw new Error('로또 가격은 0보다 커야 합니다.'); + } + if (!LOTTO_TYPE[type]) { + throw new Error('로또 타입이 올바르지 않습니다.'); + } + + this.price = price; + this.lottoType = LOTTO_TYPE[type]; + } +} From e2ecdce2577b35d7276481a2942cb7615a36abb4 Mon Sep 17 00:00:00 2001 From: HongGunWoo Date: Tue, 9 Jul 2024 01:17:32 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20view=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/View/LottoView.ts | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/View/LottoView.ts diff --git a/src/View/LottoView.ts b/src/View/LottoView.ts new file mode 100644 index 0000000..082fe36 --- /dev/null +++ b/src/View/LottoView.ts @@ -0,0 +1,47 @@ +import readline from 'readline'; + +export class LottoView { + public async getMoney(): Promise { + const inputValue = await this.ask('구입 금액을 입력해 주세요. : '); + + return Number(inputValue); + } + + public async getWinningNumbers(): Promise { + const inputValue = await this.ask('당첨 번호를 입력해 주세요. : '); + + return inputValue.split(',').map((number) => Number(number)); + } + + public async getWinningBonusNumber(): Promise { + const inputValue = await this.ask('보너스 번호를 입력해 주세요. : '); + + return Number(inputValue); + } + + public ask(query: string): Promise { + return new Promise((resolve, reject) => { + if (arguments.length !== 1) { + reject(new Error('arguments must be 1')); + } + + if (typeof query !== 'string') { + reject(new Error('query must be string')); + } + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + rl.question(query, (input) => { + rl.close(); + resolve(input); + }); + }); + } + + public printMessage(message: string) { + console.log(message); + } +} From 79a59de5da89e595350c91e3dbd6590f45a1240f Mon Sep 17 00:00:00 2001 From: HongGunWoo Date: Tue, 9 Jul 2024 01:17:50 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20lotto=20game=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/LottoGame.test.ts | 36 +++++++ src/model/LottoGame.ts | 163 ++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 src/__tests__/LottoGame.test.ts create mode 100644 src/model/LottoGame.ts diff --git a/src/__tests__/LottoGame.test.ts b/src/__tests__/LottoGame.test.ts new file mode 100644 index 0000000..54e0ef9 --- /dev/null +++ b/src/__tests__/LottoGame.test.ts @@ -0,0 +1,36 @@ +import { LottoGame } from '../model/LottoGame.js'; +import { getRandomNumber } from '../util/getRandomNumber.js'; + +describe('LottoGame 클래스 테스트', () => { + const lottoGame = new LottoGame(); + + it('유효하지 않은 로또 규칙을 설정하면 오류를 반환한다.', () => { + expect(() => lottoGame.setLottoRule(1000, 'invalid')).toThrowError(); + expect(() => lottoGame.setLottoRule(-1000, '1')).toThrowError(); + }); + + it('유효하지 않은 사용자 금액을 입력하면 오류를 반환한다.', () => { + lottoGame.setLottoRule(1000, '1'); + + expect(() => lottoGame.setTickets(-1000)).toThrowError(); + expect(() => lottoGame.setTickets(500)).toThrowError(); + }); + + it('유효하지 않은 당첨 번호를 설정하면 오류를 반환한다.', () => { + lottoGame.setLottoRule(1000, '1'); + lottoGame.setTickets(1000); + + expect(() => + lottoGame.setWinningLotto([1, 2, 3, 4, 5, 5], 6) + ).toThrowError(); + expect(() => + lottoGame.setWinningLotto([1, 2, 3, 4, 5, 6], -10) + ).toThrowError(); + expect(() => + lottoGame.setWinningLotto([1, 2, 3, 4, 5, 6, 7], 10) + ).toThrowError(); + expect(() => + lottoGame.setWinningLotto([1, 2, 3, 4, 5, 6], 6) + ).toThrowError(); + }); +}); diff --git a/src/model/LottoGame.ts b/src/model/LottoGame.ts new file mode 100644 index 0000000..3e99dc8 --- /dev/null +++ b/src/model/LottoGame.ts @@ -0,0 +1,163 @@ +import { getRandomNumber } from '../util/getRandomNumber.js'; +import { hasDuplicatesInArray } from '../util/hasDuplicatesInArray.js'; +import { isArrayLengthValid } from '../util/isArrayLengthValid.js'; +import { isNumberInRange } from '../util/isNumberInRange.js'; +import { Lotto } from './Lotto.js'; +import { LottoRule } from './LottoRule.js'; +import { WinningLotto } from './WinningLotto.js'; + +export class LottoGame { + private tickets: Lotto[] = []; + private _lottoRule?: LottoRule; + private _winningLotto?: WinningLotto; + + private get lottoRule() { + if (!this._lottoRule) throw new Error('로또 규칙이 설정되지 않았습니다.'); + return this._lottoRule; + } + + private get winningLotto() { + if (!this._winningLotto) + throw new Error('당첨 번호가 설정되지 않았습니다.'); + return this._winningLotto; + } + + public getTickets(): number[][] { + return this.tickets.map((ticket) => ticket.numbers); + } + + public setLottoRule(price: number, type: string) { + this._lottoRule = new LottoRule(price, type); + } + + public setTickets(receivedMoney: number) { + if (receivedMoney < this.lottoRule.price) { + throw new Error('금액이 부족합니다.'); + } + + const ticketCount = Math.floor(receivedMoney / this.lottoRule.price); + const tickets: Lotto[] = []; + + for (let _ = 0; _ < ticketCount; _++) { + const numbers = this.generateRandomNumbers(); + tickets.push(new Lotto(numbers)); + } + + this.tickets = tickets; + } + + public setWinningLotto(numbers: number[], bonusNumber: number) { + if (!this.isValidWinningLottoNumbers(numbers, bonusNumber)) { + throw new Error('로또 번호가 유효하지 않습니다.'); + } + + this._winningLotto = new WinningLotto(numbers, bonusNumber); + } + + public getResult() { + const result = this.tickets.map((ticket) => { + const { bonusNumber, numbers } = this.winningLotto; + + return ticket.matchNumbers(numbers, bonusNumber); + }); + + const formattedResult = this.getFormattedResult(result); + const totalRewardPercent = this.getTotalRewardPercent(formattedResult); + + return { + formattedResult, + totalRewardPercent, + }; + } + + private getFormattedResult( + result: { + matchedCount: number; + includedBonusNumber: boolean; + }[] + ) { + const { winningInfo } = this.lottoRule.lottoType; + + const formattedResults = winningInfo.map((info) => { + const count = result.filter( + (ticket) => + ticket.matchedCount === info.matchCount && + (info.includeBonus + ? ticket.includedBonusNumber === info.includeBonus + : true) + ).length; + + return { + reward: info.reward, + matchCount: info.matchCount, + includeBonus: info.includeBonus, + count, + }; + }); + + return formattedResults; + } + + private getTotalRewardPercent( + formattedResult: { + reward: number; + matchCount: number; + includeBonus: boolean | undefined; + count: number; + }[] + ) { + const totalReward = formattedResult.reduce( + (acc, { reward, count }) => acc + reward * count, + 0 + ); + + const totalInvestment = this.tickets.length * this.lottoRule.price; + + return ((totalInvestment / totalReward) * 100).toFixed(1); + } + + private generateRandomNumbers(): number[] { + const numbers = new Set(); + while (numbers.size < this.lottoRule.lottoType.selectedCount) { + const minNumber = this.lottoRule.lottoType.minNumber; + const maxNumber = this.lottoRule.lottoType.maxNumber; + + numbers.add(getRandomNumber(minNumber, maxNumber)); + } + + return Array.from(numbers); + } + + private isValidWinningLottoNumbers(numbers: number[], bonusNumber: number) { + if (hasDuplicatesInArray([...numbers, bonusNumber])) return false; + const { minNumber, maxNumber } = this.lottoRule.lottoType; + + if ( + !isNumberInRange({ + number: bonusNumber, + minRange: minNumber, + maxRange: maxNumber, + }) + ) + return false; + + return this.isValidLottoNumbers(numbers); + } + + private isValidLottoNumbers(numbers: number[]): boolean { + const { minNumber, maxNumber, selectedCount } = this.lottoRule.lottoType; + + if (hasDuplicatesInArray(numbers)) return false; + + if ( + !numbers.every((number) => + isNumberInRange({ number, minRange: minNumber, maxRange: maxNumber }) + ) + ) + return false; + + if (!isArrayLengthValid(numbers, selectedCount)) return false; + + return true; + } +} From 384f27eb648943ddc7bf1dd294dc60ea13d4f94c Mon Sep 17 00:00:00 2001 From: HongGunWoo Date: Tue, 9 Jul 2024 01:18:01 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20controller=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/LottoController.ts | 65 +++++++++++++++++++++++++++++++ src/main.ts | 15 +++++-- 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/controller/LottoController.ts diff --git a/src/controller/LottoController.ts b/src/controller/LottoController.ts new file mode 100644 index 0000000..78fa2f1 --- /dev/null +++ b/src/controller/LottoController.ts @@ -0,0 +1,65 @@ +import { LottoView } from '../View/LottoView.js'; +import { LottoGame } from '../model/LottoGame.js'; + +export class LottoController { + private view: LottoView; + private lottoGame = new LottoGame(); + + constructor(view: LottoView) { + this.view = view; + } + + public setLottoRule() { + // TODO 로또 타입을 입력 받는 기능을 추가로 구현해야함. + const lottoType = '1'; + const lottoPrice = 1000; + + this.lottoGame.setLottoRule(lottoPrice, lottoType); + } + + public async purchaseTickets() { + const receivedMoney = await this.view.getMoney(); + this.lottoGame.setTickets(receivedMoney); + } + + public displayTickets() { + const tickets = this.lottoGame.getTickets(); + + this.view.printMessage(`${tickets.length}개를 구매했습니다.`); + tickets.forEach((ticket) => { + this.view.printMessage(`[${ticket.join(', ')}]`); + }); + this.view.printMessage('\n'); + } + + public async setWinningNumbers() { + const { numbers, bonusNumber } = await this.getWinningLotto(); + this.lottoGame.setWinningLotto(numbers, bonusNumber); + } + + public displayResult() { + this.view.printMessage('당첨 통계'); + this.view.printMessage('--------------------'); + + const result = this.lottoGame.getResult(); + + result.formattedResult.forEach( + ({ count, includeBonus, matchCount, reward }) => { + this.view.printMessage( + `${matchCount}개 일치${ + includeBonus ? ', 보너스 볼 일치' : '' + } (${reward}원) - ${count}개` + ); + } + ); + + this.view.printMessage(`총 수익률은 ${result.totalRewardPercent}% 입니다.`); + } + + private async getWinningLotto() { + const numbers = await this.view.getWinningNumbers(); + const bonusNumber = await this.view.getWinningBonusNumber(); + + return { numbers, bonusNumber }; + } +} diff --git a/src/main.ts b/src/main.ts index 96bab59..6cf51ec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,14 @@ -function main() { - console.log('main의 내용을 채워주세요'); +import { LottoController } from './controller/LottoController.js'; +import { LottoView } from './View/LottoView.js'; + +async function main() { + const view = new LottoView(); + const controller = new LottoController(view); + controller.setLottoRule(); + await controller.purchaseTickets(); + controller.displayTickets(); + await controller.setWinningNumbers(); + controller.displayResult(); } -main(); +await main(); From da3722cea05d71e50004bb36ac136e452af82b24 Mon Sep 17 00:00:00 2001 From: HongGunWoo Date: Tue, 9 Jul 2024 01:27:29 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=EC=A0=95=EC=88=98=EA=B0=80=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=20=EC=9E=85=EB=A0=A5=EA=B0=92=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/isValidInteger.test.ts | 15 +++++++++++++++ src/controller/LottoController.ts | 12 ++++++++++++ src/util/isValidInteger.ts | 3 +++ 3 files changed, 30 insertions(+) create mode 100644 src/__tests__/isValidInteger.test.ts create mode 100644 src/util/isValidInteger.ts diff --git a/src/__tests__/isValidInteger.test.ts b/src/__tests__/isValidInteger.test.ts new file mode 100644 index 0000000..de7e246 --- /dev/null +++ b/src/__tests__/isValidInteger.test.ts @@ -0,0 +1,15 @@ +import { isValidInteger } from '../util/isValidInteger.js'; + +describe('isValidInteger', () => { + it('정수인 경우 true 반환', () => { + expect(isValidInteger(42)).toBe(true); + expect(isValidInteger(0)).toBe(true); + expect(isValidInteger(-10)).toBe(true); + }); + + it('정수가 아닌 경우 false 반환', () => { + expect(isValidInteger(3.14)).toBe(false); + expect(isValidInteger(-4.5)).toBe(false); + expect(isValidInteger(NaN)).toBe(false); + }); +}); diff --git a/src/controller/LottoController.ts b/src/controller/LottoController.ts index 78fa2f1..e9dc535 100644 --- a/src/controller/LottoController.ts +++ b/src/controller/LottoController.ts @@ -1,5 +1,6 @@ import { LottoView } from '../View/LottoView.js'; import { LottoGame } from '../model/LottoGame.js'; +import { isValidInteger } from '../util/isValidInteger.js'; export class LottoController { private view: LottoView; @@ -19,6 +20,10 @@ export class LottoController { public async purchaseTickets() { const receivedMoney = await this.view.getMoney(); + + if (!isValidInteger(receivedMoney)) + throw new Error('금액은 정수로 입력해야 합니다.'); + this.lottoGame.setTickets(receivedMoney); } @@ -58,8 +63,15 @@ export class LottoController { private async getWinningLotto() { const numbers = await this.view.getWinningNumbers(); + + if (numbers.some((number) => !isValidInteger(number))) + throw new Error('로또 번호는 정수로 입력해야 합니다.'); + const bonusNumber = await this.view.getWinningBonusNumber(); + if (!isValidInteger(bonusNumber)) + throw new Error('보너스 번호는 정수로 입력해야 합니다.'); + return { numbers, bonusNumber }; } } diff --git a/src/util/isValidInteger.ts b/src/util/isValidInteger.ts new file mode 100644 index 0000000..40d5308 --- /dev/null +++ b/src/util/isValidInteger.ts @@ -0,0 +1,3 @@ +export function isValidInteger(number: number): boolean { + return Number.isInteger(number); +}