diff --git a/README.md b/README.md index 5fa2560..5cf5bb4 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ # java-lotto-precourse + +# 기능 목록 + +1. 입출력 + +- 입력 : 구입 금액, 당첨 번호 목록, 보너스 번호 +- 출력 : 구입한 로또 개수와 내용물, 당첨 개수와 수익 + +2. 로또 게임 관리자 + +- 잘못된 입력 시 무한 루프 +- 입력, 처리 및 계산, 결과 출력 관장 + +3. 게임 관련 객체들 + +- 로또(변경 제한), 게임, 플레이어 객체 + +4. 로또 게임 관련 계산 처리기 + +- 로또 당첨 계산 +- 수익 계산 + +5. 예외처리 검증기 +6. 출력 메세지 관리 +7. 우승 종류 관리 \ No newline at end of file diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922..0725f96 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,17 @@ package lotto; +import lotto.config.AppConfig; +import lotto.controller.LottoController; + public class Application { + static LottoController lottoController; + public static void main(String[] args) { - // TODO: 프로그램 구현 + new Application().run(); + } + + public void run() { + lottoController = AppConfig.getLottoController(); + lottoController.startGame(); } } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java deleted file mode 100644 index 88fc5cf..0000000 --- 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("[ERROR] 로또 번호는 6개여야 합니다."); - } - } - - // TODO: 추가 기능 구현 -} diff --git a/src/main/java/lotto/config/AppConfig.java b/src/main/java/lotto/config/AppConfig.java new file mode 100644 index 0000000..c164366 --- /dev/null +++ b/src/main/java/lotto/config/AppConfig.java @@ -0,0 +1,14 @@ +package lotto.config; + +import lotto.controller.LottoController; +import lotto.service.LottoService; +import lotto.view.LottoView; + +public class AppConfig { + private static final LottoView lottoView = new LottoView(); + private static final LottoService lottoService = new LottoService(); + + public static LottoController getLottoController() { + return new LottoController(lottoView, lottoService); + } +} diff --git a/src/main/java/lotto/controller/LottoController.java b/src/main/java/lotto/controller/LottoController.java new file mode 100644 index 0000000..8261150 --- /dev/null +++ b/src/main/java/lotto/controller/LottoController.java @@ -0,0 +1,102 @@ +package lotto.controller; + +import java.util.List; +import java.util.Set; +import lotto.model.Game; +import lotto.model.Lotto; +import lotto.model.Player; +import lotto.service.LottoService; +import lotto.util.MajorErrorMessage; +import lotto.util.Validator; +import lotto.util.Validator.dataType; +import lotto.view.LottoView; + +public class LottoController { + final LottoView lottoView; + final LottoService lottoService; + + + public void startGame() { + boolean buyContinue = true; + boolean setGameContinue = true; + + while (buyContinue) { + buyContinue = buyStep(); + } + while (setGameContinue) { + setGameContinue = setGameStep(); + } + + resultStep(); + } + + public boolean buyStep() { + try { + Player player = setLottoMoney(); + attemptLottoPurchase(player); + lottoService.setPlayer(player); + return false; + } catch (IllegalArgumentException e) { + lottoView.outputCaughtError(e.getMessage()); + return true; + } + } + + public boolean setGameStep() { + try { + Game gameStep = setJackpot(); + gameStep = setBonus(gameStep); + + lottoService.setGame(gameStep); + return false; + } catch (Exception e) { + lottoView.outputCaughtError(MajorErrorMessage.LOTTONUM_WRONG.getMessage() + e.getMessage()); + return true; + } + } + + public void resultStep() { + int[] lottoResultTypes = lottoService.calculateLottoResultType(); + float lottoResultProfitPercent = lottoService.calculateLottoResultProfit(lottoResultTypes); + lottoView.outputLottoResult(lottoResultTypes, lottoResultProfitPercent); + } + + Player setLottoMoney() { + try { + String inputLine = lottoView.inputPurchaseMoney(); + return Player.setLottoMoney(Validator.isSingleInputType(inputLine, dataType.NUMBER)); + } catch (Exception e) { + throw new IllegalArgumentException(MajorErrorMessage.MONEY_WRONG.getMessage() + e.getMessage()); + } + } + + void attemptLottoPurchase(Player player) { + try { + player.buyLotto(); + lottoView.outputPurchasedLottoNum(player.getPurchasedLottoNum()); + player.getPurchasedLottoList().forEach(lotto -> lottoView.outputPurchasedLottoDetail(lotto.getNumbers())); + } catch (Exception e) { + throw new IllegalArgumentException(MajorErrorMessage.LOTTONUM_WRONG.getMessage() + e.getMessage()); + } + } + + Game setJackpot() { + String inputLine = lottoView.inputJackpotNumber(); + List jackpotNumbers = Validator.isMultipleInputType(inputLine, dataType.NUMBER, ","); + Validator.isListItemInRange(Lotto.getLottoRangeStart(), Lotto.getLottoRangeEnd(), jackpotNumbers); + Set jackpotNumberSet = Validator.isListItemDuplicated(jackpotNumbers); + return Game.setJackpot(jackpotNumberSet); + } + + Game setBonus(Game game) { + String inputLine = lottoView.inputBonusNumber(); + int bonusNumber = Validator.isSingleInputType(inputLine, dataType.NUMBER); + game.setBonus(bonusNumber); + return game; + } + + public LottoController(LottoView lottoView, LottoService lottoService) { + this.lottoView = lottoView; + this.lottoService = lottoService; + } +} diff --git a/src/main/java/lotto/model/Game.java b/src/main/java/lotto/model/Game.java new file mode 100644 index 0000000..0ca84bf --- /dev/null +++ b/src/main/java/lotto/model/Game.java @@ -0,0 +1,33 @@ +package lotto.model; + +import java.util.Set; +import lotto.util.DetailErrorMessage; + +public class Game { + private Set jackpotNumbers; + private Integer bonus; + + Game(Set jackpotNumbers) { + this.jackpotNumbers = jackpotNumbers; + } + + //static factory pattern + public static Game setJackpot(Set jackpotNumbers) { + return new Game(jackpotNumbers); + } + + public void setBonus(int bonus) { + if (jackpotNumbers.contains(bonus)) { + throw new IllegalArgumentException(DetailErrorMessage.DUPLICATED.getMessage()); + } + this.bonus = bonus; + } + + public Set getJackpotNumbers() { + return jackpotNumbers; + } + + public Integer getBonus() { + return bonus; + } +} diff --git a/src/main/java/lotto/model/Lotto.java b/src/main/java/lotto/model/Lotto.java new file mode 100644 index 0000000..042e869 --- /dev/null +++ b/src/main/java/lotto/model/Lotto.java @@ -0,0 +1,34 @@ +package lotto.model; + +import camp.nextstep.edu.missionutils.Randoms; +import java.util.List; +import lotto.util.Validator; + +public class Lotto { + private final List numbers; + static final int LOTTO_RANGE_START = 1; + static final int LOTTO_RANGE_END = 45; + static final int LOTTO_BALL_COUNT = 6; + + public Lotto(List numbers) { + Validator.isLength(numbers.size()); + Validator.isListItemDuplicated(numbers); + this.numbers = numbers; + } + + public static Lotto buyNew() { + return new Lotto(Randoms.pickUniqueNumbersInRange(LOTTO_RANGE_START, LOTTO_RANGE_END, LOTTO_BALL_COUNT)); + } + + public List getNumbers() { + return numbers; + } + + public static int getLottoRangeStart() { + return LOTTO_RANGE_START; + } + + public static int getLottoRangeEnd() { + return LOTTO_RANGE_END; + } +} diff --git a/src/main/java/lotto/model/Player.java b/src/main/java/lotto/model/Player.java new file mode 100644 index 0000000..958f4ff --- /dev/null +++ b/src/main/java/lotto/model/Player.java @@ -0,0 +1,50 @@ +package lotto.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; +import lotto.util.DetailErrorMessage; + +public class Player { + final int purchaseMoney; + int profitMoney; + List purchasedLottoList; + + public static Player setLottoMoney(int purchaseMoney) { + if (purchaseMoney <= 0) { + throw new IllegalArgumentException(DetailErrorMessage.ZERO_NEGATIVE.getMessage()); + } + if (purchaseMoney % 1000 > 0) { + throw new IllegalArgumentException(DetailErrorMessage.NOT_MULTIPLE.getMessage()); + } + return new Player(purchaseMoney); + } + + Player(int purchaseMoney) { + this.purchaseMoney = purchaseMoney; + purchasedLottoList = new ArrayList<>(); + } + + public void buyLotto() { + int howMuch = purchaseMoney / 1000; + IntStream.range(0, howMuch).forEach((i) -> { + purchasedLottoList.add(Lotto.buyNew()); + }); + } + + public List getPurchasedLottoList() { + return purchasedLottoList; + } + + public int getPurchasedLottoNum() { + return purchasedLottoList.size(); + } + + public void setProfitMoney(int profitMoney) { + this.profitMoney = profitMoney; + } + + public float getProfitPercent() { + return (float) (profitMoney * 100) / purchaseMoney; + } +} diff --git a/src/main/java/lotto/service/LottoService.java b/src/main/java/lotto/service/LottoService.java new file mode 100644 index 0000000..7ce8376 --- /dev/null +++ b/src/main/java/lotto/service/LottoService.java @@ -0,0 +1,60 @@ +package lotto.service; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.IntStream; +import lotto.model.Game; +import lotto.model.Player; +import lotto.util.GamePrize; + +public class LottoService { + private Game game; + private Player player; + private static final int PRIZE_LENGTH = GamePrize.values().length; + + public void setGame(Game game) { + this.game = game; + } + + public void setPlayer(Player player) { + this.player = player; + } + + public int[] calculateLottoResultType() { + int[] resultList = new int[PRIZE_LENGTH]; + player.getPurchasedLottoList().forEach(lotto -> updateResultList(resultList, lotto.getNumbers())); + return resultList; + } + + public float calculateLottoResultProfit(int[] resultList) { + int[] prizeMoneyList = Arrays.stream(GamePrize.values()).mapToInt(GamePrize::getPrizeMoney).toArray(); + int earnedMoney = IntStream.range(0, PRIZE_LENGTH).map(i -> resultList[i] * prizeMoneyList[i]).sum(); + player.setProfitMoney(earnedMoney); + return player.getProfitPercent(); + } + + void updateResultList(int[] resultList, List singleLotto) { + if (checkPrizeIndex(singleLotto) > -1) { + resultList[checkPrizeIndex(singleLotto)]++; + } + } + + int checkPrizeIndex(List singleLotto) { + Set jackpotNumbers = game.getJackpotNumbers(); + //일치하는 로또 공 개수 셈 + int matchingLottoBall = (int) singleLotto.stream().filter(jackpotNumbers::contains).count(); + if (matchingLottoBall == 6) { + if (hasBonusMatch(singleLotto)) { + return GamePrize.PRIZE_2ND.getIndex(); + } + return GamePrize.PRIZE_1ST.getIndex(); + } + return matchingLottoBall - 3; + } + + boolean hasBonusMatch(List singleLotto) { + int bonus = game.getBonus(); + return singleLotto.contains(bonus); + } +} diff --git a/src/main/java/lotto/util/DetailErrorMessage.java b/src/main/java/lotto/util/DetailErrorMessage.java new file mode 100644 index 0000000..2caa94e --- /dev/null +++ b/src/main/java/lotto/util/DetailErrorMessage.java @@ -0,0 +1,24 @@ +package lotto.util; + +public enum DetailErrorMessage { + BLANK("빈칸 입력됨"), + NOT_NUMBER("수 외의 문자 입력됨"), + NOT_ALPHABET("알파벳 외의 문자 입력됨"), + NOT_COMMA(", 외의 구분자 입력됨"), + NOT_LENGTH("6개 아님"), + NOT_RANGE("1부터 45까지의 숫자 아님"), + DUPLICATED("중복된 번호 입력됨"), + NOT_MULTIPLE("1000의 배수 아님"), + ZERO_NEGATIVE("0 또는 음수 입력됨"), + DEV_TYPE_WRONG("개발 오류, 지원되지 않는 자료형입니다."); + + private String message; + + DetailErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return " : " + message; + } +} diff --git a/src/main/java/lotto/util/GamePrize.java b/src/main/java/lotto/util/GamePrize.java new file mode 100644 index 0000000..1580c64 --- /dev/null +++ b/src/main/java/lotto/util/GamePrize.java @@ -0,0 +1,52 @@ +package lotto.util; + +public enum GamePrize { + PRIZE_5TH(3, 5000, false, 0), + PRIZE_4TH(4, 50000, false, 1), + PRIZE_3RD(5, 1500000, false, 2), + PRIZE_2ND(5, 30000000, true, 3), + PRIZE_1ST(6, 2000000000, false, 4); + + private final int matchingBall; + private final String formattedPrizeMoney; + private final int prizeMoney; + private final boolean isBonus; + private final int index; + private final String message; + + GamePrize(int matchingBall, int prizeMoney, boolean isBonus, int index) { + this.matchingBall = matchingBall; + this.prizeMoney = prizeMoney; + //아래는 1000을 1,000과 같이 스트링 형식으로 바꾸어줌 + this.formattedPrizeMoney = String.format("%,d", prizeMoney); + this.isBonus = isBonus; + this.index = index; + this.message = setMessage(); + } + + public String getMessage() { + return message; + } + + public int getPrizeMoney() { + return prizeMoney; + } + + public int getIndex() { + return index; + } + + public String setMessage() { + StringBuilder message = new StringBuilder(); + message.append(matchingBall).append("개 일치"); + if (isBonus) { + message.append(", 보너스 볼 일치"); + } + + message.append(" "); + + message.append("(").append(formattedPrizeMoney).append("원)"); + message.append(" - "); + return message.toString(); + } +} diff --git a/src/main/java/lotto/util/MajorErrorMessage.java b/src/main/java/lotto/util/MajorErrorMessage.java new file mode 100644 index 0000000..ef3dc70 --- /dev/null +++ b/src/main/java/lotto/util/MajorErrorMessage.java @@ -0,0 +1,17 @@ +package lotto.util; + +public enum MajorErrorMessage { + LOTTONUM_WRONG("로또 번호 입력이 잘못되었습니다."), + MONEY_WRONG("로또 구입 금액 입력이 잘못되었습니다."); + + static final String errorHeader = "[ERROR]"; + private String message; + + MajorErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return errorHeader + ' ' + message; + } +} diff --git a/src/main/java/lotto/util/SystemMessage.java b/src/main/java/lotto/util/SystemMessage.java new file mode 100644 index 0000000..eeb5961 --- /dev/null +++ b/src/main/java/lotto/util/SystemMessage.java @@ -0,0 +1,20 @@ +package lotto.util; + +public enum SystemMessage { + START_MONEY_GUIDE("구입금액을 입력해 주세요."), + RESULT_MONEY_GUIDE("%d개를 구매했습니다."), + START_TARGETLIST_GUIDE("당첨 번호를 입력해 주세요."), + START_BONUS_GUIDE("보너스 번호를 입력해 주세요."), + RESULT_JACKPOT_GUIDE("당첨 통계\n---"), + RESULT_PROFIT_GUIDE("총 수익률은 %.1f%%입니다."); + + private final String message; + + SystemMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/lotto/util/Validator.java b/src/main/java/lotto/util/Validator.java new file mode 100644 index 0000000..8550012 --- /dev/null +++ b/src/main/java/lotto/util/Validator.java @@ -0,0 +1,89 @@ +package lotto.util; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Validator { + + private final static int VALIDATE_LENGTH = 6; + + //여기서는 number밖에 안썼지만 확장성을 고려해 설계해봤습니다. + public enum dataType {NUMBER, ALPHABET;} + + public static List isMultipleInputType(String inputLine, dataType dataType, String separator) { + isBlank(inputLine); + + List process = List.of(splitter(inputLine, separator)); + isLength(process.size()); + + return process.stream() + .map(value -> isSingleInputType(value, dataType)) + .map(element -> (T) element) + .toList(); + } + + public static T isSingleInputType(String inputLine, dataType dataType) { + isBlank(inputLine); + if (dataType == Validator.dataType.ALPHABET) { + isAlphabet(inputLine); + return (T) inputLine; + } + if (dataType == Validator.dataType.NUMBER) { + isNumber(inputLine); + return (T) Integer.valueOf(inputLine); + } + throw new IllegalArgumentException(DetailErrorMessage.DEV_TYPE_WRONG.getMessage()); + } + + public static Set isListItemDuplicated(List inputList) { + Set glossary = new HashSet<>(); + for (T item : inputList) { + if (!glossary.add(item)) { + throw new IllegalArgumentException(DetailErrorMessage.DUPLICATED.getMessage()); + } + } + return glossary; + } + + public static void isListItemInRange(int startRange, int endRangeIncluded, List inputList) { + for (int item : inputList) { + if (item > endRangeIncluded || item < startRange) { + throw new IllegalArgumentException(DetailErrorMessage.NOT_RANGE.getMessage()); + } + } + } + + public static void isBlank(String inputLine) { + if (inputLine.isBlank()) { + throw new IllegalArgumentException(DetailErrorMessage.BLANK.getMessage()); + } + } + + public static void isNumber(String inputLine) { + if (!inputLine.matches("\\d+")) { + throw new NumberFormatException(DetailErrorMessage.NOT_NUMBER.getMessage()); + } + } + + public static String isAlphabet(String inputLine) { + if (!inputLine.matches("/\\^[a-zA-Z ]*\\$/")) { + throw new IllegalArgumentException(DetailErrorMessage.NOT_ALPHABET.getMessage()); + } + return inputLine; + } + + public static void isLength(int length) { + if (length != VALIDATE_LENGTH) { + throw new IllegalArgumentException(DetailErrorMessage.NOT_LENGTH.getMessage()); + } + } + + static String[] splitter(String inputLine, String separator) { + try { + return inputLine.split(separator); + } catch (Error e) { + throw new IllegalArgumentException(DetailErrorMessage.NOT_COMMA.getMessage()); + } + } +} diff --git a/src/main/java/lotto/view/LottoView.java b/src/main/java/lotto/view/LottoView.java new file mode 100644 index 0000000..02f23ff --- /dev/null +++ b/src/main/java/lotto/view/LottoView.java @@ -0,0 +1,45 @@ +package lotto.view; + +import camp.nextstep.edu.missionutils.Console; +import java.util.List; +import java.util.stream.IntStream; +import lotto.util.GamePrize; +import lotto.util.SystemMessage; + +public class LottoView { + + public String inputPurchaseMoney() { + System.out.println(SystemMessage.START_MONEY_GUIDE.getMessage()); + return Console.readLine(); + } + + public String inputJackpotNumber() { + System.out.println(SystemMessage.START_TARGETLIST_GUIDE.getMessage()); + return Console.readLine(); + } + + public String inputBonusNumber() { + System.out.println(SystemMessage.START_BONUS_GUIDE.getMessage()); + return Console.readLine(); + } + + public void outputPurchasedLottoNum(int lottoNum) { + System.out.println(String.format(SystemMessage.RESULT_MONEY_GUIDE.getMessage(), lottoNum)); + } + + public void outputPurchasedLottoDetail(List lotto) { + System.out.println(lotto); + } + + public void outputLottoResult(int[] resultList, float profitPercent) { + GamePrize[] prizes = GamePrize.values(); + System.out.println(SystemMessage.RESULT_JACKPOT_GUIDE.getMessage()); + IntStream.range(0, prizes.length) + .forEach(i -> System.out.println(prizes[i].getMessage() + resultList[i] + "개")); + System.out.println(String.format(SystemMessage.RESULT_PROFIT_GUIDE.getMessage(), profitPercent)); + } + + public void outputCaughtError(String errorMessage) { + System.out.println(errorMessage); + } +} diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java index 309f4e5..c8ca9d6 100644 --- a/src/test/java/lotto/LottoTest.java +++ b/src/test/java/lotto/LottoTest.java @@ -1,5 +1,6 @@ package lotto; +import lotto.model.Lotto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test;