Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
adf5bbd
feat: 입력 메시지 생성
mgim9316-a11y Mar 29, 2026
48c77a2
feat: Controller 호출
mgim9316-a11y Mar 29, 2026
474aadd
docs: 구현 순서 작성
mgim9316-a11y Mar 29, 2026
b5eb6ff
refactor: 메서드 이름 수정
mgim9316-a11y Mar 29, 2026
381d67b
feat: 구입 금액 작성 기능 추가
mgim9316-a11y Mar 29, 2026
d4c2f13
docs: 생략된 부분 수정
mgim9316-a11y Mar 29, 2026
15b6263
feat: 시도 횟수 관리 클래스 생성
mgim9316-a11y Mar 29, 2026
d5fff4a
feat: 시도 횟수 Controller로 호출 기능 추가
mgim9316-a11y Mar 29, 2026
b0d1d45
chore: 컨벤션 수정
mgim9316-a11y Mar 29, 2026
84eb0a5
feat: 로또 번호를 랜덤으로 작성하는 기능 추가 및 일급 컬렉션으로 관리
mgim9316-a11y Mar 29, 2026
c5de9ef
feat: 랜덤 로또 번호를 출력하는 기능 추가
mgim9316-a11y Mar 29, 2026
a5a6c94
feat: 당첨 번호를 입력 받는 기능 추가
mgim9316-a11y Mar 29, 2026
4200424
feat: 수익률을 계산하는 기능 추가
mgim9316-a11y Mar 29, 2026
96c0a6d
feat: Enum을 통한 도메인 규칙 캡슐화
mgim9316-a11y Mar 29, 2026
1a69d0a
feat: 구매값 반환
mgim9316-a11y Mar 29, 2026
41c7019
feat: 결과 출력 기능 생성
mgim9316-a11y Mar 29, 2026
e37570e
fix: 오류 수정
mgim9316-a11y Mar 29, 2026
bf79e60
feat: 당첨번호 입력 호출, 결과 출력 호출 기능 추가
mgim9316-a11y Mar 29, 2026
ab9294d
refactor: 효율적으로 출력기능 변경
mgim9316-a11y Mar 29, 2026
b2c9729
refactor: 효율적으로 출력기능 변경
mgim9316-a11y Mar 29, 2026
71766eb
refactor: 요구사항 출력 기능 생성
mgim9316-a11y Mar 29, 2026
3b833ed
feat: TrialNumber 검증기능 추가
mgim9316-a11y Mar 30, 2026
1cf15ec
feat: 당첨번호, 구입 금액 검증 기능 추가
mgim9316-a11y Mar 30, 2026
0df50c8
chore: 변수명 변경
mgim9316-a11y Mar 30, 2026
6e69fda
chore: 변수명 변경
mgim9316-a11y Mar 30, 2026
1dc050d
docs: Contoller의 기능 위주로 리드미 수정
mgim9316-a11y Mar 30, 2026
35e644e
chore: 공백 추가
mgim9316-a11y Mar 30, 2026
99e7044
chore: 변수명 수정
mgim9316-a11y Mar 30, 2026
b469635
chore: 변수명 수정
mgim9316-a11y Mar 31, 2026
1bc2243
refactor: 검증 순서 수정
mgim9316-a11y Mar 31, 2026
1bc0835
feat: 잘못 입력시 다시 숫자를 입력 받는 로직 추가
mgim9316-a11y Mar 31, 2026
4429df3
feat: 로또 배열을 생성하는 기능 분리
mgim9316-a11y Mar 31, 2026
33c6110
refactor: depth의 길이 조건 만족
mgim9316-a11y Mar 31, 2026
978b639
refactor: 변수명 변경
mgim9316-a11y Mar 31, 2026
5e9291d
refactor: 로또 생성 기능 분리
mgim9316-a11y Mar 31, 2026
167c8b0
refactor: domain test 코드 작성
mgim9316-a11y Mar 31, 2026
f22c39d
feat: 로또 번호 숫자 범위 확인 기능 추가
mgim9316-a11y Mar 31, 2026
9c7c8f2
refactor: depth1 넘지 않도록 수정
mgim9316-a11y Mar 31, 2026
a41be4e
chore: 공백추가
mgim9316-a11y Mar 31, 2026
e97bb51
chore: 공백추가
mgim9316-a11y Mar 31, 2026
ff87d7b
refactor: 와일드카드 수정, run 메서드 분리, depth1로 생성
mgim9316-a11y Apr 3, 2026
bdccdfe
refactor: depth1로 생성
mgim9316-a11y Apr 3, 2026
06e66e3
refactor: depth1로 생성
mgim9316-a11y Apr 3, 2026
9b880d8
refactor: given 주석 추가
mgim9316-a11y Apr 3, 2026
5db58c1
feat: 테스트 코드 작성
mgim9316-a11y Apr 3, 2026
f1dbc56
feat: 테스트 코드 작성
mgim9316-a11y Apr 3, 2026
40664db
refator: error메시지 생성, depth1 추가
mgim9316-a11y Apr 3, 2026
9182842
chore: 불필요한 주석 삭제
mgim9316-a11y Apr 3, 2026
b0e11d8
chore: 불필요한 주석 삭제
mgim9316-a11y Apr 3, 2026
d50e676
chore: 포멧수정
mgim9316-a11y Apr 3, 2026
cdf6530
chore: 포멧수정
mgim9316-a11y Apr 3, 2026
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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 래플 로또 (Lotto)

