Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b279268
docs(readme): 로또 미션 기능 목록·입출력·요구사항·설계 개요 초안 추가
shlish95 Nov 3, 2025
a8d4c2c
feat(lotto): Lotto에 중복 번호 검증 추가(제공 테스트 통과)
shlish95 Nov 3, 2025
4942d35
test(domain): PurchaseAmount 1000단위·장수 계산·예외 케이스 추가
shlish95 Nov 3, 2025
66bfdfe
test(domain): LottoNumber 범위·티켓 팩토리(6개/중복/정렬) 테스트 추가
shlish95 Nov 3, 2025
ae22f1f
feat: LottoNumber 값 객체·LottoTicketFactory(sortedUniqueSix) 구현
shlish95 Nov 3, 2025
6db2bd4
test(domain): WinningNumbers 파싱/중복/범위 검증 테스트 추가
shlish95 Nov 3, 2025
8a5fa0d
feat(domain): WinningNumbers(of) 구현 및 보너스 중복·범위 검증
shlish95 Nov 3, 2025
6f6fb94
test(domain): Rank 매핑·Result 집계·ProfitRate 포맷 테스트 추가
shlish95 Nov 3, 2025
5aaadad
feat(domain): Rank/Result/ProfitRate 구현(상금·반올림 규칙 포함)
shlish95 Nov 3, 2025
95b43e3
test(application): LottoMachine 발행 스모크 테스트(개수·정렬 가정)
shlish95 Nov 3, 2025
b76532d
feat(application): LottoMachine 구현(Randoms.pickUniqueNumbersInRange 사용)
shlish95 Nov 3, 2025
c9694e8
test(domain): LottoJudge 단일 티켓 매칭 시나리오 테스트 추가
shlish95 Nov 3, 2025
176dfae
feat(domain): LottoJudge 구현(티켓 vs 당첨번호 → Rank)
shlish95 Nov 3, 2025
5690628
feat(domain): Lotto에 contains/countMatchesWith 메서드 추가(캡슐화 유지)
shlish95 Nov 3, 2025
01f7dc6
test(domain): Result 집계에 매칭 로직 연동 테스트 추가
shlish95 Nov 3, 2025
0568e17
test(domain): LottoJudge 단일 티켓 매칭 시나리오 테스트 추가
shlish95 Nov 3, 2025
cc3d701
feat(domain): LottoJudge 구현(티켓 vs 당첨번호 → Rank)
shlish95 Nov 3, 2025
defebe6
feat(domain): Lotto에 numbers() 접근자 추가(출력용 불변 복사)
shlish95 Nov 3, 2025
1012f81
feat(view): InputView/OutputView 구현(재입력 루프·정확한 포맷 출력)
shlish95 Nov 3, 2025
c9b6866
feat(application): GameRunner로 흐름 제어(입력→발행→매칭→집계→출력)
shlish95 Nov 3, 2025
cbe86db
feat(app): Application main 연결(Console.close 보장)
shlish95 Nov 3, 2025
f947c3e
fix(view): 수익률 포맷에 % 기호 누락 수정(총 수익률 문구 테스트 통과)
shlish95 Nov 3, 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
139 changes: 138 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,138 @@
# java-lotto-precourse
# 로또 (Lotto)

## 📌 개요
- 로또 발매기 콘솔 프로그램을 **TDD**로 구현한다.
- 입력: 구입 금액, 당첨 번호(쉼표 구분 6개), 보너스 번호(1개)
- 출력: 발행된 로또 목록(오름차순), 당첨 통계(등수별 개수), 총 수익률(소수점 둘째 자리 반올림)

> 외부 라이브러리 제약, 예외 처리 규칙, 스타일 가이드(들여쓰기/라인 길이/메서드 길이/else·switch 금지)를 준수한다.

---

