Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
6d094ce
feat: 기본 세팅 추가
yj9107v Jan 10, 2026
37a010a
docs(readme): 기초 기능 구현 작성 완료
yj9107v Jan 10, 2026
0d499cc
refactor: 기본 세팅 중 구현에 필요없는 부분 삭제
yj9107v Jan 10, 2026
e164ae9
refactor(RetryExecutor): InputView의 형식에 맞게 ERROR 메시지 추가
yj9107v Jan 10, 2026
7c9c17d
feat(PlanerLottoErrorMessage): 구입금액에 관한 에러 메시지 추가
yj9107v Jan 10, 2026
7ab1ab2
feat(PlanetLottoPolicy): 행성 로또 규칙에 관한 final class인 PlanetLottoPolicy …
yj9107v Jan 10, 2026
81effa8
feat(PurchasedAmount): 구입금액을 관리하는 PurchaseAmount 클래스 생성
yj9107v Jan 10, 2026
0477526
feat(PlanetLottoController): 구입금액 객체 반환 로직 구현
yj9107v Jan 10, 2026
17ad7cc
docs(readme): 구입금액 입력 구현 완료로 체크 표시 추가
yj9107v Jan 10, 2026
fdaeafa
feat(PurchasedAmount): toTicketCount() 메서드 추가
yj9107v Jan 10, 2026
f4df2c5
docs(readme): 로또 구매 갯수 구현 완료로 체크 표시 추가
yj9107v Jan 10, 2026
ee6572f
test(domain/PurchaseAmountTest): 구입금액 PurchaseAmount 클래스 단위 테스트 추가
yj9107v Jan 10, 2026
3b694f5
feat(PlanetLottoNumberGenerator): 로또 번호 생성 전략 PlanetLottoNumberGenera…
yj9107v Jan 10, 2026
8f09072
feat(infra/MissionUtilsPlanetLottoNumberGenerator): 로또 번호 생성 전략 구현체인 …
yj9107v Jan 10, 2026
0fde57c
feat(domain/PlanetLottoMachine): 로또 발행해 주는 PlanetLottoMachine 추가
yj9107v Jan 10, 2026
051dbeb
feat(domain/LottoTickets): 구매한 로또 번호들을 가진 LottoTickets 추가
yj9107v Jan 10, 2026
808a5db
feat(PlanetLottoController): 티켓 수를 구하여 로또 발행 로직 구성 추가
yj9107v Jan 10, 2026
670f716
docs(readme): 로또 발행 구현 완료로 체크 표시 추가
yj9107v Jan 10, 2026
4176683
test(FakeMissionUtilsPlanetLottoNumberGenerator): 랜덤 값을 테스트 하기 위해 fak…
yj9107v Jan 10, 2026
80f67cf
test(PlanetLottoTicketsTest): 행성 로또 티켓 단위 테스트 PlanetLottoTicketsTest 생성
yj9107v Jan 10, 2026
4ba70fd
test(PlanetLottoMachineTest): fake 구현체로 랜덤 구현이 잘 되는지 PlanetLottoMachi…
yj9107v Jan 10, 2026
81bfaaa
feat(PlanetLottoErrorMessage): 행성 로또 에러 메시지 추가
yj9107v Jan 10, 2026
e433f4d
refactor(PlanetLottoTickets): List<List<String>에서 Lotto의 일급 컬렉션으로 변경
yj9107v Jan 10, 2026
33de9e1
feat(PlanetLotto): 행성 로또 PlanetLotto 생성
yj9107v Jan 10, 2026
ee9a9b4
test(PlanetLottoTest): PlanetLotto의 단위 테스트 생성
yj9107v Jan 10, 2026
d677b33
docs(readme): 로또 번호 예외 처리
yj9107v Jan 10, 2026
dec167f
refactor(RetryExecutor): OutputView에 있는 Error 메시지 메서드 활용
yj9107v Jan 10, 2026
510bbb0
feat(WinningLotto): 당첨 번호를 관리하는 WinningNumbers 객체 생성
yj9107v Jan 10, 2026
ca14dbb
refactor(DeveloperErrorMessage): 에러 메시지 수정
yj9107v Jan 10, 2026
11d9eb7
refactor(PlanetLottoErrorMessage): 에러 메시지 수정
yj9107v Jan 10, 2026
15b00ee
feat(PlanetLottoController): 당첨 번호 안내문 출력 및 가져오기
yj9107v Jan 10, 2026
443b1db
docs(readme): 당첨 번호 구현 완료로 체크 표시 추가
yj9107v Jan 10, 2026
3cf1b7b
feat(PlanetLottoErrorMessage): 보너스 번호와 당첨 번호 중복 시 에러 메시지 추가
yj9107v Jan 10, 2026
b016b99
feat(WinningNumbers): 당첨 번호와 보너스 번화가 중복될 시 에러를 던짐
yj9107v Jan 10, 2026
a8d535b
feat(BonusNumber): 보너스 번호를 관리하는 BonusNumber 객체 생성
yj9107v Jan 10, 2026
73348f7
feat(PlanetLottoController): 보너스 번호 안내문 출력과 가져오기
yj9107v Jan 10, 2026
ad7c811
docs(readme): 보너스 번호 구현 완료로 체크 표시 추가
yj9107v Jan 10, 2026
ee657a8
feat(Rank): 로또 결과를 비교하기 위해 만든 enum Rank
yj9107v Jan 10, 2026
533a1d6
feat(WinningNumbers): 로또 결과를 추리기 위해 추가
yj9107v Jan 10, 2026
174e6ec
feat(PlanetLottoTickets): 당첨 번호 보너스 번호를 가져와 구매한 로또와 비교
yj9107v Jan 10, 2026
bfbccef
feat(PlanetLottoResult): 로또 결과를 관리하는 객체 생성
yj9107v Jan 10, 2026
fa89935
feat(PlanetLottoController): 당첨 통계를 출력하는 로직 구현
yj9107v Jan 10, 2026
2b047db
docs(readme): 당첨 통계 메시지 출력 구현 완료로 체크 표시 추가
yj9107v Jan 10, 2026
b1bb6b0
refactor(InputParser): 패키지 변경
yj9107v Jan 10, 2026
0f5337e
feat(Application): PlanetController를 생성하여 행성 로또 프로그램 실행 main()
yj9107v Jan 10, 2026
f8bc34b
docs(readme): 도전 목표 설명 추가
yj9107v Jan 10, 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
101 changes: 101 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,102 @@
# java-planetlotto-precourse

