From 309359891cc1d09221abb2741054d3219d30cd0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?pol=2Eo=28=EA=B9=80=EC=84=A0=EB=8F=99=29/kakao?= Date: Mon, 14 Apr 2025 18:17:32 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20step2=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++++- src/main/java/step2/LottoGame.java | 24 ++++++++ src/main/java/step2/domain/LottoMachine.java | 22 +++++++ src/main/java/step2/domain/LottoNumber.java | 57 +++++++++++++++++++ .../step2/domain/LottoNumberGenerator.java | 21 +++++++ .../java/step2/domain/LottoNumberMatcher.java | 27 +++++++++ src/main/java/step2/domain/LottoNumbers.java | 51 +++++++++++++++++ src/main/java/step2/domain/LottoRank.java | 39 +++++++++++++ .../java/step2/domain/LottoWinningRecord.java | 23 ++++++++ src/main/java/step2/view/InputView.java | 18 ++++++ src/main/java/step2/view/ResultView.java | 31 ++++++++++ .../java/step2/domain/LottoMachineTest.java | 43 ++++++++++++++ .../domain/LottoNumberGeneratorTest.java | 52 +++++++++++++++++ .../step2/domain/LottoWinningRecordTest.java | 38 +++++++++++++ 14 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 src/main/java/step2/LottoGame.java create mode 100644 src/main/java/step2/domain/LottoMachine.java create mode 100644 src/main/java/step2/domain/LottoNumber.java create mode 100644 src/main/java/step2/domain/LottoNumberGenerator.java create mode 100644 src/main/java/step2/domain/LottoNumberMatcher.java create mode 100644 src/main/java/step2/domain/LottoNumbers.java create mode 100644 src/main/java/step2/domain/LottoRank.java create mode 100644 src/main/java/step2/domain/LottoWinningRecord.java create mode 100644 src/main/java/step2/view/InputView.java create mode 100644 src/main/java/step2/view/ResultView.java create mode 100644 src/test/java/step2/domain/LottoMachineTest.java create mode 100644 src/test/java/step2/domain/LottoNumberGeneratorTest.java create mode 100644 src/test/java/step2/domain/LottoWinningRecordTest.java diff --git a/README.md b/README.md index 7b4e53d2437..3bf6fcbd79c 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,19 @@ * 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. ## 온라인 코드 리뷰 과정 -* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) \ No newline at end of file +* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) + +# 2 단계 +## 기능 요구사항 + - 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다. + - 로또 1장의 가격은 1000원이다. + +## 구현 기능 목록 + +- [X] 랜덤하게 로또 번호를 추출기 구현 +- [X] 로또 정답 당첨 구현 +- [X] 구입금액 입력 기능 구현 +- [X] 로또 번호를 출력 기능 구현 +- [X] 당첨 번호 입력 기능 구현 +- [X] 당첨 통계 구현 +- [X] 수익률 계산기 구현 diff --git a/src/main/java/step2/LottoGame.java b/src/main/java/step2/LottoGame.java new file mode 100644 index 00000000000..d91f6fbb2ab --- /dev/null +++ b/src/main/java/step2/LottoGame.java @@ -0,0 +1,24 @@ +package step2; + +import step2.domain.*; +import step2.view.InputView; +import step2.view.ResultView; + +import java.util.List; + +public class LottoGame { + public static void main(String[] args) { + long amount = InputView.purchaseAmount(); + LottoMachine machine = new LottoMachine(); + List purchased = machine.buy(amount); + ResultView.showPurchasedLottos(purchased); + + String lastWinning = InputView.inputLastWinningLottoNumbers(); + LottoNumbers winningNumbers = LottoNumbers.fromText(lastWinning); + + LottoNumberMatcher matcher = new LottoNumberMatcher(purchased, winningNumbers); + LottoWinningRecord record = matcher.result(); + + ResultView.showLottoWinningResult(record, purchased.size()); + } +} \ No newline at end of file diff --git a/src/main/java/step2/domain/LottoMachine.java b/src/main/java/step2/domain/LottoMachine.java new file mode 100644 index 00000000000..eb2202715e1 --- /dev/null +++ b/src/main/java/step2/domain/LottoMachine.java @@ -0,0 +1,22 @@ +package step2.domain; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class LottoMachine { + public static final int PRICE_OF_LOTTO = 1_000; + + public List buy(long money) { + validate(money); + return IntStream.range(0, (int) money / PRICE_OF_LOTTO) + .mapToObj(i -> LottoNumberGenerator.generate()) + .collect(Collectors.toList()); + } + + private void validate(long money) { + if (money < PRICE_OF_LOTTO) { + throw new IllegalArgumentException("최소 1,000원 이상 입력해야 합니다."); + } + } +} \ No newline at end of file diff --git a/src/main/java/step2/domain/LottoNumber.java b/src/main/java/step2/domain/LottoNumber.java new file mode 100644 index 00000000000..78d7a863262 --- /dev/null +++ b/src/main/java/step2/domain/LottoNumber.java @@ -0,0 +1,57 @@ +package step2.domain; + +import java.util.HashMap; +import java.util.Map; + +public class LottoNumber implements Comparable { + private static final int MIN_NUMBER = 1; + private static final int MAX_NUMBER = 45; + private static final Map CACHE = new HashMap<>(); + + static { + for (int i = MIN_NUMBER; i <= MAX_NUMBER; i++) { + CACHE.put(i, new LottoNumber(i)); + } + } + + private final int number; + + private LottoNumber(int number) { + this.number = number; + } + + public static LottoNumber from(int number) { + if (number < MIN_NUMBER || number > MAX_NUMBER) { + throw new IllegalArgumentException( + String.format("숫자는 %d보다 크거나 같고, %d보다 작아야 합니다.", MIN_NUMBER, MAX_NUMBER)); + } + return CACHE.get(number); + } + + public int getNumber() { + return number; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LottoNumber)) return false; + LottoNumber that = (LottoNumber) o; + return number == that.number; + } + + @Override + public int hashCode() { + return Integer.hashCode(number); + } + + @Override + public int compareTo(LottoNumber other) { + return Integer.compare(this.number, other.number); + } + + @Override + public String toString() { + return String.valueOf(number); + } +} diff --git a/src/main/java/step2/domain/LottoNumberGenerator.java b/src/main/java/step2/domain/LottoNumberGenerator.java new file mode 100644 index 00000000000..e473ba8c7d7 --- /dev/null +++ b/src/main/java/step2/domain/LottoNumberGenerator.java @@ -0,0 +1,21 @@ +package step2.domain; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class LottoNumberGenerator { + public static final int NUMBER_COUNT = 6; + public static final int FINAL_NUMBER = 45; + + private LottoNumberGenerator() {} + + public static LottoNumbers generate() { + List all = IntStream.rangeClosed(1, FINAL_NUMBER) + .mapToObj(LottoNumber::from) + .collect(Collectors.toList()); + Collections.shuffle(all); + return new LottoNumbers(all.subList(0, NUMBER_COUNT)); + } +} diff --git a/src/main/java/step2/domain/LottoNumberMatcher.java b/src/main/java/step2/domain/LottoNumberMatcher.java new file mode 100644 index 00000000000..081b421bde3 --- /dev/null +++ b/src/main/java/step2/domain/LottoNumberMatcher.java @@ -0,0 +1,27 @@ +package step2.domain; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class LottoNumberMatcher { + private final List purchased; + private final LottoNumbers winningNumbers; + + public LottoNumberMatcher(List purchased, LottoNumbers winningNumbers) { + this.purchased = purchased; + this.winningNumbers = winningNumbers; + } + + public LottoWinningRecord result() { + Map rankMap = new EnumMap<>(LottoRank.class); + for (LottoRank rank : LottoRank.values()) { + rankMap.put(rank, 0); + } + purchased.forEach(ticket -> { + LottoRank rank = ticket.lottoRank(winningNumbers); + rankMap.put(rank, rankMap.get(rank) + 1); + }); + return new LottoWinningRecord(rankMap); + } +} diff --git a/src/main/java/step2/domain/LottoNumbers.java b/src/main/java/step2/domain/LottoNumbers.java new file mode 100644 index 00000000000..9416253b22e --- /dev/null +++ b/src/main/java/step2/domain/LottoNumbers.java @@ -0,0 +1,51 @@ +package step2.domain; + +import java.util.*; +import java.util.stream.Collectors; + +import static step2.domain.LottoNumberGenerator.NUMBER_COUNT; + +public class LottoNumbers { + private final List numbers; + + public LottoNumbers(List numbers) { + validate(numbers); + this.numbers = Collections.unmodifiableList( + numbers.stream().sorted().collect(Collectors.toList())); + } + + public static LottoNumbers from(List numbers) { + List lottoNumbers = numbers.stream() + .map(LottoNumber::from) + .collect(Collectors.toList()); + return new LottoNumbers(lottoNumbers); + } + + public static LottoNumbers fromText(String text) { + List numbers = Arrays.stream(text.split(",")) + .map(String::trim) + .map(Integer::parseInt) + .collect(Collectors.toList()); + return from(numbers); + } + + private void validate(List numbers) { + if (numbers == null || numbers.size() != NUMBER_COUNT) { + throw new IllegalArgumentException("숫자가 빈값이거나, 개수가 맞지 않습니다."); + } + Set unique = new HashSet<>(numbers); + if (unique.size() != numbers.size()) { + throw new IllegalArgumentException("중복된 숫자가 존재합니다."); + } + } + + public List numbers() { + return numbers; + } + + public LottoRank lottoRank(LottoNumbers winningNumbers) { + Set winningSet = new HashSet<>(winningNumbers.numbers()); + long matchCount = numbers.stream().filter(winningSet::contains).count(); + return LottoRank.fromMatch((int) matchCount); + } +} diff --git a/src/main/java/step2/domain/LottoRank.java b/src/main/java/step2/domain/LottoRank.java new file mode 100644 index 00000000000..201129c891f --- /dev/null +++ b/src/main/java/step2/domain/LottoRank.java @@ -0,0 +1,39 @@ +package step2.domain; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public enum LottoRank { + MATCH_3(3, 5_000, "3개 일치 (5000원)"), + MATCH_4(4, 50_000, "4개 일치 (50000원)"), + MATCH_5(5, 1_500_000, "5개 일치 (1500000원)"), + MATCH_6(6, 2_000_000_000, "6개 일치 (2000000000원)"), + NO_MATCH(0, 0, "NO_MATCH"); + + private static final Map MATCH_MAP = Arrays.stream(values()) + .collect(Collectors.toMap(rank -> rank.match, Function.identity())); + + private final int match; + private final int money; + private final String description; + + LottoRank(int match, int money, String description) { + this.match = match; + this.money = money; + this.description = description; + } + + public static LottoRank fromMatch(int match) { + return MATCH_MAP.getOrDefault(match, NO_MATCH); + } + + public int money() { + return money; + } + + public String description() { + return description; + } +} diff --git a/src/main/java/step2/domain/LottoWinningRecord.java b/src/main/java/step2/domain/LottoWinningRecord.java new file mode 100644 index 00000000000..518fb9441d3 --- /dev/null +++ b/src/main/java/step2/domain/LottoWinningRecord.java @@ -0,0 +1,23 @@ +package step2.domain; + +import java.util.Collections; +import java.util.Map; + +public class LottoWinningRecord { + private final Map rankMap; + + public LottoWinningRecord(Map rankMap) { + this.rankMap = Collections.unmodifiableMap(rankMap); + } + + public double totalLottoPrizeRatio(int totalSpent) { + long totalPrize = rankMap.entrySet().stream() + .mapToLong(e -> e.getKey().money() * e.getValue()) + .sum(); + return totalSpent == 0 ? 0 : (double) totalPrize / totalSpent; + } + + public Map rankMap() { + return rankMap; + } +} \ No newline at end of file diff --git a/src/main/java/step2/view/InputView.java b/src/main/java/step2/view/InputView.java new file mode 100644 index 00000000000..642c6e75f5e --- /dev/null +++ b/src/main/java/step2/view/InputView.java @@ -0,0 +1,18 @@ +package step2.view; + +import java.util.Scanner; + +public class InputView { + private static final Scanner scanner = new Scanner(System.in); + + public static long purchaseAmount() { + System.out.println("구입금액을 입력해주세요."); + return scanner.nextLong(); + } + + public static String inputLastWinningLottoNumbers() { + System.out.println("\n지난 주 당첨 번호를 입력해 주세요."); + scanner.nextLine(); // flush + return scanner.nextLine(); + } +} diff --git a/src/main/java/step2/view/ResultView.java b/src/main/java/step2/view/ResultView.java new file mode 100644 index 00000000000..bbf57273a63 --- /dev/null +++ b/src/main/java/step2/view/ResultView.java @@ -0,0 +1,31 @@ +package step2.view; + +import step2.domain.LottoMachine; +import step2.domain.LottoNumbers; +import step2.domain.LottoRank; +import step2.domain.LottoWinningRecord; + +import java.util.List; +import java.util.Map; + +public class ResultView { + public static void showPurchasedLottos(List lottoNumbers) { + System.out.println(lottoNumbers.size() + "개를 구매했습니다."); + lottoNumbers.forEach(number -> System.out.println(number.numbers())); + } + + public static void showLottoWinningResult(LottoWinningRecord record, int purchasedLottoCount) { + System.out.println("\n당첨 통계\n---------"); + Map rankMap = record.rankMap(); + rankMap.entrySet().stream() + .filter(entry -> entry.getKey() != LottoRank.NO_MATCH) + .forEach(entry -> + System.out.printf("%s - %d개%n", entry.getKey().description(), entry.getValue())); + + double ratio = record.totalLottoPrizeRatio(purchasedLottoCount * LottoMachine.PRICE_OF_LOTTO); + System.out.printf("총 수익률은 %.2f입니다.", ratio); + if (ratio < 1) { + System.out.println("(기준이 1이기 때문에 결과적으로 손해라는 의미임)"); + } + } +} diff --git a/src/test/java/step2/domain/LottoMachineTest.java b/src/test/java/step2/domain/LottoMachineTest.java new file mode 100644 index 00000000000..a0c12340c57 --- /dev/null +++ b/src/test/java/step2/domain/LottoMachineTest.java @@ -0,0 +1,43 @@ +package step2.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +public class LottoMachineTest { + + @Test + @DisplayName("정상적인 금액으로 로또를 구매하면 금액 / 1000 만큼 생성된다") + void buyLotto_success() { + LottoMachine machine = new LottoMachine(); + long amount = 5000; + + List lottos = machine.buy(amount); + + assertThat(lottos).hasSize(5); + assertThat(lottos).allSatisfy(lotto -> assertThat(lotto.numbers()).hasSize(6)); + } + + @Test + @DisplayName("1000원 미만의 금액으로는 로또를 구매할 수 없다") + void buyLotto_failUnderMinimum() { + LottoMachine machine = new LottoMachine(); + + assertThatThrownBy(() -> machine.buy(500)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("최소 1,000원 이상 입력해야 합니다."); + } + + @Test + @DisplayName("정확히 1000원을 입력하면 로또 1장을 구매할 수 있다") + void buyLotto_exactMinimum() { + LottoMachine machine = new LottoMachine(); + + List lottos = machine.buy(1000); + + assertThat(lottos).hasSize(1); + } +} diff --git a/src/test/java/step2/domain/LottoNumberGeneratorTest.java b/src/test/java/step2/domain/LottoNumberGeneratorTest.java new file mode 100644 index 00000000000..9d5d86a0fef --- /dev/null +++ b/src/test/java/step2/domain/LottoNumberGeneratorTest.java @@ -0,0 +1,52 @@ +package step2.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.*; + +public class LottoNumberGeneratorTest { + + @Test + @DisplayName("로또 번호는 항상 6개를 생성한다") + void generate_returnsSixNumbers() { + LottoNumbers lotto = LottoNumberGenerator.generate(); + + assertThat(lotto.numbers()).hasSize(6); + } + + @Test + @DisplayName("생성된 로또 번호는 중복이 없어야 한다") + void generate_noDuplicates() { + LottoNumbers lotto = LottoNumberGenerator.generate(); + + Set unique = lotto.numbers().stream() + .map(n -> n.getNumber()) + .collect(Collectors.toSet()); + + assertThat(unique).hasSize(6); + } + + @RepeatedTest(5) + @DisplayName("여러 번 실행해도 로또 번호가 매번 섞여 나온다") + void generate_returnsShuffledNumbers() { + LottoNumbers first = LottoNumberGenerator.generate(); + LottoNumbers second = LottoNumberGenerator.generate(); + + List firstList = first.numbers() + .stream() + .map(n -> n.getNumber()) + .collect(Collectors.toList()); + List secondList = second.numbers() + .stream() + .map(n -> n.getNumber()) + .collect(Collectors.toList()); + + assertThat(firstList).isNotEqualTo(secondList); + } +} diff --git a/src/test/java/step2/domain/LottoWinningRecordTest.java b/src/test/java/step2/domain/LottoWinningRecordTest.java new file mode 100644 index 00000000000..9eade3da11c --- /dev/null +++ b/src/test/java/step2/domain/LottoWinningRecordTest.java @@ -0,0 +1,38 @@ +package step2.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.EnumMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; + +public class LottoWinningRecordTest { + + @Test + @DisplayName("수익률을 정확히 계산한다") + void calculateTotalPrizeRatio() { + Map rankMap = new EnumMap<>(LottoRank.class); + rankMap.put(LottoRank.MATCH_3, 2); // 2 * 5000 = 10000원 당첨 + rankMap.put(LottoRank.NO_MATCH, 1); + + LottoWinningRecord record = new LottoWinningRecord(rankMap); + + double ratio = record.totalLottoPrizeRatio(5000); // 5000원 투자 + assertThat(ratio).isEqualTo(2.0); // 10000 / 5000 = 2.0 + } + + @Test + @DisplayName("지출이 0이면 수익률은 0이다") + void zeroSpentReturnsZeroRatio() { + Map rankMap = new EnumMap<>(LottoRank.class); + rankMap.put(LottoRank.MATCH_3, 1); + + LottoWinningRecord record = new LottoWinningRecord(rankMap); + + double ratio = record.totalLottoPrizeRatio(0); + assertThat(ratio).isEqualTo(0.0); + } +} +