diff --git a/docs/README.md b/docs/README.md
index e69de29..b7e5778 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -0,0 +1,90 @@
+## Feature
+
+- [X] User는 `1000원` 단위로 로또 N장을 구매한다
+ - 구매 후 로또 생성기로부터 N장의 로또를 생성한다
+ - `1000원` 단위로 입력하지 않을 경우 `IllegalArgumentException`를 발생시키고 `[Error]`로 시작하는 에러 메시지를 출력 후 `종료`한다
+
+- [X] LottoWinningMachine은 로또 당첨 번호를 생성한다
+ - 총 7개의 중복되지 않는 숫자 (기본 당첨 번호 6개 + 보너스 번호 1개)
+
+- [X] LottoStatistics는 N장의 로또에 대한 당첨 통계를 산출한다
+
+
+
+
+## Model
+
+### `Lotto`
+
+- 로또 번호 List를 추상화시킨 모델
+ - [X] 로또 번호는 1..45 범위 안에 존재해야 한다
+ - [X] 각 번호들은 중복되지 않아야 한다
+
+### `UserLotto`
+
+- `1000원` 단위의 로또 N장을 구매하는 컴포넌트
+ - `List`
+
+### `LottoWinningMachine`
+
+- 로또 당첨 번호 6개 + 보너스 번호를 생성하는 컴포넌트
+
+### `BonusNumber`
+
+- `보너스 번호`에 대한 컴포넌트
+
+### `WinningRank`
+
+- 당첨과 관련된 순위를 표현하는 컴포넌트
+
+### `LottoStatistics`
+
+- 로또 당첨 내역 및 수익률을 산출하기 위한 통계용 컴포넌트
+
+
+
+
+## Utils
+
+### `ExceptionConstants`
+
+- 전역 예외 메시지 통합 컴포넌트
+
+### `LottoConstants`
+
+- Lotto 숫자 범위 & 전체 사이즈 관련 상수 전용 컴포넌트
+
+### `LottoRandomGenerator`
+
+- 로또 번호 자동 생성기
+
+### `Validator`
+
+- 사용자 Input에 대한 기본 검증 컴포넌트
+ - `LottoPurchaseAmountValidator` -> 로또 구입금액 검증 컴포넌트
+ - `WinningNumberValidator` -> 로또 당첨번호 검증 컴포넌트
+ - `BonusNumberValidator` -> 보너스 번호 검증 컴포넌트
+
+
+
+
+## View
+
+### `InputView`
+
+- 사용자 Input을 받기 위한 컴포넌트
+
+### `OutputView`
+
+- 로또 게임 진행과 관련된 출력 컴포넌트
+
+
+
+
+## Controller
+
+### `GameController`
+
+- 로또 게임 진행과 관련된 컨트롤러
+
+
diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java
index d190922..8fb5a65 100644
--- a/src/main/java/lotto/Application.java
+++ b/src/main/java/lotto/Application.java
@@ -1,7 +1,9 @@
package lotto;
+import lotto.controller.GameController;
+
public class Application {
public static void main(String[] args) {
- // TODO: 프로그램 구현
+ new GameController().run();
}
}
diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java
deleted file mode 100644
index 519793d..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();
- }
- }
-
- // TODO: 추가 기능 구현
-}
diff --git a/src/main/java/lotto/controller/GameController.java b/src/main/java/lotto/controller/GameController.java
new file mode 100644
index 0000000..fdbd937
--- /dev/null
+++ b/src/main/java/lotto/controller/GameController.java
@@ -0,0 +1,41 @@
+package lotto.controller;
+
+import lotto.model.LottoStatistics;
+import lotto.model.LottoWinningMachine;
+import lotto.model.UserLotto;
+import lotto.view.InputView;
+import lotto.view.OutputView;
+
+import java.util.List;
+
+public class GameController {
+ private static UserLotto userLotto;
+ private static LottoWinningMachine lottoWinningMachine;
+
+ public void run() {
+ try {
+ buyLotto();
+ initLottoWinningMachine();
+ displayLottoResult();
+ } catch (IllegalArgumentException e) {
+ OutputView.printErrorMessage(e.getMessage());
+ }
+ }
+
+ private void buyLotto() {
+ final int lottoPurchaseCount = InputView.readLottoPurchaseCount();
+ userLotto = UserLotto.issueLottoByPurchaseCount(lottoPurchaseCount);
+ OutputView.printPurchaseInformation(userLotto.getUserLottos());
+ }
+
+ private void initLottoWinningMachine() {
+ final List winingNumbers = InputView.readWiningNumbers();
+ final int bonusNumber = InputView.readBonusNumber();
+ lottoWinningMachine = LottoWinningMachine.drawWinningLottery(winingNumbers, bonusNumber);
+ }
+
+ private void displayLottoResult() {
+ final LottoStatistics lottoStatistics = LottoStatistics.checkLotteryResult(lottoWinningMachine, userLotto);
+ OutputView.printWinningStatistics(lottoStatistics);
+ }
+}
diff --git a/src/main/java/lotto/model/BonusNumber.java b/src/main/java/lotto/model/BonusNumber.java
new file mode 100644
index 0000000..e0cf256
--- /dev/null
+++ b/src/main/java/lotto/model/BonusNumber.java
@@ -0,0 +1,28 @@
+package lotto.model;
+
+import static lotto.utils.ExceptionConstants.LottoMachineException.BONUS_NUMBER_IS_NOT_IN_RANGE;
+import static lotto.utils.LottoConstants.MAX_VALUE;
+import static lotto.utils.LottoConstants.MIN_VALUE;
+
+public class BonusNumber {
+ private final int value;
+
+ public BonusNumber(final int value) {
+ validateBonusNumberIsInRange(value);
+ this.value = value;
+ }
+
+ private void validateBonusNumberIsInRange(final int value) {
+ if (isOutOfRange(value)) {
+ throw new IllegalArgumentException(BONUS_NUMBER_IS_NOT_IN_RANGE.message);
+ }
+ }
+
+ private boolean isOutOfRange(final int value) {
+ return value < MIN_VALUE || value > MAX_VALUE;
+ }
+
+ public int getValue() {
+ return value;
+ }
+}
diff --git a/src/main/java/lotto/model/Lotto.java b/src/main/java/lotto/model/Lotto.java
new file mode 100644
index 0000000..63c26b0
--- /dev/null
+++ b/src/main/java/lotto/model/Lotto.java
@@ -0,0 +1,58 @@
+package lotto.model;
+
+import java.util.Collections;
+import java.util.List;
+
+import static java.util.Collections.sort;
+import static lotto.utils.ExceptionConstants.LottoException.*;
+import static lotto.utils.LottoConstants.*;
+
+public class Lotto {
+ private final List numbers;
+
+ public Lotto(final List numbers) {
+ validateEachLottoElementIsInRange(numbers);
+ validateTotalLottoSize(numbers);
+ validateLottoHasDuplicateElement(numbers);
+ sort(numbers);
+ this.numbers = numbers;
+ }
+
+ private void validateEachLottoElementIsInRange(final List numbers) {
+ if (hasOutOfRange(numbers)) {
+ throw new IllegalArgumentException(LOTTO_NUMBER_IS_NOT_IN_RANGE.message);
+ }
+ }
+
+ private boolean hasOutOfRange(final List numbers) {
+ return numbers.stream()
+ .anyMatch(number -> number < MIN_VALUE || number > MAX_VALUE);
+ }
+
+ private void validateTotalLottoSize(final List numbers) {
+ if (numbers.size() != LOTTO_SIZE) {
+ throw new IllegalArgumentException(LOTTO_SIZE_IS_NOT_FULFILL.message);
+ }
+ }
+
+ private void validateLottoHasDuplicateElement(final List numbers) {
+ if (hasDuplicateNumber(numbers)) {
+ throw new IllegalArgumentException(LOTTO_NUMBER_MUST_BE_UNIQUE.message);
+ }
+ }
+
+ private boolean hasDuplicateNumber(final List number) {
+ return number.stream()
+ .distinct()
+ .count() != LOTTO_SIZE;
+ }
+
+ public List getNumbers() {
+ return Collections.unmodifiableList(numbers);
+ }
+
+ @Override
+ public String toString() {
+ return numbers.toString();
+ }
+}
diff --git a/src/main/java/lotto/model/LottoStatistics.java b/src/main/java/lotto/model/LottoStatistics.java
new file mode 100644
index 0000000..1ed9a59
--- /dev/null
+++ b/src/main/java/lotto/model/LottoStatistics.java
@@ -0,0 +1,104 @@
+package lotto.model;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+public class LottoStatistics {
+ private final LottoWinningMachine lottoWinningMachine;
+ private final UserLotto userLotto;
+ private final Map winningResult = new EnumMap<>(WinningRank.class);
+
+ private LottoStatistics(
+ final LottoWinningMachine lottoWinningMachine,
+ final UserLotto userLotto
+ ) {
+ this.lottoWinningMachine = lottoWinningMachine;
+ this.userLotto = userLotto;
+ initWinningResult();
+ calculateLotteryWinningResult();
+ }
+
+ public static LottoStatistics checkLotteryResult(
+ final LottoWinningMachine lottoWinningMachine,
+ final UserLotto userLotto
+ ) {
+ return new LottoStatistics(lottoWinningMachine, userLotto);
+ }
+
+ private void initWinningResult() {
+ for (WinningRank winningRank : WinningRank.values()) {
+ winningResult.put(winningRank, 0);
+ }
+ }
+
+ private void calculateLotteryWinningResult() {
+ List winningLotteryNumbers = lottoWinningMachine.getWinningLotteryNumbers();
+ int bonusNumber = lottoWinningMachine.getBonusNumber();
+
+ for (Lotto lotto : userLotto.getUserLottos()) {
+ List lottoNumbers = lotto.getNumbers();
+ int matchCount = getLottoMatchCount(lottoNumbers, winningLotteryNumbers);
+ boolean hasBonus = isBonusNumberExists(lottoNumbers, bonusNumber);
+
+ WinningRank winningRank = WinningRank.of(matchCount, hasBonus);
+ updateWinningResult(winningRank);
+ }
+ }
+
+ private int getLottoMatchCount(
+ final List lottoNumbers,
+ final List winningLotteryNumbers
+ ) {
+ return (int) lottoNumbers.stream()
+ .filter(winningLotteryNumbers::contains)
+ .count();
+ }
+
+ private boolean isBonusNumberExists(
+ final List lottoNumbers,
+ final int bonusNumber
+ ) {
+ return lottoNumbers.contains(bonusNumber);
+ }
+
+ private void updateWinningResult(final WinningRank winningRank) {
+ winningResult.put(winningRank, winningResult.get(winningRank) + 1);
+ }
+
+ public Map getWinningResult() {
+ return Collections.unmodifiableMap(winningResult);
+ }
+
+ public int getWinningCountByRank(final WinningRank winningRank) {
+ return winningResult.getOrDefault(winningRank, 0);
+ }
+
+ public BigDecimal getEarningRate() {
+ final BigDecimal lottoPurchaseAmount = userLotto.getLottoPurchaseAmount();
+ final BigDecimal totalWinningAmount = calculateTotalWinningAmount();
+
+ return totalWinningAmount
+ .multiply(BigDecimal.valueOf(100)) // 백분율
+ .divide(lottoPurchaseAmount, 1, RoundingMode.HALF_UP);
+ }
+
+ private BigDecimal calculateTotalWinningAmount() {
+ BigDecimal amount = BigDecimal.ZERO;
+ for (WinningRank winningRank : winningResult.keySet()) {
+ final BigDecimal addPrize = getAddPrize(winningRank);
+ amount = amount.add(addPrize);
+ }
+ return amount;
+ }
+
+ private BigDecimal getAddPrize(final WinningRank winningRank) {
+ final int reward = winningRank.getReward();
+ final int count = winningResult.get(winningRank);
+
+ return BigDecimal.valueOf((long) reward * count);
+ }
+}
diff --git a/src/main/java/lotto/model/LottoWinningMachine.java b/src/main/java/lotto/model/LottoWinningMachine.java
new file mode 100644
index 0000000..5d32380
--- /dev/null
+++ b/src/main/java/lotto/model/LottoWinningMachine.java
@@ -0,0 +1,43 @@
+package lotto.model;
+
+import java.util.List;
+
+import static lotto.utils.ExceptionConstants.LottoMachineException.BONUS_NUMBER_MUST_BE_UNIQUE;
+
+public class LottoWinningMachine {
+ private final Lotto winningLottery;
+ private final BonusNumber bonusNumber;
+
+ private LottoWinningMachine(
+ final List winningNumbers,
+ final int bonusNumber
+ ) {
+ validateBonusNumberIsDuplicate(winningNumbers, bonusNumber);
+ this.winningLottery = new Lotto(winningNumbers);
+ this.bonusNumber = new BonusNumber(bonusNumber);
+ }
+
+ public static LottoWinningMachine drawWinningLottery(
+ final List winningNumbers,
+ final int bonusNumber
+ ) {
+ return new LottoWinningMachine(winningNumbers, bonusNumber);
+ }
+
+ private void validateBonusNumberIsDuplicate(
+ final List winningNumbers,
+ final int bonusNumber
+ ) {
+ if (winningNumbers.contains(bonusNumber)) {
+ throw new IllegalArgumentException(BONUS_NUMBER_MUST_BE_UNIQUE.message);
+ }
+ }
+
+ public List getWinningLotteryNumbers() {
+ return winningLottery.getNumbers();
+ }
+
+ public int getBonusNumber() {
+ return bonusNumber.getValue();
+ }
+}
diff --git a/src/main/java/lotto/model/UserLotto.java b/src/main/java/lotto/model/UserLotto.java
new file mode 100644
index 0000000..ca016e6
--- /dev/null
+++ b/src/main/java/lotto/model/UserLotto.java
@@ -0,0 +1,45 @@
+package lotto.model;
+
+import lotto.utils.LottoRandomGenerator;
+import org.assertj.core.util.VisibleForTesting;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class UserLotto {
+ private final List userLottos;
+
+ private UserLotto(final int lottoPurchaseCount) {
+ this.userLottos = createLottoByPurchaseCount(lottoPurchaseCount);
+ }
+
+ @VisibleForTesting
+ public UserLotto(final List userLottos) {
+ this.userLottos = userLottos;
+ }
+
+ public static UserLotto issueLottoByPurchaseCount(final int lottoPurchaseCount) {
+ return new UserLotto(lottoPurchaseCount);
+ }
+
+ private List createLottoByPurchaseCount(final int lottoPurchaseCount) {
+ return Stream.generate(() -> new Lotto(LottoRandomGenerator.generate()))
+ .limit(lottoPurchaseCount)
+ .collect(Collectors.toList());
+ }
+
+ public List getUserLottos() {
+ return Collections.unmodifiableList(userLottos);
+ }
+
+ public int getLottoPurchaseCount() {
+ return userLottos.size();
+ }
+
+ public BigDecimal getLottoPurchaseAmount() {
+ return BigDecimal.valueOf(1000).multiply(BigDecimal.valueOf(getLottoPurchaseCount()));
+ }
+}
diff --git a/src/main/java/lotto/model/WinningRank.java b/src/main/java/lotto/model/WinningRank.java
new file mode 100644
index 0000000..1773381
--- /dev/null
+++ b/src/main/java/lotto/model/WinningRank.java
@@ -0,0 +1,50 @@
+package lotto.model;
+
+import java.util.Arrays;
+import java.util.List;
+
+public enum WinningRank {
+ FIRST(6, List.of(false), 2_000_000_000, "6개 일치"),
+ SECOND(5, List.of(true), 30_000_000, "5개 일치, 보너스 볼 일치"),
+ THIRD(5, List.of(false), 1_500_000, "5개 일치"),
+ FOURTH(4, List.of(true, false), 50_000, "4개 일치"),
+ FIFTH(3, List.of(true, false), 5_000, "3개 일치"),
+ NONE(0, List.of(false), 0, "NONE..."),
+ ;
+
+ private final int matchCount;
+ private final List hasBonus;
+ private final int reward;
+ private final String description;
+
+ WinningRank(
+ final int matchCount,
+ final List hasBonus,
+ final int reward,
+ final String description
+ ) {
+ this.matchCount = matchCount;
+ this.hasBonus = hasBonus;
+ this.reward = reward;
+ this.description = description;
+ }
+
+ public static WinningRank of(
+ final int matchCount,
+ final boolean hasBonus
+ ) {
+ return Arrays.stream(values())
+ .filter(winningRank -> winningRank.matchCount == matchCount)
+ .filter(winningRank -> winningRank.hasBonus.contains(hasBonus))
+ .findFirst()
+ .orElse(NONE);
+ }
+
+ public int getReward() {
+ return reward;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/src/main/java/lotto/utils/ExceptionConstants.java b/src/main/java/lotto/utils/ExceptionConstants.java
new file mode 100644
index 0000000..339c422
--- /dev/null
+++ b/src/main/java/lotto/utils/ExceptionConstants.java
@@ -0,0 +1,43 @@
+package lotto.utils;
+
+public interface ExceptionConstants {
+ enum LottoException {
+ LOTTO_NUMBER_IS_NOT_IN_RANGE("로또 번호는 1부터 45 사이의 숫자여야 합니다."),
+ LOTTO_SIZE_IS_NOT_FULFILL("구매한 로또는 총 6개의 번호가 기입되어야 합니다."),
+ LOTTO_NUMBER_MUST_BE_UNIQUE("중복된 번호는 허용하지 않습니다."),
+ ;
+
+ public final String message;
+
+ LottoException(final String message) {
+ this.message = message;
+ }
+ }
+
+ enum LottoMachineException {
+ BONUS_NUMBER_IS_NOT_IN_RANGE("보너스 번호는 1부터 45 사이의 숫자여야 합니다."),
+ BONUS_NUMBER_MUST_BE_UNIQUE("보너스 번호는 당첨 번호와 중복되지 않아야 합니다."),
+ ;
+
+ public final String message;
+
+ LottoMachineException(final String message) {
+ this.message = message;
+ }
+ }
+
+ enum InputException {
+ INPUT_MUST_NOT_CONTAINS_SPACE("공백없이 입력해주세요."),
+ INPUT_MUST_BE_NUMERIC("숫자를 입력해주세요."),
+ PURCHASE_AMOUNT_MUST_BE_POSITIVE("구입 금액은 음수가 될 수 없습니다."),
+ PURCHASE_AMOUNT_MUST_BE_THOUSAND_UNIT("천원 단위로 구입금액을 입력해주세요."),
+ WINNING_NUMBER_MUST_BE_SPLIT_BY_COMMA("당첨번호는 콤마(,)로 구분해서 공백없이 입력해주세요."),
+ ;
+
+ public final String message;
+
+ InputException(final String message) {
+ this.message = message;
+ }
+ }
+}
diff --git a/src/main/java/lotto/utils/LottoConstants.java b/src/main/java/lotto/utils/LottoConstants.java
new file mode 100644
index 0000000..374e16c
--- /dev/null
+++ b/src/main/java/lotto/utils/LottoConstants.java
@@ -0,0 +1,7 @@
+package lotto.utils;
+
+public interface LottoConstants {
+ int MIN_VALUE = 1;
+ int MAX_VALUE = 45;
+ int LOTTO_SIZE = 6;
+}
diff --git a/src/main/java/lotto/utils/LottoRandomGenerator.java b/src/main/java/lotto/utils/LottoRandomGenerator.java
new file mode 100644
index 0000000..dda9cdc
--- /dev/null
+++ b/src/main/java/lotto/utils/LottoRandomGenerator.java
@@ -0,0 +1,20 @@
+package lotto.utils;
+
+import camp.nextstep.edu.missionutils.Randoms;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static lotto.utils.LottoConstants.*;
+
+public class LottoRandomGenerator {
+ public static List generate() {
+ return new ArrayList<>(
+ Randoms.pickUniqueNumbersInRange(
+ MIN_VALUE,
+ MAX_VALUE,
+ LOTTO_SIZE
+ )
+ );
+ }
+}
diff --git a/src/main/java/lotto/utils/validator/BonusNumberValidator.java b/src/main/java/lotto/utils/validator/BonusNumberValidator.java
new file mode 100644
index 0000000..c860a97
--- /dev/null
+++ b/src/main/java/lotto/utils/validator/BonusNumberValidator.java
@@ -0,0 +1,9 @@
+package lotto.utils.validator;
+
+public class BonusNumberValidator extends Validator {
+ @Override
+ public void validate(final String userInput) {
+ validateInputHasSpace(userInput);
+ validateInputIsNumeric(userInput);
+ }
+}
diff --git a/src/main/java/lotto/utils/validator/LottoPurchaseAmountValidator.java b/src/main/java/lotto/utils/validator/LottoPurchaseAmountValidator.java
new file mode 100644
index 0000000..061f837
--- /dev/null
+++ b/src/main/java/lotto/utils/validator/LottoPurchaseAmountValidator.java
@@ -0,0 +1,32 @@
+package lotto.utils.validator;
+
+import static lotto.utils.ExceptionConstants.InputException.PURCHASE_AMOUNT_MUST_BE_POSITIVE;
+import static lotto.utils.ExceptionConstants.InputException.PURCHASE_AMOUNT_MUST_BE_THOUSAND_UNIT;
+
+public class LottoPurchaseAmountValidator extends Validator {
+ private static final int PURCHASE_UNIT = 1000;
+
+ @Override
+ public void validate(final String userInput) {
+ validateInputHasSpace(userInput);
+ validateInputIsNumeric(userInput);
+ validatePurchaseAmountIsPositive(userInput);
+ validateUnitOfAmountIsThousand(userInput);
+ }
+
+ private void validatePurchaseAmountIsPositive(final String userInput) {
+ if (isUserInputNegative(userInput)) {
+ throw new IllegalArgumentException(PURCHASE_AMOUNT_MUST_BE_POSITIVE.message);
+ }
+ }
+
+ private boolean isUserInputNegative(final String userInput) {
+ return Integer.parseInt(userInput) < 0;
+ }
+
+ private void validateUnitOfAmountIsThousand(final String userInput) {
+ if (Integer.parseInt(userInput) % PURCHASE_UNIT != 0) {
+ throw new IllegalArgumentException(PURCHASE_AMOUNT_MUST_BE_THOUSAND_UNIT.message);
+ }
+ }
+}
diff --git a/src/main/java/lotto/utils/validator/Validator.java b/src/main/java/lotto/utils/validator/Validator.java
new file mode 100644
index 0000000..3170014
--- /dev/null
+++ b/src/main/java/lotto/utils/validator/Validator.java
@@ -0,0 +1,27 @@
+package lotto.utils.validator;
+
+import static lotto.utils.ExceptionConstants.InputException.INPUT_MUST_BE_NUMERIC;
+import static lotto.utils.ExceptionConstants.InputException.INPUT_MUST_NOT_CONTAINS_SPACE;
+
+public abstract class Validator {
+ abstract void validate(final String userInput);
+
+ public void validateInputHasSpace(final String userInput) {
+ if (hasSpace(userInput)) {
+ throw new IllegalArgumentException(INPUT_MUST_NOT_CONTAINS_SPACE.message);
+ }
+ }
+
+ private boolean hasSpace(final String userInput) {
+ return userInput.chars()
+ .anyMatch(Character::isWhitespace);
+ }
+
+ public void validateInputIsNumeric(final String userInput) {
+ try {
+ Integer.parseInt(userInput);
+ } catch (NumberFormatException exception) {
+ throw new IllegalArgumentException(INPUT_MUST_BE_NUMERIC.message);
+ }
+ }
+}
diff --git a/src/main/java/lotto/utils/validator/WinningNumberValidator.java b/src/main/java/lotto/utils/validator/WinningNumberValidator.java
new file mode 100644
index 0000000..fd0b65c
--- /dev/null
+++ b/src/main/java/lotto/utils/validator/WinningNumberValidator.java
@@ -0,0 +1,31 @@
+package lotto.utils.validator;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static lotto.utils.ExceptionConstants.InputException.WINNING_NUMBER_MUST_BE_SPLIT_BY_COMMA;
+
+public class WinningNumberValidator extends Validator {
+ private static final String COMMA = ",";
+
+ @Override
+ public void validate(final String userInput) {
+ validateInputHasSpace(userInput);
+ inputSplitByComma(userInput)
+ .forEach(this::validateInputElementIsNumeric);
+ }
+
+ private List inputSplitByComma(final String userInput) {
+ return Arrays.stream(userInput.split(COMMA))
+ .collect(Collectors.toList());
+ }
+
+ private void validateInputElementIsNumeric(final String userInput) {
+ try {
+ Integer.parseInt(userInput);
+ } catch (NumberFormatException exception) {
+ throw new IllegalArgumentException(WINNING_NUMBER_MUST_BE_SPLIT_BY_COMMA.message);
+ }
+ }
+}
diff --git a/src/main/java/lotto/view/InputView.java b/src/main/java/lotto/view/InputView.java
new file mode 100644
index 0000000..0566fb5
--- /dev/null
+++ b/src/main/java/lotto/view/InputView.java
@@ -0,0 +1,54 @@
+package lotto.view;
+
+import camp.nextstep.edu.missionutils.Console;
+import lotto.utils.validator.BonusNumberValidator;
+import lotto.utils.validator.LottoPurchaseAmountValidator;
+import lotto.utils.validator.WinningNumberValidator;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InputView {
+ private static final int PURCHASE_UNIT = 1000;
+ private static final String COMMA = ",";
+ private static final LottoPurchaseAmountValidator LOTTO_PURCHASE_AMOUNT_VALIDATOR
+ = new LottoPurchaseAmountValidator();
+ private static final WinningNumberValidator WINNING_NUMBER_VALIDATOR
+ = new WinningNumberValidator();
+ private static final BonusNumberValidator BONUS_NUMBER_VALIDATOR
+ = new BonusNumberValidator();
+
+ public static int readLottoPurchaseCount() {
+ System.out.println("구입금액을 입력해 주세요.");
+
+ final String userInput = Console.readLine();
+ LOTTO_PURCHASE_AMOUNT_VALIDATOR.validate(userInput);
+
+ return Integer.parseInt(userInput) / PURCHASE_UNIT;
+ }
+
+ public static List readWiningNumbers() {
+ System.out.println("당첨 번호를 입력해 주세요.");
+
+ final String userInput = Console.readLine();
+ WINNING_NUMBER_VALIDATOR.validate(userInput);
+
+ return convertUserInputToIntegerList(userInput);
+ }
+
+ private static List convertUserInputToIntegerList(final String userInput) {
+ return Arrays.stream(userInput.split(COMMA))
+ .map(Integer::parseInt)
+ .collect(Collectors.toList());
+ }
+
+ public static int readBonusNumber() {
+ System.out.println("보너스 번호를 입력해 주세요.");
+
+ final String userInput = Console.readLine();
+ BONUS_NUMBER_VALIDATOR.validate(userInput);
+
+ return Integer.parseInt(userInput);
+ }
+}
diff --git a/src/main/java/lotto/view/OutputView.java b/src/main/java/lotto/view/OutputView.java
new file mode 100644
index 0000000..7c8bbdb
--- /dev/null
+++ b/src/main/java/lotto/view/OutputView.java
@@ -0,0 +1,70 @@
+package lotto.view;
+
+import lotto.model.Lotto;
+import lotto.model.LottoStatistics;
+import lotto.model.WinningRank;
+
+import java.text.DecimalFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class OutputView {
+ private static final String ENTER = "\n";
+ private static final String ERROR_MESSAGE_FORMAT = "[ERROR] %s";
+ private static final String WINNING_FORMAT = "%s (%s원) - %d개";
+ private static final String REWARD_FORMAT = "#,###";
+ private static final String EARNING_FORMAT = "총 수익률은 %s%%입니다.";
+
+ public static void printPurchaseInformation(final List userLottos) {
+ System.out.printf("%d개를 구매했습니다." + ENTER, userLottos.size());
+ userLottos.forEach(System.out::println);
+ }
+
+ public static void printWinningStatistics(final LottoStatistics lottoStatistics) {
+ StringBuilder result = new StringBuilder("당첨 통계" + ENTER + "---" + ENTER);
+ addWinningStatistics(lottoStatistics, result);
+ addEarningRate(lottoStatistics, result);
+ System.out.println(result);
+ }
+
+ private static void addWinningStatistics(
+ final LottoStatistics lottoStatistics,
+ final StringBuilder result
+ ) {
+ List filteredWinningRank = getFilteredWinningRank();
+ for (WinningRank winningRank : filteredWinningRank) {
+ result.append(
+ String.format(
+ WINNING_FORMAT,
+ winningRank.getDescription(),
+ refineReward(winningRank.getReward()),
+ lottoStatistics.getWinningCountByRank(winningRank)
+ )
+ ).append(ENTER);
+ }
+ }
+
+ private static List getFilteredWinningRank() {
+ return Arrays.stream(WinningRank.values())
+ .filter(winningRank -> winningRank != WinningRank.NONE)
+ .sorted(Collections.reverseOrder()) // Enum Position DESC
+ .collect(Collectors.toList());
+ }
+
+ private static String refineReward(final int reward) {
+ return new DecimalFormat(REWARD_FORMAT).format(reward);
+ }
+
+ private static void addEarningRate(
+ final LottoStatistics lottoStatistics,
+ final StringBuilder result
+ ) {
+ result.append(String.format(EARNING_FORMAT, lottoStatistics.getEarningRate()));
+ }
+
+ public static void printErrorMessage(final String message) {
+ System.out.printf(ERROR_MESSAGE_FORMAT, message);
+ }
+}
diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java
deleted file mode 100644
index 0f3af0f..0000000
--- a/src/test/java/lotto/LottoTest.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package lotto;
-
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-class LottoTest {
- @DisplayName("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.")
- @Test
- void createLottoByOverSize() {
- assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7)))
- .isInstanceOf(IllegalArgumentException.class);
- }
-
- @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.")
- @Test
- void createLottoByDuplicatedNumber() {
- // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성
- assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5)))
- .isInstanceOf(IllegalArgumentException.class);
- }
-
- // 아래에 추가 테스트 작성 가능
-}
diff --git a/src/test/java/lotto/model/BonusNumberTest.java b/src/test/java/lotto/model/BonusNumberTest.java
new file mode 100644
index 0000000..4b0c873
--- /dev/null
+++ b/src/test/java/lotto/model/BonusNumberTest.java
@@ -0,0 +1,45 @@
+package lotto.model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static lotto.utils.ExceptionConstants.LottoMachineException.BONUS_NUMBER_IS_NOT_IN_RANGE;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+class BonusNumberTest {
+ @ParameterizedTest
+ @MethodSource("invalidRange")
+ @DisplayName("보너스 번호의 범위가 1..45 이외라면 예외가 발생한다")
+ void throwExceptionByBonusNumberIsNotInRange(int value) {
+ assertThatThrownBy(() -> new BonusNumber(value))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(BONUS_NUMBER_IS_NOT_IN_RANGE.message);
+ }
+
+ private static Stream invalidRange() {
+ return Stream.of(
+ Arguments.of(0),
+ Arguments.of(46)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("validRange")
+ @DisplayName("BonusNumber를 생성한다")
+ void success(int value) {
+ assertDoesNotThrow(() -> new BonusNumber(value));
+ }
+
+ private static Stream validRange() {
+ return Stream.of(
+ Arguments.of(1),
+ Arguments.of(10),
+ Arguments.of(45)
+ );
+ }
+}
diff --git a/src/test/java/lotto/model/LottoStatisticsTest.java b/src/test/java/lotto/model/LottoStatisticsTest.java
new file mode 100644
index 0000000..996cb7a
--- /dev/null
+++ b/src/test/java/lotto/model/LottoStatisticsTest.java
@@ -0,0 +1,165 @@
+package lotto.model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+class LottoStatisticsTest {
+ @Test
+ @DisplayName("구매한 로또 N장에 대한 당첨 통계를 조회한다")
+ void getWinningResult() {
+ // given
+ final LottoWinningMachine lottoWinningMachine = createLottoWinningMachine();
+ final UserLotto userLottoCaseA = createUserLottosCaseA();
+ final UserLotto userLottoCaseB = createUserLottosCaseB();
+ final UserLotto userLottoCaseC = createUserLottosCaseB();
+
+ // when
+ final LottoStatistics caseA = LottoStatistics.checkLotteryResult(lottoWinningMachine, userLottoCaseA);
+ final LottoStatistics caseB = LottoStatistics.checkLotteryResult(lottoWinningMachine, userLottoCaseB);
+ final LottoStatistics caseC = LottoStatistics.checkLotteryResult(lottoWinningMachine, userLottoCaseC);
+
+ // then
+ final Map winningResultA = caseA.getWinningResult();
+ assertAll(
+ () -> assertThat(winningResultA.get(WinningRank.FIRST)).isEqualTo(1),
+ () -> assertThat(winningResultA.get(WinningRank.SECOND)).isEqualTo(1),
+ () -> assertThat(winningResultA.get(WinningRank.THIRD)).isEqualTo(1),
+ () -> assertThat(winningResultA.get(WinningRank.FOURTH)).isEqualTo(2),
+ () -> assertThat(winningResultA.get(WinningRank.FIFTH)).isEqualTo(2),
+ () -> assertThat(winningResultA.get(WinningRank.NONE)).isEqualTo(5)
+ );
+
+ final Map winningResultB = caseB.getWinningResult();
+ assertAll(
+ () -> assertThat(winningResultB.get(WinningRank.FIRST)).isEqualTo(0),
+ () -> assertThat(winningResultB.get(WinningRank.SECOND)).isEqualTo(0),
+ () -> assertThat(winningResultB.get(WinningRank.THIRD)).isEqualTo(0),
+ () -> assertThat(winningResultB.get(WinningRank.FOURTH)).isEqualTo(0),
+ () -> assertThat(winningResultB.get(WinningRank.FIFTH)).isEqualTo(3),
+ () -> assertThat(winningResultB.get(WinningRank.NONE)).isEqualTo(14)
+ );
+
+ final Map winningResultC = caseC.getWinningResult();
+ assertAll(
+ () -> assertThat(winningResultC.get(WinningRank.FIRST)).isEqualTo(0),
+ () -> assertThat(winningResultC.get(WinningRank.SECOND)).isEqualTo(0),
+ () -> assertThat(winningResultC.get(WinningRank.THIRD)).isEqualTo(0),
+ () -> assertThat(winningResultC.get(WinningRank.FOURTH)).isEqualTo(0),
+ () -> assertThat(winningResultC.get(WinningRank.FIFTH)).isEqualTo(3),
+ () -> assertThat(winningResultC.get(WinningRank.NONE)).isEqualTo(14)
+ );
+ }
+
+ @Test
+ @DisplayName("구매한 로또 N장에 대한 수익률을 조회한다")
+ void getEarningRate() {
+ // given
+ final LottoWinningMachine lottoWinningMachine = createLottoWinningMachine();
+ final UserLotto userLottoCaseA = createUserLottosCaseA();
+ final UserLotto userLottoCaseB = createUserLottosCaseB();
+ final UserLotto userLottoCaseC = createUserLottosCaseC();
+
+ // when
+ final LottoStatistics caseA = LottoStatistics.checkLotteryResult(lottoWinningMachine, userLottoCaseA);
+ final LottoStatistics caseB = LottoStatistics.checkLotteryResult(lottoWinningMachine, userLottoCaseB);
+ final LottoStatistics caseC = LottoStatistics.checkLotteryResult(lottoWinningMachine, userLottoCaseC);
+
+ // then
+ assertAll(
+ () -> assertThat(caseA.getEarningRate()).isEqualTo(BigDecimal.valueOf(16930083.3)),
+ () -> assertThat(caseB.getEarningRate()).isEqualTo(BigDecimal.valueOf(88.2)),
+ () -> assertThat(caseC.getEarningRate()).isEqualTo(BigDecimal.valueOf(62.5))
+ );
+ }
+
+ private LottoWinningMachine createLottoWinningMachine() {
+ return LottoWinningMachine.drawWinningLottery(
+ Arrays.asList(1, 2, 3, 4, 5, 6),
+ 7
+ );
+ }
+
+ private UserLotto createUserLottosCaseA() {
+ return new UserLotto(
+ List.of(
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(1, 2, 3, 11, 12, 13)), // 5등 (당첨 3개)
+ new Lotto(Arrays.asList(1, 2, 3, 7, 12, 13)), // 5등 (당첨 3개 + 보너스 1개)
+ new Lotto(Arrays.asList(1, 2, 3, 4, 12, 13)), // 4등 (당첨 4개)
+ new Lotto(Arrays.asList(1, 2, 3, 4, 7, 13)), // 4등 (당첨 4개 + 보너스 1개)
+ new Lotto(Arrays.asList(1, 2, 3, 4, 5, 13)), // 3등 (당첨 5개)
+ new Lotto(Arrays.asList(1, 2, 3, 4, 5, 7)), // 2등 (당첨 5개 + 보너스 1개)
+ new Lotto(Arrays.asList(1, 2, 3, 4, 5, 6)) // 1등 (당첨 6개)
+ )
+ );
+
+ /**
+ * 구매 금액 = 12_000
+ * 당첨 금액 = 2,031,610,000
+ * -> 수익률 = 169,300.83333333333333333333333333... = 16930083.33% = 16930083.3%
+ */
+ }
+
+ private UserLotto createUserLottosCaseB() {
+ return new UserLotto(
+ List.of(
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(8, 9, 10, 11, 12, 13)), // None
+ new Lotto(Arrays.asList(1, 2, 3, 11, 12, 13)), // 5등 (당첨 3개)
+ new Lotto(Arrays.asList(1, 2, 3, 11, 12, 13)), // 5등 (당첨 3개)
+ new Lotto(Arrays.asList(1, 2, 3, 7, 12, 13)) // 5등 (당첨 3개 + 보너스 1개)
+ )
+ );
+
+ /**
+ * 구매 금액 = 17000
+ * 당첨 금액 = 15000
+ * -> 수익률 = 0.88235294117647058823529411764706... = 88.23% = 88.2%
+ */
+ }
+
+ private UserLotto createUserLottosCaseC() {
+ return new UserLotto(
+ List.of(
+ new Lotto(Arrays.asList(8, 21, 23, 41, 42, 43)), // None
+ new Lotto(Arrays.asList(3, 5, 11, 16, 32, 38)), // None
+ new Lotto(Arrays.asList(7, 11, 16, 35, 36, 44)), // None
+ new Lotto(Arrays.asList(1, 8, 11, 31, 41, 42)), // None
+ new Lotto(Arrays.asList(13, 14, 16, 38, 42, 45)), // None
+ new Lotto(Arrays.asList(7, 11, 30, 40, 42, 43)), // None
+ new Lotto(Arrays.asList(2, 13, 22, 32, 38, 45)), // None
+ new Lotto(Arrays.asList(1, 3, 5, 14, 22, 45)) // 5등 (당첨 3개)
+ )
+ );
+
+ /**
+ * 구매 금액 = 8000
+ * 당첨 금액 = 5000
+ * -> 수익률 = 0.625 = 62.5%
+ */
+ }
+}
diff --git a/src/test/java/lotto/model/LottoTest.java b/src/test/java/lotto/model/LottoTest.java
new file mode 100644
index 0000000..407eff2
--- /dev/null
+++ b/src/test/java/lotto/model/LottoTest.java
@@ -0,0 +1,87 @@
+package lotto.model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static lotto.utils.ExceptionConstants.LottoException.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+class LottoTest {
+ @ParameterizedTest
+ @MethodSource("invalidRange")
+ @DisplayName("로또 번호의 범위가 1..45 이외라면 예외가 발생한다")
+ void throwExceptionByLottoNumberIsNotInRange(List numbers) {
+ assertThatThrownBy(() -> new Lotto(numbers))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(LOTTO_NUMBER_IS_NOT_IN_RANGE.message);
+ }
+
+ private static Stream invalidRange() {
+ return Stream.of(
+ Arguments.of(List.of(0, 1, 2, 3, 4, 5)),
+ Arguments.of(List.of(1, 2, 3, 4, 5, 46))
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidSize")
+ @DisplayName("로또 번호의 개수가 6개가 아니면 예외가 발생한다")
+ void throwExceptionByLottoSizeNotFulfill(List numbers) {
+ assertThatThrownBy(() -> new Lotto(numbers))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(LOTTO_SIZE_IS_NOT_FULFILL.message);
+ }
+
+ private static Stream invalidSize() {
+ return Stream.of(
+ Arguments.of(List.of()),
+ Arguments.of(List.of(1)),
+ Arguments.of(List.of(1, 2)),
+ Arguments.of(List.of(1, 2, 3)),
+ Arguments.of(List.of(1, 2, 3, 4)),
+ Arguments.of(List.of(1, 2, 3, 4, 5))
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("duplicateNumber")
+ @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다")
+ void throwExceptionByLottoNumberIsNotUnique(List numbers) {
+ assertThatThrownBy(() -> new Lotto(numbers))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(LOTTO_NUMBER_MUST_BE_UNIQUE.message);
+ }
+
+ private static Stream duplicateNumber() {
+ return Stream.of(
+ Arguments.of(List.of(1, 1, 2, 3, 4, 5)),
+ Arguments.of(List.of(1, 1, 1, 2, 3, 4)),
+ Arguments.of(List.of(1, 1, 1, 1, 2, 3)),
+ Arguments.of(List.of(1, 1, 1, 1, 1, 2)),
+ Arguments.of(List.of(1, 1, 1, 1, 1, 1))
+ );
+ }
+
+ @Test
+ @DisplayName("Lotto를 생성한다")
+ void construct() {
+ // when
+ final Lotto lottoA = new Lotto(Arrays.asList(1, 3, 2, 4, 5, 6));
+ final Lotto lottoB = new Lotto(Arrays.asList(44, 1, 10, 23, 18, 6));
+
+ // then
+ assertAll(
+ () -> assertThat(lottoA.getNumbers()).containsExactly(1, 2, 3, 4, 5, 6),
+ () -> assertThat(lottoB.getNumbers()).containsExactly(1, 6, 10, 18, 23, 44)
+ );
+ }
+}
diff --git a/src/test/java/lotto/model/LottoWinningMachineTest.java b/src/test/java/lotto/model/LottoWinningMachineTest.java
new file mode 100644
index 0000000..01264b3
--- /dev/null
+++ b/src/test/java/lotto/model/LottoWinningMachineTest.java
@@ -0,0 +1,44 @@
+package lotto.model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static lotto.utils.ExceptionConstants.LottoMachineException.BONUS_NUMBER_MUST_BE_UNIQUE;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+class LottoWinningMachineTest {
+ @Test
+ @DisplayName("보너스 번호가 당첨 번호에 중복되면 예외가 발생한다")
+ void throwExceptionByBonusNumberIsNotUnique() {
+ // given
+ final List winningNumbers = Arrays.asList(1, 2, 3, 4, 5, 6);
+ final int bonusNumber = 1;
+
+ // when - then
+ assertThatThrownBy(() -> LottoWinningMachine.drawWinningLottery(winningNumbers, bonusNumber))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(BONUS_NUMBER_MUST_BE_UNIQUE.message);
+ }
+
+ @Test
+ @DisplayName("LottoWinningMachine을 생성한다")
+ void success() {
+ // given
+ final List winningNumbers = Arrays.asList(1, 2, 3, 4, 5, 6);
+ final int bonusNumber = 7;
+
+ // when
+ final LottoWinningMachine lottoWinningMachine = LottoWinningMachine.drawWinningLottery(winningNumbers, bonusNumber);
+
+ // then
+ assertAll(
+ () -> assertThat(lottoWinningMachine.getWinningLotteryNumbers()).containsExactlyInAnyOrderElementsOf(winningNumbers),
+ () -> assertThat(lottoWinningMachine.getBonusNumber()).isEqualTo(bonusNumber)
+ );
+ }
+}
diff --git a/src/test/java/lotto/model/UserLottoTest.java b/src/test/java/lotto/model/UserLottoTest.java
new file mode 100644
index 0000000..0c45ab2
--- /dev/null
+++ b/src/test/java/lotto/model/UserLottoTest.java
@@ -0,0 +1,36 @@
+package lotto.model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.math.BigDecimal;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+class UserLottoTest {
+ @ParameterizedTest
+ @MethodSource("issueCase")
+ @DisplayName("구매한 개수만큼 UseLotto를 발급받는다")
+ void issueLottoByPurchaseCount(int lottoPurchaseCount, BigDecimal purchaseAmount) {
+ // when
+ final UserLotto userLotto = UserLotto.issueLottoByPurchaseCount(lottoPurchaseCount);
+
+ // then
+ assertAll(
+ () -> assertThat(userLotto.getLottoPurchaseCount()).isEqualTo(lottoPurchaseCount),
+ () -> assertThat(userLotto.getLottoPurchaseAmount()).isEqualTo(purchaseAmount)
+ );
+ }
+
+ private static Stream issueCase() {
+ return Stream.of(
+ Arguments.of(5, BigDecimal.valueOf(5_000)),
+ Arguments.of(10, BigDecimal.valueOf(10_000)),
+ Arguments.of(1_000_000, BigDecimal.valueOf(1_000_000_000))
+ );
+ }
+}
diff --git a/src/test/java/lotto/model/WinningRankTest.java b/src/test/java/lotto/model/WinningRankTest.java
new file mode 100644
index 0000000..aa7346d
--- /dev/null
+++ b/src/test/java/lotto/model/WinningRankTest.java
@@ -0,0 +1,37 @@
+package lotto.model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static lotto.model.WinningRank.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class WinningRankTest {
+ @ParameterizedTest
+ @MethodSource("winningCase")
+ @DisplayName("로또 번호 당첨 개수 + 보너스 볼 일치 여부에 따른 WinningRank를 배정받는다")
+ void getWinningRank(int matchCount, boolean hasBonus, WinningRank expect) {
+ assertThat(WinningRank.of(matchCount, hasBonus)).isEqualTo(expect);
+ }
+
+ private static Stream winningCase() {
+ return Stream.of(
+ Arguments.of(6, false, FIRST),
+ Arguments.of(5, true, SECOND),
+ Arguments.of(5, false, THIRD),
+ Arguments.of(4, true, FOURTH),
+ Arguments.of(4, false, FOURTH),
+ Arguments.of(3, true, FIFTH),
+ Arguments.of(3, false, FIFTH),
+ Arguments.of(2, true, NONE),
+ Arguments.of(2, false, NONE),
+ Arguments.of(1, true, NONE),
+ Arguments.of(1, false, NONE),
+ Arguments.of(0, false, NONE)
+ );
+ }
+}
diff --git a/src/test/java/lotto/utils/validator/BonusNumberValidatorTest.java b/src/test/java/lotto/utils/validator/BonusNumberValidatorTest.java
new file mode 100644
index 0000000..9189cbe
--- /dev/null
+++ b/src/test/java/lotto/utils/validator/BonusNumberValidatorTest.java
@@ -0,0 +1,36 @@
+package lotto.utils.validator;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static lotto.utils.ExceptionConstants.InputException.INPUT_MUST_BE_NUMERIC;
+import static lotto.utils.ExceptionConstants.InputException.INPUT_MUST_NOT_CONTAINS_SPACE;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+class BonusNumberValidatorTest {
+ private static final BonusNumberValidator BONUS_NUMBER_VALIDATOR
+ = new BonusNumberValidator();
+
+ @Test
+ @DisplayName("보너스 번호에 공백이 존재하면 예외가 발생한다")
+ void throwExceptionByInputHasSpace() {
+ assertThatThrownBy(() -> BONUS_NUMBER_VALIDATOR.validate("7 "))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(INPUT_MUST_NOT_CONTAINS_SPACE.message);
+ }
+
+ @Test
+ @DisplayName("보너스 번호가 숫자가 아니면 예외가 발생한다")
+ void throwExceptionByInputIsNotNumeric() {
+ assertThatThrownBy(() -> BONUS_NUMBER_VALIDATOR.validate("a"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(INPUT_MUST_BE_NUMERIC.message);
+ }
+
+ @Test
+ @DisplayName("보너스 번호 검증에 성공한다")
+ void success() {
+ assertDoesNotThrow(() -> BONUS_NUMBER_VALIDATOR.validate("7"));
+ }
+}
diff --git a/src/test/java/lotto/utils/validator/LottoPurchaseAmountValidatorTest.java b/src/test/java/lotto/utils/validator/LottoPurchaseAmountValidatorTest.java
new file mode 100644
index 0000000..5a538da
--- /dev/null
+++ b/src/test/java/lotto/utils/validator/LottoPurchaseAmountValidatorTest.java
@@ -0,0 +1,51 @@
+package lotto.utils.validator;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static lotto.utils.ExceptionConstants.InputException.*;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+class LottoPurchaseAmountValidatorTest {
+ private static final LottoPurchaseAmountValidator LOTTO_PURCHASE_AMOUNT_VALIDATOR
+ = new LottoPurchaseAmountValidator();
+
+ @Test
+ @DisplayName("로또 구입금액에 공백이 존재하면 예외가 발생한다")
+ void throwExceptionByInputHasSpace() {
+ assertThatThrownBy(() -> LOTTO_PURCHASE_AMOUNT_VALIDATOR.validate("8000 "))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(INPUT_MUST_NOT_CONTAINS_SPACE.message);
+ }
+
+ @Test
+ @DisplayName("로또 구입금액이 숫자가 아니면 예외가 발생한다")
+ void throwExceptionByInputIsNotNumeric() {
+ assertThatThrownBy(() -> LOTTO_PURCHASE_AMOUNT_VALIDATOR.validate("abcde"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(INPUT_MUST_BE_NUMERIC.message);
+ }
+
+ @Test
+ @DisplayName("로또 구입금액이 음수면 예외가 발생한다")
+ void throwExceptionByPurchaseAmountIsNegative() {
+ assertThatThrownBy(() -> LOTTO_PURCHASE_AMOUNT_VALIDATOR.validate("-8000"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(PURCHASE_AMOUNT_MUST_BE_POSITIVE.message);
+ }
+
+ @Test
+ @DisplayName("로또 구입금액이 1000원 단위가 아니면 예외가 발생한다")
+ void throwExceptionByUnitOfAmountIsNotThousand() {
+ assertThatThrownBy(() -> LOTTO_PURCHASE_AMOUNT_VALIDATOR.validate("800"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(PURCHASE_AMOUNT_MUST_BE_THOUSAND_UNIT.message);
+ }
+
+ @Test
+ @DisplayName("로또 구입금액 검증에 성공한다")
+ void success() {
+ assertDoesNotThrow(() -> LOTTO_PURCHASE_AMOUNT_VALIDATOR.validate("8000"));
+ }
+}
diff --git a/src/test/java/lotto/utils/validator/WinningNumberValidatorTest.java b/src/test/java/lotto/utils/validator/WinningNumberValidatorTest.java
new file mode 100644
index 0000000..8e91451
--- /dev/null
+++ b/src/test/java/lotto/utils/validator/WinningNumberValidatorTest.java
@@ -0,0 +1,36 @@
+package lotto.utils.validator;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static lotto.utils.ExceptionConstants.InputException.INPUT_MUST_NOT_CONTAINS_SPACE;
+import static lotto.utils.ExceptionConstants.InputException.WINNING_NUMBER_MUST_BE_SPLIT_BY_COMMA;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+class WinningNumberValidatorTest {
+ private static final WinningNumberValidator WINNING_NUMBER_VALIDATOR
+ = new WinningNumberValidator();
+
+ @Test
+ @DisplayName("당첨번호에 공백이 존재하면 예외가 발생한다")
+ void throwExceptionByInputHasSpace() {
+ assertThatThrownBy(() -> WINNING_NUMBER_VALIDATOR.validate("1,2,3,4, 5, 6, "))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(INPUT_MUST_NOT_CONTAINS_SPACE.message);
+ }
+
+ @Test
+ @DisplayName("당첨번호의 각 번호들이 숫자가 아니면 예외가 발생한다")
+ void throwExceptionByInputIsNotNumeric() {
+ assertThatThrownBy(() -> WINNING_NUMBER_VALIDATOR.validate("1,2,3,a,4,5,b,6"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(WINNING_NUMBER_MUST_BE_SPLIT_BY_COMMA.message);
+ }
+
+ @Test
+ @DisplayName("당첨번호 검증에 성공한다")
+ void success() {
+ assertDoesNotThrow(() -> WINNING_NUMBER_VALIDATOR.validate("1,2,3,4,5,6"));
+ }
+}