# 🪐🔢 행성 로또 - 기능 목록

## 도전 목표!!
- OOP를 최대한 지키면서 코딩하는 것!
- 기존 긴 시간 동안 못한 것을 짧은 시간에 해결하는 훈련
- 20,000원이라는 금액 한계 설정
- 객체에서 null을 개발자가 주었을 때, 잘 대처할 수 있게 예외를 더 틈틈히 잡는다.

- 단위 테스트를 만드는 것과 1~3주차, 오픈미션은 모두 긴 시간 동안 로직을 구현했습니다.
- 이렇게 주어진 4시간 안에 난잡해지는 코드가 아닌 명확한 OOP를 지키면서 코드를 만드려고 더욱 더 노력했습니다.

### 0️⃣ 규칙
- 사용자 입력에 대한 예외가 발생한 경우 **재입력**
- 사용자 입력은 **엄격히 검증**되며, 형식 오류는 모두 예외로 처리한다.
- 공백, 불필요한 0, 범위 초과 입력 등은 허용되지 않는다.
- 모든 예외에 대한 에러 문구 `"[ERROR] ..."`로 시작
- **로또 규칙**
- 로또 1회 구입금액: 500원
- 로또 1인당 최대 구입금액: 20,000원
- 로또 번호 범위 1 ~ 30
- 로또 1개당 총 5개 번호 소유
- 당첨 번호(5자리)와 보너스 번호가 중복이라면 보너스 번호를 다시 입력 받는다

