Skip to content

feat: step2 구현 #4188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: sundongkim-dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,19 @@
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.

## 온라인 코드 리뷰 과정
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)

# 2 단계
## 기능 요구사항
- 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
- 로또 1장의 가격은 1000원이다.

## 구현 기능 목록

- [X] 랜덤하게 로또 번호를 추출기 구현
- [X] 로또 정답 당첨 구현
- [X] 구입금액 입력 기능 구현
- [X] 로또 번호를 출력 기능 구현
- [X] 당첨 번호 입력 기능 구현
- [X] 당첨 통계 구현
- [X] 수익률 계산기 구현
24 changes: 24 additions & 0 deletions src/main/java/step2/LottoGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package step2;

import step2.domain.*;
import step2.view.InputView;
import step2.view.ResultView;

import java.util.List;

public class LottoGame {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai를 이용해 바이브 코딩을 하셨으면 TDD를 하기 더 쉬웠을것 같은데요. 전체적인 클래스에 비해 테스트 코드가 빈약한것 같습니다. 😢

ai를 활용하는 것은 좋지만 지금 교육 프로그램에 참여하신 목적에 더 알맞게 객체지향적으로 TDD 사이클을 이용해 생각하는 연습을 하시면 더 도움이 되지 않을까 싶습니다.

지금은 너무 시간에 쫓겨 미션을 수행하기 위해 급히 바이브 코딩을 하신것 같은데 그러면 이 과정에서 얻어가시는게 너무 적지 않으실까 싶습니다.

public static void main(String[] args) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LottoGame자체는 테스트가 불가능할까요?

long amount = InputView.purchaseAmount();
LottoMachine machine = new LottoMachine();
List<LottoNumbers> 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());
}
}
22 changes: 22 additions & 0 deletions src/main/java/step2/domain/LottoMachine.java
Original file line number Diff line number Diff line change
@@ -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<LottoNumbers> 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원 이상 입력해야 합니다.");
}
}
}
57 changes: 57 additions & 0 deletions src/main/java/step2/domain/LottoNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package step2.domain;

import java.util.HashMap;
import java.util.Map;

public class LottoNumber implements Comparable<LottoNumber> {
private static final int MIN_NUMBER = 1;
private static final int MAX_NUMBER = 45;
private static final Map<Integer, LottoNumber> CACHE = new HashMap<>();

static {
for (int i = MIN_NUMBER; i <= MAX_NUMBER; i++) {
CACHE.put(i, new LottoNumber(i));
}
}
Comment on lines +11 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache 좋네요~! 👍


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);
}
}
21 changes: 21 additions & 0 deletions src/main/java/step2/domain/LottoNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -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<LottoNumber> all = IntStream.rangeClosed(1, FINAL_NUMBER)
.mapToObj(LottoNumber::from)
.collect(Collectors.toList());
Collections.shuffle(all);
return new LottoNumbers(all.subList(0, NUMBER_COUNT));
}
}
27 changes: 27 additions & 0 deletions src/main/java/step2/domain/LottoNumberMatcher.java
Original file line number Diff line number Diff line change
@@ -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<LottoNumbers> purchased;
private final LottoNumbers winningNumbers;

public LottoNumberMatcher(List<LottoNumbers> purchased, LottoNumbers winningNumbers) {
this.purchased = purchased;
this.winningNumbers = winningNumbers;
}

public LottoWinningRecord result() {
Map<LottoRank, Integer> 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);
}
Comment on lines +16 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아무런 매개변수 없는 메서드를 이용하는 것이라면 matcher라는 클래스가 반드시 필요할까요?

}
51 changes: 51 additions & 0 deletions src/main/java/step2/domain/LottoNumbers.java
Original file line number Diff line number Diff line change
@@ -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<LottoNumber> numbers;

public LottoNumbers(List<LottoNumber> numbers) {
validate(numbers);
this.numbers = Collections.unmodifiableList(
numbers.stream().sorted().collect(Collectors.toList()));
}

public static LottoNumbers from(List<Integer> numbers) {
List<LottoNumber> lottoNumbers = numbers.stream()
.map(LottoNumber::from)
.collect(Collectors.toList());
return new LottoNumbers(lottoNumbers);
}

public static LottoNumbers fromText(String text) {
List<Integer> numbers = Arrays.stream(text.split(","))
.map(String::trim)
.map(Integer::parseInt)
.collect(Collectors.toList());
return from(numbers);
}
Comment on lines +24 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view와 밀접한 로직인것 같습니다.


private void validate(List<LottoNumber> numbers) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

호출되는 위치에 가깝게 정의해주면 좋을것 같습니다.

if (numbers == null || numbers.size() != NUMBER_COUNT) {
throw new IllegalArgumentException("숫자가 빈값이거나, 개수가 맞지 않습니다.");
}
Set<LottoNumber> unique = new HashSet<>(numbers);
if (unique.size() != numbers.size()) {
throw new IllegalArgumentException("중복된 숫자가 존재합니다.");
}
}

public List<LottoNumber> numbers() {
return numbers;
}

public LottoRank lottoRank(LottoNumbers winningNumbers) {
Set<LottoNumber> winningSet = new HashSet<>(winningNumbers.numbers());
long matchCount = numbers.stream().filter(winningSet::contains).count();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한줄에 하나의 .만 찍어서 메서드 체이닝을 해주세요.

return LottoRank.fromMatch((int) matchCount);
}
}
39 changes: 39 additions & 0 deletions src/main/java/step2/domain/LottoRank.java
Original file line number Diff line number Diff line change
@@ -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<Integer, LottoRank> MATCH_MAP = Arrays.stream(values())
.collect(Collectors.toMap(rank -> rank.match, Function.identity()));
Comment on lines +15 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map으로 별도의 구현체를 만들어두신 이유가 있나요?


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;
}
}
23 changes: 23 additions & 0 deletions src/main/java/step2/domain/LottoWinningRecord.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package step2.domain;

import java.util.Collections;
import java.util.Map;

public class LottoWinningRecord {
private final Map<LottoRank, Integer> rankMap;

public LottoWinningRecord(Map<LottoRank, Integer> 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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삼항연산자는 사용하지 말아주세요.

}

public Map<LottoRank, Integer> rankMap() {
return rankMap;
}
}
18 changes: 18 additions & 0 deletions src/main/java/step2/view/InputView.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
31 changes: 31 additions & 0 deletions src/main/java/step2/view/ResultView.java
Original file line number Diff line number Diff line change
@@ -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> 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<LottoRank, Integer> 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이기 때문에 결과적으로 손해라는 의미임)");
}
}
}
43 changes: 43 additions & 0 deletions src/test/java/step2/domain/LottoMachineTest.java
Original file line number Diff line number Diff line change
@@ -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<LottoNumbers> 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<LottoNumbers> lottos = machine.buy(1000);

assertThat(lottos).hasSize(1);
}
}
Loading