diff --git a/docs/README.md b/docs/README.md index e69de29bb2..69dd513bf6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,53 @@ +# 기능 목록 ++ 각 기능에 대한 예외사항과 핸들링도 기재 ++ MVC 패턴을 사용 + +- model 패키지 + - [x] 로또 번호 관리 - Lotto class + - [x] 특정 번호가 로또 번호 리스트에 포함되어 있는지 확인 - contains() + - [x] 당첨 번호 리스트와 로또 번호 리스트의 일치 개수를 반환 - matchCount() + - [x] 로또 번호 리스트 반환 - getNumbers() + - [x] 로또 당첨 등수 관리 - LottoRank class + - [x] 일치하는 등수를 찾음 - findMatchingRank() + - [x] 등수의 설명을 반환 - getDescription() + - [x] 등수의 상금을 반환 - getPrize() + - [x] 로또 당첨 결과 관리 - LottoResult class + - [x] 당첨 결과를 추가 - addResult() + - [x] 전체 상금 합계 반환 - calculateTotalPrize() + - [x] 랭크 설명을 역순으로 반환 - reverseRankDescriptions() + - [x] 각 랭크의 당첨 개수를 역순으로 반환 - reverseRankCounts() +- view 패키지 + - [x] 사용자 입력 처리 - LottoInputView class + - [x] 로또 구입 금액 입력 받기 - receiveUserMoneyInput() + - 예외사항 : 구입 금액이 1,000원 단위가 아닌 경우 ➔ IllegalArgumentException + - 예외사항 : 입력이 숫자가 아닌 경우 ➔ IllegalArgumentException + - [x] 당첨 번호 입력 받기 - receiveWinningNumberInput() + - 예외사항 : 당첨번호가 6개가 아닌 경우 ➔ IllegalArgumentException + - 예외사항 : 당첨번호에 중복된 숫자가 있는 경우 ➔ IllegalArgumentException + - 예외사항 : 로또번호가 1부터 45 사이가 아닌 경우 ➔ IllegalArgumentException + - [x] 보너스 번호 입력 받기 - receiveBonusNumberInput() + - 예외사항 : 당첨번호에 중복된 숫자가 있는 경우 ➔ IllegalArgumentException + - 예외사항 : 로또번호가 1부터 45 사이가 아닌 경우 ➔ IllegalArgumentException + - [x] 결과 출력 - LottoOutputView class + - [x] 로또 번호 목록 출력 - printLotto() + - [x] 당첨 통계 출력 - printWinningStatistics() + - [x] 총 수익률 출력 - printProfitRate() +- controller 패키지 + - [x] 로또게임 구현 - GameController class + - [x] 게임 시작 - start() + - [x] 로또 게임 로직 처리 - LottoController class + - [x] 로또 번호 생성 - generateLottoNumber() + - [x] 당첨 결과 확인 - checkWinningResult() + - [x] 수익률 계산 - calculateProfitRate() +- util 패키지 + - [x] 입력 변환 유틸리티 - LottoParser class + - [x] 입력 정수 리스트로 변환 - parseNumbers() + - [x] 입력 검증 유틸리티 - LottoValidator class + - [x] 구입 금액 검증 - validateMoneyInput() + - [x] 로또 번호 리스트 검증 - validateNumbers() + - [x] 보너스 번호 검증 - validateBonusNumber() + +------- + +* 당첨 번호와 보너스 번호를 입력하는 기능을 분리하는게 좋을까? + * 가독성을 위해 분리하는 방향으로 선택 \ No newline at end of file diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..932744f713 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,10 @@ package lotto; +import lotto.controller.GameController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + GameController gameController = new GameController(); + gameController.start(); } } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java deleted file mode 100644 index 519793d1f7..0000000000 --- a/src/main/java/lotto/Lotto.java +++ /dev/null @@ -1,20 +0,0 @@ -package lotto; - -import java.util.List; - -public class Lotto { - private final List numbers; - - public Lotto(List numbers) { - validate(numbers); - this.numbers = numbers; - } - - private void validate(List numbers) { - if (numbers.size() != 6) { - throw new IllegalArgumentException(); - } - } - - // TODO: 추가 기능 구현 -} diff --git a/src/main/java/lotto/controller/GameController.java b/src/main/java/lotto/controller/GameController.java new file mode 100644 index 0000000000..53ef7503dc --- /dev/null +++ b/src/main/java/lotto/controller/GameController.java @@ -0,0 +1,42 @@ +package lotto.controller; + +import lotto.view.LottoInputView; +import lotto.view.LottoOutputView; +import lotto.model.Lotto; +import lotto.model.LottoResult; + +import java.util.List; +import java.util.stream.Collectors; + +public class GameController { + private final LottoInputView lottoInputView; + private final LottoController lottoService; + private final LottoOutputView lottoOutputView; + private static final int LOTTO_PRICE = 1000; + + public GameController() { + this.lottoService = new LottoController(); + this.lottoOutputView = new LottoOutputView(); + this.lottoInputView = new LottoInputView(); + } + + /** + * 게임 시작 + */ + public void start() { + int money = lottoInputView.receiveUserMoneyInput(); + List lottoList = lottoService.generateLottoNumber(money / LOTTO_PRICE); + lottoOutputView.printLotto(lottoList.size(), lottoList.stream() + .map(Lotto::getNumbers) + .collect(Collectors.toList())); + + List winningNumbers = lottoInputView.receiveWinningNumberInput(); + int bonusNumber = lottoInputView.receiveBonusNumberInput(winningNumbers); + + LottoResult result = lottoService.checkWinningResult(lottoList, winningNumbers, bonusNumber); + lottoOutputView.printWinningStatistics(result.getRankDescriptions(), result.getRankCounts()); + + double profitRate = lottoService.calculateProfitRate(money, result); + lottoOutputView.printProfitRate(profitRate); + } +} diff --git a/src/main/java/lotto/controller/LottoController.java b/src/main/java/lotto/controller/LottoController.java new file mode 100644 index 0000000000..1515dcc942 --- /dev/null +++ b/src/main/java/lotto/controller/LottoController.java @@ -0,0 +1,50 @@ +package lotto.controller; + +import camp.nextstep.edu.missionutils.Randoms; +import lotto.model.Lotto; +import lotto.model.LottoRank; +import lotto.model.LottoResult; + +import java.util.ArrayList; +import java.util.List; + +public class LottoController { + + private static final int LOTTO_MIN_NUMBER = 1; // 로또 번호의 최소값 + private static final int LOTTO_MAX_NUMBER = 45; // 로또 번호의 최대값 + private static final int LOTTO_NUMBER_COUNT = 6; // 로또 번호의 개수 + private static final double PERCENTAGE_CONVERSION = 100.0; // 수익률 계산 비율 + + /** + * 로또 번호 생성 + */ + public List generateLottoNumber(int count) { + List result = new ArrayList<>(); + for(int i = 0; i < count; i++) { + List numbers = Randoms.pickUniqueNumbersInRange(LOTTO_MIN_NUMBER, LOTTO_MAX_NUMBER, LOTTO_NUMBER_COUNT); + result.add(new Lotto(numbers)); + } + return result; + } + + /** + * 당첨 결과 확인 + */ + public LottoResult checkWinningResult(List lottoList, List winningNumbers, int bonusNumber) { + LottoResult result = new LottoResult(); + for (Lotto lotto : lottoList) { + int matchCount = lotto.matchCount(winningNumbers); + boolean bonusMatch = lotto.contains(bonusNumber); + result.addResult(LottoRank.findMatchingRank(matchCount, bonusMatch)); + } + return result; + } + + /** + * 수익률 계산 + */ + public double calculateProfitRate(int money, LottoResult result) { + long totalPrize = result.getTotalPrize(); + return (totalPrize * PERCENTAGE_CONVERSION) / money; + } +} diff --git a/src/main/java/lotto/model/Lotto.java b/src/main/java/lotto/model/Lotto.java new file mode 100644 index 0000000000..fec5ecec78 --- /dev/null +++ b/src/main/java/lotto/model/Lotto.java @@ -0,0 +1,35 @@ +package lotto.model; + +import java.util.List; + +import static lotto.util.LottoValidator.validateNumbers; + +public class Lotto { + private final List numbers; + + public Lotto(List numbers) { + validateNumbers(numbers); + this.numbers = numbers; + } + + /** + * 특정 번호가 로또 번호 리스트에 포함되어 있는지 확인 + */ + public boolean contains(int number) { + return numbers.contains(number); + } + + /** + * 당첨 번호 리스트와 로또 번호 리스트의 일치 개수를 반환 + */ + public int matchCount(List winningNumbers) { + return (int) numbers.stream().filter(winningNumbers::contains).count(); //numbers에 있는 int를 loop 돌려서 winningNumbers에 포함되어있다면 개수를 셈 + } + + /** + * 로또 번호 리스트 반환 + */ + public List getNumbers() { + return numbers; + } +} diff --git a/src/main/java/lotto/model/LottoRank.java b/src/main/java/lotto/model/LottoRank.java new file mode 100644 index 0000000000..2fae9d1784 --- /dev/null +++ b/src/main/java/lotto/model/LottoRank.java @@ -0,0 +1,38 @@ +package lotto.model; + +import java.util.Arrays; + +public enum LottoRank { + FIRST(6, 2_000_000_000, "6개 일치 (2,000,000,000원)"), + SECOND(5, 30_000_000, "5개 일치, 보너스 볼 일치 (30,000,000원)"), + THIRD(5, 1_500_000, "5개 일치 (1,500,000원)"), + FOURTH(4, 50_000, "4개 일치 (50,000원)"), + FIFTH(3, 5_000, "3개 일치 (5,000원)"), + NONE(0, 0, ""); + + private final int matchCount; + private final int prize; + private final String description; + + LottoRank(int matchCount, int prize, String description) { + this.matchCount = matchCount; + this.prize = prize; + this.description = description; + } + + public static LottoRank findMatchingRank(int count, boolean bonusMatch) { + return Arrays.stream(values()) //모든 LottoRank 값을 스트림으로 변환 + .filter(rank -> rank.matchCount == count) + .filter(rank -> rank != SECOND || (rank == SECOND && bonusMatch)) + .findFirst() + .orElse(NONE); + } + + public String getDescription() { + return description; + } + + public long getPrize() { + return prize; + } +} diff --git a/src/main/java/lotto/model/LottoResult.java b/src/main/java/lotto/model/LottoResult.java new file mode 100644 index 0000000000..552787eae1 --- /dev/null +++ b/src/main/java/lotto/model/LottoResult.java @@ -0,0 +1,62 @@ +package lotto.model; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class LottoResult { + private final HashMap rankCounts; + + public LottoResult() { + this.rankCounts = new HashMap<>(); + for (LottoRank lottoRank : LottoRank.values()) { + rankCounts.put(lottoRank, 0); + } + } + + /** + * 당첨 결과를 추가 + */ + public void addResult(LottoRank rank) { + rankCounts.put(rank, rankCounts.get(rank) + 1); //현재 랭크 갯수보다 하나 업 + } + + /** + * 특정 랭크의 당첨 개수 반환 + */ + public int getCountForRank(LottoRank lottoRank) { + return rankCounts.get(lottoRank); + } + + /** + * 전체 상금 합계 반환 + */ + public long getTotalPrize() { + return rankCounts.entrySet().stream() + .mapToLong(entry -> entry.getKey().getPrize() * entry.getValue()) + .sum(); + } + + /** + * 랭크 설명을 역순으로 반환 + */ + public List getRankDescriptions() { + return rankCounts.keySet().stream() + .filter(rank -> rank != LottoRank.NONE) // NONE 랭크 제외 + .sorted((r1, r2) -> r2.ordinal() - r1.ordinal()) // 역순 정렬 (ordinal 값을 기준으로) + .map(LottoRank::getDescription) // 각 랭크의 설명 문자열 추출 + .collect(Collectors.toList()); + } + + /** + * 각 랭크의 당첨 개수를 역순으로 반환 + */ + public List getRankCounts() { + return rankCounts.entrySet().stream() + .filter(entry -> entry.getKey() != LottoRank.NONE) // NONE 랭크 제외 + .sorted((entry1, entry2) -> entry2.getKey().ordinal() - entry1.getKey().ordinal()) // 역순 정렬 (ordinal 값을 기준으로) + .map(Map.Entry::getValue) // 각 랭크의 개수만 추출 + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/lotto/util/LottoParser.java b/src/main/java/lotto/util/LottoParser.java new file mode 100644 index 0000000000..8776a70ac8 --- /dev/null +++ b/src/main/java/lotto/util/LottoParser.java @@ -0,0 +1,22 @@ +package lotto.util; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class LottoParser { + + + private LottoParser() { + } + + /** + * 입력 정수 리스트로 변환 + */ + public static List parseNumbers(String input) { + return Arrays.stream(input.split(",")) + .map(String::trim) //스페이스 제거 + .map(Integer::parseInt) //정수 변환 + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/lotto/util/LottoValidator.java b/src/main/java/lotto/util/LottoValidator.java new file mode 100644 index 0000000000..c0908c0b4b --- /dev/null +++ b/src/main/java/lotto/util/LottoValidator.java @@ -0,0 +1,56 @@ +package lotto.util; + +import java.util.List; + +public class LottoValidator { + + private static final int LOTTO_MIN_NUMBER = 1; // 로또 번호의 최소값 + private static final int LOTTO_MAX_NUMBER = 45; // 로또 번호의 최대값 + private static final int LOTTO_NUMBER_COUNT = 6; // 로또 번호의 개수 + private static final int LOTTO_PRICE_UNIT = 1000; // 로또 구입 금액 단위 + + private LottoValidator() { + // 객체 생성 방지 + } + + /** + * 구입 금액 검증 + */ + public static void validateMoneyInput(String input) { + try { + int money = Integer.parseInt(input); + if (money % LOTTO_PRICE_UNIT != 0) { + throw new IllegalArgumentException("[ERROR] 구입 금액은 1,000원 단위여야 합니다."); + } + } catch (NumberFormatException e) { //숫자가 아닐 경우 + throw new IllegalArgumentException("[ERROR] 올바른 숫자를 입력하세요."); + } + } + + /** + * 로또 번호 리스트 검증 + */ + public static void validateNumbers(List numbers) { + if (numbers.size() != LOTTO_NUMBER_COUNT) { + throw new IllegalArgumentException("[ERROR] 당첨 번호는 6개여야 합니다."); + } + if (numbers.stream().distinct().count() != LOTTO_NUMBER_COUNT) { + throw new IllegalArgumentException("[ERROR] 당첨 번호에 중복된 숫자가 있습니다."); + } + if (numbers.stream().anyMatch(n -> n < LOTTO_MIN_NUMBER || n > LOTTO_MAX_NUMBER)) { + throw new IllegalArgumentException("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + } + + /** + * 보너스 번호 검증 + */ + public static void validateBonusNumber(int bonusNumber, List winningList) { + if (bonusNumber < LOTTO_MIN_NUMBER || bonusNumber > LOTTO_MAX_NUMBER) { + throw new IllegalArgumentException("[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다."); + } + if (winningList.contains(bonusNumber)) { + throw new IllegalArgumentException("[ERROR] 당첨 번호에 중복된 숫자가 있습니다."); + } + } +} diff --git a/src/main/java/lotto/view/LottoInputView.java b/src/main/java/lotto/view/LottoInputView.java new file mode 100644 index 0000000000..09a05efafe --- /dev/null +++ b/src/main/java/lotto/view/LottoInputView.java @@ -0,0 +1,45 @@ +package lotto.view; + +import camp.nextstep.edu.missionutils.Console; + +import java.util.List; + +import static lotto.util.LottoParser.parseNumbers; +import static lotto.util.LottoValidator.*; + +public class LottoInputView { + + /** + * 로또 구입 금액 입력 받기 + */ + public int receiveUserMoneyInput() { + System.out.println("구입금액을 입력해 주세요."); + String input = Console.readLine(); + validateMoneyInput(input); + return Integer.parseInt(input); + } + + /** + * 당첨 번호 입력 받기 + */ + public List receiveWinningNumberInput() { + System.out.println("당첨 번호를 입력해 주세요."); + String input = Console.readLine(); + List numbers = parseNumbers(input); + validateNumbers(numbers); + return numbers; + } + + /** + * 보너스 번호 입력 받기 + */ + public int receiveBonusNumberInput(List winningList) { + System.out.println("보너스 번호를 입력해 주세요."); + String input = Console.readLine(); + int result = Integer.parseInt(input); + validateBonusNumber(result, winningList); + return result; + } + + +} diff --git a/src/main/java/lotto/view/LottoOutputView.java b/src/main/java/lotto/view/LottoOutputView.java new file mode 100644 index 0000000000..9378dc7842 --- /dev/null +++ b/src/main/java/lotto/view/LottoOutputView.java @@ -0,0 +1,27 @@ +package lotto.view; + +import java.util.List; + +public class LottoOutputView { + + public void printLotto(int numberOfLotto, List> lottoNumbersList) { + System.out.println(numberOfLotto + "개를 구매했습니다."); + for (List lottoNumbers : lottoNumbersList) { + System.out.println(lottoNumbers); + } + System.out.println(); + } + + public void printWinningStatistics(List rankDescriptions, List rankCounts) { + System.out.println("당첨 통계"); + System.out.println("---"); + for (int i = 0; i < rankDescriptions.size(); i++) { +// System.out.printf("%s - %d개\n", rankDescriptions.get(i), rankCounts.get(i)); + System.out.println(rankDescriptions.get(i) + " - " + rankCounts.get(i) + "개"); + } + } + + public void printProfitRate(double profitRate) { + System.out.printf("총 수익률은 %.1f%%입니다.\n", profitRate); + } +}