---
### 1️⃣ 로또 구입금액 입력 받기
- [x] `"구입금액을 입력해 주세요."` 메시지 출력 (정수 변환 포함)
- [x] 로또 구입금액 입력
- 구입금액 입력 예외 처리

---
### 2️⃣ 로또 구매 갯수
- [x] 로또 한장의 가격은 500원 `(구입금액 / 500 = 로또 발행 횟수)`
- 나머지가 생길 시 예외 처리

---
### 3️⃣ 로또 발행
- [x] `"%d개를 구매했습니다."` 메시지 출력
- [x] 로또 번호를 나타내는 5개의 랜덤 숫자, 범위(1 ~ 30), 중복 X
- `Randoms.pickUniqueNumbersInRange(1, 30 ,5)`
- [x] 로또 발행 수 `ex) 1000원 구입 금액이면 2번 로또를 발행`
- [x] 로또 번호를 리스트에 오름차순으로 저장
- [x] 저장된 로또 리스트 출력
- [x] 로또 번호 예외 처리

---
### 4️⃣ 당첨 번호 입력 및 파싱
- [x] `당첨 번호를 입력해 주세요.` 메시지 출력
- [x] 당첨 번호 입력
- [x] 당첨 번호 입력 예외 처리
- [x] 당첨 번호 저장

---
### 5️⃣ 보너스 번호 입력
- [x] `보너스 번호 번호를 입력해 주세요.` 메시지 출력
- [x] 보너스 번호 입력
- [x] 입력 예외 처리

---
### 6️⃣ 로또 당첨 통계
- [x] `"당첨 통계"` 메시지 출력
- [x] `---` 메시지 출력
- [x] 로또 번호와 당첨 번호 비교하여 일치한 번호 수 구하기
- [x] 일치한 번호 수가 1등 ~ 5등 중에 해당하면 해당 등수 +1
- ex) 비교 결과 3개 일치이면 5등 개수 0 -> 1 증가
- [x] 당첨 내역 메시지 출력

```text
5개 일치 (100,000,000원) - 0개
4개 일치, 보너스 번호 일치 (10,000,000원) - 0개
4개 일치 (1,500,000원) - 0개
3개 일치, 보너스 번호 일치 (500,000원) - 0개
2개 일치, 보너스 번호 일치 (5,000원) - 1개
0개 일치 (0원) - 1개
```

---
### ⚠️ 예외 처리

> **`구입금액 입력` 예외 처리**
> - [x] 입력값이 비어있는지, 숫자인지, 정수형 범위를 넘었는지 전부 InputView의 NumberFormatException 예외로 잡음
> - [x] 구입금액 % 500 = 나머지 값 존재 시 예외
> - [x] 1인당 구입금액은 최소 500원에서 최대 20,000원

> **`5개의 랜덤 로또 번호` 예외 처리**
> - [x] 로또 번호가 5개인지
> - [x] 로또 번호 5개가 중복이 아닌지
> - [x] 로또 번호가 1 ~ 30 사이인지

> **`당첨 번호 입력` 예외 처리**
> - [x] 입력이 비어있는지
> - [x] 문자열 리스트 -> 정수형 리스트 변환 시 숫자에 해당하는지
> - [x] 입력값이 정수형 범위를 넘었는지 (예시: 9999999999999)
> - [x] 당첨 번호 객체 만들면서 PlanetLotto를 생성하여 로또 번호 검증 같이함

> **`보너스 번호 입력` 예외 처리**
> - [x] 입력값이 비어있는지, 구입금액 입력값이 정수형 범위를 넘었는지 (예시: 9999999999999), 숫자인지 InputView에서 예외를 다 잡음
> - [x] 1 ~ 30 범위에 속하는지
> - [x] 보너스 번호와 당첨 번호가 중복일 경우

---
6 changes: 6 additions & 0 deletions src/main/java/planetlotto/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package planetlotto;