## ✅ 기능 요구 사항 정리 (무엇을)
- 로또 번호의 범위는 **1~45**.
- **1장당 6개**의 **중복 없는** 번호로 구성.
- 구입 금액 입력 시, **1,000원 단위**인지 확인하고 금액에 비례해 **장 수**를 계산해 발행한다. (1장 가격: 1,000원)
- 당첨 번호 6개(중복 없음, 범위 내)를 입력받는다.
- 보너스 번호 1개(범위 내, 당첨 번호와 **중복 불가**)를 입력받는다.
- 구매한 각 로또를 당첨 번호/보너스 번호와 비교해 **당첨 등수**를 계산한다.
- 등수/상금
- 1등: 6개 일치 / 2,000,000,000원
- 2등: 5개 일치 + 보너스 일치 / 30,000,000원
- 3등: 5개 일치 / 1,500,000원
- 4등: 4개 일치 / 50,000원
- 5등: 3개 일치 / 5,000원
- 당첨 통계(등수별 개수)와 **총 수익률**을 출력한다.
수익률 = (총 당첨금 합 / 총 구입 금액) × 100, **소수점 둘째 자리에서 반올림** (예: `100.0%`, `51.5%`)
- **예외 발생 시**: 반드시 `IllegalArgumentException` 또는 `IllegalStateException` 등의 **명확한 유형**으로 던지고,
메시지는 **"[ERROR]"로 시작**해야 하며, **그 부분부터 입력을 다시 받는다**.

---

## 🧾 입출력 명세

### 입력
1. 구입 금액: 정수, **1,000원 단위**, 1,000으로 나누어 떨어지지 않으면 예외
예) `14000`
2. 당첨 번호: 쉼표(`,`)로 구분된 6개 정수, **중복 없음**, 범위 1~45
예) `1,2,3,4,5,6`
3. 보너스 번호: 정수 1개, 범위 1~45, **당첨 번호와 중복 불가**
예) `7`

### 출력
- 발행 개수 및 각 로또 번호(오름차순)
8개를 구매했습니다.</br>
[8, 21, 23, 41, 42, 43]</br>
...
- 당첨 통계 및 총 수익률</br>
당첨 통계</br>
3개 일치 (5,000원) - 1개</br>
4개 일치 (50,000원) - 0개</br>
5개 일치 (1,500,000원) - 0개</br>
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개</br>
6개 일치 (2,000,000,000원) - 0개</br>
총 수익률은 62.5%입니다.

### 예외 메시지 예시
- `[ERROR] 구입 금액은 1,000원 단위여야 합니다.`
- `[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.`
- `[ERROR] 당첨 번호는 6개의 중복되지 않는 숫자여야 합니다.`
- `[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.`

---

## 🔧 라이브러리/제약
- **Randoms**: `camp.nextstep.edu.missionutils.Randoms`
- 사용: `Randoms.pickUniqueNumbersInRange(1, 45, 6)` → **중복 없는 6개** 번호
- **Console**: `camp.nextstep.edu.missionutils.Console`
- 사용: `Console.readLine()` (입력) / 종료 시 `Console.close()`
- `System.exit()` **사용 금지**
- **JDK 21**, **build.gradle 수정 불가**, 외부 라이브러리 추가 금지
- **Java Style Guide**(4 space 들여쓰기, 120자 라인 등) 준수
- **indent depth ≤ 2**, **메서드 길이 ≤ 15라인**, **삼항 연산자/else/switch 금지**
→ 조건 분기: **조기 return**과 **메서드 분리**로 처리
- **Java Enum 적용**(등수/상금/판정 로직 등)

---

## 🧱 설계 개요 (레이어/의존성)
- **domain**: 순수 로직(발행, 검증, 당첨 판정, 수익률 계산) — Random/Console 의존 없음
- `Lotto` (제공 클래스 사용, numbers 외 필드 추가 금지)
- `LottoNumber` (값 객체, 범위/비교)
- `Lottos` (구매한 로또 컬렉션)
- `WinningNumbers` (당첨 6개 + 보너스)
- `Rank` (Enum: 일치 개수/보너스 여부 → 상금/표시문구)
- `Result` (등수별 집계, 총 상금, 수익률 계산)
- `PurchaseAmount` (구입 금액, 1,000원 단위 검증, 개수 계산)
- **application**: 흐름 제어/조립
- `LottoMachine` (발행기: Randoms 사용 지점은 여기 또는 별도의 어댑터)
- `GameRunner` (입력→도메인→출력)
- **view**: 입출력 전담
- `InputView` (`Console.readLine()`)
- `OutputView` (발행 목록/통계/수익률/에러 출력)
- **main**: `Application.main()` — `try/finally`에서 `Console.close()` 처리

