Skip to content
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
849e2fe
RaceLotto 클래스에 금액 입력→번호 생성→당첨번호→통계 출력까지 실행 흐름 추가
Sep 24, 2025
84ad2df
RaceLottoMethod에 로또 게임 핵심 로직(입력·생성·당첨·통계) 구현
Sep 24, 2025
cf566ce
InputView 클래스 구현 (구입 금액·당첨 번호 입력)
Sep 24, 2025
84fe5a6
OutputView 클래스 구현 (구입 금액·로또·통계 출력)
Sep 24, 2025
ee9214d
구입 금액을 포장하는 Price 값 객체 구현 (검증·로또 개수 계산)
Sep 24, 2025
b0320e9
LottoNumber를 원시값 포장 객체로 구현 (1~45 범위 검증 포함)
Sep 24, 2025
caad7b4
LottoNumbers를 일급 컬렉션으로 구현 (6개 검증·정렬 기능 포함)
Sep 24, 2025
3625fd2
여러 LottoNumbers를 보관·조회하는 LottoNumbersRepository 구현
Sep 24, 2025
3c52ba5
자동 로또 번호 생성 구현
Sep 24, 2025
5256990
Main 클래스 추가 – RaceLotto 실행 엔트리 포인트
Sep 24, 2025
6eaeb8f
test: Price 값 객체 생성·예외·로또 개수 계산 단위 테스트 추가
Sep 24, 2025
35d5b27
test: LottoNumbersRepository에 로또번호 추가 및 불변리스트 반환 단위 테스트 추가
Sep 24, 2025
1925a09
test: LottoNumbers 생성·예외·불변리스트·정렬 기능 단위 테스트 추가
Sep 24, 2025
59a8dbd
test: LottoNumber 유효성 및 예외 처리 단위 테스트 추가
Sep 24, 2025
3960e78
test: RandomLottoNumberGenerator가 항상 6개의 번호를 생성하는지 단위 테스트 추가
Sep 24, 2025
8f78037
README 추가
Sep 24, 2025
bf9eb51
Controller 클래스명 RaceLotto → AutoLotooController로 변경
Sep 27, 2025
8a1ed8f
검증 메서드명을 validate… 패턴으로 통일
Sep 28, 2025
ea203ae
로또 일치 결과 계산 메서드로 분리 및 이름 명확화
Sep 28, 2025
53e53c6
printLotteryStatistics가 matchCounts와 profitRate를 받아 통계와 수익률을 출력하도록 변경
Sep 28, 2025
c0d24e0
로또 일치 결과/수익률 계산 메서드 분리 (countMatchResults, calculateProfitRate)
Sep 28, 2025
2578a3e
AutoLottoController에 showLotteryStatistics 메서드 추가 및 통계 출력 로직 분리
Sep 28, 2025
3b73ece
Controller 보조 클래스 → 모델 비즈니스 로직 클래스(LottoBusiness)로 역할 변경 및 View 의존성 제거
Sep 29, 2025
8f4157d
AutoLottoController를 단계별 메서드(showPrice, showLottoNumbers, readLastLot…
Sep 29, 2025
fa8c3ae
변수명을 더 의미 있게 변경
Sep 29, 2025
dcc3c70
변수명을 더 의미 있게 변경
Sep 29, 2025
32ff258
Main에서 AutoLottoController 메서드들을 호출해 프로그램 실행하도록 구성
Sep 29, 2025
0293eee
로또 번호 생성 로직을 모델로 이동하고 전략 패턴 적용
Sep 30, 2025
5dcf37f
몇 개의 번호가 맞았는지를 표현하는 MatchResult enum 추가
Oct 1, 2025
a45f213
로또 통계 계산 로직을 int[]에서 EnumMap 기반으로 변경
Oct 1, 2025
d87205d
AutoLottoController에서 EnumMap 기반 통계 출력 적용
Oct 1, 2025
98ea697
도메인 객체들에 equals/hashCode 구현
Oct 1, 2025
036ccdb
테스트 용이성을 위해 LottoBusiness에 전략 패턴 적용
Oct 1, 2025
510b0c0
테스트 코드 추가
Oct 1, 2025
9f18556
feat: 로또 3단계(수동 구매) 및 4단계(보너스볼 당첨 결과) 구현
Oct 2, 2025
f74a8ac
클래스/메서드 네이밍 개선
Oct 2, 2025
fd6c52f
클래스 네이밍 개선
Oct 2, 2025
87965f2
test: 보너스볼 포함 케이스 테스트 추가
Oct 2, 2025
3c5d1a1
buyTickets 메서드 책임 분리 및 리팩터링
Oct 2, 2025
eb187e4
createInputLotto, countMatchResults 메서드 로직 분리
Oct 2, 2025
6d47438
README 수정
Oct 2, 2025
de36cc9
와일드카드 import 제거, InputView 패키지명 및 mergeRepositories 메서드명 변경
Oct 8, 2025
2393907
LottoNumbers getter의 중복 불변 처리 로직 제거
Oct 8, 2025
b32bfa9
matchLottoNumber의 if문 내부 로직을 containsNumber 메서드로 분리하여 depth 감소
Oct 8, 2025
28647f0
MatchResult enum의 불필요한 description 파라미터 제거
Oct 8, 2025
579c2f3
fromCount의 for문 로직을 메서드로 분리하여 depth 감소
Oct 8, 2025
450573a
FixedLottoNumberGenerator 예외 상황 테스트 추가
Oct 8, 2025
022567f
Fixture 적용으로 테스트 중복 제거 및 가독성 향상
Oct 8, 2025
fb04843
빈줄 출력을printBlankLine() 메서드 호출로 변경
Oct 11, 2025
e1680af
LottoNumbers → LottoTicket 이름 변경 및 equals/hashCode 제거
Oct 11, 2025
e4cddbd
지역 변수와 함수 인자명을 명확하게 변경
Oct 11, 2025
6a4da86
LottoNumber 스트림을 사용해 매칭 로직 단순화
Oct 11, 2025
58fe1f0
Stream을 활용해 MatchResult 조회 로직 단순화
Oct 11, 2025
7b87bf7
동일객체 테스트 제거
Oct 11, 2025
5cda9d9
PurchaseAmount를 도메인 모델로 이동
Oct 11, 2025
679ffc8
Kotlin 관련 삭제
Oct 24, 2025
f0de06b
countMatchResults 메서드 인자 가독성 개선
Oct 24, 2025
65046e1
사용하지 않는 메서드 및 필드 정리
Oct 24, 2025
243dcd2
사용하지 않는 메서드 및 필드 정리
Oct 24, 2025
73c7c05
Merge branch 'tae-wooo' into Lotto-tae-woo3
tae-wooo Oct 24, 2025
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
18 changes: 18 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 로또 미션

## 기능 요구사항

- 로또 당첨 번호를 받아 일치한 번호 수에 따라 당첨 결과를 보여준다.

## 프로그래밍 요구사항

- 모든 원시 값과 문자열을 포장한다.
- 일급 컬렉션을 쓴다.

## 추가 기능 요구사항

- 2등을 위한 보너스볼을 추첨한다.
- 당첨 통계에 2등을 추가한다.
- 2등 당첨 조건은 당첨 번호 5개 + 보너스 볼이다
- 사용자가 수동으로 추첨 번호를 입력할 수 있도록 해야 한다.
- 입력한 금액, 자동 생성 숫자, 수동 생성 번호를 입력하도록 해야 한다.
30 changes: 30 additions & 0 deletions src/main/java/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import controller.LottoController;
import view.PurchaseAmount;
import model.LottoNumber;
import model.LottoNumbers;
import model.LottoTicketBundle;

public class Main {
public static void main(String[] args) {
LottoController controller = new LottoController();

// 1. 구입 금액 입력
PurchaseAmount purchaseAmount = controller.askPurchaseAmount();

// 2. 수동 및 자동 로또 번호 생성
LottoTicketBundle allTickets = controller.buyTickets(purchaseAmount.howManyLottos());

// 3. 당첨 번호 입력
LottoNumbers winningNumbers = controller.askWinningNumbers();

// 4. 보너스 볼 입력
LottoNumber bonusBall = controller.askBonusBall();

// 5. 통계 출력
controller.showStatistics(allTickets.readLottoNumbersRepository(),
winningNumbers.getNumbers(),
purchaseAmount.getValue(),
bonusBall
);
}
}
100 changes: 100 additions & 0 deletions src/main/java/controller/LottoController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package controller;

import view.InputView;
import view.OutputView;
import view.PurchaseAmount;
import model.LottoNumber;
import model.LottoNumbers;
import model.LottoService;
import model.LottoTicketBundle;
import model.MatchResult;


import java.util.List;
import java.util.Map;

public class LottoController {
private final LottoService lottoService = new LottoService();
private final OutputView outView = new OutputView();
private final InputView inputView = new InputView();

private PurchaseAmount readPrice() {
outView.printInputPrice();
while (true) {
PurchaseAmount purchaseAmount = readPriceException();
if (purchaseAmount != null) return purchaseAmount;
}
}

private PurchaseAmount readPriceException() {
try {
return new PurchaseAmount(Integer.parseInt(inputView.inputPrice()));
} catch (NumberFormatException input) {
outView.printInvalidNumber();
} catch (IllegalArgumentException input) {
outView.printInvalidPrice();
}
return null;
}

public PurchaseAmount askPurchaseAmount() {
PurchaseAmount purchaseAmount = readPrice();
outView.printPriceValue(purchaseAmount.getValue());
System.out.println();
return purchaseAmount;
}

public LottoTicketBundle buyTickets(int ticketCount) {
int manualCount = requestManualTicketCount();
int autoCount = calculateAutoCount(ticketCount, manualCount);

LottoTicketBundle tickets = generateTickets(manualCount, autoCount);
displayTickets(manualCount, autoCount, tickets);

return tickets;
}

private int requestManualTicketCount() {
outView.printManualTicketCount();
return Integer.parseInt(inputView.inputManualCount());
}

private int calculateAutoCount(int totalTickets, int manualCount) {
return totalTickets - manualCount;
}

private LottoTicketBundle generateTickets(int manualCount, int autoCount) {
outView.printManualLottoNumbersPrompt();
List<String> manualInputs = inputView.inputManualLottos(manualCount);

LottoTicketBundle manualTickets = lottoService.createManualLottos(manualInputs);
LottoTicketBundle autoTickets = lottoService.createLottos(autoCount);

return lottoService.mergeAutoAndManualLottos(manualTickets, autoTickets);
}

private void displayTickets(int manualCount, int autoCount, LottoTicketBundle tickets) {
outView.printBuyCount(manualCount, autoCount);
outView.printLottos(tickets.readLottoNumbersRepository());
}

public LottoNumbers askWinningNumbers() {
outView.printInputWinningNumbers();
LottoNumbers lastLotto = lottoService.createInputLotto(inputView.inputWinningNumbers());
System.out.println();
return lastLotto;
}

public LottoNumber askBonusBall() {
outView.printBonusBall();
LottoNumber bonusNumber = new LottoNumber(Integer.parseInt(inputView.inputBonusBall()));
System.out.println();
return bonusNumber;
}

public void showStatistics(List<LottoNumbers> allTickets, List<LottoNumber> winningNumbers, int purchaseAmount, LottoNumber bonusBall) {
Map<MatchResult, Integer> matchCounts = lottoService.countMatchResults(allTickets, winningNumbers, bonusBall);
String profitRate = lottoService.calculateProfitRate(matchCounts, purchaseAmount);
outView.printLotteryStatistics(matchCounts, profitRate);
}
}

Choose a reason for hiding this comment

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

전체적으로 Controller 클래스임에도 출력문이 직접적으로 작성되어 있는 것 같아요!
MVC 패턴을 적용시키신 김에 출력의 책임은 컨트롤러에서 제외하는 것이 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

저는 Controller가 출력까지 책임지는 것으로 이해하고 있었는데,
상희님 말씀대로라면 Controller가 그 책임을 갖지 않는 구조로 보입니다.
혹시 출력의 책임은 어느 계층에서 가져가는 게 더 적절할지 조언해주실 수 있을까요?

Copy link

@SANGHEEJEONG SANGHEEJEONG Oct 10, 2025

Choose a reason for hiding this comment

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

제가 말한 부분은 아래 코드인데요, 현재 print문은 모두 OutputView에서 작성해주신 것 같아요!
컨트롤러에서의 print문은 제거하고, OutputView가 출력의 책임을 모두 담당하게 하는 것이 어떨까요?

System.out.println();

Copy link
Author

Choose a reason for hiding this comment

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

view와 controller의 역할을 분리하려고 신경 썼는데, 말씀해주신 부분은 놓쳤네요!
System.out.println();을 printBlankLine() 메서드로 분리해서 OutputView에서 책임을 가지도록 수정했습니다.

21 changes: 21 additions & 0 deletions src/main/java/model/FixedLottoNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package model;

import java.util.List;

public class FixedLottoNumberGenerator implements LottoNumberGenerator {
private final List<LottoNumber> fixedNumbers;

public FixedLottoNumberGenerator(List<Integer> numbers) {
if (numbers.size() != 6) {
throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
}
this.fixedNumbers = numbers.stream()
.map(LottoNumber::new)
.toList();
}

@Override
public LottoNumbers generate() {
return new LottoNumbers(fixedNumbers);
}
}
41 changes: 41 additions & 0 deletions src/main/java/model/LottoNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package model;

import java.util.Objects;

public class LottoNumber {
private final int number;

public LottoNumber(int number) {
validateRange(number);
this.number = number;
}

private void validateRange(int number) {
if (number < 1 || number > 45) {
throw new IllegalArgumentException("로또 번호는 1~45 사이여야 합니다");
}
}

public int getNumber() {
return number;
}

@Override
public String toString() {
return String.valueOf(number);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof LottoNumber)) return false;
LottoNumber lottoNumber = (LottoNumber) o;
return number == lottoNumber.number;
}

