diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..778bab9ccf 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,14 @@ package lotto; +import lotto.config.AppConfig; +import lotto.controller.LottoController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + + AppConfig appConfig = new AppConfig(); + LottoController controller = appConfig.lottoController(); + + controller.run(); } } diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java deleted file mode 100644 index 88fc5cf12b..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("[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 0000000000..b1e68c8cf3 --- /dev/null +++ b/src/main/java/lotto/config/AppConfig.java @@ -0,0 +1,25 @@ +package lotto.config; + +import lotto.controller.LottoController; +import lotto.domain.LottoMachine; +import lotto.view.InputView; +import lotto.view.OutputView; + +public class AppConfig { + + public LottoController lottoController() { + return new LottoController(inputView(), outputView(), lottoMachine()); + } + + private InputView inputView() { + return new InputView(); + } + + private OutputView outputView() { + return new OutputView(); + } + + private LottoMachine lottoMachine() { + return new LottoMachine(); + } +} diff --git a/src/main/java/lotto/constants/ErrorMessage.java b/src/main/java/lotto/constants/ErrorMessage.java new file mode 100644 index 0000000000..dc19833459 --- /dev/null +++ b/src/main/java/lotto/constants/ErrorMessage.java @@ -0,0 +1,22 @@ +package lotto.constants; + +public enum ErrorMessage { + INVALID_SIZE("로또 번호는 " + LottoRule.LOTTO_SIZE + "개여야 합니다."), + DUPLICATE_NUMBER("로또 번호는 중복될 수 없습니다."), + INVALID_RANGE("로또 번호는 " + LottoRule.MIN_NUMBER + "부터 " + LottoRule.MAX_NUMBER + " 사이여야 합니다."), + INVALID_MONEY("로또 구입 금액은 " + LottoRule.TICKET_PRICE + "원 단위여야 합니다."), + DUPLICATE_BONUS_NUMBER("보너스 번호는 당첨 번호와 중복될 수 없습니다."), + EMPTY_INPUT("입력값이 비어있습니다."), + NOT_NUMERIC("숫자만 입력해 주세요."); + + private static final String PREFIX = "[ERROR] "; + private final String message; + + ErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return PREFIX + message; + } +} diff --git a/src/main/java/lotto/constants/InputMessage.java b/src/main/java/lotto/constants/InputMessage.java new file mode 100644 index 0000000000..9ee2b47800 --- /dev/null +++ b/src/main/java/lotto/constants/InputMessage.java @@ -0,0 +1,17 @@ +package lotto.constants; + +public enum InputMessage { + REQUEST_PURCHASE_AMOUNT("구입금액을 입력해 주세요."), + REQUEST_WINNING_NUMBERS("\n당첨 번호를 입력해 주세요."), + REQUEST_BONUS_NUMBER("\n보너스 번호를 입력해 주세요."); + + private final String message; + + InputMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/lotto/constants/LottoRule.java b/src/main/java/lotto/constants/LottoRule.java new file mode 100644 index 0000000000..efc2573d06 --- /dev/null +++ b/src/main/java/lotto/constants/LottoRule.java @@ -0,0 +1,11 @@ +package lotto.constants; + +public class LottoRule { + public static final int MIN_NUMBER = 1; + public static final int MAX_NUMBER = 45; + public static final int LOTTO_SIZE = 6; + public static final int TICKET_PRICE = 1000; + + private LottoRule() { + } +} diff --git a/src/main/java/lotto/constants/OutputMessage.java b/src/main/java/lotto/constants/OutputMessage.java new file mode 100644 index 0000000000..5d11923b7e --- /dev/null +++ b/src/main/java/lotto/constants/OutputMessage.java @@ -0,0 +1,19 @@ +package lotto.constants; + +public enum OutputMessage { + PURCHASE_COUNT_MESSAGE("\n%d개를 구매했습니다.\n"), + WINNING_STATISTICS_HEADER("\n당첨 통계\n---"), + YIELD_MESSAGE("총 수익률은 %.1f%%입니다.\n"), + NORMAL_RANK_FORMAT("%d개 일치 (%,d원) - %d개\n"), + SECOND_RANK_FORMAT("%d개 일치, 보너스 볼 일치 (%,d원) - %d개\n"); + + private final String message; + + OutputMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/lotto/controller/LottoController.java b/src/main/java/lotto/controller/LottoController.java new file mode 100644 index 0000000000..42c313506a --- /dev/null +++ b/src/main/java/lotto/controller/LottoController.java @@ -0,0 +1,46 @@ +package lotto.controller; + +import lotto.domain.Lotto; +import lotto.domain.LottoMachine; +import lotto.domain.LottoResult; +import lotto.domain.WinningLotto; +import lotto.view.InputView; +import lotto.view.OutputView; + +import java.util.List; + +public class LottoController { + private final InputView inputView; + private final OutputView outputView; + private final LottoMachine lottoMachine; // 의존성 주입받을 도메인 + + public LottoController(InputView inputView, OutputView outputView, LottoMachine lottoMachine) { + this.inputView = inputView; + this.outputView = outputView; + this.lottoMachine = lottoMachine; + } + + public void run() { + try { + int purchaseAmount = inputView.readPurchaseAmount(); + List purchasedLottos = lottoMachine.buyLottos(purchaseAmount); + outputView.printPurchasedLottos(purchasedLottos); + + List winningNumbers = inputView.readWinningNumbers(); + Lotto winningLottoNumbers = new Lotto(winningNumbers); + + int bonusNumber = inputView.readBonusNumber(); + WinningLotto winningLotto = new WinningLotto(winningLottoNumbers, bonusNumber); + + LottoResult lottoResult = new LottoResult(); + lottoResult.compareLottos(purchasedLottos, winningLotto); + + outputView.printWinningStatistics(lottoResult); + double yield = lottoResult.calculateYield(purchaseAmount); + outputView.printTotalYield(yield); + + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e); + } + } +} diff --git a/src/main/java/lotto/domain/Lotto.java b/src/main/java/lotto/domain/Lotto.java new file mode 100644 index 0000000000..f58cabe97c --- /dev/null +++ b/src/main/java/lotto/domain/Lotto.java @@ -0,0 +1,45 @@ +package lotto.domain; + +import lotto.constants.ErrorMessage; +import lotto.constants.LottoRule; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Lotto { + private final List numbers; + + public Lotto(List numbers) { + validateSize(numbers); + validateDuplicate(numbers); + this.numbers = numbers; + } + + public List getNumbers() { + return numbers; + } + + private void validateSize(List numbers) { + if (numbers.size() != LottoRule.LOTTO_SIZE) { + throw new IllegalArgumentException(ErrorMessage.INVALID_SIZE.getMessage()); + } + } + + private void validateDuplicate(List numbers) { + Set uniqueNumbers = new HashSet<>(numbers); + if (uniqueNumbers.size() != numbers.size()) { + throw new IllegalArgumentException(ErrorMessage.DUPLICATE_NUMBER.getMessage()); + } + } + + public boolean contains(int number) { + return numbers.contains(number); + } + + public int countMatchingNumbers(Lotto otherLotto) { + return (int) numbers.stream() + .filter(otherLotto::contains) + .count(); + } +} diff --git a/src/main/java/lotto/domain/LottoMachine.java b/src/main/java/lotto/domain/LottoMachine.java new file mode 100644 index 0000000000..adaa452c5d --- /dev/null +++ b/src/main/java/lotto/domain/LottoMachine.java @@ -0,0 +1,45 @@ +package lotto.domain; + +import camp.nextstep.edu.missionutils.Randoms; +import lotto.constants.ErrorMessage; +import lotto.constants.LottoRule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LottoMachine { + + public List buyLottos(int money) { + validateMoney(money); + int count = money / LottoRule.TICKET_PRICE; + return generateLottos(count); + } + + private void validateMoney(int money) { + if (money <= 0 || money % LottoRule.TICKET_PRICE != 0) { + throw new IllegalArgumentException(ErrorMessage.INVALID_MONEY.getMessage()); + } + } + + private List generateLottos(int count) { + List lottos = new ArrayList<>(); + for (int i = 0; i < count; i++) { + lottos.add(generateSingleLotto()); + } + return lottos; + } + + private Lotto generateSingleLotto() { + List numbers = Randoms.pickUniqueNumbersInRange( + LottoRule.MIN_NUMBER, + LottoRule.MAX_NUMBER, + LottoRule.LOTTO_SIZE + ); + + List sortedNumbers = new ArrayList<>(numbers); + Collections.sort(sortedNumbers); + + return new Lotto(sortedNumbers); + } +} diff --git a/src/main/java/lotto/domain/LottoResult.java b/src/main/java/lotto/domain/LottoResult.java new file mode 100644 index 0000000000..56503b7e9a --- /dev/null +++ b/src/main/java/lotto/domain/LottoResult.java @@ -0,0 +1,46 @@ +package lotto.domain; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class LottoResult { + private final Map result; + + public LottoResult() { + this.result = new EnumMap<>(Rank.class); + for (Rank rank : Rank.values()) { + result.put(rank, 0); + } + } + + public void compareLottos(List userLottos, WinningLotto winningLotto) { + for (Lotto lotto : userLottos) { + Rank rank = winningLotto.match(lotto); + result.put(rank, result.get(rank) + 1); + } + } + + private long calculateTotalPrize() { + long totalPrize = 0; + for (Map.Entry entry : result.entrySet()) { + totalPrize += (long) entry.getKey().getPrizeMoney() * entry.getValue(); + } + return totalPrize; + } + + public double calculateYield(int purchaseAmount) { + if (purchaseAmount == 0) { + return 0.0; + } + long totalPrize = calculateTotalPrize(); + double yield = ((double) totalPrize / purchaseAmount) * 100; + + return Math.round(yield * 10.0) / 10.0; + } + + public Map getResult() { + return Collections.unmodifiableMap(result); + } +} diff --git a/src/main/java/lotto/domain/Rank.java b/src/main/java/lotto/domain/Rank.java new file mode 100644 index 0000000000..61e022da42 --- /dev/null +++ b/src/main/java/lotto/domain/Rank.java @@ -0,0 +1,45 @@ +package lotto.domain; + +public enum Rank { + FIRST(6, 2_000_000_000), + SECOND(5, 30_000_000), + THIRD(5, 1_500_000), + FOURTH(4, 50_000), + FIFTH(3, 5_000), + NONE(0, 0); + + private final int matchCount; + private final int prizeMoney; + + Rank(int matchCount, int prizeMoney) { + this.matchCount = matchCount; + this.prizeMoney = prizeMoney; + } + + public static Rank valueOf(int matchCount, boolean matchBonus) { + if (matchCount == 6) { + return FIRST; + } + if (matchCount == 5 && matchBonus) { + return SECOND; + } + if (matchCount == 5 && !matchBonus) { + return THIRD; + } + if (matchCount == 4) { + return FOURTH; + } + if (matchCount == 3) { + return FIFTH; + } + return NONE; + } + + public int getMatchCount() { + return matchCount; + } + + public int getPrizeMoney() { + return prizeMoney; + } +} diff --git a/src/main/java/lotto/domain/WinningLotto.java b/src/main/java/lotto/domain/WinningLotto.java new file mode 100644 index 0000000000..5cb23857e5 --- /dev/null +++ b/src/main/java/lotto/domain/WinningLotto.java @@ -0,0 +1,31 @@ +package lotto.domain; + +import lotto.constants.ErrorMessage; +import lotto.constants.LottoRule; + +public class WinningLotto { + private final Lotto winningLotto; + private final int bonusNumber; + + public WinningLotto(Lotto winningLotto, int bonusNumber) { + validateBonusNumber(winningLotto, bonusNumber); + this.winningLotto = winningLotto; + this.bonusNumber = bonusNumber; + } + + private void validateBonusNumber(Lotto winningLotto, int bonusNumber) { + if (bonusNumber < LottoRule.MIN_NUMBER || bonusNumber > LottoRule.MAX_NUMBER) { + throw new IllegalArgumentException(ErrorMessage.INVALID_RANGE.getMessage()); + } + if (winningLotto.contains(bonusNumber)) { + throw new IllegalArgumentException(ErrorMessage.DUPLICATE_BONUS_NUMBER.getMessage()); + } + } + + public Rank match(Lotto userLotto) { + int matchCount = userLotto.countMatchingNumbers(winningLotto); + boolean matchBonus = userLotto.contains(bonusNumber); + + return Rank.valueOf(matchCount, matchBonus); + } +} diff --git a/src/main/java/lotto/util/InputValidator.java b/src/main/java/lotto/util/InputValidator.java new file mode 100644 index 0000000000..02139e4f74 --- /dev/null +++ b/src/main/java/lotto/util/InputValidator.java @@ -0,0 +1,21 @@ +package lotto.util; + +import lotto.constants.ErrorMessage; + +public class InputValidator { + + private InputValidator() { + } + + public static void validateEmpty(String input) { + if (input == null || input.trim().isEmpty()) { + throw new IllegalArgumentException(ErrorMessage.EMPTY_INPUT.getMessage()); + } + } + + public static void validateNumeric(String input) { + if (!input.matches("^[0-9]+$")) { + throw new IllegalArgumentException(ErrorMessage.NOT_NUMERIC.getMessage()); + } + } +} diff --git a/src/main/java/lotto/util/Parser.java b/src/main/java/lotto/util/Parser.java new file mode 100644 index 0000000000..aac41fc178 --- /dev/null +++ b/src/main/java/lotto/util/Parser.java @@ -0,0 +1,34 @@ +package lotto.util; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Parser { + private static final String DELIMITER = ","; + + private Parser() { + } + + public static int parsePurchaseAmount(String input) { + return parseToInt(input); + } + + public static List parseWinningNumbers(String input) { + InputValidator.validateEmpty(input); + return Arrays.stream(input.split(DELIMITER)) + .map(String::trim) + .map(Parser::parseToInt) // 내부 비공개 메서드 재사용 + .collect(Collectors.toList()); + } + + public static int parseBonusNumber(String input) { + return parseToInt(input); + } + + private static int parseToInt(String input) { + InputValidator.validateEmpty(input); + InputValidator.validateNumeric(input); + return Integer.parseInt(input); + } +} diff --git a/src/main/java/lotto/view/InputView.java b/src/main/java/lotto/view/InputView.java new file mode 100644 index 0000000000..39efb67696 --- /dev/null +++ b/src/main/java/lotto/view/InputView.java @@ -0,0 +1,27 @@ +package lotto.view; + +import camp.nextstep.edu.missionutils.Console; +import lotto.constants.InputMessage; +import lotto.util.Parser; +import java.util.List; + +public class InputView { + + public int readPurchaseAmount() { + System.out.println(InputMessage.REQUEST_PURCHASE_AMOUNT.getMessage()); + String input = Console.readLine(); + return Parser.parsePurchaseAmount(input); + } + + public List readWinningNumbers() { + System.out.println(InputMessage.REQUEST_WINNING_NUMBERS.getMessage()); + String input = Console.readLine(); + return Parser.parseWinningNumbers(input); + } + + public int readBonusNumber() { + System.out.println(InputMessage.REQUEST_BONUS_NUMBER.getMessage()); + String input = Console.readLine(); + return Parser.parseBonusNumber(input); + } +} diff --git a/src/main/java/lotto/view/OutputView.java b/src/main/java/lotto/view/OutputView.java new file mode 100644 index 0000000000..daa9bf6ab7 --- /dev/null +++ b/src/main/java/lotto/view/OutputView.java @@ -0,0 +1,48 @@ +package lotto.view; + +import lotto.constants.OutputMessage; +import lotto.domain.Lotto; +import lotto.domain.LottoResult; +import lotto.domain.Rank; + +import java.util.List; +import java.util.Map; + +public class OutputView { + + public void printPurchasedLottos(List lottos) { + System.out.printf(OutputMessage.PURCHASE_COUNT_MESSAGE.getMessage(), lottos.size()); + for (Lotto lotto : lottos) { + System.out.println(lotto.getNumbers()); + } + } + + public void printWinningStatistics(LottoResult lottoResult) { + System.out.println(OutputMessage.WINNING_STATISTICS_HEADER.getMessage()); + Map result = lottoResult.getResult(); + + printRankCondition(Rank.FIFTH, result.getOrDefault(Rank.FIFTH, 0)); + printRankCondition(Rank.FOURTH, result.getOrDefault(Rank.FOURTH, 0)); + printRankCondition(Rank.THIRD, result.getOrDefault(Rank.THIRD, 0)); + printRankCondition(Rank.SECOND, result.getOrDefault(Rank.SECOND, 0)); + printRankCondition(Rank.FIRST, result.getOrDefault(Rank.FIRST, 0)); + } + + private void printRankCondition(Rank rank, int count) { + if (rank == Rank.SECOND) { + System.out.printf(OutputMessage.SECOND_RANK_FORMAT.getMessage(), + rank.getMatchCount(), rank.getPrizeMoney(), count); + return; + } + System.out.printf(OutputMessage.NORMAL_RANK_FORMAT.getMessage(), + rank.getMatchCount(), rank.getPrizeMoney(), count); + } + + public void printTotalYield(double yield) { + System.out.printf(OutputMessage.YIELD_MESSAGE.getMessage(), yield); + } + + public void printErrorMessage(Exception e) { + System.out.println(e.getMessage()); + } +} diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java index 309f4e50ae..c74fb4b676 100644 --- a/src/test/java/lotto/LottoTest.java +++ b/src/test/java/lotto/LottoTest.java @@ -1,5 +1,6 @@ package lotto; +import lotto.domain.Lotto; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test;