> `Lotto`는 제공된 형태를 유지하고, 검증/정렬은 외부에서 완료된 리스트를 넘겨 생성하는 방식을 우선 고려한다.

---

## 🧪 테스트 전략 (UI 제외)
- **단위 테스트 우선**: 도메인(검증/집계/수익률)부터 작은 단위로
- `PurchaseAmount`: 1000 단위/양수 검증, 개수 계산
- `LottoNumber`: 범위/동등성
- `WinningNumbers`: 입력 파싱/중복/범위/보너스 중복 금지
- `Rank`: 일치 개수/보너스 여부 → 등수 매핑, 상금
- `Result`: 등수별 개수 집계/총 상금/수익률 반올림 규칙
- **발행기(Randoms)**: 도메인 테스트에서는 **난수 직접 사용 금지**
전략/포트 도입 또는 **고정 번호로 생성**해 검증.
- **통합 흐름**: 최종 한 번만(또는 NsTest)로 입출력 시나리오 확인

---

## 📐 규칙/정책 메모
- **정렬**: 발행된 각 로또 번호는 **오름차순** 정렬하여 출력
- **수익률 포맷**: 소수점 둘째 자리에서 반올림, `%` 포함 (예: `62.5%`)
- **재입력 로직**: 예외 발생 시 해당 입력부터 다시 받되, 프로그램 종료는 하지 않음
- **에러 포맷**: 메시지는 반드시 `"[ERROR]"`로 시작

---

## 🚀 실행 예시(요약)
구입금액을 입력해 주세요. </br>
8000

8개를 구매했습니다.</br>
[8, 21, 23, 41, 42, 43]
...

당첨 번호를 입력해 주세요.</br>
1,2,3,4,5,6

보너스 번호를 입력해 주세요.</br>
7

당첨 통계

3개 일치 (5,000원) - 1개</br>
...</br>
총 수익률은 62.5%입니다.
5 changes: 4 additions & 1 deletion src/main/java/lotto/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package lotto;


import camp.nextstep.edu.missionutils.Console;

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

import java.util.List;

public class GameRunner {

public void run() {
PurchaseAmount amount = InputView.readPurchaseAmount();

LottoMachine machine = new LottoMachine();
List<Lotto> ticket = machine.issue(amount);
System.out.println();
OutputView.printTickets(ticket);

WinningNumbers winning = InputView.readWinningNumbers();
System.out.println();

Result result = new Result();
for (Lotto t : ticket) {
Rank rank = LottoJudge.judge(t, winning);
result.add(rank);
}

OutputView.printStatistics(result, amount.value());
}
}
39 changes: 39 additions & 0 deletions src/main/java/lotto/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package lotto;

import camp.nextstep.edu.missionutils.Console;

public class InputView {

private InputView() {}

public static PurchaseAmount readPurchaseAmount() {
while (true) {
try {
System.out.println("구입금액을 입력해 주세요.");
String raw = Console.readLine();

return new PurchaseAmount(raw);
} catch (IllegalArgumentException | IllegalStateException e) {
System.out.println(e.getMessage());
}
}
}

public static WinningNumbers readWinningNumbers() {
while (true) {
try {
System.out.println();
System.out.println("당첨 번호를 입력해 주세요");
String main = Console.readLine();

System.out.println();
System.out.println("보너스 번호를 입력해 주세요.");
String bonus = Console.readLine();

return WinningNumbers.of(main, bonus);
} catch (IllegalArgumentException | IllegalStateException e) {
System.out.println(e.getMessage());
}
}
}
}
31 changes: 29 additions & 2 deletions src/main/java/lotto/Lotto.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,47 @@
package lotto;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class Lotto {
private final List<Integer> numbers;

private static final String ERR_PREFIX = "[ERROR] ";
private static final String ERR_SIZE = ERR_PREFIX + "로또 번호는 6개여야 합니다.";
private static final String ERR_DUP = ERR_PREFIX + "로또 번호는 중복될 수 없습니다.";

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

private void validate(List<Integer> numbers) {
if (numbers.size() != 6) {
throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다.");
throw new IllegalArgumentException(ERR_SIZE);
}

Set<Integer> unique = new HashSet<>(numbers);
if (unique.size() != 6) {
throw new IllegalArgumentException(ERR_DUP);
}
}

// TODO: 추가 기능 구현
public boolean contains(int n) {
return numbers.contains(n);
}

public int countMatchesWith(List<Integer> main) {
int cnt = 0;

for (int n : main) {
if (numbers.contains(n)) cnt++;
}

return cnt;
}

public List<Integer> numbers() {
return List.copyOf(numbers);
}
}
12 changes: 12 additions & 0 deletions src/main/java/lotto/LottoJudge.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package lotto;