구입 금액을 입력받아 로또를 자동 발행하고, 당첨 번호와 비교하여 당첨 내역 및 수익률을 계산하는 콘솔 기반 애플리케이션입니다.

## 🚀 기능 구현 순서 및 명세

**[1] 구입 금액을 입력 받는다.**
* `InputView`를 통해 사용자 입력을 받는다.
* 입력 문자열 검증: 숫자 외의 문자가 포함되어 있으면 `IllegalArgumentException`을 발생시킨다.
* 도메인 검증: 0 이하의 값이거나 1,000원으로 나누어 떨어지지 않으면 예외를 발생시킨다.

**[2] 구입 금액으로 시도 횟수(로또 수량) 만들기**
* `TrialNumber` 도메인 객체를 생성하여 수량을 산출한다.
* 계산 로직: 구입 금액 / 1,000

**[3] 시도 횟수만큼 랜덤 로또 생성하기**
* `LottoNumberGenerator` (전략 패턴)를 통해 1~45 사이의 중복되지 않는 난수 6개를 추출한다.
* 생성된 번호들은 오름차순으로 정렬된다.
* `Lotto` 객체 생성 시 번호가 6개가 아니거나 중복이 존재하면 `IllegalArgumentException`을 발생시켜 무결성을 보장한다.
* 시도 횟수만큼 생성된 `Lotto` 객체들을 일급 컬렉션인 `LottoNumber`로 래핑하여 관리한다.

**[4] 생성된 로또 번호 출력하기**
* `OutputView`를 통해 발행된 로또의 총 수량과 각 로또의 번호 배열을 콘솔에 출력한다.

**[5] 지난주 당첨 번호 입력 받기**
* `InputView`를 통해 당첨 번호를 입력받는다.
* 입력 문자열 검증: 숫자, 쉼표(,), 공백 외의 문자가 포함되어 있으면 예외를 발생시킨다.
* 쉼표를 기준으로 문자열을 분리하여 `List<Integer>` 형태로 파싱한다.

**[6] 당첨 번호와 로또 번호 비교해서 결과 탐색하기**
* `CalculateLottoNumber` 도메인에서 발행된 로또 리스트와 당첨 번호를 대조한다.
* 일치하는 숫자의 개수를 산출하고, 이를 `Rank` Enum 객체(당첨 기준 및 상금 캡슐화)와 매핑하여 상태를 저장한다.

**[7] 결과(통계 및 수익률) 출력하기**
* `OutputView`를 통해 3개~6개 일치에 대한 각 등수별 당첨 횟수를 출력한다.
* 총 수익률을 계산하여 출력한다.
* 수익률 계산식: 총 상금 / 구입 금액
* 수익률은 소수점 셋째 자리에서 내림(버림) 처리하여 소수점 둘째 자리까지 표기한다. (예: 0.35)
8 changes: 8 additions & 0 deletions src/main/java/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import controller.Controller;

