diff --git a/__tests__/LottoTest.js b/__tests__/domain/LottoTest.js
similarity index 51%
rename from __tests__/LottoTest.js
rename to __tests__/domain/LottoTest.js
index 97bd457659..e17d672c6e 100644
--- a/__tests__/LottoTest.js
+++ b/__tests__/domain/LottoTest.js
@@ -1,4 +1,4 @@
-import Lotto from "../src/Lotto.js";
+import Lotto from "../../src/domain/Lotto";
describe("로또 클래스 테스트", () => {
test("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.", () => {
@@ -7,12 +7,20 @@ describe("로또 클래스 테스트", () => {
}).toThrow("[ERROR]");
});
- // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성
test("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.", () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 5]);
}).toThrow("[ERROR]");
});
- // 아래에 추가 테스트 작성 가능
+ test("로또 번호 1~45 범위를 초과하면 예외가 발생한다.", () => {
+ expect(() => {
+ new Lotto([1, 2, 3, 4, 5, 46]);
+ }).toThrow("[ERROR]");
+ });
+ test("로또 번호가 자연수가 아니면 예외가 발생한다.", () => {
+ expect(() => {
+ new Lotto([1, 2, 3, 4, 5, 6.5]);
+ }).toThrow("[ERROR]");
+ });
});
diff --git a/__tests__/view/InputTest.js b/__tests__/view/InputTest.js
new file mode 100644
index 0000000000..d2dd5666d2
--- /dev/null
+++ b/__tests__/view/InputTest.js
@@ -0,0 +1,34 @@
+import { MissionUtils } from "@woowacourse/mission-utils";
+import InputView from "../../src/view/input-view";
+
+const mockInput = (input) => {
+ MissionUtils.Console.readLineAsync = jest.fn();
+ MissionUtils.Console.readLineAsync.mockResolvedValue(input);
+};
+
+describe("입력값 예외처리 테스트", () => {
+ test("구입 금액이 1000으로 나누어 떨어지지 않을 경우 예외 처리", async () => {
+ //given
+ const input = 14500;
+ mockInput(input);
+
+ //when
+ const inputValue = new InputView();
+
+ //then
+ await expect(inputValue.readPurchaseAmount()).rejects.toThrow("[ERROR]");
+ });
+
+ test.each([
+ 0, -1000
+ ])("구입 금액이 0 이하 일 경우 예외 처리", async (inputs) => {
+ //given
+ mockInput(inputs);
+
+ //when
+ const inputValue = new InputView();
+
+ //then
+ await expect(inputValue.readPurchaseAmount()).rejects.toThrow("[ERROR]");
+ });
+});
\ No newline at end of file
diff --git a/docs/readme.md b/docs/readme.md
new file mode 100644
index 0000000000..deb7eb780b
--- /dev/null
+++ b/docs/readme.md
@@ -0,0 +1,160 @@
+## 기능 요구 사항
+
+ 로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.
+
+```
+- 로또 번호의 숫자 범위는 1~45까지이다.
+- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
+- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
+- 당첨은 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원
+```
+
+- 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
+- 로또 1장의 가격은 1,000원이다.
+- 당첨 번호와 보너스 번호를 입력받는다.
+- 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
+- 사용자가 잘못된 값을 입력할 경우 `throw`문을 사용해 예외를 발생시킨다. 그런 다음, "[ERROR]"로 시작하는 에러 메시지를 출력하고 해당 부분부터 입력을 다시 받는다.
+ ```
+ 예시) [ERROR] 숫자가 잘못된 형식입니다.
+ ```
+
+### 입출력 요구 사항
+
+#### 입력
+
+- 로또 구입 금액을 입력 받는다. 구입 금액은 1,000원 단위로 입력 받으며 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다.
+
+```
+14000
+```
+
+- 당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다.
+
+```
+1,2,3,4,5,6
+```
+
+- 보너스 번호를 입력 받는다.
+
+```
+7
+```
+
+#### 출력
+
+- 발행한 로또 수량 및 번호를 출력한다. 로또 번호는 오름차순으로 정렬하여 보여준다.
+
+```
+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]
+```
+
+- 당첨 내역을 출력한다.
+
+```
+3개 일치 (5,000원) - 1개
+4개 일치 (50,000원) - 0개
+5개 일치 (1,500,000원) - 0개
+5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
+6개 일치 (2,000,000,000원) - 0개
+```
+
+- 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)
+
+```
+총 수익률은 62.5%입니다.
+```
+
+- 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 "[ERROR]"로 시작해야 한다.
+
+```
+[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.
+```
+
+#
+### 구현 완료 여부
+
+#### 입력 기능
+
+
+
+
+
+
+#### 출력 기능
+
+
diff --git a/src/App.js b/src/App.js
index c38b30d5b2..76de5dfee7 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,5 +1,15 @@
+import LottoController from "./controller/lotto-controller.js";
+
class App {
- async play() {}
+ #lottoController;
+
+ constructor() {
+ this.#lottoController = new LottoController();
+ }
+
+ async play() {
+ await this.#lottoController.play();
+ }
}
export default App;
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/constants/constants.js b/src/constants/constants.js
new file mode 100644
index 0000000000..902c30c7aa
--- /dev/null
+++ b/src/constants/constants.js
@@ -0,0 +1,26 @@
+export const INPUT_MESSAGE = {
+ inputPurchaseAmount : "구입 금액을 입력해 주세요.\n",
+ inputWinningNumbers : "\n당첨 번호를 입력해 주세요.\n",
+ inputBonusNumber : "\n보너스 번호를 입력해 주세요.\n",
+};
+
+export const OUTPUT_MESSAGE = {
+ printPurchaseNumber : (count) => `\n${count}개 구매했습니다.`,
+ printWinningStatistics : "\n당첨 통계\n---",
+ printFifth : (count) => `3개 일치 (5,000원) - ${count}개`,
+ printFourth : (count) => `4개 일치 (50,000원) - ${count}개`,
+ printThird : (count) => `5개 일치 (1,500,000원) - ${count}개`,
+ printSecond : (count) => `5개 일치, 보너스 볼 일치 (30,000,000원) - ${count}개`,
+ printFirst : (count) => `6개 일치 (2,000,000,000원) - ${count}개`,
+ printRateReturn : (rate) => `총 수익률은 ${rate}%입니다.`,
+};
+
+export const ERROR_MESSAGE = {
+ purchaseError : "[ERROR] 구입 금액은 1000으로 나누어 떨어지는 수 입니다.",
+ purchaseRangeError : "[ERROR] 구입 금액 범위는 양수인 정수입니다.",
+ lottoLengthError : "[ERROR] 로또 번호는 6개여야 합니다.",
+ lottoDuplicatedError : "[ERROR] 로또 번호는 중복될 수 없습니다.",
+ lottoRangeError : "[ERROR] 로또 번호의 범위는 1~45 폐구간 입니다.",
+ lottoTypeError : "[ERROR] 로또 번호는 자연수입니다.",
+ bonusLengthError : "[ERROR] 보너스 번호는 1개여야 합니다.",
+};
\ No newline at end of file
diff --git a/src/controller/lotto-controller.js b/src/controller/lotto-controller.js
new file mode 100644
index 0000000000..94799f1430
--- /dev/null
+++ b/src/controller/lotto-controller.js
@@ -0,0 +1,26 @@
+import MyLotto from "../domain/MyLotto.js";
+import InputView from "../view/input-view.js";
+import OutputView from "../view/output-view.js";
+
+class LottoController {
+ #inputView;
+ #outputView;
+
+ constructor() {
+ this.#inputView = new InputView();
+ this.#outputView = new OutputView();
+ }
+
+ async play() {
+ let perchaseNumber = await this.#inputView.readPurchaseAmount();
+ let myLotto = new MyLotto(perchaseNumber);
+
+ this.#outputView.printMyLotto(perchaseNumber, myLotto);
+
+ let winningLotto = await this.#inputView.readWinningLotto();
+
+ this.#outputView.printResult(myLotto, winningLotto);
+ }
+}
+
+export default LottoController;
\ No newline at end of file
diff --git a/src/domain/Lotto.js b/src/domain/Lotto.js
new file mode 100644
index 0000000000..97ec8c8f8d
--- /dev/null
+++ b/src/domain/Lotto.js
@@ -0,0 +1,23 @@
+import LottoValidator from "../validator/lotto-validator.js";
+
+class Lotto {
+ #numbers;
+
+ constructor(numbers) {
+ this.#validate(numbers);
+ this.#numbers = numbers;
+ }
+
+ #validate(numbers) {
+ LottoValidator.lottoLengthValidation(numbers);
+ LottoValidator.lottoDuplicatedValidation(numbers);
+ LottoValidator.lottoRangeValidation(numbers);
+ LottoValidator.lottoTypeValidation(numbers);
+ }
+
+ getNumbers() {
+ return this.#numbers;
+ }
+}
+
+export default Lotto;
diff --git a/src/domain/LottoResult.js b/src/domain/LottoResult.js
new file mode 100644
index 0000000000..e1e67b61ea
--- /dev/null
+++ b/src/domain/LottoResult.js
@@ -0,0 +1,52 @@
+class LottoResult{
+ #myLotto;
+ #winningLotto;
+ #matching;
+ #matchReward;
+
+ constructor(myLotto, winningLotto) {
+ this.#myLotto = myLotto;
+ this.#winningLotto = winningLotto;
+ this.#matching = [0, 0, 0, 0, 0];
+ this.#matchReward = [5000, 50000, 1500000, 30000000, 2000000000];
+ this.#compareLotto();
+ }
+
+ #updateMatch(match, isHave) {
+ switch(match){
+ case 3 : this.#matching[0]++; break;
+ case 4 : this.#matching[1]++; break;
+ case 6 : this.#matching[4]++; break;
+ }
+ if (match == 5 && !isHave){
+ this.#matching[2]++;
+ }
+ if (match == 5 && isHave){
+ this.#matching[3]++;
+ }
+ }
+
+ #compareLotto() {
+ for (let lotto of this.#myLotto.getMyLottos()){
+ let match = lotto.getNumbers().filter(element => this.#winningLotto.getNumbers().includes(element));
+ this.#updateMatch(match.length, lotto.getNumbers().includes(this.#winningLotto.getBonusNumber()));
+ }
+ }
+
+ calculateRateResult() {
+ let purchaseAmount = this.#myLotto.getLottoSize() * 1000;
+ let reward = this.#matching.reduce((sum, value, index) => {
+ return sum + (value * this.#matchReward[index]);
+ }, 0);
+
+ return (reward/purchaseAmount).toFixed(1);
+ }
+
+ getMatching() {
+ return this.#matching;
+ }
+
+
+}
+
+export default LottoResult;
\ No newline at end of file
diff --git a/src/domain/MyLotto.js b/src/domain/MyLotto.js
new file mode 100644
index 0000000000..01ded3557b
--- /dev/null
+++ b/src/domain/MyLotto.js
@@ -0,0 +1,28 @@
+import Lotto from "./Lotto.js";
+import lottoNumberGenerator from "../util/lotto-number-generator.js";
+
+class MyLotto {
+ #lottos;
+
+ constructor(purchaseAmount) {
+ this.#lottos = this.#generate(purchaseAmount);
+ }
+
+ #generate(purchaseAmount) {
+ let generator = [];
+ for (let i = 0; i < purchaseAmount; i++) {
+ generator.push(new Lotto(lottoNumberGenerator()));
+ }
+ return generator;
+ }
+
+ getMyLottos() {
+ return this.#lottos;
+ }
+
+ getLottoSize() {
+ return this.#lottos.length;
+ }
+}
+
+export default MyLotto;
\ No newline at end of file
diff --git a/src/domain/WinningLotto.js b/src/domain/WinningLotto.js
new file mode 100644
index 0000000000..1ad19c3223
--- /dev/null
+++ b/src/domain/WinningLotto.js
@@ -0,0 +1,24 @@
+import Lotto from "./Lotto.js";
+import LottoBonusValidator from "../validator/lotto-bonus-validator.js";
+
+class WinningLotto extends Lotto {
+ #bonusNumber;
+
+ constructor(winningNumbers, bonusNumber) {
+ super(winningNumbers);
+ this.#bonusValidate(bonusNumber);
+ this.#bonusNumber = bonusNumber;
+ }
+
+ #bonusValidate(bonusNumber){
+ LottoBonusValidator.bonusLengthValidation(bonusNumber);
+ LottoBonusValidator.bonusRangeValidation(bonusNumber);
+ LottoBonusValidator.bonusTypeValidation(bonusNumber[0]);
+ }
+
+ getBonusNumber() {
+ return this.#bonusNumber[0];
+ }
+}
+
+export default WinningLotto;
\ No newline at end of file
diff --git a/src/util/lotto-number-generator.js b/src/util/lotto-number-generator.js
new file mode 100644
index 0000000000..f6aa08980d
--- /dev/null
+++ b/src/util/lotto-number-generator.js
@@ -0,0 +1,12 @@
+import { Random } from "@woowacourse/mission-utils";
+
+const randomNumberGenerator = () => {
+ return Random.pickUniqueNumbersInRange(1, 45, 6);
+};
+
+const lottoNumberGenerator = () => {
+ const lottoNumbers = randomNumberGenerator();
+ return lottoNumbers.sort((a, b) => a - b);
+};
+
+export default lottoNumberGenerator;
\ No newline at end of file
diff --git a/src/validator/input-validator.js b/src/validator/input-validator.js
new file mode 100644
index 0000000000..eb0d25cb1e
--- /dev/null
+++ b/src/validator/input-validator.js
@@ -0,0 +1,17 @@
+import { ERROR_MESSAGE } from "../constants/constants.js";
+
+class InputValidator {
+ static inputPurchaseValidation(price) {
+ if (!Number.isInteger(price / 1000)) {
+ throw new Error(ERROR_MESSAGE.purchaseError);
+ }
+ }
+
+ static purchaseRangeValidation(price) {
+ if (price <= 0) {
+ throw new Error(ERROR_MESSAGE.purchaseRangeError);
+ }
+ }
+}
+
+export default InputValidator;
diff --git a/src/validator/lotto-bonus-validator.js b/src/validator/lotto-bonus-validator.js
new file mode 100644
index 0000000000..e271a4856b
--- /dev/null
+++ b/src/validator/lotto-bonus-validator.js
@@ -0,0 +1,25 @@
+import { ERROR_MESSAGE } from "../constants/constants.js";
+
+class LottoBonusValidator {
+ static bonusLengthValidation(number) {
+ if (number.length !== 1) {
+ throw new Error(ERROR_MESSAGE.bonusLengthError);
+ }
+ }
+
+ static bonusRangeValidation(number) {
+ const isOutOfRange = number < 1 || number > 45;
+ if (isOutOfRange) {
+ throw new Error(ERROR_MESSAGE.lottoRangeError);
+ }
+ }
+
+ static bonusTypeValidation(number) {
+ const isTypeValid = Number.isInteger(number);
+ if (!isTypeValid) {
+ throw new Error(ERROR_MESSAGE.lottoTypeError);
+ }
+ }
+}
+
+export default LottoBonusValidator;
diff --git a/src/validator/lotto-validator.js b/src/validator/lotto-validator.js
new file mode 100644
index 0000000000..13462417c8
--- /dev/null
+++ b/src/validator/lotto-validator.js
@@ -0,0 +1,38 @@
+import { ERROR_MESSAGE } from "../constants/constants.js";
+
+class LottoValidator {
+ static lottoLengthValidation(numbers) {
+ if (numbers.length !== 6) {
+ throw new Error(ERROR_MESSAGE.lottoLengthError);
+ }
+ }
+
+ static lottoDuplicatedValidation(numbers) {
+ const isDuplicated = numbers.some(function(x) {
+ return numbers.indexOf(x) !== numbers.lastIndexOf(x);
+ });
+ if (isDuplicated) {
+ throw new Error(ERROR_MESSAGE.lottoDuplicatedError);
+ }
+ }
+
+ static lottoRangeValidation(numbers) {
+ const isOutOfRange = numbers.some(function(number) {
+ return number < 1 || number > 45;
+ });
+ if (isOutOfRange) {
+ throw new Error(ERROR_MESSAGE.lottoRangeError);
+ }
+ }
+
+ static lottoTypeValidation(numbers) {
+ const isTypeValid = numbers.every((element) => {
+ return Number.isInteger(element);
+ });
+ if (!isTypeValid) {
+ throw new Error(ERROR_MESSAGE.lottoTypeError);
+ }
+ }
+}
+
+export default LottoValidator;
diff --git a/src/view/input-view.js b/src/view/input-view.js
new file mode 100644
index 0000000000..350028fc49
--- /dev/null
+++ b/src/view/input-view.js
@@ -0,0 +1,40 @@
+import { Console } from "@woowacourse/mission-utils";
+import { INPUT_MESSAGE } from "../constants/constants.js";
+import InputValidator from "../validator/input-validator.js";
+import WinningLotto from "../domain/WinningLotto.js";
+
+class InputView {
+ async readPurchaseAmount() {
+ try{
+ const inputPrice = await Console.readLineAsync(INPUT_MESSAGE.inputPurchaseAmount);
+
+ InputValidator.purchaseRangeValidation(inputPrice);
+ InputValidator.inputPurchaseValidation(inputPrice);
+ return inputPrice / 1000;
+ }
+ catch(error){
+ Console.print(error);
+ await this.readPurchaseAmount();
+ }
+ }
+
+ async readWinningLotto() {
+ try{
+ const readWinningNumber = await Console.readLineAsync(INPUT_MESSAGE.inputWinningNumbers);
+ const winningNumbers = readWinningNumber.split(",").map(Number);
+
+ const readBonusNumber = await Console.readLineAsync(INPUT_MESSAGE.inputBonusNumber);
+ const bonusNumber = readBonusNumber.split(",").map(Number);
+
+ const winningLotto = new WinningLotto(winningNumbers, bonusNumber);
+
+ return winningLotto;
+ }
+ catch(error){
+ Console.print(error);
+ await this.readWinningLotto();
+ }
+ }
+}
+
+export default InputView;
\ No newline at end of file
diff --git a/src/view/output-view.js b/src/view/output-view.js
new file mode 100644
index 0000000000..bdeb10b40b
--- /dev/null
+++ b/src/view/output-view.js
@@ -0,0 +1,28 @@
+import { OUTPUT_MESSAGE } from "../constants/constants.js";
+import LottoResult from "../domain/LottoResult.js";
+import { Console } from "@woowacourse/mission-utils";
+
+
+class OutputView {
+ printMyLotto(perchaseNumber, myLotto) {
+ Console.print(OUTPUT_MESSAGE.printPurchaseNumber(perchaseNumber));
+
+ for (let element of myLotto.getMyLottos()) {
+ Console.print(element.getNumbers());
+ }
+ }
+
+ printResult(myLotto, winningLotto) {
+ let lottoResult = new LottoResult(myLotto, winningLotto);
+ let match = lottoResult.getMatching();
+ Console.print(OUTPUT_MESSAGE.printWinningStatistics);
+ Console.print(OUTPUT_MESSAGE.printFifth(match[0]));
+ Console.print(OUTPUT_MESSAGE.printFourth(match[1]));
+ Console.print(OUTPUT_MESSAGE.printThird(match[2]));
+ Console.print(OUTPUT_MESSAGE.printSecond(match[3]));
+ Console.print(OUTPUT_MESSAGE.printFirst(match[4]));
+ Console.print(OUTPUT_MESSAGE.printRateReturn(lottoResult.calculateRateResult()));
+ }
+};
+
+export default OutputView;
\ No newline at end of file