import planetlotto.view.InputView;
import planetlotto.view.OutputView;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
PlanetLottoController planetLottoController = new PlanetLottoController();
planetLottoController.run();
}
}
99 changes: 99 additions & 0 deletions src/main/java/planetlotto/PlanetLottoController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package planetlotto;

import java.util.List;
import planetlotto.common.retry.RetryExecutor;
import planetlotto.domain.BonusNumber;
import planetlotto.domain.PlanetLottoMachine;
import planetlotto.domain.PlanetLottoTickets;
import planetlotto.domain.PurchaseAmount;
import planetlotto.domain.WinningNumbers;
import planetlotto.domain.policy.PlanetLottoResult;
import planetlotto.infra.MissionUtilsPlanetLottoNumberGenerator;
import planetlotto.view.InputView;
import planetlotto.view.OutputView;

public class PlanetLottoController {

public PlanetLottoController() {
}

public void run() {
PurchaseAmount purchaseAmount = getPurchasedAmount();

PlanetLottoTickets lottoTickets = getPlanetLottoTickets(createPlanetLottoMachine(),
getTicketCount(purchaseAmount));

WinningNumbers winningNumbers = getWinningNumbers();

BonusNumber bonusNumber = getBonusNumber(winningNumbers);

PlanetLottoResult planetLottoResult = getLottoResult(lottoTickets, winningNumbers, bonusNumber);

printPlanetLottoResult(planetLottoResult);
}

private PurchaseAmount getPurchasedAmount() {
return RetryExecutor.retryUntilValid(() -> {
int purchaseAmount = getUserInput();

return PurchaseAmount.of(purchaseAmount);
});
}

private int getUserInput() {
return InputView.askAmount();
}

private PlanetLottoMachine createPlanetLottoMachine() {
return PlanetLottoMachine.withGenerator(new MissionUtilsPlanetLottoNumberGenerator());
}

private int getTicketCount(PurchaseAmount purchaseAmount) {
return purchaseAmount.toTicketCount();
}

private PlanetLottoTickets getPlanetLottoTickets(PlanetLottoMachine planetLottoMachine, int ticketCount) {
List<List<Integer>> tickets = planetLottoMachine.issueTickets(ticketCount);
printTicketCountAndPurchasedLotto(tickets);
return PlanetLottoTickets.of(tickets);
}

private static void printTicketCountAndPurchasedLotto(List<List<Integer>> tickets) {
OutputView.printPurchasedLottos(tickets);
lineChange();
}

private WinningNumbers getWinningNumbers() {
return RetryExecutor.retryUntilValid(() -> {
List<Integer> winningLotto = InputView.askWinningLotto();
return WinningNumbers.of(winningLotto);
});
}

private BonusNumber getBonusNumber(WinningNumbers winningNumbers) {
return RetryExecutor.retryUntilValid(() -> {
lineChange();
int bonusNumber = InputView.askBonusNumber();
return BonusNumber.of(winningNumbers, bonusNumber);
});
}

private PlanetLottoResult getLottoResult(PlanetLottoTickets lottoTickets, WinningNumbers winningNumbers,
BonusNumber bonusNumber) {
return PlanetLottoResult.of(getRanks(lottoTickets, winningNumbers, bonusNumber));
}

private List<Integer> getRanks(PlanetLottoTickets lottoTickets, WinningNumbers winningNumbers,
BonusNumber bonusNumber) {
return lottoTickets.evaluateRanks(winningNumbers, bonusNumber);
}

private static void printPlanetLottoResult(PlanetLottoResult planetLottoResult) {
lineChange();
OutputView.printResult(planetLottoResult.getPlanetLottoResult());
}

private static void lineChange() {
System.out.print(System.lineSeparator());
}
}
24 changes: 24 additions & 0 deletions src/main/java/planetlotto/common/retry/RetryExecutor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package planetlotto.common.retry;

import java.util.function.Supplier;
import planetlotto.message.developer.DeveloperErrorMessage;
import planetlotto.view.OutputView;