public class Application {
public static void main(String[] args) {
Controller controller = new Controller();
controller.run();
}
}
57 changes: 57 additions & 0 deletions src/main/java/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package controller;

import domain.*;
import view.InputView;
import view.OutputView;

import java.util.List;
import java.util.function.Supplier;

public class Controller {
public void run() {
TrialNumber trialNumber = getTrialNumber();
LottoTickets lottoTickets = issueLottoTickets(trialNumber);
Lotto winningLotto = getWinningLotto();
calculateAndPrintResults(trialNumber, lottoTickets, winningLotto);
}

private TrialNumber getTrialNumber() {
return retry(() -> {
OutputView.printInputPurchaseAmount();
int amount = InputView.inputPurchaseMoney();
return new TrialNumber(amount);
});
}

private LottoTickets issueLottoTickets(TrialNumber trialNumber) {
int trialCount = trialNumber.getTrialNumber();
LottoMachine lottoMachine = new LottoMachine(new RandomLottoNumberGenerator());
List<Lotto> generatedLottos = lottoMachine.issue(trialCount);
LottoTickets lottoTickets = new LottoTickets(generatedLottos);
OutputView.printLottoNumber(lottoTickets, trialCount);
return lottoTickets;
}

private Lotto getWinningLotto() {
return retry(() -> {
OutputView.printInputWinningNumber();
List<Integer> inputWinningNumbers = InputView.inputWinningNumber();
return new Lotto(inputWinningNumbers);
});
}

private void calculateAndPrintResults(TrialNumber trialNumber, LottoTickets lottoTickets, Lotto winningLotto) {
LottoResult statisticsResult = new LottoResult(lottoTickets, winningLotto);
int purchaseAmount = trialNumber.getPurchaseAmount();
OutputView.printWinningStatistics(statisticsResult, purchaseAmount);
}
Comment on lines +11 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

run()메소드가 살짝 길어보이는 감이 있는데요, 각 단계를 각각의 메소드로 분리해보면 전체 흐름이 한눈에 보이도록 정리할 수 있을 것 같아요!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

좋은 생각입니다!


private <T> T retry(Supplier<T> supplier) {
try {
return supplier.get();
} catch (IllegalArgumentException e) {
OutputView.printErrorMessage(e.getMessage());
return retry(supplier);
}
}
}
43 changes: 43 additions & 0 deletions src/main/java/domain/Lotto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package domain;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;

public class Lotto {
private static final int MIN_NUMBER = 1;
private static final int MAX_NUMBER = 45;
private static final int LOTTO_SIZE = 6;

private final List<Integer> lottoNumber;

public Lotto(List<Integer> lottoNumber) {
validate(lottoNumber);
validateRange(lottoNumber);
this.lottoNumber = lottoNumber;
}

private void validate(List<Integer> lottoNumber) {
if (lottoNumber.size() != LOTTO_SIZE) {
throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다.");
}
if (new HashSet<>(lottoNumber).size() != LOTTO_SIZE) {
throw new IllegalArgumentException("[ERROR] 로또 번호에 중복된 숫자가 있습니다.");
}
}
Comment on lines +20 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

현재 번호 개수와 중복 여부만 검증하고 번호 범위(1 ~ 45)는 검증하지 않고 있어요.
RandomLottoNumberGenerator가 생성 시점에서 범위를 보장하기는 하지만, LottoLottoNumberGenerator 인터페이스를 통해 생성되기 때문에 어떤 구현체가 올지 알 수 없어요.
만약 다른 구현체에서 범위 밖 숫자를 넣는다면 현재 위 검증 로직에서는 정상적으로 통과되는 문제가 있을 것 같아요.
따라서 도메인 객체는 입력 출처와는 상관 없이 자신을 스스로 검증하는 방식이 더 적절해 보이는데, 어떻게 생각하시나요?

만약 그렇게 수정한다면, Lotto 내부에 범위 검증을 추가하는 방식으로 접근하면 좋을 것 같아요~

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

사용자기 입력한 winningNumber를 객체화를 한다면 값이 검증이 안 되기 때문에 해당 validate가 필요하다고 생각합니다! 리팩토링 하겠습니다!



private void validateRange(List<Integer> lottoNumber) {
if (lottoNumber.stream().anyMatch(this::isOutOfRange)) {
throw new IllegalArgumentException("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.");
}
}

private boolean isOutOfRange(int number) {
return number < MIN_NUMBER || number > MAX_NUMBER;
}

public List<Integer> getNumbers() {
return Collections.unmodifiableList(lottoNumber);
}
}
19 changes: 19 additions & 0 deletions src/main/java/domain/LottoMachine.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package domain;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class LottoMachine {
private final LottoNumberGenerator generator;

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

public List<Lotto> issue(int trialCount) {
return IntStream.range(0, trialCount)
.mapToObj(i -> new Lotto(generator.generate()))
.collect(Collectors.toList());
}
}
7 changes: 7 additions & 0 deletions src/main/java/domain/LottoNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package domain;

