Skip to content
141 changes: 140 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,140 @@
# java-lotto-precourse
# 3주차 미션 : 로또
<details>
<summary>과제 세부 내용</summary>

## 과제
- 로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.

- 로또 번호의 숫자 범위는 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원이다.
- 당첨 번호와 보너스 번호를 입력받는다.
- 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
- `Exception`이 아닌 `IllegalArgumentException`, `IllegalStateException` 등과 같은 명확한 유형을 처리한다.
### 입출력
- 로또 구입 금액을 입력 받는다. 구입 금액은 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 사이의 숫자여야 합니다.

### 실행 결과 예시
구입금액을 입력해 주세요.
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%입니다.

### 요구 사항
- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- 3항 연산자를 쓰지 않는다.
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
- 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
- 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
- else 예약어를 쓰지 않는다.
- 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
- else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
- Java Enum을 적용한다.
- 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(System.out, [System.in](http://system.in/), Scanner) 로직은 제외한다.
- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현한다.
- 단위 테스트 작성이 익숙하지 않다면 `test/java/lotto/LottoTest`를 참고하여 학습한 후 테스트를 구현한다.

</details>

## 구현할 기능 목록
### Model
Lotto
- numbers[List<Integer>]
LottoData
- 로또[List<Lotto>]
- 당첨 번호[List<Integer>]
- 보너스 번호[int]
- 등수 결과[intArray[5]]
### Utils
- Enum
- 로또 번호 생성
- 입력값 검증
### Service
Calculator
- 로또 장수 계산
- 수익률 계산
LottoService
- 당첨 로또 확인
### View
- 구입 금액 입력
- 당첨 번호 입력
- 보너스 번호 입력
- 발행한 로또 수량 및 번호 출력
- 당첨 내역 및 수익률 출력
### Controller
- 입력값으로 객체들 선언 및 초기화
- 로또 확인
- 결과 출력
34 changes: 34 additions & 0 deletions src/main/java/lotto/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package lotto;

import lotto.controller.LottoController;
import lotto.service.Calculator;
import lotto.service.LottoService;
import lotto.view.InputView;
import lotto.view.OutputView;

public class AppConfig {
public InputView inputView() {
return new InputView();
}

public OutputView outputView() {
return new OutputView();
}

public Calculator calculator() {
return new Calculator();
}

public LottoService lottoService() {
return new LottoService();
}

public LottoController lottoController() {
return new LottoController(
inputView(),
outputView(),
calculator(),
lottoService()
);
}
}
6 changes: 5 additions & 1 deletion src/main/java/lotto/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package lotto;

import lotto.controller.LottoController;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
AppConfig appConfig = new AppConfig();
LottoController controller = appConfig.lottoController();
controller.run();
}
}
68 changes: 68 additions & 0 deletions src/main/java/lotto/controller/LottoController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package lotto.controller;

import lotto.service.Calculator;
import lotto.service.LottoService;
import lotto.utils.ErrorConstants;
import lotto.utils.RandomNumber;
import lotto.utils.Validator;
import lotto.view.InputView;
import lotto.view.OutputView;

import java.util.List;

public class LottoController {
InputView inputView;
OutputView outputView;
Calculator calculator;
LottoService lottoService;

private int money;
private double profit;

public LottoController(InputView inputView, OutputView outputView, Calculator calculator, LottoService lottoService) {
this.inputView = inputView;
this.outputView = outputView;
this.calculator = calculator;
this.lottoService = lottoService;
}

public void run() {
initialize();
execute();
printResult();
}

private void initialize() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 outputView.newLine()이 너무 반복적으로 호출되는데 한번만 호출하는 방식으로 변경하거나 아니면 아예 출력순서를 담당하는 별도 메서드로 분리하는게 더 좋을 것 같아요!

this.money = inputView.insertMoney();
outputView.newLine();
List<List<Integer>> lottos = RandomNumber.pickNumbers(calculator.calculateSheet(money));
outputView.printLotto(lottos);
outputView.newLine();
List<Integer> numbers = inputView.selectNumber();
outputView.newLine();
int bonusNumber = readBonusNumber(numbers);
outputView.newLine();
lottoService.init(lottos, numbers, bonusNumber);
}

private void execute() {
lottoService.matchNumber();
profit = calculator.calculateProfit(money, lottoService.getResult());
}

private void printResult() {
outputView.printStats(lottoService.getResult(), profit);
}