public class RetryExecutor {

private static final String ERROR_MESSAGE = "[ERROR] ";

private RetryExecutor() {
throw new AssertionError(DeveloperErrorMessage.CANNOT_INSTANCE_CLASS.format(RetryExecutor.class.getName()));
}

public static <T> T retryUntilValid(Supplier<T> supplier) {
do {
try {
return supplier.get();
} catch (IllegalArgumentException e) {
OutputView.printErrorMessage(e.getMessage());
}
} while (true);
}
}
33 changes: 33 additions & 0 deletions src/main/java/planetlotto/domain/BonusNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package planetlotto.domain;

import planetlotto.domain.policy.PlanetLottoPolicy;
import planetlotto.message.user.PlanetLottoErrorMessage;

public class BonusNumber {

private final int number;

private BonusNumber(WinningNumbers winningNumbers, int number) {
validate(winningNumbers, number);
this.number = number;
}

public static BonusNumber of(WinningNumbers winningNumbers, int number) {
return new BonusNumber(winningNumbers, number);
}

private void validate(WinningNumbers winningNumbers, int number) {
validateRange(number);
winningNumbers.validateNoDuplicateBonusNumber(number);
}

private void validateRange(int number) {
if ((number < PlanetLottoPolicy.MIN_NUMBER) || (number > PlanetLottoPolicy.MAX_NUMBER)) {
throw new IllegalArgumentException(PlanetLottoErrorMessage.OUT_OF_RANGE_NUMBER.getMessage());
}
}

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

import java.util.List;
import java.util.Objects;
import planetlotto.domain.policy.PlanetLottoPolicy;
import planetlotto.message.developer.DeveloperErrorMessage;
import planetlotto.message.user.PlanetLottoErrorMessage;

public class PlanetLotto {

private static final class LottoValidator {

private LottoValidator(List<Integer> numbers) {
throw new AssertionError(
DeveloperErrorMessage.CANNOT_INSTANCE_CLASS.format(LottoValidator.class.getName()));
}

public static void validate(List<Integer> numbers) {
validateNotNullOrEmpty(numbers);
validateCount(numbers);
validateNoDuplicate(numbers);
validateRange(numbers);
}

private static void validateNotNullOrEmpty(List<Integer> numbers) {
if (numbers == null || numbers.isEmpty()) {
throw new IllegalArgumentException(DeveloperErrorMessage.NULL_VALUE.getMessage());
}
}

private static void validateCount(List<Integer> numbers) {
if (numbers.size() != PlanetLottoPolicy.NUMBER_COUNT) {
throw new IllegalArgumentException(PlanetLottoErrorMessage.INVALID_NUMBER_COUNT.getMessage());
}
}

private static void validateNoDuplicate(List<Integer> numbers) {
if (numbers.stream().distinct().count() != PlanetLottoPolicy.NUMBER_COUNT) {
throw new IllegalArgumentException(PlanetLottoErrorMessage.DUPLICATED_NUMBER.getMessage());
}
}

private static void validateRange(List<Integer> numbers) {
if (numbers.stream()
.anyMatch(number -> (number < PlanetLottoPolicy.MIN_NUMBER) || (number > PlanetLottoPolicy.MAX_NUMBER))) {
throw new IllegalArgumentException(PlanetLottoErrorMessage.OUT_OF_RANGE_NUMBER.getMessage());
}
}
}

private final List<Integer> numbers;

private PlanetLotto(List<Integer> numbers) {
LottoValidator.validate(numbers);
this.numbers = List.copyOf(numbers);
}

public static PlanetLotto of(List<Integer> numbers) {
return new PlanetLotto(numbers);
}

public int countMatchWith(PlanetLotto winningLotto) {
return (int) numbers.stream()
.filter(winningLotto::isContain)
.count();
}

public boolean isContain(int number) {
return numbers.contains(number);
}

@Override
public boolean equals(Object object) {
if (object == null || getClass() != object.getClass()) {
return false;
}
PlanetLotto lotto = (PlanetLotto) object;
return Objects.equals(numbers, lotto.numbers);
}

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

@Override
public String toString() {
return numbers.toString();
}
}
Loading