import java.util.List;

public interface LottoNumberGenerator {
List<Integer> generate();
}
48 changes: 48 additions & 0 deletions src/main/java/domain/LottoResult.java
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

지금 클래스명 CalculateLottoNumber는 동사 형태인데요, 명사로 수정이 필요해보여요.
맥락 상 당첨 통계 결과를 담고 있는 클래스인 것 같은데, LottoResult 또는 LottoStatistics는 어떨까요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

클래스명을 동사로 하는 것보다는 명사로 하는군요!
하나 또 배워갑니다!!

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package domain;

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

public class LottoResult {
private final Map<Rank, Integer> matchResults;

public LottoResult(LottoTickets lottoTickets, Lotto winningLotto) {
this.matchResults = new EnumMap<>(Rank.class);
initResults();
calculate(lottoTickets.getLottoNumber(), winningLotto.getNumbers());
}

private void initResults() {
for (Rank rank : Rank.values()) {
matchResults.put(rank, 0);
}
}

private void calculate(List<Lotto> lottoNumber, List<Integer> winningNumbers) {
for (Lotto lotto : lottoNumber) {
int matchCount = countMatch(lotto.getNumbers(), winningNumbers);
Rank rank = Rank.valueOfMatchCount(matchCount);
matchResults.put(rank, matchResults.get(rank) + 1);
}
}

private int countMatch(List<Integer> lottoNumber, List<Integer> winningNumbers) {
return (int) lottoNumber.stream()
.filter(winningNumbers::contains)
.count();
}

public double calculateProfitRate(int purchaseAmount) {
long totalPrize = 0;
for (Map.Entry<Rank, Integer> entry : matchResults.entrySet()) {
totalPrize += (long) entry.getKey().getPrizeMoney() * entry.getValue();
}
double rawProfitRate = (double) totalPrize / purchaseAmount;
return Math.floor(rawProfitRate * 100) / 100.0;
}

public int getRankCount(Rank rank) {
return matchResults.get(rank);
}
}
16 changes: 16 additions & 0 deletions src/main/java/domain/LottoTickets.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package domain;

import java.util.Collections;
import java.util.List;