private int readBonusNumber(List<Integer> numbers) {
int bn;
while (true) {
try {
bn = Validator.checkDistinctBonusNumber(inputView.selectBonusNumber(), numbers);
return bn;
} catch (IllegalArgumentException e) {
System.out.println(ErrorConstants.INPUT_ERROR.getMessage());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package lotto;
package lotto.model;

import java.util.List;

Expand All @@ -7,14 +7,17 @@ public class Lotto {

public Lotto(List<Integer> numbers) {
validate(numbers);
this.numbers = numbers;
this.numbers = numbers.stream().sorted().toList();
}

private void validate(List<Integer> numbers) {
numbers = numbers.stream().distinct().toList();
if (numbers.size() != 6) {
throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다.");
}
}

// TODO: 추가 기능 구현
public List<Integer> getNumbers() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LottoData.java에서도 똑같은 getNumbers가 있던데 LottoData만 service에서 사용하고있더라구요 여기에 있는 lotto의 중복코드를 삭제해도 되지않을까요??

return numbers;
}
}
47 changes: 47 additions & 0 deletions src/main/java/lotto/model/LottoData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package lotto.model;

import java.util.ArrayList;
import java.util.List;

public class LottoData {
private final List<Lotto> lottos;
private final List<Integer> numbers;
private int bonusNumber;
private final int[] result;

private LottoData() {
lottos = new ArrayList<>();
numbers = new ArrayList<>();
bonusNumber = 0;
result = new int[5];
}

public static LottoData init(List<List<Integer>> l, List<Integer> n, int bn) {
LottoData data = new LottoData();
data.lottos.addAll(l.stream().map(Lotto::new).toList());
data.numbers.addAll(n);
data.bonusNumber = bn;
data.numbers.add(data.bonusNumber);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

numebrs에서 보너스번호와 당첨번호를 둘다 관리하는데 따로 관리하는게 명확한 데이터구조를 유지하고 확장성면에서 더 좋지 않을까요??

return data;
}

public List<Lotto> getLottos() {
return lottos;
}

public List<Integer> getNumbers() {
return numbers;
}

public int getBonusNumber() {
return bonusNumber;
}

public int[] getResult() {
return result;
}

public void addPrize(int number) {
result[number]++;
}
}
17 changes: 17 additions & 0 deletions src/main/java/lotto/service/Calculator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto.service;

import lotto.utils.PrizeConstants;

public class Calculator {
public int calculateSheet(int money) {
return money/1000;
}

public double calculateProfit(int money, int[] result) {
PrizeConstants[] p = PrizeConstants.values();
double sum = 0;
for (int i = 0; i < result.length; i++)
sum += result[i] * p[i].getPrize();
return Math.round(sum/(double)money*100 * 100) / 100.0;
}
}
35 changes: 35 additions & 0 deletions src/main/java/lotto/service/LottoService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package lotto.service;

import lotto.model.Lotto;
import lotto.model.LottoData;

import java.util.List;

public class LottoService {
private LottoData lottoData;

public void init(List<List<Integer>> l, List<Integer> n, int bn) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가독성을 위해 파라미터명을 축약어보단 풀네임으로 쓰는게 더 좋을 것 같습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동의합니다!

this.lottoData = LottoData.init(l, n, bn);
}

public int[] getResult() {
return lottoData.getResult();
}

public void matchNumber() {
List<Integer> intersectionList;
for (Lotto l : lottoData.getLottos()) {
intersectionList = l.getNumbers().stream().filter(n -> lottoData.getNumbers().contains(n)).toList();
if (intersectionList.size() < 3) continue;
if (intersectionList.size() == 6 && !isContainBonus(intersectionList)) {
lottoData.addPrize(4);
continue;
}
lottoData.addPrize(intersectionList.size() - 3);
}
}

Comment on lines +19 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public void matchNumber() {
List<Integer> intersectionList;
for (Lotto l : lottoData.getLottos()) {
intersectionList = l.getNumbers().stream().filter(n -> lottoData.getNumbers().contains(n)).toList();
if (intersectionList.size() < 3) continue;
if (intersectionList.size() == 6 && !isContainBonus(intersectionList)) {
lottoData.addPrize(4);
continue;
}
lottoData.addPrize(intersectionList.size() - 3);
}
}
public void matchNumber() {
for (Lotto lotto : lottoData.getLottos()) {
int matchCount = (int) lotto.getNumbers().stream()
.filter(lottoData.getNumbers()::contains)
.count();
if (matchCount >= 3) {
int index = (matchCount == 6 && !isContainBonus(lotto.getNumbers())) ? 4 : matchCount - 3;
lottoData.addPrize(index);
}
}
}

여기 if문에서 .size()를 계속 중복호출하고있는것같아요 matchCount라는 변수에 담아서 사용하는것은 어떨까요??

private boolean isContainBonus(List<Integer> l) {
return l.contains(lottoData.getBonusNumber());
}
}
Loading