public final class LottoJudge {

private LottoJudge() {}

public static Rank judge(Lotto ticket, WinningNumbers winning) {
int match = ticket.countMatchesWith(winning.main());
boolean bonus = (match == 5) && ticket.contains(winning.bonus());
return Rank.of(match, bonus);
}
}
23 changes: 23 additions & 0 deletions src/main/java/lotto/LottoMachine.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package lotto;

import camp.nextstep.edu.missionutils.Randoms;

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

public class LottoMachine {

public List<Lotto> issue(PurchaseAmount amount) {
int count = amount.countTickets();
List<Lotto> tickets = new ArrayList<>(count);

for (int i = 0; i < count; i++) {
List<Integer> numbers = new ArrayList<>(Randoms.pickUniqueNumbersInRange(1, 45, 6));
Collections.sort(numbers);
tickets.add(new Lotto(numbers));
}

return tickets;
}
}
20 changes: 20 additions & 0 deletions src/main/java/lotto/LottoNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package lotto;

public class LottoNumber {
public static final int MIN = 1;
public static final int MAX = 45;
private static final String ERR = "[ERROR] 로또 번호는 1부터 45 사이여야 합니다.";

private final int value;

public LottoNumber(int value) {
if (value < MIN || value > MAX) {
throw new IllegalArgumentException(ERR);
}
this.value = value;
}

public int value() {
return value;
}
}
31 changes: 31 additions & 0 deletions src/main/java/lotto/LottoTicketFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package lotto;

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

public class LottoTicketFactory {
private static final String ERR_PREFIX = "[ERROR] ";
private static final String ERR_SIZE = ERR_PREFIX + "로또 번호는 6개여야 합니다.";
private static final String ERR_DUP = ERR_PREFIX + "로또 번호는 중복될 수 없습니다.";

private LottoTicketFactory() {}

public static List<Integer> sortedUniqueSix(List<Integer> src) {
if (src == null || src.size() != 6) {
throw new IllegalArgumentException(ERR_SIZE);
}
if (new HashSet<>(src).size() != 6) {
throw new IllegalArgumentException(ERR_DUP);
}

for (int n : src) {
new LottoNumber(n);
}

List<Integer> copy = new ArrayList<>(src);
Collections.sort(copy);
return copy;
}
}
39 changes: 39 additions & 0 deletions src/main/java/lotto/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package lotto;

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

public class OutputView {
private static final String PURCHASED_FORMAT = "%d개를 구매했습니다.%n";
private static final String STATS_TITLE = "당첨 통계";
private static final String STATS_DIVIDER = "---";

private OutputView() {}

public static void printTickets(List<Lotto> tickets) {
System.out.printf(PURCHASED_FORMAT, tickets.size());

for (Lotto t : tickets) {
System.out.println(formatTicket(t.numbers()));
}
}

public static void printStatistics(Result result, long spent) {
System.out.println(STATS_TITLE);
System.out.println(STATS_DIVIDER);

Map<Rank, Integer> m = result.snapshot();
System.out.printf("3개 일치 (5,000원) - %d개%n", m.get(Rank.FIFTH));
System.out.printf("4개 일치 (50,000원) - %d개%n", m.get(Rank.FOURTH));
System.out.printf("5개 일치 (1,500,000원) - %d개%n", m.get(Rank.THIRD));
System.out.printf("5개 일치, 보너스 볼 일치 (30,000,000원) - %d개%n", m.get(Rank.SECOND));
System.out.printf("6개 일치 (2,000,000,000원) - %d개%n", m.get(Rank.FIRST));

ProfitRate rate = ProfitRate.of(result.totalPrize(), spent);
System.out.printf("총 수익률은 %s입니다.%n", rate.format());
}

private static String formatTicket(List<Integer> numbers) {
return numbers.toString();
}
}
Loading