public class LottoTickets {
private final List<Lotto> lottoTickets;

public LottoTickets(List<Lotto> lottoTickets) {
this.lottoTickets = lottoTickets;
}

public List<Lotto> getLottoNumber() {
return Collections.unmodifiableList(lottoTickets);
}
}
30 changes: 30 additions & 0 deletions src/main/java/domain/RandomLottoNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package domain;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class RandomLottoNumberGenerator implements LottoNumberGenerator {


private static final int MIN_LOTTO_NUMBER = 1;
private static final int MAX_LOTTO_NUMBER = 45;
private static final int LOTTO_NUMBER_COUNT = 6;

private final List<Integer> lottoPool;

public RandomLottoNumberGenerator() {
this.lottoPool = new ArrayList<>();
for (int i = MIN_LOTTO_NUMBER; i <= MAX_LOTTO_NUMBER; i++) {
lottoPool.add(i);
}
}

@Override
public List<Integer> generate() {
Collections.shuffle(lottoPool);
List<Integer> selectedNumbers = new ArrayList<>(lottoPool.subList(0, LOTTO_NUMBER_COUNT));
Collections.sort(selectedNumbers);
return selectedNumbers;
}
}
42 changes: 42 additions & 0 deletions src/main/java/domain/Rank.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package domain;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public enum Rank {
NONE(0, 0),
THREE(3, 5000),
FOUR(4, 50000),
FIVE(5, 1500000),
SIX(6, 2000000000);

private final int matchCount;
private final int prizeMoney;

Rank(int matchCount, int prizeMoney) {
this.matchCount = matchCount;
this.prizeMoney = prizeMoney;
}

public static Rank valueOfMatchCount(int matchCount) {
return Arrays.stream(values())
.filter(rank -> rank.matchCount == matchCount)
.findFirst()
.orElse(NONE);
}

public static List<Rank> getWinningRanks() {
return Arrays.stream(values())
.filter(rank -> rank != NONE)
.collect(Collectors.toList());
}

public int getPrizeMoney() {
return prizeMoney;
}

public int getMatchCount() {
return matchCount;
}
}
33 changes: 33 additions & 0 deletions src/main/java/domain/TrialNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package domain;

public class TrialNumber {
private static final int LOTTO_PRICE = 1000;

private final int trialCount;
private final int purchaseAmount;

public TrialNumber(int purchaseAmount) {
validateAmount(purchaseAmount);
this.purchaseAmount = purchaseAmount;
this.trialCount = purchaseAmount / LOTTO_PRICE;

}

private void validateAmount(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("[ERROR] 구입 금액은 0보다 커야 합니다.");
}
if (amount % LOTTO_PRICE != 0) {
throw new IllegalArgumentException("[ERROR] 구입 금액은 1000원 단위여야 합니다.");
}
}

public int getTrialNumber() {
return trialCount;
}

public int getPurchaseAmount() {
return purchaseAmount;
}
Comment on lines +25 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
public int getTrialNumber() {
return trialCount;
}public int getPurchaseAmount() {
return purchaseAmount;
}
public int getTrialNumber() {
return trialCount;
}
public int getPurchaseAmount() {
return purchaseAmount;
}

요거 개행 추가해주세요!
IntelliJ 사용중이사라면 Cmd + Option + L 단축키로 자동으로 포맷팅 할 수 있어요! (윈도우는 Ctrl + Alt + L이라고 합니다)

++ 이 파일 말고도 다른 파일들도 포맷팅이 필요한 부분이 좀 보이는데, 전체적으로 한번 수행해주시면 좋을 것 같아요~

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

넵 !


}
42 changes: 42 additions & 0 deletions src/main/java/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package view;

import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

public class InputView {
private static final Scanner SCANNER = new Scanner(System.in);

public static int inputPurchaseMoney() {
String input = SCANNER.nextLine().trim();
validatePurchaseMoneyFormat(input); // 파싱 전 String 상태로 형식 검증
return Integer.parseInt(input);
}

public static List<Integer> inputWinningNumber() {
String winningNumber = SCANNER.nextLine();
validateWinningNumberFormat(winningNumber); // 파싱 전 String 상태로 형식 검증
return parse(winningNumber);
}

private static List<Integer> parse(String input) {
return Arrays.stream(input.split(","))
.map(String::trim)
.map(Integer::parseInt)
.collect(Collectors.toList());
}

private static void validatePurchaseMoneyFormat(String input) {
if (!input.matches("^[0-9]+$")) {
throw new IllegalArgumentException("[ERROR] 구입 금액은 숫자만 입력해야 합니다.");
}
}

private static void validateWinningNumberFormat(String input) {
// 숫자, 쉼표, 공백 외의 문자가 포함되어 있는지 정규표현식으로 검사
if (!input.matches("^[0-9,\\s]+$")) {
throw new IllegalArgumentException("[ERROR] 당첨 번호는 숫자, 공백, 쉼표(,)만 포함할 수 있습니다.");
}
}
}
Loading