diff --git a/README.md b/README.md
index 5fa2560..22742a9 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,130 @@
# java-lotto-precourse
+
+> 로또 맞게 해주세요...ㅠ
+
+
+ 과제 세부 내용
+
+## 과제 내용
+로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.
+```
+- 로또 번호의 숫자 범위는 1~45까지이다.
+- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
+- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
+- 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.
+ - 1등: 6개 번호 일치 / 2,000,000,000원
+ - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
+ - 3등: 5개 번호 일치 / 1,500,000원
+ - 4등: 4개 번호 일치 / 50,000원
+ - 5등: 3개 번호 일치 / 5,000원
+```
+- 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
+- 로또 1장의 가격은 1,000원이다.
+- 당첨 번호와 보너스 번호를 입력받는다.
+- 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
+- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
+ - `Exception`이 아닌 `IllegalArgumentException`, `IllegalStateException` 등과 같은 명확한 유형을 처리한다.
+
+### 입출력
+- 입력
+ - 로또 구입 금액
+ - 당첨 번호 6개
+ - 보너스 번호
+- 출력
+ - 발행한 로또 수량 및 번호
+ - 당첨 내역
+ - 수익률
+ - (예외 문구)
+
+ex)
+
+```
+구입금액을 입력해 주세요.
+8000
+
+8개를 구매했습니다.
+[8, 21, 23, 41, 42, 43]
+[3, 5, 11, 16, 32, 38]
+[7, 11, 16, 35, 36, 44]
+[1, 8, 11, 31, 41, 42]
+[13, 14, 16, 38, 42, 45]
+[7, 11, 30, 40, 42, 43]
+[2, 13, 22, 32, 38, 45]
+[1, 3, 5, 14, 22, 45]
+
+당첨 번호를 입력해 주세요.
+1,2,3,4,5,6
+
+보너스 번호를 입력해 주세요.
+7
+
+당첨 통계
+---
+3개 일치 (5,000원) - 1개
+4개 일치 (50,000원) - 0개
+5개 일치 (1,500,000원) - 0개
+5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
+6개 일치 (2,000,000,000원) - 0개
+총 수익률은 62.5%입니다.
+```
+
+
+
+## 코드 흐름
+- 로또 구매 금액을 입력받는다.
+- 로또를 번호를 생성하고 출력한다.
+- 당첨 번호를 입력받는다.
+- 결과를 계산한 후 출력한다.
+
+```mermaid
+sequenceDiagram
+ participant View
+ participant Controller
+ participant Model
+
+ Controller->>View: 금액 입력 요청
+ View->>Controller: 구입 금액 반환
+ Controller->>Model: 구입 금액 전달
+ Model-->Model: 로또 생성
+ Model->>Controller: 로또 번호 반환
+ Controller->>View: 로또 번호 출력
+ View->>Controller: -
+ Controller->>View: 당첨 번호 입력 요청
+ View->>Controller: 당첨 번호 반환
+ Controller->>Model: 당첨 번호 전달
+ Model-->Model: 당첨 여부 확인
+ Model->>Controller: 결과 반환
+ Controller->>View: 결과 출력
+ View->>Controller: -
+ Controller-->Controller: 프로그램 종료
+
+```
+
+## 구현 기능 목록
+- 입출력
+ - [ ] 구입 금액 입력
+ - [ ] 당첨 번호 입력
+ - [ ] 보너스 번호 입력
+ - [ ] 발행한 로또 수량 및 번호
+ - [ ] 당첨 내역
+ - [ ] 수익률
+ - [ ] 예외
+- 로또
+ - [ ] 로또 생성
+ - [ ] 당첨 확인
+ - [ ] 수익률 계산
+
+## 처리할 예외
+- 나누어 떨어지지 않는 금액 (1000 단위로 떨어지지 않을 때)
+ - `[ERROR] 1,000원 단위로 입력해 주세요.`
+- 너무 큰 구입 금액
+ - `[ERROR] $입력한 금액 보다 작은 금액을 입력해 주세요.`
+- 부적절한 구입 금액 (음수 입력 등 포함, 입력 예외)
+ - `[ERROR] 유효한 구입 금액을 입력해 주세요`
+- 부적절한 로또의 범위 (1-45 밖의 숫자)
+ - `[ERROR] 1부터 45 사이의 값을 입력해 주세요.`
+- 중복되는 번호 / 보너스 번호
+ - `[ERROR] 중복되지 않은 번호를 입력해주세요.`
+- 부적절한 양식 혹은 갯수
+ - `[ERROR] 당첨 번호 6개를 정확히 입력해주세요. ex)1,2,3,4,5,6`
+ - `[ERROR] 보너스 번호 하나를 정확히 입력해주세요. ex)7`
diff --git a/src/main/java/lotto/AppConfig.java b/src/main/java/lotto/AppConfig.java
new file mode 100644
index 0000000..edf23b1
--- /dev/null
+++ b/src/main/java/lotto/AppConfig.java
@@ -0,0 +1,42 @@
+package lotto;
+
+import lotto.controller.LottoController;
+import lotto.service.LottoControlService;
+import lotto.service.LottoControlServiceImpl;
+import lotto.service.RandomService;
+import lotto.service.RandomServiceImpl;
+import lotto.view.InputView;
+import lotto.view.OutputView;
+import lotto.view.provider.InputProvider;
+import lotto.view.provider.WoowaInputProvider;
+
+public class AppConfig {
+ public InputProvider inputProvider() {
+ return new WoowaInputProvider();
+ }
+
+ public InputView inputView() {
+ return new InputView(inputProvider());
+ }
+
+ public OutputView outputView() {
+ return new OutputView();
+ }
+
+ public LottoControlService lottoControlService() {
+ return new LottoControlServiceImpl();
+ }
+
+ public RandomService randomService() {
+ return new RandomServiceImpl();
+ }
+
+ public LottoController lottoController() {
+ return new LottoController(
+ inputView(),
+ outputView(),
+ lottoControlService(),
+ randomService()
+ );
+ }
+}
diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java
index d190922..3496af7 100644
--- a/src/main/java/lotto/Application.java
+++ b/src/main/java/lotto/Application.java
@@ -1,7 +1,13 @@
package lotto;
+import lotto.controller.LottoController;
+
public class Application {
+
public static void main(String[] args) {
// TODO: 프로그램 구현
+ AppConfig appConfig = new AppConfig();
+ LottoController lottoController = appConfig.lottoController();
+ lottoController.run();
}
}
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/controller/LottoController.java b/src/main/java/lotto/controller/LottoController.java
new file mode 100644
index 0000000..d6fcdf6
--- /dev/null
+++ b/src/main/java/lotto/controller/LottoController.java
@@ -0,0 +1,135 @@
+package lotto.controller;
+
+import static lotto.utils.LottoConstants.LOTTO_PRICE;
+
+import java.util.List;
+import lotto.model.dto.WinningDataDto;
+import lotto.service.LottoControlService;
+import lotto.service.RandomService;
+import lotto.utils.ExceptionConstants;
+import lotto.utils.Utility;
+import lotto.view.InputView;
+import lotto.view.OutputView;
+
+public class LottoController {
+ InputView inputView;
+ OutputView outputView;
+ LottoControlService lottoControlService;
+ RandomService randomService;
+ ErrorHandler errorHandler;
+
+ private static final int MAX_ATTEMPTS = 100;
+
+ public LottoController(InputView inputView, OutputView outputView, LottoControlService lottoControlService,
+ RandomService randomService) {
+ this.inputView = inputView;
+ this.outputView = outputView;
+ this.lottoControlService = lottoControlService;
+ this.randomService = randomService;
+ errorHandler = new ErrorHandler();
+ }
+
+ public void run() {
+ try {
+ int amount = inputPrice();
+ inputLottoNumber();
+ outputLottoData(amount);
+ inputBonusNumber();
+ composeLottoSystem();
+ printResult(amount);
+ } catch (Exception e) {
+ outputView.printException(e);
+ }
+ }
+
+ private int inputPrice() {
+ while (true) {
+ if (errorHandler.willGenerateError()) {
+ throw new IllegalStateException(ExceptionConstants.INTERNAL_SERVER_ERROR.getMessage());
+ }
+ try {
+ int price = inputView.getPrice();
+ validatePrice(price);
+ lottoControlService.buyLotto(price / LOTTO_PRICE, randomService::getLottoNumbers);
+ errorHandler.resetAttemptCount();
+ return price / LOTTO_PRICE;
+ } catch (Exception e) {
+ outputView.printException(e);
+ }
+ }
+ }
+
+ private void validatePrice(int price) {
+ if (!Utility.isDividedByThousand(price)) {
+ throw new IllegalArgumentException(ExceptionConstants.INDIVISIBLE_PRICE.getMessage());
+ }
+ }
+
+ private void outputLottoData(int amount) {
+ outputView.printResultGuide(amount);
+ lottoControlService.getLotto().forEach(lotto -> outputView.printLotto(lotto));
+ }
+
+ private void inputLottoNumber() {
+ while (true) {
+ if (errorHandler.willGenerateError()) {
+ throw new IllegalStateException(ExceptionConstants.INTERNAL_SERVER_ERROR.getMessage());
+ }
+ try {
+ String line = inputView.getLottoNumber();
+ lottoControlService.setWinningNumbers(line);
+ errorHandler.resetAttemptCount();
+ break;
+ } catch (Exception e) {
+ outputView.printException(e);
+ }
+ }
+ }
+
+ private void inputBonusNumber() {
+ while (true) {
+ if (errorHandler.willGenerateError()) {
+ throw new IllegalStateException(ExceptionConstants.INTERNAL_SERVER_ERROR.getMessage());
+ }
+ try {
+ int bonusNumber = inputView.getBonusNumber();
+ lottoControlService.setBonusNumber(bonusNumber);
+ errorHandler.resetAttemptCount();
+ break;
+ } catch (Exception e) {
+ outputView.printException(e);
+ }
+ }
+ }
+
+ private void composeLottoSystem() {
+ lottoControlService.composeLotto();
+ }
+
+ private void printResult(int amount) {
+ List lottoResult = lottoControlService.checkWinning();
+ outputView.printResult(lottoResult);
+ float result = lottoControlService.calculateROI(lottoResult, amount * LOTTO_PRICE);
+ outputView.printRateOfReturn(result);
+ }
+
+ static class ErrorHandler {
+ private int attempts;
+
+ ErrorHandler() {
+ this.attempts = 0;
+ }
+
+ boolean willGenerateError() {
+ attempts++;
+ if (attempts > MAX_ATTEMPTS) {
+ return true;
+ }
+ return false;
+ }
+
+ void resetAttemptCount() {
+ attempts = 0;
+ }
+ }
+}
diff --git a/src/main/java/lotto/model/Lotto.java b/src/main/java/lotto/model/Lotto.java
new file mode 100644
index 0000000..a376abf
--- /dev/null
+++ b/src/main/java/lotto/model/Lotto.java
@@ -0,0 +1,53 @@
+package lotto.model;
+
+import static lotto.utils.ExceptionConstants.DUPLICATED_LOTTO_NUMBER;
+import static lotto.utils.ExceptionConstants.INVALID_LOTTO_RANGE;
+import static lotto.utils.LottoConstants.LOWER_BOUND_NUMBER;
+import static lotto.utils.LottoConstants.UPPER_BOUND_NUMBER;
+
+import java.util.List;
+import java.util.stream.Stream;
+import lotto.utils.LottoPrize;
+import lotto.utils.Utility;
+
+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개여야 합니다.");
+ }
+ if (Utility.hasDuplicatedValue(numbers)) {
+ throw new IllegalArgumentException(DUPLICATED_LOTTO_NUMBER.getMessage());
+ }
+ if (!Utility.isInRange(numbers, LOWER_BOUND_NUMBER, UPPER_BOUND_NUMBER)) {
+ throw new IllegalArgumentException(INVALID_LOTTO_RANGE.getMessage());
+ }
+ }
+
+ public LottoPrize checkWinning(List winningNumbers, int bonusNumber) {
+ List totalWinningNumber = Stream.concat(winningNumbers.stream(), Stream.of(bonusNumber)).toList();
+ int matchCount = (int) numbers.stream().filter(totalWinningNumber::contains).count();
+ boolean matchBonus = numbers.contains(bonusNumber);
+ return LottoPrize.getLottoPrize(matchCount, matchBonus);
+ }
+
+ public void validateAdditionalNumber(int value) {
+ List tempNumbers = Stream.concat(numbers.stream(), Stream.of(value)).toList();
+ if (Utility.hasDuplicatedValue(tempNumbers)) {
+ throw new IllegalArgumentException(DUPLICATED_LOTTO_NUMBER.getMessage());
+ }
+ if (!Utility.isInRange(value, LOWER_BOUND_NUMBER, UPPER_BOUND_NUMBER)) {
+ throw new IllegalArgumentException(INVALID_LOTTO_RANGE.getMessage());
+ }
+ }
+
+ public List getNumbers() {
+ return numbers;
+ }
+}
diff --git a/src/main/java/lotto/model/LottoRepository.java b/src/main/java/lotto/model/LottoRepository.java
new file mode 100644
index 0000000..35d4c22
--- /dev/null
+++ b/src/main/java/lotto/model/LottoRepository.java
@@ -0,0 +1,78 @@
+package lotto.model;
+
+import static lotto.utils.LottoConstants.NUMBER_SEPARATOR;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import lotto.model.dto.WinningDataDto;
+import lotto.utils.ExceptionConstants;
+import lotto.utils.LottoPrize;
+import lotto.utils.Utility;
+
+public class LottoRepository {
+ private final List purchasedLotto;
+ private final List winningNumbers;
+ private int bonusNumber;
+
+ private LottoRepository(LottoRepositoryBuilder lottoRepositoryBuilder) {
+ purchasedLotto = lottoRepositoryBuilder.purchasedLotto;
+ winningNumbers = lottoRepositoryBuilder.winningNumbers;
+ bonusNumber = lottoRepositoryBuilder.bonusNumber;
+ }
+
+ public List countGrade() {
+ List prizes = purchasedLotto.stream()
+ .map(lotto -> lotto.checkWinning(winningNumbers, bonusNumber))
+ .toList();
+ return LottoPrize.getPrizeTypes().stream()
+ .map(lottoPrize -> new WinningDataDto(lottoPrize, (int) Utility.countValue(prizes, lottoPrize)))
+ .toList();
+ }
+
+ public static class LottoRepositoryBuilder {
+ private List purchasedLotto;
+ private List winningNumbers;
+ private Integer bonusNumber;
+
+ public LottoRepositoryBuilder setPurchasedLotto(List purchasedLotto) {
+ this.purchasedLotto = new ArrayList<>();
+ this.purchasedLotto.addAll(purchasedLotto);
+ return this;
+ }
+
+ public LottoRepositoryBuilder setWinningNumber(String line) {
+ winningNumbers = new ArrayList<>();
+ List numbers = Arrays.stream(line.split(NUMBER_SEPARATOR))
+ .map(Integer::parseInt)
+ .toList();
+ setWinningNumbers(numbers);
+ return this;
+ }
+
+ private LottoRepositoryBuilder setWinningNumbers(List winningNumbers) {
+ new Lotto(winningNumbers);
+ this.winningNumbers.clear();
+ this.winningNumbers.addAll(winningNumbers);
+ return this;
+ }
+
+ public LottoRepositoryBuilder setBonusNumber(int bonusNumber) {
+ Lotto tempLotto = new Lotto(winningNumbers);
+ tempLotto.validateAdditionalNumber(bonusNumber);
+ this.bonusNumber = bonusNumber;
+ return this;
+ }
+
+ public LottoRepository build() {
+ if (purchasedLotto == null || winningNumbers == null || bonusNumber == null) {
+ throw new IllegalStateException(ExceptionConstants.INTERNAL_SERVER_ERROR.getMessage());
+ }
+ return new LottoRepository(this);
+ }
+
+ public List getPurchasedLotto() {
+ return purchasedLotto;
+ }
+ }
+}
diff --git a/src/main/java/lotto/model/dto/WinningDataDto.java b/src/main/java/lotto/model/dto/WinningDataDto.java
new file mode 100644
index 0000000..0197333
--- /dev/null
+++ b/src/main/java/lotto/model/dto/WinningDataDto.java
@@ -0,0 +1,6 @@
+package lotto.model.dto;
+
+import lotto.utils.LottoPrize;
+
+public record WinningDataDto(LottoPrize lottoPrize, int count) {
+}
diff --git a/src/main/java/lotto/service/LottoControlService.java b/src/main/java/lotto/service/LottoControlService.java
new file mode 100644
index 0000000..f2c6588
--- /dev/null
+++ b/src/main/java/lotto/service/LottoControlService.java
@@ -0,0 +1,21 @@
+package lotto.service;
+
+import java.util.List;
+import java.util.function.Supplier;
+import lotto.model.dto.WinningDataDto;
+
+public interface LottoControlService {
+ void buyLotto(int amount, Supplier> pickFunction);
+
+ public void setWinningNumbers(String line);
+
+ public void setBonusNumber(int bonusNumber);
+
+ public void composeLotto();
+
+ List checkWinning();
+
+ float calculateROI(List winningDataDto, int price);
+
+ List> getLotto();
+}
diff --git a/src/main/java/lotto/service/LottoControlServiceImpl.java b/src/main/java/lotto/service/LottoControlServiceImpl.java
new file mode 100644
index 0000000..0b2c889
--- /dev/null
+++ b/src/main/java/lotto/service/LottoControlServiceImpl.java
@@ -0,0 +1,55 @@
+package lotto.service;
+
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+import lotto.model.Lotto;
+import lotto.model.LottoRepository;
+import lotto.model.LottoRepository.LottoRepositoryBuilder;
+import lotto.model.dto.WinningDataDto;
+
+public class LottoControlServiceImpl implements LottoControlService {
+ LottoRepositoryBuilder lottoRepositoryBuilder;
+ LottoRepository lottoRepository;
+
+ public LottoControlServiceImpl() {
+ lottoRepositoryBuilder = new LottoRepositoryBuilder();
+ }
+
+ public void buyLotto(int amount, Supplier> pickFunction) {
+ lottoRepositoryBuilder.setPurchasedLotto(
+ IntStream.range(0, amount)
+ .mapToObj(i -> new Lotto(pickFunction.get()))
+ .toList()
+ );
+ }
+
+ public void setWinningNumbers(String line) {
+ lottoRepositoryBuilder.setWinningNumber(line);
+ }
+
+ public void setBonusNumber(int bonusNumber) {
+ lottoRepositoryBuilder.setBonusNumber(bonusNumber);
+ }
+
+ public void composeLotto() {
+ this.lottoRepository = lottoRepositoryBuilder.build();
+ }
+
+ public List checkWinning() {
+ return lottoRepository.countGrade();
+ }
+
+ public float calculateROI(List winningDataDto, int price) {
+ long returnValue = winningDataDto.stream()
+ .mapToLong(dto -> (long) dto.lottoPrize().getPrice() * dto.count())
+ .sum();
+ return ((float) returnValue / price) * 100;
+ }
+
+ public List> getLotto() {
+ return lottoRepositoryBuilder.getPurchasedLotto().stream()
+ .map(Lotto::getNumbers)
+ .toList();
+ }
+}
diff --git a/src/main/java/lotto/service/RandomService.java b/src/main/java/lotto/service/RandomService.java
new file mode 100644
index 0000000..a48bada
--- /dev/null
+++ b/src/main/java/lotto/service/RandomService.java
@@ -0,0 +1,7 @@
+package lotto.service;
+
+import java.util.List;
+
+public interface RandomService {
+ List getLottoNumbers();
+}
diff --git a/src/main/java/lotto/service/RandomServiceImpl.java b/src/main/java/lotto/service/RandomServiceImpl.java
new file mode 100644
index 0000000..530eace
--- /dev/null
+++ b/src/main/java/lotto/service/RandomServiceImpl.java
@@ -0,0 +1,14 @@
+package lotto.service;
+
+import static lotto.utils.LottoConstants.LOTTO_NUMBER_COUNT;
+import static lotto.utils.LottoConstants.LOWER_BOUND_NUMBER;
+import static lotto.utils.LottoConstants.UPPER_BOUND_NUMBER;
+
+import camp.nextstep.edu.missionutils.Randoms;
+import java.util.List;
+
+public class RandomServiceImpl implements RandomService {
+ public List getLottoNumbers() {
+ return Randoms.pickUniqueNumbersInRange(LOWER_BOUND_NUMBER, UPPER_BOUND_NUMBER, LOTTO_NUMBER_COUNT);
+ }
+}
diff --git a/src/main/java/lotto/utils/ExceptionConstants.java b/src/main/java/lotto/utils/ExceptionConstants.java
new file mode 100644
index 0000000..38acdb7
--- /dev/null
+++ b/src/main/java/lotto/utils/ExceptionConstants.java
@@ -0,0 +1,23 @@
+package lotto.utils;
+
+public enum ExceptionConstants {
+ INDIVISIBLE_PRICE("1,000원 단위로 입력해 주세요."),
+ TOO_BIG_PRICE("입력한 금액 보다 작은 금액을 입력해 주세요."),
+ INVALID_PRICE("유효한 구입 금액을 입력해 주세요"),
+ INVALID_LOTTO_RANGE("1부터 45 사이의 값을 입력해 주세요."),
+ DUPLICATED_LOTTO_NUMBER("중복되지 않은 번호를 입력해주세요."),
+ INVALID_LOTTO_NUMBER_FORM("당첨 번호 6개를 정확히 입력해주세요. ex)1,2,3,4,5,6"),
+ INVALID_BONUS_NUMBER_FORM("보너스 번호 하나를 정확히 입력해주세요. ex)7"),
+ INTERNAL_SERVER_ERROR("알 수 없는 이유로 프로그램이 정삭 작동하지 않습니다.");
+
+
+ private final String message;
+
+ ExceptionConstants(String message) {
+ this.message = "[ERROR] " + message;
+ }
+
+ public String getMessage() {
+ return 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..b14c8b9
--- /dev/null
+++ b/src/main/java/lotto/utils/LottoConstants.java
@@ -0,0 +1,10 @@
+package lotto.utils;
+
+public class LottoConstants {
+ public static int LOTTO_PRICE = 1000;
+ public static int UPPER_BOUND_NUMBER = 45;
+ public static int LOWER_BOUND_NUMBER = 1;
+ public static int LOTTO_NUMBER_COUNT = 6;
+
+ public static String NUMBER_SEPARATOR = ",";
+}
diff --git a/src/main/java/lotto/utils/LottoPrize.java b/src/main/java/lotto/utils/LottoPrize.java
new file mode 100644
index 0000000..25efb84
--- /dev/null
+++ b/src/main/java/lotto/utils/LottoPrize.java
@@ -0,0 +1,55 @@
+package lotto.utils;
+
+import java.util.List;
+
+public enum LottoPrize {
+ NOTHING("꽝", 0),
+ FIFTH_PRICE("3개 일치", 5_000),
+ FOURTH_PRICE("4개 일치", 50_000),
+ THIRD_PRICE("5개 일치", 1_500_000),
+ SECOND_PRICE("5개 일치, 보너스 볼 일치", 30_000_000),
+ FIRST_PRICE("6개 일치", 2_000_000_000);
+
+ private final String condition;
+ private final int price;
+
+ LottoPrize(String condition, int price) {
+ this.condition = condition;
+ this.price = price;
+ }
+
+ public String getCondition() {
+ return condition;
+ }
+
+ public int getPrice() {
+ return price;
+ }
+
+ public static List getPrizeTypes() {
+ return List.of(FIFTH_PRICE, FOURTH_PRICE, THIRD_PRICE, SECOND_PRICE, FIRST_PRICE);
+ }
+
+ public static LottoPrize getLottoPrize(int matchCount, boolean matchBonus) {
+ if (matchCount == 6) {
+ return getFirstOrSecondPrize(matchBonus);
+ }
+ if (matchCount == 5) {
+ return THIRD_PRICE;
+ }
+ if (matchCount == 4) {
+ return FOURTH_PRICE;
+ }
+ if (matchCount == 3) {
+ return FIFTH_PRICE;
+ }
+ return NOTHING;
+ }
+
+ private static LottoPrize getFirstOrSecondPrize(boolean matchBonus) {
+ if (matchBonus) {
+ return SECOND_PRICE;
+ }
+ return FIRST_PRICE;
+ }
+}
diff --git a/src/main/java/lotto/utils/MessageConstants.java b/src/main/java/lotto/utils/MessageConstants.java
new file mode 100644
index 0000000..5981d7c
--- /dev/null
+++ b/src/main/java/lotto/utils/MessageConstants.java
@@ -0,0 +1,21 @@
+package lotto.utils;
+
+public enum MessageConstants {
+ PURCHASE_GUIDE_MESSAGE("구입금액을 입력해 주세요."),
+ PURCHASE_AMOUNT_MESSAGE("%d개를 구매했습니다."),
+ LOTTO_NUMBER_GUIDE_MESSAGE("당첨 번호를 입력해 주세요."),
+ BONUS_NUMBER_GUIDE_MESSAGE("보너스 번호를 입력해 주세요."),
+ WINNING_RESULTS_MESSAGE("당첨 통계\n---"),
+ WINNING_NUMBER_MESSAGE("%s (%,d원) - %d개"),
+ RATE_OF_RETURN_MESSAGE("총 수익률은 %.1f%%입니다.");
+
+ private final String message;
+
+ MessageConstants(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/src/main/java/lotto/utils/Utility.java b/src/main/java/lotto/utils/Utility.java
new file mode 100644
index 0000000..7c54173
--- /dev/null
+++ b/src/main/java/lotto/utils/Utility.java
@@ -0,0 +1,40 @@
+package lotto.utils;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class Utility {
+ private static final String SEPARATOR = ",";
+
+ public static long countValue(List items, T target) {
+ return items.stream().filter(item -> item.equals(target)).count();
+ }
+
+ public static boolean hasDuplicatedValue(List itemList) {
+ Set uniqueNames = new HashSet<>(itemList);
+ return uniqueNames.size() != itemList.size();
+ }
+
+ public static boolean isInRange(int value, int min, int max) {
+ return value >= min && value <= max;
+ }
+
+ public static boolean isInRange(List values, int min, int max) {
+ return !values.stream()
+ .map(value -> isInRange(value, min, max))
+ .toList()
+ .contains(false);
+ }
+
+ public static boolean isDividedByThousand(int value) {
+ return value % 1000 == 0;
+ }
+
+ public static List parseInt(String line) {
+ return Arrays.stream(line.trim().split(SEPARATOR))
+ .map(Integer::parseInt)
+ .toList();
+ }
+}
diff --git a/src/main/java/lotto/view/InputView.java b/src/main/java/lotto/view/InputView.java
new file mode 100644
index 0000000..3cd21b6
--- /dev/null
+++ b/src/main/java/lotto/view/InputView.java
@@ -0,0 +1,40 @@
+package lotto.view;
+
+import lotto.utils.ExceptionConstants;
+import lotto.utils.MessageConstants;
+import lotto.view.provider.InputProvider;
+
+public class InputView {
+ private final InputProvider inputProvider;
+
+ public InputView(InputProvider inputProvider) {
+ this.inputProvider = inputProvider;
+ }
+
+ public int getPrice() {
+ try {
+ System.out.println(MessageConstants.PURCHASE_GUIDE_MESSAGE.getMessage());
+ return Integer.parseInt(inputProvider.readLine());
+ } catch (Exception e) {
+ throw new IllegalArgumentException(ExceptionConstants.INVALID_PRICE.getMessage());
+ }
+ }
+
+ public String getLottoNumber() {
+ try {
+ System.out.println(MessageConstants.LOTTO_NUMBER_GUIDE_MESSAGE.getMessage());
+ return inputProvider.readLine();
+ } catch (Exception e) {
+ throw new IllegalArgumentException(ExceptionConstants.INVALID_LOTTO_NUMBER_FORM.getMessage());
+ }
+ }
+
+ public int getBonusNumber() {
+ try {
+ System.out.println(MessageConstants.BONUS_NUMBER_GUIDE_MESSAGE.getMessage());
+ return Integer.parseInt(inputProvider.readLine());
+ } catch (Exception e) {
+ throw new IllegalArgumentException(ExceptionConstants.INVALID_BONUS_NUMBER_FORM.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/lotto/view/OutputView.java b/src/main/java/lotto/view/OutputView.java
new file mode 100644
index 0000000..3b75f72
--- /dev/null
+++ b/src/main/java/lotto/view/OutputView.java
@@ -0,0 +1,35 @@
+package lotto.view;
+
+import java.util.List;
+import lotto.model.dto.WinningDataDto;
+import lotto.utils.MessageConstants;
+
+public class OutputView {
+ private final String SEPARATOR = ", ";
+ private final String WRAPPER = "[%s]";
+
+ public void printLotto(List lottoNumber) {
+ List numbers = lottoNumber.stream().map(Object::toString).toList();
+ System.out.printf(WRAPPER + "%n", String.join(SEPARATOR, numbers));
+ }
+
+ public void printResultGuide(int amount) {
+ System.out.printf(MessageConstants.PURCHASE_AMOUNT_MESSAGE.getMessage() + "%n", amount);
+ }
+
+ public void printResult(List result) {
+ System.out.println(MessageConstants.WINNING_RESULTS_MESSAGE.getMessage());
+ result.forEach((data) ->
+ System.out.printf(MessageConstants.WINNING_NUMBER_MESSAGE.getMessage() + "%n",
+ data.lottoPrize().getCondition(), data.lottoPrize().getPrice(), data.count())
+ );
+ }
+
+ public void printRateOfReturn(float rateOfReturn) {
+ System.out.printf(MessageConstants.RATE_OF_RETURN_MESSAGE.getMessage() + "%n", rateOfReturn);
+ }
+
+ public void printException(Exception exception) {
+ System.out.println(exception.getMessage());
+ }
+}
diff --git a/src/main/java/lotto/view/provider/InputProvider.java b/src/main/java/lotto/view/provider/InputProvider.java
new file mode 100644
index 0000000..dd05cb7
--- /dev/null
+++ b/src/main/java/lotto/view/provider/InputProvider.java
@@ -0,0 +1,7 @@
+package lotto.view.provider;
+
+import java.io.IOException;
+
+public interface InputProvider {
+ String readLine() throws IOException;
+}
diff --git a/src/main/java/lotto/view/provider/WoowaInputProvider.java b/src/main/java/lotto/view/provider/WoowaInputProvider.java
new file mode 100644
index 0000000..0709fa2
--- /dev/null
+++ b/src/main/java/lotto/view/provider/WoowaInputProvider.java
@@ -0,0 +1,10 @@
+package lotto.view.provider;
+
+import camp.nextstep.edu.missionutils.Console;
+
+public class WoowaInputProvider implements InputProvider {
+ @Override
+ public String readLine() {
+ return Console.readLine();
+ }
+}
diff --git a/src/test/java/lotto/ApplicationTest.java b/src/test/java/lotto/ApplicationTest.java
index a15c7d1..8a2283d 100644
--- a/src/test/java/lotto/ApplicationTest.java
+++ b/src/test/java/lotto/ApplicationTest.java
@@ -1,14 +1,18 @@
package lotto;
-import camp.nextstep.edu.missionutils.test.NsTest;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomUniqueNumbersInRangeTest;
import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest;
import static org.assertj.core.api.Assertions.assertThat;
+import camp.nextstep.edu.missionutils.test.NsTest;
+import java.util.List;
+import java.util.stream.Stream;
+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;
+
class ApplicationTest extends NsTest {
private static final String ERROR_MESSAGE = "[ERROR]";
@@ -54,6 +58,104 @@ class ApplicationTest extends NsTest {
});
}
+ @Test
+ void 로또번호_중복_Error1() {
+ assertSimpleTest(() -> {
+ run("5000", "1,2,3,3,5,6", "7");
+ assertThat(output()).contains(ERROR_MESSAGE);
+ });
+ }
+
+ @Test
+ void 로또번호_중복_Error2() {
+ assertSimpleTest(() -> {
+ run("5000", "1,2,3,4,5,6", "4");
+ assertThat(output()).contains(ERROR_MESSAGE);
+ });
+ }
+
+ @Test
+ void 로또번호_범위_Error() {
+ assertSimpleTest(() -> {
+ run("5000", "0,2,3,4,5,6", "7");
+ assertThat(output()).contains(ERROR_MESSAGE);
+
+ run("5000", "1,2,3,4,5,46", "7");
+ assertThat(output()).contains(ERROR_MESSAGE);
+ });
+ }
+
+ @Test
+ void 로또번호_갯수_Error() {
+ assertSimpleTest(() -> {
+ runException("5000", "1,2,3,4,5", "7");
+ assertThat(output()).contains(ERROR_MESSAGE);
+
+ runException("5000", "1,2,3,4,5,6,7", "7");
+ assertThat(output()).contains(ERROR_MESSAGE);
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideNumbersForTest")
+ @DisplayName("로또 당첨 결과 테스트")
+ void 로또_당첨_등수_확인(String purchaseAmount, String winningNumbers, String bonusNumber, String expectedOutput) {
+ assertRandomUniqueNumbersInRangeTest(
+ () -> {
+ run(purchaseAmount, winningNumbers, bonusNumber);
+ assertThat(output()).contains(expectedOutput);
+ },
+ List.of(1, 2, 3, 4, 5, 6)
+ );
+ }
+
+ static Stream provideNumbersForTest() {
+ return Stream.of(
+ Arguments.of("1000", "1,2,3,43,44,45", "7", "3개 일치 (5,000원) - 1개"),
+ Arguments.of("1000", "1,2,3,4,44,45", "7", "4개 일치 (50,000원) - 1개"),
+ Arguments.of("1000", "1,2,3,4,5,45", "7", "5개 일치 (1,500,000원) - 1개"),
+ Arguments.of("1000", "1,2,3,4,5,45", "6", "5개 일치, 보너스 볼 일치 (30,000,000원) - 1개"),
+ Arguments.of("1000", "1,2,3,4,5,6", "7", "6개 일치 (2,000,000,000원) - 1개")
+
+ );
+ }
+
+ @Test
+ void 수익률_반올림_테스트() {
+ assertRandomUniqueNumbersInRangeTest(
+ () -> {
+ run("3000", "1,2,3,4,5,6", "7");
+ try {
+ assertThat(output()).contains(ERROR_MESSAGE);
+ } catch (AssertionError e) {
+ assertThat(output()).contains("총 수익률은 166.7%입니다.");
+ }
+ },
+ List.of(1, 2, 3, 43, 44, 45),
+ List.of(40, 41, 42, 43, 44, 45),
+ List.of(40, 41, 42, 43, 44, 45)
+ );
+ }
+
+ @Test
+ @DisplayName("죽지도 않고 돌아온 정수 오버플로우")
+ void OverflowTest() {
+ assertRandomUniqueNumbersInRangeTest(
+ () -> {
+ run("2000", "1,2,3,4,5,6", "7");
+ try {
+ assertThat(output()).contains(ERROR_MESSAGE);
+ } catch (AssertionError e) {
+ assertThat(output()).contains("6개 일치 (2,000,000,000원) - 2개");
+ assertThat(output()).contains("총 수익률은 200000000.0%입니다.");
+ }
+ },
+ List.of(1, 2, 3, 4, 5, 6),
+ List.of(1, 2, 3, 4, 5, 6),
+ List.of(1, 2, 3, 4, 5, 6)
+ );
+ }
+
@Override
public void runMain() {
Application.main(new String[]{});
diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java
index 309f4e5..37da87f 100644
--- a/src/test/java/lotto/LottoTest.java
+++ b/src/test/java/lotto/LottoTest.java
@@ -1,11 +1,11 @@
package lotto;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import lotto.model.Lotto;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
class LottoTest {
@Test
@@ -14,6 +14,12 @@ class LottoTest {
.isInstanceOf(IllegalArgumentException.class);
}
+ @Test
+ void 로또_번호의_개수가_6개가_안되면_예외가_발생한다() {
+ assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5)))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
@DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.")
@Test
void 로또_번호에_중복된_숫자가_있으면_예외가_발생한다() {
@@ -22,4 +28,9 @@ class LottoTest {
}
// TODO: 추가 기능 구현에 따른 테스트 코드 작성
+ @Test
+ void 로또_번호에_범위가_맞지_않으면_예외가_발생한다() {
+ assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 46)))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
}
diff --git a/src/test/java/lotto/MockInputProvider.java b/src/test/java/lotto/MockInputProvider.java
new file mode 100644
index 0000000..6fcf60b
--- /dev/null
+++ b/src/test/java/lotto/MockInputProvider.java
@@ -0,0 +1,21 @@
+package lotto;
+
+import java.io.IOException;
+import lotto.view.provider.InputProvider;
+
+public class MockInputProvider implements InputProvider {
+ private final String[] inputs;
+ private int index = 0;
+
+ public MockInputProvider(String... inputs) {
+ this.inputs = inputs;
+ }
+
+ @Override
+ public String readLine() throws IOException {
+ if (index < inputs.length) {
+ return inputs[index++];
+ }
+ throw new IOException();
+ }
+}
diff --git a/src/test/java/lotto/UtilsTest.java b/src/test/java/lotto/UtilsTest.java
new file mode 100644
index 0000000..6dc5764
--- /dev/null
+++ b/src/test/java/lotto/UtilsTest.java
@@ -0,0 +1,36 @@
+package lotto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.stream.Stream;
+import lotto.utils.LottoPrize;
+import lotto.utils.MessageConstants;
+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;
+
+public class UtilsTest {
+ @Test
+ void messageConstantsTest() {
+ String result = MessageConstants.PURCHASE_GUIDE_MESSAGE.getMessage();
+ assertEquals("구입금액을 입력해 주세요.", result);
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideLottoNumber")
+ void lottoPrizeTest(int matchCount, boolean matchBonus, LottoPrize prize) {
+ assertEquals(prize, LottoPrize.getLottoPrize(matchCount, matchBonus));
+ }
+
+ private static Stream provideLottoNumber() {
+ return Stream.of(
+ Arguments.of(6, false, LottoPrize.FIRST_PRICE),
+ Arguments.of(6, true, LottoPrize.SECOND_PRICE),
+ Arguments.of(5, true, LottoPrize.THIRD_PRICE),
+ Arguments.of(4, true, LottoPrize.FOURTH_PRICE),
+ Arguments.of(3, true, LottoPrize.FIFTH_PRICE),
+ Arguments.of(2, true, LottoPrize.NOTHING)
+ );
+ }
+}
diff --git a/src/test/java/lotto/ViewTest.java b/src/test/java/lotto/ViewTest.java
new file mode 100644
index 0000000..a88f76f
--- /dev/null
+++ b/src/test/java/lotto/ViewTest.java
@@ -0,0 +1,95 @@
+package lotto;
+
+import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import camp.nextstep.edu.missionutils.test.NsTest;
+import java.util.List;
+import lotto.model.dto.WinningDataDto;
+import lotto.utils.LottoPrize;
+import lotto.view.InputView;
+import lotto.view.OutputView;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+public class ViewTest extends NsTest {
+ private InputView inputView;
+ private OutputView outputView;
+
+ @BeforeEach
+ void setUp() {
+ inputView = new InputView(new MockInputProvider("1000", "1,2,3,4,5,6", "7"));
+ outputView = new OutputView();
+ }
+
+ @Nested
+ class InputTest {
+ @Test
+ void priceInputTest() throws Exception {
+ int price = inputView.getPrice();
+ assertEquals(1000, price);
+ }
+
+ @Test
+ void lottoNumberInputTest() throws Exception {
+ inputView.getPrice();
+ String lottoNumber = inputView.getLottoNumber();
+ assertEquals("1,2,3,4,5,6", lottoNumber);
+ }
+
+ @Test
+ void bonusNumberInputTest() throws Exception {
+ inputView.getPrice();
+ inputView.getLottoNumber();
+ int bonusNumber = inputView.getBonusNumber();
+ assertEquals(7, bonusNumber);
+ }
+ }
+
+ @Nested
+ class OutputTest {
+ @Test
+ void lottoNumberOutputTest() throws Exception {
+ assertSimpleTest(() -> {
+ outputView.printLotto(List.of(1, 2, 3, 4, 5, 6));
+ assertThat(output()).contains("[1, 2, 3, 4, 5, 6]");
+ });
+ }
+
+ @Test
+ void resultOutputTest() throws Exception {
+ assertSimpleTest(() -> {
+ outputView.printResult(List.of(
+ new WinningDataDto(LottoPrize.FIFTH_PRICE, 5),
+ new WinningDataDto(LottoPrize.FOURTH_PRICE, 4),
+ new WinningDataDto(LottoPrize.THIRD_PRICE, 3),
+ new WinningDataDto(LottoPrize.SECOND_PRICE, 2),
+ new WinningDataDto(LottoPrize.FIRST_PRICE, 1)
+ ));
+ assertThat(output()).contains("""
+ 당첨 통계
+ ---
+ 3개 일치 (5,000원) - 5개
+ 4개 일치 (50,000원) - 4개
+ 5개 일치 (1,500,000원) - 3개
+ 5개 일치, 보너스 볼 일치 (30,000,000원) - 2개
+ 6개 일치 (2,000,000,000원) - 1개"""
+ );
+ });
+ }
+ }
+
+ @Test
+ void ROIOutputTest() throws Exception {
+ assertSimpleTest(() -> {
+ outputView.printRateOfReturn(62.5f);
+ assertThat(output()).contains("총 수익률은 62.5%입니다.");
+ });
+ }
+
+ @Override
+ public void runMain() {
+ }
+}