diff --git a/__tests__/LottoTest.js b/__tests__/LottoTest.js
index 97bd457659..ff0ddde87f 100644
--- a/__tests__/LottoTest.js
+++ b/__tests__/LottoTest.js
@@ -1,4 +1,4 @@
-import Lotto from "../src/Lotto.js";
+import Lotto from "../src/Model/Lotto.js";
describe("로또 클래스 테스트", () => {
test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => {
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000000..c0f3383886
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,161 @@
+# 게임 흐름
+1. 로또 시작
+2. 로또를 구입하기 위해 지불할 금액 입력받기
+ - 금액에 대한 유효성 검사
+ - 금액을 로또 개수로 변환 후, 출력
+4. 로또 번호 생성 로직
+ - 로또 개수만큼
+ 1) 로또 번호의 숫자를 1~45까지 무작위로 6개를 생성
+ 2) 로또 번호의 배열을 오름차순으로 정렬 후, 출력
+5. 당첨 번호 입력받기
+ - 당첨 번호에 대한 유효성 검사
+6. 보너스 당첨 번호 입력받기
+ - 보너스 번호에 대한 유효성 검사
+7. 로또 결과
+ - 주어진 로또 번호가 당첨 번호와 일치하는지 확인 후, 출력
+ - 수익률 계산 후, 출력
+
+
+
+
+
+# 구현할 기능 목록
+
+# Controller
+## LottoContoller.js
+> 사용자 입력을 받아 모델을 업데이트하고, 뷰를 통해 결과를 표시하는 함수들을 관리
+
+
+
+handlePurchase
+- [x] 로또 구매를 처리
+
+handleLottoWinningNumbers
+- [x] 당첨 번호 입력을 처리
+- [x] 보너스 당첨 번호 입력을 처리
+
+calculateProfitRates
+- [x] 수익률을 계산
+
+
+
+# Model
+## Lotto.js
+> 로또 번호를 생성하고 관리
+
+
+
+generateLottoNumbers
+- [x] 로또 번호의 숫자를 1~45까지 무작위로 6개를 생성
+
+sortLottoNumbers
+- [x] 로또 번호를 오름차순으로 정렬
+
+
+
+## LottoMachine.js
+> 로또 발행과 관련된 로직 관리
+
+
+
+calculateLottoCount
+- [x] 지불한 금액으로 구매할 수 있는 로또의 수를 계산
+
+
+
+## WinningLotto.js
+> 당첨 번호와 보너스 번호를 관리, 당첨 여부를 확인
+
+
+
+calculateNumberOfMatchingNumbers
+- [x] 로또에 있는 번호 중 당첨 번호와 일치하는 개수를 계산
+
+isBonusNumberMatched
+- [x] 보너스 번호가 로또 번호에 포함되어 있는지 확인
+
+determinePrizeCategory
+- [x] 일치하는 번호의 개수와 보너스 번호의 존재 여부에 따라 상금 등급을 결정
+
+countAndPrintResult
+- [x] 상금 등급에 따라 화면에 출력하도록 함
+
+checkWinning
+- [x] 주어진 로또 번호가 당첨 번호와 일치하는지 확인
+
+
+
+# View
+## InputView.js
+> **사용자로부터 입력을 받는 부분을 담당**
+ 금액 입력, 당첨 번호 입력
+
+
+
+promptPurchaseAmount
+- [x] 사용자에게 구입 금액을 입력받기
+- `구입금액을 입력해 주세요.`
+- 예외처리
+ - [x] 값이 비어있는지 확인
+ - [x] 숫자 형식인지 확인
+ - [x] 1,000원 단위로 입력했는지 확인
+
+promptWinnningNumbers
+- [x] 사용자에게 당첨 번호(배열)를 입력받기
+- `당첨 번호를 입력해 주세요.`
+- 예외처리
+ - [x] 값이 비어있는지 확인
+ - [x] 로또 번호 중복되어있는지 확인
+ - [x] 로또 번호 1~45 사이인지 확인
+ - [x] 6개 입력되어있는지 확인
+
+promptBonusNumber
+- [x] 사용자에게 보너스 번호(숫자)를 입력받기
+- `보너스 번호를 입력해 주세요.`
+- 예외처리
+ - [x] 값이 비어있는지 확인
+ - [x] 숫자 형식인지 확인
+ - [x] 로또 번호 1~45 사이인지 확인
+ - [x] 로또 번호와 중복되는지 확인
+
+
+
+## OutputView.js
+> **결과나 메세지를 사용자에게 표시하는 부분을 담당**
+ 로또 번호, 당첨 결과, 수익률 등을 사용자에게 표시하는 함수들을 관리
+
+
+
+printLottoCounts
+- [x] 구매한 로또 개수를 화면에 출력하기
+- `${lottoCount}개를 구매했습니다.`
+
+printLottos
+- [x] 구매한 로또 번호를 화면에 출력하기
+- `[8, 21, 23, 41, 42, 43]`
+ `[3, 5, 11, 16, 32, 38]` ...
+
+printLottoResult
+- [x] 당첨 결과를 화면에 출력하기
+- `당첨 통계`
+ `---`
+ `3개 일치 (5,000원) - 1개`
+ `4개 일치 (50,000원) - 0개`
+ `5개 일치 (1,500,000원) - 0개`
+ `5개 일치, 보너스 볼 일치 (30,000,000원) - 0개`
+ `6개 일치 (2,000,000,000원) - 0개`
+
+printProfitRates
+- [x] 수익률을 화면에 출력하기
+- `총 수익률은 ${winningRates}%입니다.`
+
+
+
+# Utils
+## Constants.js
+> Enum 관리(게임 메세지, 에러 메세지, 각 등수별 상금 정보)
+
+
+
+## InputValidator.js
+> 입력값이 유효한지 검사하는 로직을 관리
\ No newline at end of file
diff --git a/src/App.js b/src/App.js
index c38b30d5b2..8f97e044f6 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,5 +1,38 @@
+import OutputView from "./View/OutputView.js";
+import LottoController from "./Controller/LottoController.js";
+import LottoMachine from "./Model/LottoMachine.js";
+import WinningLotto from "./Model/WinningLotto.js";
+
class App {
- async play() {}
+ constructor() {
+ this.outputView = new OutputView();
+ this.lottoMachine = new LottoMachine();
+ this.winningLotto = new WinningLotto();
+ this.lottoController = new LottoController();
+ }
+
+ async play() {
+ await this.startLotto();
+ }
+
+ async startLotto() {
+ const numberOfLottos = await this.lottoController.handlePurchase();
+ this.outputView.printLottoCounts(numberOfLottos);
+
+ const lottoNumbersArray = this.lottoMachine.generateLottoNumbers(numberOfLottos);
+ this.outputView.printLottoNumbers(lottoNumbersArray);
+
+ const winningNumbers = await this.lottoController.handleLottoWinningNumbers();
+
+ const lottoResult = lottoNumbersArray.map(lottoNumbers => {
+ return this.winningLotto.checkWinning(lottoNumbers, winningNumbers);
+ })
+
+ const lottoWinningResult = this.winningLotto.countAndPrintResult(lottoResult);
+
+ const profitRates = this.lottoController.calculateProfitRates(lottoWinningResult);
+ this.outputView.printProfitRates(profitRates.toFixed(1));
+ }
}
export default App;
diff --git a/src/Controller/LottoController.js b/src/Controller/LottoController.js
new file mode 100644
index 0000000000..59cd893e75
--- /dev/null
+++ b/src/Controller/LottoController.js
@@ -0,0 +1,45 @@
+import InputView from "../View/InputView.js";
+import LottoMachine from "../Model/LottoMachine.js";
+import { RANK_PRIZE } from "../utils/Constants.js";
+
+class LottoController {
+ constructor() {
+ this.purchaseAmount = 0;
+ this.inputView = new InputView();
+ this.lottoMachine = new LottoMachine();
+ }
+
+ async handlePurchase() {
+ this.purchaseAmount = await this.inputView.promptPurchaseAmount();
+ const numberOfLottos = this.lottoMachine.calculateLottoCount(this.purchaseAmount);
+
+ return numberOfLottos;
+ }
+
+ async handleLottoWinningNumbers() {
+ const winningNumbers = await this.inputView.promptWinningNumbers();
+ const bonusNumbers = await this.inputView.promptBonusNumber();
+
+ return [winningNumbers, bonusNumbers];
+ }
+
+ convertPrizeToNumber(prize) {
+ return parseInt(prize.replace(/[^0-9]/g, ''));
+ }
+
+ calculateTotalPrize(lottoResult) {
+ return lottoResult.reduce((total, count, rank) => {
+ if (rank === 0 || !RANK_PRIZE[rank]) return total;
+
+ const prize = this.convertPrizeToNumber(RANK_PRIZE[rank]);
+ return total + (prize * count);
+ }, 0);
+ }
+
+ calculateProfitRates(lottoResult) {
+ const totalPrize = this.calculateTotalPrize(lottoResult);
+ return (totalPrize / this.purchaseAmount) * 100;
+ }
+}
+
+export default LottoController;
\ No newline at end of file
diff --git a/src/Lotto.js b/src/Lotto.js
deleted file mode 100644
index cb0b1527e9..0000000000
--- a/src/Lotto.js
+++ /dev/null
@@ -1,18 +0,0 @@
-class Lotto {
- #numbers;
-
- constructor(numbers) {
- this.#validate(numbers);
- this.#numbers = numbers;
- }
-
- #validate(numbers) {
- if (numbers.length !== 6) {
- throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
- }
- }
-
- // TODO: 추가 기능 구현
-}
-
-export default Lotto;
diff --git a/src/Model/Lotto.js b/src/Model/Lotto.js
new file mode 100644
index 0000000000..17cd5694c7
--- /dev/null
+++ b/src/Model/Lotto.js
@@ -0,0 +1,20 @@
+import InputValidator from '../utils/InputValidator.js';
+
+class Lotto {
+ #numbers;
+
+ constructor(numbers) {
+ this.#validate(numbers);
+ this.#numbers = numbers;
+ }
+
+ #validate(numbers) {
+ InputValidator.validateLottoWinningNumber(numbers);
+ }
+
+ getLottoNumbers() {
+ return this.#numbers.sort((num1, num2) => num1 - num2);
+ }
+}
+
+export default Lotto;
diff --git a/src/Model/LottoMachine.js b/src/Model/LottoMachine.js
new file mode 100644
index 0000000000..c18cd9f9c8
--- /dev/null
+++ b/src/Model/LottoMachine.js
@@ -0,0 +1,28 @@
+import { Random } from '@woowacourse/mission-utils';
+import OutputView from "../View/OutputView.js";
+import Lotto from './Lotto.js';
+
+class LottoMachine {
+ constructor() {
+ this.lottoNumbersArray = [];
+ this.outputView = new OutputView();
+ }
+
+ calculateLottoCount(amount) {
+ return Math.floor(amount / 1000);
+ }
+
+ generateLottoNumbers(lottoCount) {
+ for (let i = 0; i < lottoCount; i += 1) {
+ const lottoNumbers = Random.pickUniqueNumbersInRange(1, 45, 6);
+ const lottos = new Lotto(lottoNumbers);
+ const lottoSortNumbers = lottos.getLottoNumbers(lottoNumbers);
+
+ this.lottoNumbersArray.push(lottoSortNumbers);
+ }
+
+ return this.lottoNumbersArray;
+ }
+}
+
+export default LottoMachine;
diff --git a/src/Model/WinningLotto.js b/src/Model/WinningLotto.js
new file mode 100644
index 0000000000..ca3ef84597
--- /dev/null
+++ b/src/Model/WinningLotto.js
@@ -0,0 +1,55 @@
+import OutputView from "../View/OutputView.js";
+
+class WinningLotto {
+ constructor() {
+ this.outputView = new OutputView();
+ }
+
+ calculateNumberOfMatchingNumbers(lottoNumbers, winningNumbers) {
+ return lottoNumbers.filter((number) => winningNumbers.includes(number)).length;
+ }
+
+ isBonusNumberMatched(lottoNumbers, bonusNumber) {
+ return lottoNumbers.includes(bonusNumber);
+ }
+
+ determinePrizeCategory(matchCount, isBonusMatched) {
+ if (matchCount === 6) return 1;
+ if (matchCount === 5 && isBonusMatched) return 2;
+ if (matchCount === 5) return 3;
+ if (matchCount === 4) return 4;
+ if (matchCount === 3) return 5;
+
+ return 0;
+ }
+
+ checkWinning(lottoNumbers, winningNumbers) {
+ const winningNums = winningNumbers[0].map(Number);
+ const bonusNum = Number(winningNumbers[1]);
+
+ const matchCount = this.calculateNumberOfMatchingNumbers(lottoNumbers, winningNums);
+ const isBonusMatched = this.isBonusNumberMatched(lottoNumbers, bonusNum);
+
+ return this.determinePrizeCategory(matchCount, isBonusMatched);
+ }
+
+ countAndPrintResult(lottoResult) {
+ const counts = {};
+
+ for (let i = 0; i<=5; i+= 1) {
+ counts[i] = 0;
+ }
+
+ lottoResult.forEach(num => {
+ if (num >= 1 && num <= 5) {
+ counts[num] += 1;
+ }
+ });
+
+ this.outputView.printWinningResult(counts);
+
+ return Object.values(counts);
+ }
+}
+
+export default WinningLotto;
\ No newline at end of file
diff --git a/src/View/InputView.js b/src/View/InputView.js
new file mode 100644
index 0000000000..8cf693a51c
--- /dev/null
+++ b/src/View/InputView.js
@@ -0,0 +1,63 @@
+import { Console } from '@woowacourse/mission-utils';
+import { GAME_MESSAGES } from '../utils/Constants.js';
+import InputValidator from '../utils/InputValidator.js';
+
+class InputView {
+ constructor() {
+ this.winningNumberArray = [];
+ }
+
+ async getUserInput(question) {
+ return await Console.readLineAsync(question);
+ }
+
+ async promptPurchaseAmount() {
+ while(true) {
+ try {
+ const purchaseAmount = await this.getUserInput(GAME_MESSAGES.ENTER_PURCHASE_AMOUNT);
+
+ const userMoney = Number(purchaseAmount);
+ console.log('userMoney: ', userMoney);
+ InputValidator.validatePurchaseAmount(userMoney);
+
+ return userMoney;
+ } catch(error) {
+ Console.print(error.message);
+ }
+ }
+ }
+
+ async promptWinningNumbers() {
+ while(true) {
+ try {
+ const winningNumbers = await this.getUserInput(GAME_MESSAGES.ENTER_WINNING_NUMBERS);
+ const winningNumbersArray = winningNumbers.split(',').map((number) => number.trim());
+
+ this.winningNumberArray = winningNumbersArray;
+
+ InputValidator.validateLottoWinningNumber(winningNumbersArray);
+
+ return winningNumbersArray;
+ } catch(error) {
+ Console.print(error);
+ }
+ }
+
+ }
+
+ async promptBonusNumber() {
+ while(true){
+ try {
+ const bonusNumber = await this.getUserInput(GAME_MESSAGES.ENTER_BONUS_NUMBER);
+
+ InputValidator.validateLottoBonusNumber(this.winningNumberArray, Number(bonusNumber));
+
+ return bonusNumber;
+ } catch(error) {
+ Console.print(error);
+ }
+ }
+ }
+}
+
+export default InputView;
diff --git a/src/View/OutputView.js b/src/View/OutputView.js
new file mode 100644
index 0000000000..4a09bec568
--- /dev/null
+++ b/src/View/OutputView.js
@@ -0,0 +1,34 @@
+import { Console } from '@woowacourse/mission-utils';
+import { GAME_MESSAGES, RANK_MESSAGES } from '../utils/Constants.js';
+
+class OutputView {
+ constructor() {}
+
+ printLottoCounts(lottoCounts) {
+ Console.print(lottoCounts + GAME_MESSAGES.CONFIRM_PURCHASED_LOTTOS_AMOUNT);
+ // Console.print('');
+ }
+
+ printLottoNumbers(lottos) {
+ lottos.map((lotto, _) => {
+ Console.print(`[${lotto.join(", ")}]`);
+ })
+ Console.print('');
+ }
+
+ printWinningResult(counts) {
+ Console.print(GAME_MESSAGES.STATICS_HEADER);
+ Console.print(GAME_MESSAGES.DIVIDER);
+
+ for (let rank = 5; rank >= 1; rank -= 1) {
+ const message = RANK_MESSAGES[rank] + `${counts[rank]}` + GAME_MESSAGES.MESSSAGE_SUFFIX;
+ Console.print(message);
+ }
+ }
+
+ printProfitRates(profitRates) {
+ Console.print(GAME_MESSAGES.TOTAL_PRIZE_RATE(profitRates));
+ }
+}
+
+export default OutputView;
\ No newline at end of file
diff --git a/src/utils/Constants.js b/src/utils/Constants.js
new file mode 100644
index 0000000000..5467e553f3
--- /dev/null
+++ b/src/utils/Constants.js
@@ -0,0 +1,38 @@
+export const GAME_MESSAGES = {
+ ENTER_PURCHASE_AMOUNT: "구입금액을 입력해 주세요. ",
+ CONFIRM_PURCHASED_LOTTOS_AMOUNT: "개를 구매했습니다.",
+ ENTER_WINNING_NUMBERS: "당첨 번호를 입력해 주세요. ",
+ ENTER_BONUS_NUMBER: "보너스 번호를 입력해 주세요. ",
+ TOTAL_PRIZE_RATE: (winningRates) => `총 수익률은 ${winningRates}%입니다.`,
+ STATICS_HEADER: "당첨 통계",
+ DIVIDER: "---------",
+ MESSSAGE_SUFFIX: "개"
+}
+
+export const ERROR_MESSAGES = {
+ EMPTY_INPUT: '[ERROR] 값이 비어있습니다.',
+ INVALID_NUMBER_FORMAT: "[ERROR] 숫자 형식이 아닙니다.",
+ DUPLICATE_LOTTO_NUMBER: "[ERROR] 로또 번호가 중복되면 안됩니다.",
+ NUMBER_OUT_OF_RANGE: `[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.`,
+ BONUS_NUMBER_OUT_OF_RANGE: `[ERROR] 보너스 로또 번호도 1부터 45 사이의 숫자여야 합니다.`,
+ NUMBER_OUT_OF_RANGE: "[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.",
+ LOTTO_NUMBER_OUT_OF_RANGE: "[ERROR] 로또 번호는 6개를 입력해주세요.",
+ INVAID_PURCHASE_AMOUNT: "[ERROR] 구입 금액은 1,000원 단위로 입력해야 합니다."
+}
+
+export const RANK_PRIZE = {
+ 1: '2,000,000,000원',
+ 2: '30,000,000원',
+ 3: '1,500,000원',
+ 4: '50,000원',
+ 5: '5,000원',
+}
+
+export const RANK_MESSAGES = {
+ 1: `6개 일치 (${RANK_PRIZE[1]}) - `,
+ 2: `5개 일치, 보너스 볼 일치 (${RANK_PRIZE[2]}) - `,
+ 3: `5개 일치 (${RANK_PRIZE[3]}) - `,
+ 4: `4개 일치 (${RANK_PRIZE[4]}) - `,
+ 5: `3개 일치 (${RANK_PRIZE[5]}) - `,
+}
+
diff --git a/src/utils/InputValidator.js b/src/utils/InputValidator.js
new file mode 100644
index 0000000000..fc0af22c28
--- /dev/null
+++ b/src/utils/InputValidator.js
@@ -0,0 +1,26 @@
+import Validation from "./Validation.js";
+
+class InputValidator {
+ static validatePurchaseAmount(amount) {
+ Validation.validateNumberEmpty(amount);
+ Validation.validateIsNumber(amount);
+ Validation.validateInputThousandWonUnit(amount);
+ }
+
+ static validateLottoWinningNumber(array) {
+ Validation.validateArrayEmpty(array);
+ Validation.validateArrayIsNumber(array);
+ Validation.validateHasDuplicate(array);
+ Validation.validateArrayOfRange(array);
+ Validation.validateArrayLength(array);
+ }
+
+ static validateLottoBonusNumber(array, number) {
+ Validation.validateNumberEmpty(number);
+ Validation.validateIsNumber(number);
+ Validation.validateNumberOfRange(number);
+ Validation.validateHasDuplicateArrayAndNumber(array, number);
+ }
+}
+
+export default InputValidator;
diff --git a/src/utils/Validation.js b/src/utils/Validation.js
new file mode 100644
index 0000000000..8c6a710028
--- /dev/null
+++ b/src/utils/Validation.js
@@ -0,0 +1,67 @@
+import { ERROR_MESSAGES } from "./Constants.js";
+
+class Validation {
+ static validateArrayEmpty(array) {
+ if (array.includes('')) {
+ throw new Error(ERROR_MESSAGES.EMPTY_INPUT);
+ }
+ }
+
+ static validateNumberEmpty(input) {
+ if (input === '') {
+ throw new Error(ERROR_MESSAGES.EMPTY_INPUT);
+ }
+ }
+
+ static validateIsNumber(input) {
+ if (isNaN(input)) {
+ throw new Error(ERROR_MESSAGES.INVALID_NUMBER_FORMAT);
+ }
+ }
+
+ static validateInputThousandWonUnit(amount) {
+ if (amount % 1000 !== 0) {
+ throw new Error(ERROR_MESSAGES.INVAID_PURCHASE_AMOUNT);
+ }
+ }
+
+ static validateHasDuplicate(input) {
+ const uniqueNumbers = new Set(input);
+ if (uniqueNumbers.size !== input.length) {
+ throw new Error(ERROR_MESSAGES.DUPLICATE_LOTTO_NUMBER);
+ }
+ }
+
+ static validateHasDuplicateArrayAndNumber(array, input) {
+ if (array.includes(input)) {
+ throw new Error(ERROR_MESSAGES.DUPLICATE_LOTTO_NUMBER);
+ }
+ }
+
+ static validateNumberOfRange(input) {
+ if (input < 1 || input > 45) {
+ throw new Error(ERROR_MESSAGES.BONUS_NUMBER_OUT_OF_RANGE);
+ }
+ }
+
+ static validateArrayOfRange(array) {
+ console.log(array);
+ if (array.some(number => number < 1 || number > 45)) {
+ throw new Error(ERROR_MESSAGES.NUMBER_OUT_OF_RANGE);
+ }
+ }
+
+ static validateArrayIsNumber(array) {
+ if (array.some(number => isNaN(number))) {
+ throw new Error(ERROR_MESSAGES.INVALID_NUMBER_FORMAT);
+ }
+ }
+
+ static validateArrayLength(array) {
+ if (array.length !== 6) {
+ throw new Error(ERROR_MESSAGES.LOTTO_NUMBER_OUT_OF_RANGE);
+ }
+ }
+}
+
+export default Validation;