@Override
public int hashCode() {
return Objects.hash(number);
}
}

5 changes: 5 additions & 0 deletions src/main/java/model/LottoNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package model;

public interface LottoNumberGenerator {
LottoNumbers generate();
}
41 changes: 41 additions & 0 deletions src/main/java/model/LottoNumbers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package model;

import java.util.*;

public class LottoNumbers {
private final List<LottoNumber> numbers;

public LottoNumbers(List<LottoNumber> numbers) {
validateSize(numbers);
this.numbers = List.copyOf(numbers);
}

private void validateSize(List<LottoNumber> numbers) {
if (numbers.size() != 6) {
throw new IllegalArgumentException("로또 번호가 6개가 아닙니다");
}
}

public LottoNumbers sortNumbers() {
List<LottoNumber> sortNumber = new ArrayList<>(numbers);
Collections.sort(sortNumber, Comparator.comparingInt(LottoNumber::getNumber));
return new LottoNumbers(sortNumber);
}

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

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof LottoNumbers)) return false;
LottoNumbers lottoNumbers = (LottoNumbers) o;
return Objects.equals(numbers, lottoNumbers.numbers);
}

@Override
public int hashCode() {
return Objects.hash(numbers);
}
}
118 changes: 118 additions & 0 deletions src/main/java/model/LottoService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package model;


