diff --git a/build.gradle b/build.gradle index ae93bdf..ddd2650 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ version = '1.0-SNAPSHOT' java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(21) // JDK 21 사용 } } @@ -17,9 +17,17 @@ repositories { } dependencies { - implementation 'com.github.woowacourse-projects:mission-utils:1.2.0' + implementation 'com.github.woowacourse-projects:mission-utils:1.2.0' // 사용자 입력/출력용 + + //테스트를 위한 의존성 추가 gpt사용 + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation 'org.assertj:assertj-core:3.26.0' } test { - useJUnitPlatform() + useJUnitPlatform() // JUnit5 테스트 실행 +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' // 인코딩 설정 } diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922..6519caa 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,10 @@ package lotto; +import lotto.controller.LottoController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + LottoController controller = new LottoController(); + controller.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..2be32e4 --- /dev/null +++ b/src/main/java/lotto/controller/LottoController.java @@ -0,0 +1,117 @@ +package lotto.controller; + +import camp.nextstep.edu.missionutils.Console; +import camp.nextstep.edu.missionutils.Randoms; +import lotto.domain.Lotto; +import lotto.domain.LottoResult; + +import java.util.List; +import java.util.stream.Collectors; + +import lotto.view.OutputView; + +public class LottoController { + private static final int LOTTO_PRICE = 1000; + + public void run() { + int purchaseAmount = inputPurchaseAmount(); + List purchasedLottos = purchaseLottos(purchaseAmount); + + OutputView.printLottoCount(purchasedLottos.size()); + OutputView.printLottos(purchasedLottos); + + Lotto winningLotto = inputWinningLotto(); + int bonusNumber = inputBonusNumber(winningLotto); + + LottoResult result = LottoResult.of(purchasedLottos, winningLotto, bonusNumber); + OutputView.printStatistics(result, purchaseAmount); + } + + private int inputPurchaseAmount() { + while (true) { + try { + OutputView.printMessage("구입금액을 입력해 주세요."); + String input = Console.readLine(); + int amount = Integer.parseInt(input); + validatePurchaseAmount(amount); + return amount; + } catch (IllegalArgumentException e) { + OutputView.printError(e.getMessage()); + } + } + } + + private void validatePurchaseAmount(int amount) { + if (amount < LOTTO_PRICE || amount % LOTTO_PRICE != 0) { + throw new IllegalArgumentException("[ERROR] 구입 금액은 1000원 단위여야 합니다."); + } + } + + private List purchaseLottos(int amount) { + int count = amount / LOTTO_PRICE; + return java.util.stream.IntStream.range(0, count) + .mapToObj(i -> new Lotto(Randoms.pickUniqueNumbersInRange(1, 45, 6))) + .collect(Collectors.toList()); + } + + private Lotto inputWinningLotto() { + while (true) { + try { + OutputView.printMessage("당첨 번호를 입력해 주세요."); + String input = Console.readLine(); + List numbers = parseAndValidateWinningNumbers(input); + return new Lotto(numbers); + } catch (IllegalArgumentException e) { + OutputView.printError(e.getMessage()); + } + } + } + + private List parseAndValidateWinningNumbers(String input) { + String[] tokens = input.split(","); + if (tokens.length != 6) { + throw new IllegalArgumentException("[ERROR] 당첨 번호는 6개여야 합니다."); + } + List numbers = new java.util.ArrayList<>(); + for (String token : tokens) { + int num = parseNumber(token.trim()); + validateNumberRange(num); + if (numbers.contains(num)) { + throw new IllegalArgumentException("[ERROR] 당첨 번호는 중복될 수 없습니다."); + } + numbers.add(num); + } + return numbers; + } + + private int parseNumber(String token) { + try { + return Integer.parseInt(token); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("[ERROR] 숫자를 올바르게 입력해 주세요."); + } + } + + private void validateNumberRange(int num) { + if (num < 1 || num > 45) { + throw new IllegalArgumentException("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + } + + private int inputBonusNumber(Lotto winningLotto) { + while (true) { + try { + OutputView.printMessage("보너스 번호를 입력해 주세요."); + String input = Console.readLine(); + int bonus = parseNumber(input.trim()); + validateNumberRange(bonus); + if (winningLotto.contains(bonus)) { + throw new IllegalArgumentException("[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다."); + } + return bonus; + } catch (IllegalArgumentException e) { + OutputView.printError(e.getMessage()); + } + } + } +} diff --git a/src/main/java/lotto/domain/Lotto.java b/src/main/java/lotto/domain/Lotto.java new file mode 100644 index 0000000..fd11284 --- /dev/null +++ b/src/main/java/lotto/domain/Lotto.java @@ -0,0 +1,46 @@ +package lotto.domain; + +import java.util.*; + +public class Lotto { + private final List numbers; + + public Lotto(List numbers) { + validate(numbers); + this.numbers = new ArrayList<>(numbers); + Collections.sort(this.numbers); + } + + private void validate(List numbers) { + if (numbers == null || numbers.size() != 6) { + throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다."); + } + Set set = new HashSet<>(numbers); + if (set.size() != 6) { + throw new IllegalArgumentException("[ERROR] 로또 번호는 중복될 수 없습니다."); + } + for (int num : numbers) { + if (num < 1 || num > 45) { + throw new IllegalArgumentException("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); + } + } + } + + public boolean contains(int number) { + return numbers.contains(number); + } + + public int countMatch(Lotto other) { + int count = 0; + for (int num : numbers) { + if (other.contains(num)) { + count++; + } + } + return count; + } + + public List getNumbers() { + return Collections.unmodifiableList(numbers); + } +} diff --git a/src/main/java/lotto/domain/LottoRank.java b/src/main/java/lotto/domain/LottoRank.java new file mode 100644 index 0000000..8bc636b --- /dev/null +++ b/src/main/java/lotto/domain/LottoRank.java @@ -0,0 +1,54 @@ +package lotto.domain; + +public enum LottoRank { + FIRST(6, false, 2_000_000_000), + SECOND(5, true, 30_000_000), + THIRD(5, false, 1_500_000), + FOURTH(4, false, 50_000), + FIFTH(3, false, 5_000), + NONE(0, false, 0); + + private final int matchCount; + private final boolean requiresBonus; + private final int prize; + + LottoRank(int matchCount, boolean requiresBonus, int prize) { + this.matchCount = matchCount; + this.requiresBonus = requiresBonus; + this.prize = prize; + } + + public static LottoRank valueOf(int matchCount, boolean bonusMatch) { + if (matchCount == 6) { + return FIRST; + } + if (matchCount == 5 && bonusMatch) { + return SECOND; + } + if (matchCount == 5) { + return THIRD; + } + if (matchCount == 4) { + return FOURTH; + } + if (matchCount == 3) { + return FIFTH; + } + return NONE; + } + + public int getPrize() { + return prize; + } + + public String toDisplayString() { + return switch (this) { + case FIRST -> "6개 일치 (2,000,000,000원)"; + case SECOND -> "5개 일치, 보너스 볼 일치 (30,000,000원)"; + case THIRD -> "5개 일치 (1,500,000원)"; + case FOURTH -> "4개 일치 (50,000원)"; + case FIFTH -> "3개 일치 (5,000원)"; + default -> ""; + }; + } +} diff --git a/src/main/java/lotto/domain/LottoResult.java b/src/main/java/lotto/domain/LottoResult.java new file mode 100644 index 0000000..d976449 --- /dev/null +++ b/src/main/java/lotto/domain/LottoResult.java @@ -0,0 +1,40 @@ +package lotto.domain; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class LottoResult { + private final Map rankCount = new EnumMap<>(LottoRank.class); + private final int totalPrize; + + private LottoResult(Map rankCount, int totalPrize) { + this.rankCount.putAll(rankCount); + this.totalPrize = totalPrize; + } + + public static LottoResult of(List purchased, Lotto winning, int bonusNumber) { + Map counts = new EnumMap<>(LottoRank.class); + for (LottoRank rank : LottoRank.values()) { + counts.put(rank, 0); + } + + int prizeSum = 0; + for (Lotto lotto : purchased) { + int matchCount = lotto.countMatch(winning); + boolean bonusMatch = lotto.contains(bonusNumber); + LottoRank rank = LottoRank.valueOf(matchCount, bonusMatch); + counts.put(rank, counts.get(rank) + 1); + prizeSum += rank.getPrize(); + } + return new LottoResult(counts, prizeSum); + } + + public int getCount(LottoRank rank) { + return rankCount.getOrDefault(rank, 0); + } + + public int getTotalPrize() { + return totalPrize; + } +} diff --git a/src/main/java/lotto/view/OutputView.java b/src/main/java/lotto/view/OutputView.java new file mode 100644 index 0000000..2814328 --- /dev/null +++ b/src/main/java/lotto/view/OutputView.java @@ -0,0 +1,45 @@ +package lotto.view; + +import lotto.domain.Lotto; +import lotto.domain.LottoRank; +import lotto.domain.LottoResult; + +import java.util.List; + +public class OutputView { + public static void printMessage(String message) { + System.out.println(message); + } + + public static void printError(String errorMessage) { + System.out.println(errorMessage); + } + + public static void printLottoCount(int count) { + System.out.printf("%d개를 구매했습니다.%n", count); + } + + public static void printLottos(List lottos) { + for (Lotto lotto : lottos) { + System.out.println(lotto.getNumbers()); + } + } + + public static void printStatistics(LottoResult result, int purchaseAmount) { + System.out.println(); + System.out.println("당첨 통계"); + System.out.println("---"); + printRankCount(LottoRank.FIFTH, result); + printRankCount(LottoRank.FOURTH, result); + printRankCount(LottoRank.THIRD, result); + printRankCount(LottoRank.SECOND, result); + printRankCount(LottoRank.FIRST, result); + + double rate = ((double) result.getTotalPrize() / purchaseAmount) * 100; + System.out.printf("총 수익률은 %.1f%%입니다.%n", Math.round(rate * 10) / 10.0); + } + + private static void printRankCount(LottoRank rank, LottoResult result) { + System.out.printf("%s - %d개%n", rank.toDisplayString(), result.getCount(rank)); + } +} diff --git a/src/test/java/lotto/ApplicationTest.java b/src/test/java/lotto/ApplicationTest.java index a15c7d1..cb9369b 100644 --- a/src/test/java/lotto/ApplicationTest.java +++ b/src/test/java/lotto/ApplicationTest.java @@ -1,61 +1,63 @@ -package lotto; +package lotto.domain; -import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.util.Arrays; 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; - -class ApplicationTest extends NsTest { - private static final String ERROR_MESSAGE = "[ERROR]"; - - @Test - void 기능_테스트() { - assertRandomUniqueNumbersInRangeTest( - () -> { - run("8000", "1,2,3,4,5,6", "7"); - assertThat(output()).contains( - "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]", - "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%입니다." - ); - }, - List.of(8, 21, 23, 41, 42, 43), - List.of(3, 5, 11, 16, 32, 38), - List.of(7, 11, 16, 35, 36, 44), - List.of(1, 8, 11, 31, 41, 42), - List.of(13, 14, 16, 38, 42, 45), - List.of(7, 11, 30, 40, 42, 43), - List.of(2, 13, 22, 32, 38, 45), - List.of(1, 3, 5, 14, 22, 45) - ); - } - - @Test - void 예외_테스트() { - assertSimpleTest(() -> { - runException("1000j"); - assertThat(output()).contains(ERROR_MESSAGE); - }); - } - - @Override - public void runMain() { - Application.main(new String[]{}); - } +import static org.assertj.core.api.Assertions.*; + +class LottoTest { + + @Test + @DisplayName("6개 중복 없는 번호로 생성 성공") + void createLotto_success() { + List numbers = Arrays.asList(1, 2, 3, 4, 5, 6); + Lotto lotto = new Lotto(numbers); + assertThat(lotto.getNumbers()).containsExactly(1, 2, 3, 4, 5, 6); + } + + @Test + @DisplayName("6개 아닌 번호로 생성 시 예외 발생") + void createLotto_fail_not6() { + List numbers = Arrays.asList(1, 2, 3, 4, 5); + assertThatThrownBy(() -> new Lotto(numbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("6개여야 합니다"); + } + + @Test + @DisplayName("중복된 번호가 있으면 예외 발생") + void createLotto_fail_duplicate() { + List numbers = Arrays.asList(1, 2, 3, 4, 5, 5); + assertThatThrownBy(() -> new Lotto(numbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("중복될 수 없습니다"); + } + + @Test + @DisplayName("번호 범위 벗어나면 예외 발생") + void createLotto_fail_outOfRange() { + List numbers = Arrays.asList(0, 2, 3, 4, 5, 6); + assertThatThrownBy(() -> new Lotto(numbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("1부터 45 사이"); + } + + @Test + @DisplayName("다른 로또와 일치 번호 개수 계산") + void countMatch_correct() { + Lotto lotto1 = new Lotto(Arrays.asList(1, 2, 3, 4, 5, 6)); + Lotto lotto2 = new Lotto(Arrays.asList(4, 5, 6, 7, 8, 9)); + assertThat(lotto1.countMatch(lotto2)).isEqualTo(3); + } + + @Test + @DisplayName("로또 번호 포함 여부 테스트") + void containsNumber_correct() { + Lotto lotto = new Lotto(Arrays.asList(1, 2, 3, 4, 5, 6)); + assertThat(lotto.contains(3)).isTrue(); + assertThat(lotto.contains(10)).isFalse(); + } } diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java deleted file mode 100644 index 309f4e5..0000000 --- a/src/test/java/lotto/LottoTest.java +++ /dev/null @@ -1,25 +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 { - @Test - void 로또_번호의_개수가_6개가_넘어가면_예외가_발생한다() { - assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7))) - .isInstanceOf(IllegalArgumentException.class); - } - - @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.") - @Test - void 로또_번호에_중복된_숫자가_있으면_예외가_발생한다() { - assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) - .isInstanceOf(IllegalArgumentException.class); - } - - // TODO: 추가 기능 구현에 따른 테스트 코드 작성 -}