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() { + } +}