import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

public class LottoService {
private final LottoNumberGenerator generator;

public LottoService() {
this(new RandomLottoNumberGenerator());
}

public LottoService(LottoNumberGenerator generator) {
this.generator = generator;
}

public LottoNumbers createOneLotto() {
return generator.generate();
}

public LottoTicketBundle createLottos(int count) {
LottoTicketBundle repository = new LottoTicketBundle();
for (int i = 0; i < count; i++) {
repository.addLottoNumbers(createOneLotto().sortNumbers());
}
return repository;
}

public LottoNumbers createInputLotto(String inputNumber) {
List<LottoNumber> numbers = parseInputToNumbers(inputNumber);
return new LottoNumbers(numbers);
}

private List<LottoNumber> parseInputToNumbers(String inputNumber) {
String[] rawNumbers = inputNumber.split(",");
List<LottoNumber> numbers = new ArrayList<>();
for (String raw : rawNumbers) {
numbers.add(new LottoNumber(Integer.parseInt(raw.trim())));
}
return numbers;
}

public LottoTicketBundle createManualLottos(List<String> inputs) {
LottoTicketBundle repository = new LottoTicketBundle();
for (String input : inputs) {
repository.addLottoNumbers(createInputLotto(input));
}
return repository;
}

public LottoTicketBundle mergeAutoAndManualLottos(LottoTicketBundle manual, LottoTicketBundle auto) {
LottoTicketBundle repository = new LottoTicketBundle();
for (LottoNumbers lotto : manual.readLottoNumbersRepository()) {
repository.addLottoNumbers(lotto);
}

for (LottoNumbers lotto : auto.readLottoNumbersRepository()) {
repository.addLottoNumbers(lotto);
}
return repository;
}

public Map<MatchResult, Integer> countMatchResults(List<LottoNumbers> allLotteries,
List<LottoNumber> lastLotto,
LottoNumber bonusBall) {
Map<MatchResult, Integer> matchCounts = initializeMatchCounts();
for (LottoNumbers oneLotto : allLotteries) {
int count = matchLottoNumber(oneLotto.getNumbers(), lastLotto);
boolean bonusMatch = matchBonus(oneLotto.getNumbers(), bonusBall, count);
MatchResult result = MatchResult.fromCount(count, bonusMatch);
matchCounts.put(result, matchCounts.get(result) + 1);
}
return matchCounts;
}

private Map<MatchResult, Integer> initializeMatchCounts() {
Map<MatchResult, Integer> counts = new EnumMap<>(MatchResult.class);
for (MatchResult result : MatchResult.values()) {
counts.put(result, 0);
}
return counts;
}

private int matchLottoNumber(List<LottoNumber> oneLotto, List<LottoNumber> lastLotto) {

Choose a reason for hiding this comment

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

메서드 인자에 oneLotto, lastLotto라고 적힌 부분이 꽤 있는 것 같은데,
다른 사람들이 이 네이밍을 통해 해당 변수들이 의미하는 것을 알기 힘들 것 같습니다.
조금 더 해당 변수들의 의미를 담은 네이밍으로 변경하는 것은 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

말씀해주신 대로 기존 변수명이 직관적이지 않은 부분이 있어
oneLotto → lotto, lastLotto → winningLotto로 네이밍을 변경했습니다.
이렇게 하면 의미 전달이 더 명확해질 것 같습니다.

List<Integer> lastNumbers = lastLotto.stream()
.map(LottoNumber::getNumber)
.toList();
int matchCount = 0;
for (LottoNumber number : oneLotto) {
matchCount += containsNumber(lastNumbers, number);
}
return matchCount;
}

private int containsNumber(List<Integer> lastNumbers, LottoNumber number) {
if (lastNumbers.contains(number.getNumber())) return 1;

Choose a reason for hiding this comment

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

이 부분은 사실 굳이 number.getNumber() 없이도 바로 비교를 진행할 수 있을 것 같아요.
태우님께서 리뷰를 반영하셔서 equals, hashcode를 재정의해주셨는데,
그렇기 때문에 값 객체(값이 같으면 같은 객체로 취급)의 특성을 살려 객체 자체를 비교해도 정상 동작할 것 같습니다!

private int containsNumber(List<Integer> lastNumbers, LottoNumber number) {
    if (lastNumbers.contains(number)) {
        return 1;
    }
    return 0;
}

추가적으로 스트림을 사용하면 이 부분도 depth를 줄이며 간결하게 표현 가능합니다!

private int matchLottoNumber(List<LottoNumber> oneLotto, List<LottoNumber> lastLotto) {
    return (int) oneLotto.stream()
                         .filter(number -> lastLotto.contains(number)) // 메소드 참조로 lastLotto::contains 로도 가능
                         .count();
}

Copy link
Author

@tae-wooo tae-wooo Oct 11, 2025

Choose a reason for hiding this comment

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

피드백을 통해 equals / hashCode 재정의를 실제로 활용할 수 있다는 걸 알게 되었습니다.
처음에는 아래와 같이 구현했습니다.

private int matchLottoNumber(List<LottoNumber> lotto, List<LottoNumber> winningLotto) {
    int matchCount = 0;
    for (LottoNumber number : lotto) {
        matchCount += containsNumber(winningLotto, number);
    }
    return matchCount;
}

private int containsNumber(List<LottoNumber> winningLotto, LottoNumber number) {
    if (winningLotto.contains(number)) return 1;
    return 0;
}

이후 스트림을 활용하면 두 개의 메서드를 하나로 합칠 수 있다는 것을 깨닫고, 아래와 같이 수정했습니다.

private int matchLottoNumber(List<LottoNumber> oneLotto, List<LottoNumber> lastLotto) {
    return (int) oneLotto.stream()
                         .filter(lastLotto::contains)
                         .count();
}

return 0;
}

private boolean matchBonus(List<LottoNumber> oneLotto, LottoNumber bonusBall, int count) {
if (count == 5) {
return oneLotto.contains(bonusBall);
}
return false;
}

public String calculateProfitRate(Map<MatchResult, Integer> matchCounts, int money) {
double profitRate = 0;
for (MatchResult result : matchCounts.keySet()) {
profitRate += matchCounts.get(result) * result.getReward();
}
profitRate = profitRate / (double) money;
return String.format("%.2f", profitRate);
}
}
Loading