diff --git a/README.md b/README.md index 5fa2560b46..1f4cdf770b 100644 --- a/README.md +++ b/README.md @@ -1 +1,640 @@ -# java-lotto-precourse +# 🎰 둜또 κ²Œμž„ (java-lotto-precourse) + +## πŸ“– ν”„λ‘œμ νŠΈ μ†Œκ°œ + +> μš°μ•„ν•œν…Œν¬μ½”μŠ€ ν”„λ¦¬μ½”μŠ€ 3μ£Όμ°¨ λ―Έμ…˜μž…λ‹ˆλ‹€. +> +> 둜또 ꡬ맀뢀터 당첨 ν™•μΈκΉŒμ§€ 전체 ν”„λ‘œμ„ΈμŠ€λ₯Ό κ΅¬ν˜„ν•œ μ½˜μ†” 기반 둜또 κ²Œμž„μž…λ‹ˆλ‹€. + +### μ£Όμš” κΈ°λŠ₯ +- πŸ’° ꡬ맀 κΈˆμ•‘ μž…λ ₯ 및 둜또 μžλ™ λ°œν–‰ +- 🎲 1~45 λ²”μœ„μ˜ μ€‘λ³΅λ˜μ§€ μ•ŠλŠ” 번호 6개 생성 +- 🎯 당첨 번호 및 λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ +- πŸ† 당첨 톡계 및 수읡λ₯  계산 +- βœ… μž…λ ₯κ°’ 검증 및 μ˜ˆμ™Έ 처리 (μž¬μ‹œλ„ 둜직) + +--- + +## πŸ› οΈ 기술 μŠ€νƒ + +- **Language:** Java 21 +- **Build Tool:** Gradle +- **Testing:** JUnit 5, AssertJ +- **Library:** camp.nextstep.edu.missionutils + +--- + +## πŸš€ μž…μΆœλ ₯ μ˜ˆμ‹œ + +**μž…λ ₯:** +``` +κ΅¬μž…κΈˆμ•‘μ„ μž…λ ₯ν•΄ μ£Όμ„Έμš”. +8000 + +당첨 번호λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”. +1,2,3,4,5,6 + +λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”. +7 +``` + +**좜λ ₯:** +``` +8개λ₯Ό κ΅¬λ§€ν–ˆμŠ΅λ‹ˆλ‹€. +[8, 21, 23, 41, 42, 43] +[3, 5, 11, 16, 32, 38] +[7, 11, 16, 35, 36, 44] +[1, 8, 11, 31, 41, 42] +[13, 14, 16, 38, 42, 45] +[7, 11, 30, 40, 42, 43] +[2, 13, 22, 32, 38, 45] +[1, 3, 5, 14, 22, 45] + +당첨 톡계 +--- +3개 일치 (5,000원) - 1개 +4개 일치 (50,000원) - 0개 +5개 일치 (1,500,000원) - 0개 +5개 일치, λ³΄λ„ˆμŠ€ λ³Ό 일치 (30,000,000원) - 0개 +6개 일치 (2,000,000,000원) - 0개 +총 수읡λ₯ μ€ 62.5%μž…λ‹ˆλ‹€. +``` + +--- + +## πŸ“¦ νŒ¨ν‚€μ§€ ꡬ쑰 +**λ ˆμ΄μ–΄λ³„ μ±…μž„:** +- **Application**: ν”„λ‘œκ·Έλž¨ μ‹œμž‘μ  +- **Domain**: 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 +- **Service**: 전체 κ²Œμž„ 흐름 쑰율 및 μ˜ˆμ™Έ μž¬μ‹œλ„ 처리 +- **View**: μž…μΆœλ ₯ λ‹΄λ‹Ή (`Console.readLine()`, `System.out.println()`) +- **Parser**: λ¬Έμžμ—΄ β†’ 객체 λ³€ν™˜ +- **Validator**: λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ 검증 +- **Util**: 둜또 번호 생성 λ“± μœ ν‹Έλ¦¬ν‹° + +``` +src/main/java/lotto/ +β”œβ”€β”€ Application.java # ν”„λ‘œκ·Έλž¨ μ§„μž…μ  +β”œβ”€β”€ domain/ # 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 +β”‚ β”œβ”€β”€ Lotto.java # 둜또 번호 (6개) +β”‚ β”œβ”€β”€ LottoTickets.java # μ—¬λŸ¬ 둜또 관리 +β”‚ β”œβ”€β”€ WinningLotto.java # 당첨 번호 + λ³΄λ„ˆμŠ€ 번호 +β”‚ β”œβ”€β”€ Rank.java # 당첨 λ“±μˆ˜ (Enum) +β”‚ └── LottoResult.java # 당첨 κ²°κ³Ό 및 수읡λ₯  +β”œβ”€β”€ service/ # κ²Œμž„ μ§„ν–‰ 쑰율 +β”‚ └── LottoGameService.java +β”œβ”€β”€ view/ # μž…μΆœλ ₯ 처리 +β”‚ β”œβ”€β”€ InputView.java # μ‚¬μš©μž μž…λ ₯ (Console.readLine()) +β”‚ └── OutputView.java # κ²°κ³Ό 좜λ ₯ +β”œβ”€β”€ parser/ # λ¬Έμžμ—΄ νŒŒμ‹± +β”‚ β”œβ”€β”€ PurchaseAmountParser.java +β”‚ β”œβ”€β”€ WinningNumbersParser.java +β”‚ └── BonusNumberParser.java +β”œβ”€β”€ validator/ # μž…λ ₯ 검증 +β”‚ └── InputValidator.java +└── util/ # μœ ν‹Έλ¦¬ν‹° + └── LottoGenerator.java # 둜또 번호 생성 +``` + +--- + +## βš™οΈ κΈ°λŠ₯ κ΅¬ν˜„ λͺ©λ‘ + +### 1. 도메인 객체 κ΅¬ν˜„ + +#### 1.2 Rank μ—΄κ±°ν˜• κ΅¬ν˜„ + +**정상 μΌ€μ΄μŠ€:** +- [ ] 일치 κ°œμˆ˜μ™€ λ³΄λ„ˆμŠ€ 일치 μ—¬λΆ€λ‘œ λ“±μˆ˜λ₯Ό νŒλ³„ν•œλ‹€ +- [ ] 각 λ“±μˆ˜λ³„ μƒκΈˆμ„ λ°˜ν™˜ν•œλ‹€ +- [ ] 5λ“±: 3개 일치 / 5,000원 +- [ ] 4λ“±: 4개 일치 / 50,000원 +- [ ] 3λ“±: 5개 일치 / 1,500,000원 +- [ ] 2λ“±: 5개 + λ³΄λ„ˆμŠ€ 일치 / 30,000,000원 +- [ ] 1λ“±: 6개 일치 / 2,000,000,000원 +- [ ] λ‹Ήμ²¨λ˜μ§€ μ•ŠμœΌλ©΄ NONE λ°˜ν™˜ + +**컀밋:** `feat(domain): Rank μ—΄κ±°ν˜• κ΅¬ν˜„` + +#### 1.2 Lotto 객체 κ΅¬ν˜„ + +**정상 μΌ€μ΄μŠ€:** +- [ ] 6개의 둜또 번호λ₯Ό μ €μž₯ν•œλ‹€ +- [ ] 둜또 λ²ˆν˜ΈλŠ” 1~45 λ²”μœ„μ˜ μ€‘λ³΅λ˜μ§€ μ•ŠλŠ” μˆ«μžμ΄λ‹€ +- [ ] 둜또 번호λ₯Ό μ˜€λ¦„μ°¨μˆœμœΌλ‘œ μ •λ ¬ν•˜μ—¬ λ°˜ν™˜ν•œλ‹€ +- [ ] μ£Όμ–΄μ§„ 번호 λ¦¬μŠ€νŠΈμ™€ μΌμΉ˜ν•˜λŠ” 개수λ₯Ό λ°˜ν™˜ν•œλ‹€ +- [ ] νŠΉμ • 번호 포함 μ—¬λΆ€λ₯Ό ν™•μΈν•œλ‹€ + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] 둜또 λ²ˆν˜Έκ°€ null이면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] 둜또 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.` +- [ ] 둜또 λ²ˆν˜Έκ°€ 6κ°œκ°€ μ•„λ‹ˆλ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] 둜또 λ²ˆν˜ΈλŠ” 6κ°œμ—¬μ•Ό ν•©λ‹ˆλ‹€.` +- [ ] μ€‘λ³΅λœ λ²ˆν˜Έκ°€ 있으면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] 둜또 λ²ˆν˜ΈλŠ” 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€.` +- [ ] 1~45 λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] 둜또 λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.` + +**컀밋:** `feat(domain): Lotto 객체 κΈ°λ³Έ κΈ°λŠ₯ κ΅¬ν˜„` + +#### 1.3 WinningLotto 객체 κ΅¬ν˜„ + +**정상 μΌ€μ΄μŠ€:** +- [ ] 당첨 번호 6κ°œμ™€ λ³΄λ„ˆμŠ€ 번호 1개λ₯Ό μ €μž₯ν•œλ‹€ +- [ ] λ‘œλ˜μ™€ λΉ„κ΅ν•˜μ—¬ λ“±μˆ˜λ₯Ό νŒλ³„ν•œλ‹€ +- [ ] 일치 개수λ₯Ό κ³„μ‚°ν•œλ‹€ +- [ ] λ³΄λ„ˆμŠ€ 번호 일치 μ—¬λΆ€λ₯Ό ν™•μΈν•œλ‹€ + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] 당첨 λ²ˆν˜Έκ°€ null이면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] 당첨 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.` +- [ ] λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 당첨 λ²ˆν˜Έμ™€ μ€‘λ³΅λ˜λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 당첨 λ²ˆν˜Έμ™€ 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€.` +- [ ] λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 1~45 λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.` + +**컀밋:** `feat(domain): WinningLotto 객체 κ΅¬ν˜„` + +#### 1.4 LottoTickets 객체 κ΅¬ν˜„ + +**정상 μΌ€μ΄μŠ€:** +- [ ] μ—¬λŸ¬ 개의 둜또λ₯Ό κ΄€λ¦¬ν•œλ‹€ +- [ ] κ΅¬λ§€ν•œ 둜또 개수λ₯Ό λ°˜ν™˜ν•œλ‹€ +- [ ] λͺ¨λ“  둜또λ₯Ό λ°˜ν™˜ν•œλ‹€ +- [ ] 당첨 λ²ˆν˜Έμ™€ λΉ„κ΅ν•˜μ—¬ 각 둜또의 λ“±μˆ˜λ₯Ό νŒλ³„ν•œλ‹€ + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] 둜또 λ¦¬μŠ€νŠΈκ°€ null이면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] 둜또 λͺ©λ‘μ΄ μ—†μŠ΅λ‹ˆλ‹€.` +- [ ] 둜또 λ¦¬μŠ€νŠΈκ°€ λΉ„μ–΄μžˆμœΌλ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] μ΅œμ†Œ 1개 μ΄μƒμ˜ 둜또λ₯Ό ꡬ맀해야 ν•©λ‹ˆλ‹€.` + +**컀밋:** `feat(domain): LottoTickets 객체 κ΅¬ν˜„` + +#### 1.5 LottoResult 객체 κ΅¬ν˜„ + +**정상 μΌ€μ΄μŠ€:** +- [ ] λ“±μˆ˜λ³„ 당첨 개수λ₯Ό μ €μž₯ν•œλ‹€ +- [ ] 총 μˆ˜μ΅κΈˆμ„ κ³„μ‚°ν•œλ‹€ +- [ ] 수읡λ₯ μ„ κ³„μ‚°ν•œλ‹€ (μ†Œμˆ˜μ  λ‘˜μ§Έ 자리 반올림) +- [ ] λ“±μˆ˜λ³„ 당첨 개수λ₯Ό λ°˜ν™˜ν•œλ‹€ + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] ꡬ맀 κΈˆμ•‘μ΄ 0 μ΄ν•˜λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] ꡬ맀 κΈˆμ•‘μ€ 0보닀 컀야 ν•©λ‹ˆλ‹€.` + +**컀밋:** `feat(domain): LottoResult 객체 κ΅¬ν˜„` + +--- + +### 2. μž…λ ₯ νŒŒμ‹± κ΅¬ν˜„ + +#### 2.1 PurchaseAmountParser κ΅¬ν˜„ + +**정상 μΌ€μ΄μŠ€:** +- [ ] λ¬Έμžμ—΄μ„ μ •μˆ˜λ‘œ λ³€ν™˜ν•œλ‹€ +- [ ] μ•žλ’€ 곡백을 μ œκ±°ν•œλ‹€ + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] μž…λ ₯이 null이면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] κ΅¬μž… κΈˆμ•‘μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”.` +- [ ] μž…λ ₯이 빈 λ¬Έμžμ—΄μ΄λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] κ΅¬μž… κΈˆμ•‘μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”.` +- [ ] μˆ«μžκ°€ μ•„λ‹Œ λ¬Έμžκ°€ ν¬ν•¨λ˜λ©΄ `NumberFormatException` λ°œμƒ + - `[ERROR] κ΅¬μž… κΈˆμ•‘μ€ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.` + +**컀밋:** `feat(parser): ꡬ맀 κΈˆμ•‘ νŒŒμ‹± κ΅¬ν˜„` + +#### 2.2 WinningNumbersParser κ΅¬ν˜„ + +**정상 μΌ€μ΄μŠ€:** +- [ ] μ‰Όν‘œ κΈ°μ€€μœΌλ‘œ λ¬Έμžμ—΄μ„ λΆ„λ¦¬ν•œλ‹€ +- [ ] 각 번호의 μ•žλ’€ 곡백을 μ œκ±°ν•œλ‹€ +- [ ] λ¬Έμžμ—΄ 리슀트λ₯Ό μ •μˆ˜ 리슀트둜 λ³€ν™˜ν•œλ‹€ + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] μž…λ ₯이 null이면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] 당첨 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.` +- [ ] μž…λ ₯이 빈 λ¬Έμžμ—΄μ΄λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] 당첨 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.` +- [ ] μ‰Όν‘œλ‘œ κ΅¬λΆ„λ˜μ§€ μ•Šμ€ 경우 `IllegalArgumentException` λ°œμƒ + - `[ERROR] 당첨 λ²ˆν˜ΈλŠ” μ‰Όν‘œ(,)둜 ꡬ뢄해야 ν•©λ‹ˆλ‹€.` +- [ ] μˆ«μžκ°€ μ•„λ‹Œ 값이 ν¬ν•¨λ˜λ©΄ `NumberFormatException` λ°œμƒ + - `[ERROR] 당첨 λ²ˆν˜ΈλŠ” μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.` +- [ ] 곡백만 μžˆλŠ” λ²ˆν˜Έκ°€ 있으면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] 당첨 λ²ˆν˜ΈλŠ” 빈 값일 수 μ—†μŠ΅λ‹ˆλ‹€.` + +**컀밋:** `feat(parser): 당첨 번호 νŒŒμ‹± κ΅¬ν˜„` + +#### 2.3 BonusNumberParser κ΅¬ν˜„ + +**정상 μΌ€μ΄μŠ€:** +- [ ] λ¬Έμžμ—΄μ„ μ •μˆ˜λ‘œ λ³€ν™˜ν•œλ‹€ +- [ ] μ•žλ’€ 곡백을 μ œκ±°ν•œλ‹€ + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] μž…λ ₯이 null이면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.` +- [ ] μž…λ ₯이 빈 λ¬Έμžμ—΄μ΄λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.` +- [ ] μˆ«μžκ°€ μ•„λ‹Œ λ¬Έμžκ°€ ν¬ν•¨λ˜λ©΄ `NumberFormatException` λ°œμƒ + - `[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.` + +**컀밋:** `feat(parser): λ³΄λ„ˆμŠ€ 번호 νŒŒμ‹± κ΅¬ν˜„` + +--- + +### 3. μž…λ ₯ 검증 κ΅¬ν˜„ + +#### 3.1 ꡬ맀 κΈˆμ•‘ 검증 + +**정상 μΌ€μ΄μŠ€:** +- [ ] 1,000원 μ΄μƒμ˜ κΈˆμ•‘μ΄λ©΄ 톡과 +- [ ] 1,000원 λ‹¨μœ„λ‘œ λ‚˜λˆ„μ–΄λ–¨μ–΄μ§€λ©΄ 톡과 + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] 음수λ₯Ό μž…λ ₯ν•˜λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] κ΅¬μž… κΈˆμ•‘μ€ μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€.` +- [ ] 0을 μž…λ ₯ν•˜λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] κ΅¬μž… κΈˆμ•‘μ€ 0보닀 컀야 ν•©λ‹ˆλ‹€.` +- [ ] 1,000원 미만이면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] κ΅¬μž… κΈˆμ•‘μ€ 1,000원 이상이어야 ν•©λ‹ˆλ‹€.` +- [ ] 1,000μ›μœΌλ‘œ λ‚˜λˆ„μ–΄λ–¨μ–΄μ§€μ§€ μ•ŠμœΌλ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] κ΅¬μž… κΈˆμ•‘μ€ 1,000원 λ‹¨μœ„μ—¬μ•Ό ν•©λ‹ˆλ‹€.` + +**컀밋:** `feat(validator): ꡬ맀 κΈˆμ•‘ 검증 κ΅¬ν˜„` + +#### 3.2 당첨 번호 검증 + +**정상 μΌ€μ΄μŠ€:** +- [ ] 6개의 번호면 톡과 +- [ ] 각 λ²ˆν˜Έκ°€ 1~45 λ²”μœ„ 내에 있으면 톡과 +- [ ] μ€‘λ³΅λ˜μ§€ μ•ŠμœΌλ©΄ 톡과 + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] 번호 λ¦¬μŠ€νŠΈκ°€ null이면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] 당첨 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.` +- [ ] λ²ˆν˜Έκ°€ 6κ°œκ°€ μ•„λ‹ˆλ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] 당첨 λ²ˆν˜ΈλŠ” 6κ°œμ—¬μ•Ό ν•©λ‹ˆλ‹€.` +- [ ] 1~45 λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] 둜또 λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.` +- [ ] μ€‘λ³΅λœ λ²ˆν˜Έκ°€ 있으면 `IllegalArgumentException` λ°œμƒ + - `[ERROR] 당첨 λ²ˆν˜ΈλŠ” 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€.` + +**컀밋:** `feat(validator): 당첨 번호 검증 κ΅¬ν˜„` + +#### 3.3 λ³΄λ„ˆμŠ€ 번호 검증 + +**정상 μΌ€μ΄μŠ€:** +- [ ] 1~45 λ²”μœ„μ˜ 숫자면 톡과 +- [ ] 당첨 λ²ˆν˜Έμ™€ μ€‘λ³΅λ˜μ§€ μ•ŠμœΌλ©΄ 톡과 + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] 1~45 λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.` +- [ ] 당첨 λ²ˆν˜Έμ™€ μ€‘λ³΅λ˜λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 당첨 λ²ˆν˜Έμ™€ 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€.` + +**컀밋:** `feat(validator): λ³΄λ„ˆμŠ€ 번호 검증 κ΅¬ν˜„` + +--- + +### 4. 둜또 번호 생성 + +#### 4.1 μžλ™ 둜또 번호 생성 + +**정상 μΌ€μ΄μŠ€:** +- [ ] 1~45 λ²”μœ„μ—μ„œ μ€‘λ³΅λ˜μ§€ μ•ŠλŠ” 6개의 숫자λ₯Ό μƒμ„±ν•œλ‹€ +- [ ] `Randoms.pickUniqueNumbersInRange()` μ‚¬μš© +- [ ] ꡬ맀 κΈˆμ•‘μ— λ§žλŠ” 개수만큼 둜또λ₯Ό μƒμ„±ν•œλ‹€ + +**μ˜ˆμ™Έ μΌ€μ΄μŠ€:** +- [ ] 생성할 둜또 κ°œμˆ˜κ°€ 0 μ΄ν•˜λ©΄ `IllegalArgumentException` λ°œμƒ + - `[ERROR] 둜또 κ°œμˆ˜λŠ” 1개 이상이어야 ν•©λ‹ˆλ‹€.` + +**컀밋:** `feat(util): 둜또 번호 μžλ™ 생성 κ΅¬ν˜„` + +--- + +### 5. μž…μΆœλ ₯ κΈ°λŠ₯ + +#### 5.1 ꡬ맀 κΈˆμ•‘ μž…λ ₯ +- [ ] μž…λ ₯ μ•ˆλ‚΄ λ©”μ‹œμ§€ 좜λ ₯: "κ΅¬μž…κΈˆμ•‘μ„ μž…λ ₯ν•΄ μ£Όμ„Έμš”." +- [ ] `Console.readLine()`으둜 μž…λ ₯ λ°›κΈ° +- [ ] μž…λ ₯받은 λ¬Έμžμ—΄μ„ κ·ΈλŒ€λ‘œ λ°˜ν™˜ + +**컀밋:** `feat(view): ꡬ맀 κΈˆμ•‘ μž…λ ₯ κΈ°λŠ₯ κ΅¬ν˜„` + +#### 5.2 당첨 번호 μž…λ ₯ +- [ ] 빈 쀄 좜λ ₯ +- [ ] μž…λ ₯ μ•ˆλ‚΄ λ©”μ‹œμ§€ 좜λ ₯: "당첨 번호λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”." +- [ ] `Console.readLine()`으둜 μž…λ ₯ λ°›κΈ° +- [ ] μž…λ ₯받은 λ¬Έμžμ—΄μ„ κ·ΈλŒ€λ‘œ λ°˜ν™˜ + +**컀밋:** `feat(view): 당첨 번호 μž…λ ₯ κΈ°λŠ₯ κ΅¬ν˜„` + +#### 5.3 λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ +- [ ] 빈 쀄 좜λ ₯ +- [ ] μž…λ ₯ μ•ˆλ‚΄ λ©”μ‹œμ§€ 좜λ ₯: "λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”." +- [ ] `Console.readLine()`으둜 μž…λ ₯ λ°›κΈ° +- [ ] μž…λ ₯받은 λ¬Έμžμ—΄μ„ κ·ΈλŒ€λ‘œ λ°˜ν™˜ + +**컀밋:** `feat(view): λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ κΈ°λŠ₯ κ΅¬ν˜„` + +#### 5.4 κ΅¬λ§€ν•œ 둜또 좜λ ₯ +- [ ] 빈 쀄 좜λ ₯ +- [ ] "n개λ₯Ό κ΅¬λ§€ν–ˆμŠ΅λ‹ˆλ‹€." 좜λ ₯ +- [ ] 각 둜또λ₯Ό μ˜€λ¦„μ°¨μˆœ μ •λ ¬ν•˜μ—¬ 좜λ ₯ +- [ ] ν˜•μ‹: `[번호1, 번호2, 번호3, 번호4, 번호5, 번호6]` +- [ ] 각 λ‘œλ˜λ§ˆλ‹€ μ€„λ°”κΏˆ + +**컀밋:** `feat(view): ꡬ맀 둜또 좜λ ₯ κΈ°λŠ₯ κ΅¬ν˜„` + +#### 5.5 당첨 톡계 좜λ ₯ +- [ ] 빈 쀄 좜λ ₯ +- [ ] "당첨 톡계" 헀더 좜λ ₯ +- [ ] ꡬ뢄선 "---" 좜λ ₯ +- [ ] 5λ“±λΆ€ν„° 1λ“±κΉŒμ§€ μˆœμ„œλŒ€λ‘œ 좜λ ₯ +- [ ] ν˜•μ‹: "n개 일치 (κΈˆμ•‘μ›) - m개" +- [ ] 2λ“±: "5개 일치, λ³΄λ„ˆμŠ€ λ³Ό 일치 (κΈˆμ•‘μ›) - m개" +- [ ] κΈˆμ•‘μ€ 천 λ‹¨μœ„ μ‰Όν‘œ 포함 + +**컀밋:** `feat(view): 당첨 톡계 좜λ ₯ κΈ°λŠ₯ κ΅¬ν˜„` + +#### 5.6 수읡λ₯  좜λ ₯ +- [ ] "총 수읡λ₯ μ€ n%μž…λ‹ˆλ‹€." ν˜•μ‹μœΌλ‘œ 좜λ ₯ +- [ ] μ†Œμˆ˜μ  λ‘˜μ§Έ μžλ¦¬μ—μ„œ 반올림 +- [ ] 천 λ‹¨μœ„ μ‰Όν‘œ 포함 + +**컀밋:** `feat(view): 수읡λ₯  좜λ ₯ κΈ°λŠ₯ κ΅¬ν˜„` + +#### 5.7 μ—λŸ¬ λ©”μ‹œμ§€ 좜λ ₯ +- [ ] "[ERROR]"둜 μ‹œμž‘ν•˜λŠ” μ—λŸ¬ λ©”μ‹œμ§€ 좜λ ₯ +- [ ] `System.out.println()` μ‚¬μš© + +**컀밋:** `feat(view): μ—λŸ¬ λ©”μ‹œμ§€ 좜λ ₯ κΈ°λŠ₯ κ΅¬ν˜„` + +--- + +### 6. κ²Œμž„ μ§„ν–‰ μ„œλΉ„μŠ€ + +#### 6.1 LottoGameService κ΅¬ν˜„ +- [ ] ꡬ맀 κΈˆμ•‘ μž…λ ₯ 및 검증 (μž¬μ‹œλ„ 둜직) +- [ ] 둜또 μžλ™ λ°œν–‰ +- [ ] κ΅¬λ§€ν•œ 둜또 좜λ ₯ +- [ ] 당첨 번호 μž…λ ₯ 및 검증 (μž¬μ‹œλ„ 둜직) +- [ ] λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ 및 검증 (μž¬μ‹œλ„ 둜직) +- [ ] 당첨 κ²°κ³Ό 계산 +- [ ] 당첨 톡계 및 수읡λ₯  좜λ ₯ +- [ ] 각 μž…λ ₯ λ‹¨κ³„μ—μ„œ μ˜ˆμ™Έ λ°œμƒ μ‹œ ν•΄λ‹Ή 단계뢀터 μž¬μ‹œλ„ + +**μž¬μ‹œλ„ 흐름:** +- [ ] ꡬ맀 κΈˆμ•‘ μ˜ˆμ™Έ β†’ ꡬ맀 κΈˆμ•‘ μž…λ ₯λΆ€ν„° μž¬μ‹œλ„ +- [ ] 당첨 번호 μ˜ˆμ™Έ β†’ 당첨 번호 μž…λ ₯λΆ€ν„° μž¬μ‹œλ„ +- [ ] λ³΄λ„ˆμŠ€ 번호 μ˜ˆμ™Έ β†’ λ³΄λ„ˆμŠ€ 번호 μž…λ ₯λΆ€ν„° μž¬μ‹œλ„ + +**컀밋:** `feat(service): LottoGameService κ²Œμž„ μ§„ν–‰ 둜직 κ΅¬ν˜„` + +--- + +### 7. 메인 μ‹€ν–‰ + +#### 7.1 Application μ§„μž…μ  +- [ ] LottoGameService μ‹€ν–‰ +- [ ] μ˜ˆμ™ΈλŠ” Service λ ˆμ΄μ–΄μ—μ„œ 처리 + +**컀밋:** `feat(app): Application 메인 μ‹€ν–‰ κ΅¬ν˜„` + +--- + +## 🎯 μ˜ˆμ™Έ 처리 μ „λž΅ + +### Exception νƒ€μž…λ³„ μ‚¬μš© κΈ°μ€€ + +#### 1. `IllegalArgumentException` +**μ‚¬μš© μ‹œμ :** λ©”μ„œλ“œμ— μ „λ‹¬λœ μΈμžκ°€ μœ νš¨ν•˜μ§€ μ•Šμ„ λ•Œ +- 숫자 λ²”μœ„ 였λ₯˜ (1~45 λ²”μœ„ 벗어남) +- 개수 였λ₯˜ (6κ°œκ°€ μ•„λ‹˜) +- 쀑볡 였λ₯˜ +- λ‹¨μœ„ 였λ₯˜ (1,000원 λ‹¨μœ„ μ•„λ‹˜) +- null λ˜λŠ” 빈 κ°’ + +**μ˜ˆμ‹œ:** +```java +if (numbers.size() != 6) { + throw new IllegalArgumentException("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 6κ°œμ—¬μ•Ό ν•©λ‹ˆλ‹€."); +} +``` + +#### 2. `NumberFormatException` +**μ‚¬μš© μ‹œμ :** λ¬Έμžμ—΄μ„ 숫자둜 λ³€ν™˜ν•  수 없을 λ•Œ +- νŒŒμ‹± κ³Όμ •μ—μ„œ λ°œμƒ +- `Integer.parseInt()` μ‹€νŒ¨ μ‹œ + +**μ˜ˆμ‹œ:** +```java +try { + return Integer.parseInt(input); +} catch (NumberFormatException e) { + throw new NumberFormatException("[ERROR] κ΅¬μž… κΈˆμ•‘μ€ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); +} +``` + +#### 3. `IllegalStateException` (선택적 μ‚¬μš©) +**μ‚¬μš© μ‹œμ :** 객체의 μƒνƒœκ°€ λ©”μ„œλ“œ ν˜ΈμΆœμ— μ ν•©ν•˜μ§€ μ•Šμ„ λ•Œ +- κ²Œμž„μ΄ 이미 μ’…λ£Œλœ μƒνƒœμ—μ„œ μΆ”κ°€ μž‘μ—… μ‹œλ„ +- μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ€ 객체 μ‚¬μš© + +**μ˜ˆμ‹œ:** +```java +if (lottoTickets == null) { + throw new IllegalStateException("[ERROR] λ‘œλ˜κ°€ λ°œν–‰λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."); +} +``` +--- + +## πŸ—οΈ μ•„ν‚€ν…μ²˜ + +- **λ ˆμ΄μ–΄λ“œ μ•„ν‚€ν…μ²˜** (Layered Architecture) 기반 +- **Package by Layer** 방식 +- **Inside-Out 개발** (Domain β†’ Parser β†’ Validator β†’ Util β†’ View β†’ Service β†’ Application) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Application β”‚ (μ§„μž…μ ) +β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Service β”‚ (쑰율 + μž¬μ‹œλ„ 둜직) +β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β–Ό β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Domain β”‚ β”‚ Parser β”‚ β”‚ View β”‚ β”‚ Util β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β–² β–² + β”‚ β”‚ +β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β” +β”‚ Validator β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## βœ… ν”„λ‘œκ·Έλž˜λ° μš”κ΅¬μ‚¬ν•­ μ€€μˆ˜ + +- [ ] JDK 21μ—μ„œ μ‹€ν–‰ κ°€λŠ₯ +- [ ] indent depth 2 μ΄ν•˜ +- [ ] 3ν•­ μ—°μ‚°μž λ―Έμ‚¬μš© +- [ ] else μ˜ˆμ•½μ–΄ λ―Έμ‚¬μš© +- [ ] ν•¨μˆ˜ 길이 15라인 μ΄ν•˜ +- [ ] ν•¨μˆ˜κ°€ ν•œ κ°€μ§€ 일만 μˆ˜ν–‰ +- [ ] Java Enum ν™œμš© (Rank) +- [ ] JUnit 5, AssertJ ν…ŒμŠ€νŠΈ μž‘μ„± +- [ ] `Randoms.pickUniqueNumbersInRange()` μ‚¬μš© +- [ ] `Console.readLine()` μ‚¬μš© +- [ ] 제곡된 Lotto 클래슀 ν™œμš© (ν•„λ“œ μΆ”κ°€ λΆˆκ°€) +- [ ] UI 둜직 μ œμ™Έ λ‹¨μœ„ ν…ŒμŠ€νŠΈ μž‘μ„± +- [ ] `IllegalArgumentException`, `IllegalStateException` λ“± λͺ…ν™•ν•œ μ˜ˆμ™Έ νƒ€μž… μ‚¬μš© + +--- + +## πŸ“Š μƒμˆ˜ 관리 ν˜„ν™© + +### ErrorMessages μ‚¬μš© ν˜„ν™© + +| μƒμˆ˜ 이름 | μ‚¬μš© μœ„μΉ˜ | μƒνƒœ | +|-----------|-----------|------| +| `PURCHASE_AMOUNT_ZERO` | InputValidator | βœ… | +| `PURCHASE_AMOUNT_MINIMUM` | InputValidator | βœ… | +| `PURCHASE_AMOUNT_UNIT` | InputValidator | βœ… | +| `PURCHASE_AMOUNT_EMPTY` | PurchaseAmountParser | βœ… | +| `PURCHASE_AMOUNT_INVALID_FORMAT` | PurchaseAmountParser | βœ… | +| `LOTTO_NUMBERS_NULL` | Lotto, WinningLotto | βœ… | +| `LOTTO_NUMBERS_SIZE` | Lotto | βœ… | +| `LOTTO_NUMBERS_DUPLICATE` | Lotto | βœ… | +| `LOTTO_NUMBERS_RANGE` | Lotto | βœ… | +| `WINNING_NUMBERS_EMPTY` | WinningNumbersParser | βœ… | +| `WINNING_NUMBERS_INVALID_FORMAT` | WinningNumbersParser | βœ… | +| `WINNING_NUMBERS_EMPTY_VALUE` | WinningNumbersParser | βœ… | +| `BONUS_NUMBER_EMPTY` | BonusNumberParser | βœ… | +| `BONUS_NUMBER_INVALID_FORMAT` | BonusNumberParser | βœ… | +| `BONUS_NUMBER_RANGE` | WinningLotto | βœ… | +| `BONUS_NUMBER_DUPLICATE` | WinningLotto | βœ… | +| `LOTTO_TICKETS_NULL` | LottoTickets | βœ… | +| `LOTTO_TICKETS_EMPTY` | LottoTickets | βœ… | +| `LOTTO_RESULT_PURCHASE_AMOUNT` | LottoResult | βœ… | + +### ViewMessages μ‚¬μš© ν˜„ν™© + +| μƒμˆ˜ 이름 | μ‚¬μš© μœ„μΉ˜ | μš©λ„ | μƒνƒœ | +|-----------|-----------|------|------| +| `INPUT_PURCHASE_AMOUNT` | InputView | ꡬ맀 κΈˆμ•‘ μž…λ ₯ μ•ˆλ‚΄ | βœ… | +| `INPUT_WINNING_NUMBERS` | InputView | 당첨 번호 μž…λ ₯ μ•ˆλ‚΄ | βœ… | +| `INPUT_BONUS_NUMBER` | InputView | λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ μ•ˆλ‚΄ | βœ… | +| `PURCHASED_LOTTO_COUNT_FORMAT` | OutputView | ꡬ맀 개수 좜λ ₯ | βœ… | +| `WINNING_STATISTICS_HEADER` | OutputView | 톡계 헀더 좜λ ₯ | βœ… | +| `SEPARATOR` | OutputView | ꡬ뢄선 좜λ ₯ | βœ… | +| `RANK_FORMAT` | OutputView | λ“±μˆ˜λ³„ 당첨 κ²°κ³Ό 좜λ ₯ | βœ… | +| `PROFIT_RATE_FORMAT` | OutputView | 수읡λ₯  좜λ ₯ | βœ… | + +### LottoConstants μ‚¬μš© ν˜„ν™© + +| μƒμˆ˜ 이름 | κ°’ | μ‚¬μš© μœ„μΉ˜ | μƒνƒœ | +|-----------|-----|-----------|------| +| `MIN_LOTTO_NUMBER` | 1 | Lotto, WinningLotto, LottoGenerator | βœ… | +| `MAX_LOTTO_NUMBER` | 45 | Lotto, WinningLotto, LottoGenerator | βœ… | +| `LOTTO_NUMBER_COUNT` | 6 | Lotto, LottoGenerator | βœ… | +| `LOTTO_PRICE` | 1000 | InputValidator, LottoGenerator | βœ… | + +--- + +## 🎨 μƒμˆ˜ 섀계 원칙 + +### 1. 단일 μ±…μž„ 원칙 (SRP) +- **ErrorMessages**: μ—λŸ¬ λ©”μ‹œμ§€λ§Œ 관리 +- **ViewMessages**: View 좜λ ₯ λ©”μ‹œμ§€λ§Œ 관리 +- **LottoConstants**: λΉ„μ¦ˆλ‹ˆμŠ€ μƒμˆ˜λ§Œ 관리 + +### 2. 쀑볡 제거 (DRY) +- λͺ¨λ“  λ¬Έμžμ—΄ λ¦¬ν„°λŸ΄μ„ μƒμˆ˜λ‘œ μΆ”μΆœ +- ν•œ κ³³μ—μ„œ μˆ˜μ •ν•˜λ©΄ 전체에 반영 + +### 3. λͺ…ν™•ν•œ 넀이밍 +- μƒμˆ˜ μ΄λ¦„λ§ŒμœΌλ‘œ μš©λ„λ₯Ό νŒŒμ•… κ°€λŠ₯ +- 접두사λ₯Ό ν†΅ν•œ κ·Έλ£Ήν•‘ (PURCHASE_AMOUNT_*, LOTTO_NUMBERS_*) + +### 4. νŒ¨ν‚€μ§€ ꡬ쑰 +``` +lotto/ +└── common/ + β”œβ”€β”€ ErrorMessages.java # μ—λŸ¬ λ©”μ‹œμ§€ + β”œβ”€β”€ ViewMessages.java # View λ©”μ‹œμ§€ + └── LottoConstants.java # λΉ„μ¦ˆλ‹ˆμŠ€ μƒμˆ˜ +``` +--- + +## πŸ§ͺ ν…ŒμŠ€νŠΈ + +### ν…ŒμŠ€νŠΈ ꡬ쑰 +``` +src/test/java/lotto/ +β”œβ”€β”€ domain/ +β”‚ β”œβ”€β”€ LottoTest.java +β”‚ β”œβ”€β”€ RankTest.java +β”‚ β”œβ”€β”€ WinningLottoTest.java +β”‚ β”œβ”€β”€ LottoTicketsTest.java +β”‚ └── LottoResultTest.java +β”œβ”€β”€ parser/ +β”‚ β”œβ”€β”€ PurchaseAmountParserTest.java +β”‚ β”œβ”€β”€ WinningNumbersParserTest.java +β”‚ └── BonusNumberParserTest.java +β”œβ”€β”€ validator/ +β”‚ └── InputValidatorTest.java +β”œβ”€β”€ util/ +β”‚ └── LottoGeneratorTest.java +└── service/ + └── LottoGameServiceTest.java +``` + +### ν…ŒμŠ€νŠΈ μž‘μ„± 원칙 +- [ ] 정상 μΌ€μ΄μŠ€μ™€ μ˜ˆμ™Έ μΌ€μ΄μŠ€ λͺ¨λ‘ ν…ŒμŠ€νŠΈ +- [ ] 경계값 ν…ŒμŠ€νŠΈ (0, 1, 45, 46 λ“±) +- [ ] `@ParameterizedTest` ν™œμš©ν•˜μ—¬ μ—¬λŸ¬ μΌ€μ΄μŠ€ 검증 +- [ ] `assertThatThrownBy()` 둜 μ˜ˆμ™Έ λ©”μ‹œμ§€κΉŒμ§€ 검증 +- [ ] UI 둜직(InputView, OutputView)은 ν…ŒμŠ€νŠΈ μ œμ™Έ + +--- + +## πŸ’­ 회고 + +### 이번 μ£Όμ°¨ ν•™μŠ΅ λͺ©ν‘œ +- [ ] Java Enum을 ν™œμš©ν•œ λ“±μˆ˜ 관리 +- [ ] 일급 μ»¬λ ‰μ…˜ νŒ¨ν„΄ 적용 (LottoTickets, LottoResult) +- [ ] μ˜ˆμ™Έ λ°œμƒ μ‹œ μž¬μž…λ ₯ 둜직 κ΅¬ν˜„ +- [ ] Parser λ ˆμ΄μ–΄ λΆ„λ¦¬λ‘œ μ±…μž„ λͺ…ν™•ν™” +- [ ] λ‹€μ–‘ν•œ μ˜ˆμ™Έ νƒ€μž… ν™œμš© (IllegalArgumentException, NumberFormatException) +- [ ] 2μ£Όμ°¨ 곡톡 ν”Όλ“œλ°± 반영 + +### 2μ£Όμ°¨ ν”Όλ“œλ°± 반영 사항 +- [ ] ν•¨μˆ˜ 뢄리λ₯Ό ν†΅ν•œ indent depth 관리 +- [ ] Early return νŒ¨ν„΄μœΌλ‘œ else 제거 +- [ ] 맀직 λ„˜λ²„ μƒμˆ˜ν™” +- [ ] 도메인 둜직과 UI 둜직 뢄리 +- [ ] ν•¨μˆ˜ 길이 15라인 μ΄ν•˜ μœ μ§€ + +### μ˜ˆμƒ κ³ λ―Ό 포인트 +- Parser와 Validator의 경계 μ„€μ • +- μž¬μ‹œλ„ 둜직의 쀑볡 제거 +- Enum을 ν™œμš©ν•œ λ“±μˆ˜ 및 μƒκΈˆ 관리 +- 일급 μ»¬λ ‰μ…˜ 적용 λ²”μœ„ + +--- + +## πŸ“š ν•™μŠ΅ λ‚΄μš© + +- Java Enum ν™œμš© (μƒνƒœμ™€ ν–‰μœ„λ₯Ό ν•¨κ»˜ 관리) +- 일급 μ»¬λ ‰μ…˜ (First-Class Collection) +- μ˜ˆμ™Έ 처리 \ No newline at end of file diff --git a/src/main/java/lotto/Application.java b/src/main/java/lotto/Application.java index d190922ba4..ff20da09af 100644 --- a/src/main/java/lotto/Application.java +++ b/src/main/java/lotto/Application.java @@ -1,7 +1,10 @@ package lotto; +import lotto.service.LottoGameService; + public class Application { - public static void main(String[] args) { - // TODO: ν”„λ‘œκ·Έλž¨ κ΅¬ν˜„ - } -} + public static void main(String[] args) { + LottoGameService lottoGameService = new LottoGameService(); + lottoGameService.run(); + } +} \ No newline at end of file diff --git a/src/main/java/lotto/Lotto.java b/src/main/java/lotto/Lotto.java deleted file mode 100644 index 88fc5cf12b..0000000000 --- a/src/main/java/lotto/Lotto.java +++ /dev/null @@ -1,20 +0,0 @@ -package lotto; - -import java.util.List; - -public class Lotto { - private final List numbers; - - public Lotto(List numbers) { - validate(numbers); - this.numbers = numbers; - } - - private void validate(List numbers) { - if (numbers.size() != 6) { - throw new IllegalArgumentException("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 6κ°œμ—¬μ•Ό ν•©λ‹ˆλ‹€."); - } - } - - // TODO: μΆ”κ°€ κΈ°λŠ₯ κ΅¬ν˜„ -} diff --git a/src/main/java/lotto/common/ErrorMessages.java b/src/main/java/lotto/common/ErrorMessages.java new file mode 100644 index 0000000000..1bba3f67b4 --- /dev/null +++ b/src/main/java/lotto/common/ErrorMessages.java @@ -0,0 +1,40 @@ +package lotto.common; + +/** + * 둜또 κ²Œμž„μ—μ„œ μ‚¬μš©ν•˜λŠ” μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€. + */ +public class ErrorMessages { + + private ErrorMessages() { + // μƒμˆ˜ ν΄λž˜μŠ€λŠ” μΈμŠ€ν„΄μŠ€ν™” λ°©μ§€ + } + + // ꡬ맀 κΈˆμ•‘ κ΄€λ ¨ + public static final String PURCHASE_AMOUNT_ZERO = "[ERROR] κ΅¬μž… κΈˆμ•‘μ€ 0보닀 컀야 ν•©λ‹ˆλ‹€."; + public static final String PURCHASE_AMOUNT_MINIMUM = "[ERROR] κ΅¬μž… κΈˆμ•‘μ€ 1,000원 이상이어야 ν•©λ‹ˆλ‹€."; + public static final String PURCHASE_AMOUNT_UNIT = "[ERROR] κ΅¬μž… κΈˆμ•‘μ€ 1,000원 λ‹¨μœ„μ—¬μ•Ό ν•©λ‹ˆλ‹€."; + public static final String PURCHASE_AMOUNT_EMPTY = "[ERROR] κ΅¬μž… κΈˆμ•‘μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”."; + public static final String PURCHASE_AMOUNT_INVALID_FORMAT = "[ERROR] κ΅¬μž… κΈˆμ•‘μ€ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."; + + // 둜또 번호 κ΄€λ ¨ + public static final String LOTTO_NUMBERS_NULL = "[ERROR] 둜또 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."; + public static final String LOTTO_NUMBERS_SIZE = "[ERROR] 둜또 λ²ˆν˜ΈλŠ” 6κ°œμ—¬μ•Ό ν•©λ‹ˆλ‹€."; + public static final String LOTTO_NUMBERS_DUPLICATE = "[ERROR] 둜또 λ²ˆν˜ΈλŠ” 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€."; + public static final String LOTTO_NUMBERS_RANGE = "[ERROR] 둜또 λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."; + + // 당첨 번호 κ΄€λ ¨ + public static final String WINNING_NUMBERS_EMPTY = "[ERROR] 당첨 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."; + public static final String WINNING_NUMBERS_INVALID_FORMAT = "[ERROR] 당첨 λ²ˆν˜ΈλŠ” μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."; + public static final String WINNING_NUMBERS_EMPTY_VALUE = "[ERROR] 당첨 λ²ˆν˜ΈλŠ” 빈 값일 수 μ—†μŠ΅λ‹ˆλ‹€."; + + // λ³΄λ„ˆμŠ€ 번호 κ΄€λ ¨ + public static final String BONUS_NUMBER_EMPTY = "[ERROR] λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."; + public static final String BONUS_NUMBER_INVALID_FORMAT = "[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."; + public static final String BONUS_NUMBER_RANGE = "[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."; + public static final String BONUS_NUMBER_DUPLICATE = "[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 당첨 λ²ˆν˜Έμ™€ 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€."; + + // 기타 + public static final String LOTTO_TICKETS_NULL = "[ERROR] 둜또 λͺ©λ‘μ΄ μ—†μŠ΅λ‹ˆλ‹€."; + public static final String LOTTO_TICKETS_EMPTY = "[ERROR] μ΅œμ†Œ 1개 μ΄μƒμ˜ 둜또λ₯Ό ꡬ맀해야 ν•©λ‹ˆλ‹€."; + public static final String LOTTO_RESULT_PURCHASE_AMOUNT = "[ERROR] ꡬ맀 κΈˆμ•‘μ€ 0보닀 컀야 ν•©λ‹ˆλ‹€."; +} \ No newline at end of file diff --git a/src/main/java/lotto/common/LottoConstants.java b/src/main/java/lotto/common/LottoConstants.java new file mode 100644 index 0000000000..faffb036b0 --- /dev/null +++ b/src/main/java/lotto/common/LottoConstants.java @@ -0,0 +1,21 @@ +package lotto.common; + +/** + * 둜또 κ²Œμž„μ—μ„œ μ‚¬μš©ν•˜λŠ” 곡톡 μƒμˆ˜λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€. + */ +public class LottoConstants { + + private LottoConstants() { + // μƒμˆ˜ ν΄λž˜μŠ€λŠ” μΈμŠ€ν„΄μŠ€ν™” λ°©μ§€ + } + + // 둜또 번호 λ²”μœ„ + public static final int MIN_LOTTO_NUMBER = 1; + public static final int MAX_LOTTO_NUMBER = 45; + public static final int LOTTO_NUMBER_COUNT = 6; + + // 둜또 가격 + public static final int LOTTO_PRICE = 1000; + + public static final int MIN_PURCHASE_AMOUNT = 1000; +} \ No newline at end of file diff --git a/src/main/java/lotto/common/ViewMessages.java b/src/main/java/lotto/common/ViewMessages.java new file mode 100644 index 0000000000..f0b7d64a26 --- /dev/null +++ b/src/main/java/lotto/common/ViewMessages.java @@ -0,0 +1,23 @@ +package lotto.common; + +/** + * 둜또 κ²Œμž„μ—μ„œ μ‚¬μš©ν•˜λŠ” 좜λ ₯ λ©”μ‹œμ§€λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€. + */ +public class ViewMessages { + + private ViewMessages() { + // μƒμˆ˜ ν΄λž˜μŠ€λŠ” μΈμŠ€ν„΄μŠ€ν™” λ°©μ§€ + } + + // μž…λ ₯ μ•ˆλ‚΄ + public static final String INPUT_PURCHASE_AMOUNT = "κ΅¬μž…κΈˆμ•‘μ„ μž…λ ₯ν•΄ μ£Όμ„Έμš”."; + public static final String INPUT_WINNING_NUMBERS = "당첨 번호λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”."; + public static final String INPUT_BONUS_NUMBER = "λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”."; + + // κ²°κ³Ό 좜λ ₯ + public static final String PURCHASED_LOTTO_COUNT_FORMAT = "%d개λ₯Ό κ΅¬λ§€ν–ˆμŠ΅λ‹ˆλ‹€."; + public static final String WINNING_STATISTICS_HEADER = "당첨 톡계"; + public static final String SEPARATOR = "---"; + public static final String RANK_FORMAT = "%s (%s원) - %d개"; + public static final String PROFIT_RATE_FORMAT = "총 수읡λ₯ μ€ %.1f%%μž…λ‹ˆλ‹€."; +} \ No newline at end of file diff --git a/src/main/java/lotto/domain/Lotto.java b/src/main/java/lotto/domain/Lotto.java new file mode 100644 index 0000000000..011cb381a9 --- /dev/null +++ b/src/main/java/lotto/domain/Lotto.java @@ -0,0 +1,71 @@ +package lotto.domain; + +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; +import static lotto.common.ErrorMessages.*; +import static lotto.common.LottoConstants.*; + +public class Lotto { + + private final List numbers; + + public Lotto(List numbers) { + validate(numbers); + this.numbers = numbers.stream() + .sorted() + .collect(Collectors.toList()); + } + + private void validate(List numbers) { + validateNotNull(numbers); + validateSize(numbers); + validateDuplicate(numbers); + validateRange(numbers); + } + + private void validateNotNull(List numbers) { + if (numbers == null) { + throw new IllegalArgumentException(LOTTO_NUMBERS_NULL); + } + } + + private void validateSize(List numbers) { + if (numbers.size() != LOTTO_NUMBER_COUNT) { + throw new IllegalArgumentException(LOTTO_NUMBERS_SIZE); + } + } + private void validateDuplicate(List numbers) { + if (numbers.size() != new HashSet<>(numbers).size()) { + throw new IllegalArgumentException(LOTTO_NUMBERS_DUPLICATE); + } + } + + private void validateRange(List numbers) { + boolean isOutOfRange = numbers.stream() + .anyMatch(number -> number < MIN_LOTTO_NUMBER || number > MAX_LOTTO_NUMBER); + + if (isOutOfRange) { + throw new IllegalArgumentException(LOTTO_NUMBERS_RANGE); + } + } + + public List getNumbers() { + return numbers; + } + + public int countMatches(List winningNumbers) { + return (int) numbers.stream() + .filter(winningNumbers::contains) + .count(); + } + + public boolean contains(int number) { + return numbers.contains(number); + } + + @Override + public String toString() { + return numbers.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/lotto/domain/LottoResult.java b/src/main/java/lotto/domain/LottoResult.java new file mode 100644 index 0000000000..ae1a748cb4 --- /dev/null +++ b/src/main/java/lotto/domain/LottoResult.java @@ -0,0 +1,69 @@ +package lotto.domain; + +import static lotto.common.ErrorMessages.*; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class LottoResult { + private static final int MIN_PURCHASE_AMOUNT = 1; + private static final String PROFIT_RATE_FORMAT = "%.1f"; + + private final Map result; + private final int purchaseAmount; + + public LottoResult(List ranks, int purchaseAmount) { + if (purchaseAmount < MIN_PURCHASE_AMOUNT) { + throw new IllegalArgumentException(LOTTO_RESULT_PURCHASE_AMOUNT); + } + this.purchaseAmount = purchaseAmount; + this.result = countRanks(ranks); + } + + private Map countRanks(List ranks) { + Map counts = new EnumMap<>(Rank.class); + + // NONE λ“±μˆ˜λ₯Ό μ œμ™Έν•œ λͺ¨λ“  당첨 λ“±μˆ˜λ₯Ό 미리 0으둜 μ΄ˆκΈ°ν™” + for (Rank rank : Rank.values()) { + if (rank.isWinning()) { + counts.put(rank, 0); + } + } + + // μ‹€μ œ 당첨 λ“±μˆ˜μ˜ 개수λ₯Ό 카운트 + ranks.stream() + .filter(Rank::isWinning) + .forEach(rank -> counts.merge(rank, 1, Integer::sum)); + + return counts; + } + + public int getPrizeCount(Rank rank) { + return result.getOrDefault(rank, 0); + } + + public long calculateTotalProfit() { + return result.entrySet().stream() + .mapToLong(entry -> (long) entry.getKey().getPrize() * entry.getValue()) + .sum(); + } + + /** + * 총 수읡λ₯ μ„ κ³„μ‚°ν•©λ‹ˆλ‹€. + * + * @return 수읡λ₯  (νΌμ„ΌνŠΈ, μ†Œμˆ˜μ  포함) + */ + public double calculateProfitRate() { + long totalProfit = calculateTotalProfit(); + return (double) totalProfit * 100 / purchaseAmount; + } + + public Map getResult() { + return result; + } + + public int getPurchaseAmount() { + return purchaseAmount; + } +} \ No newline at end of file diff --git a/src/main/java/lotto/domain/LottoTickets.java b/src/main/java/lotto/domain/LottoTickets.java new file mode 100644 index 0000000000..2108a8fc94 --- /dev/null +++ b/src/main/java/lotto/domain/LottoTickets.java @@ -0,0 +1,44 @@ +package lotto.domain; + +import static lotto.common.ErrorMessages.*; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class LottoTickets { + private static final int MIN_LOTTO_COUNT = 1; + + private final List lottos; + + public LottoTickets(List lottos) { + if (Objects.isNull(lottos)) { + throw new IllegalArgumentException(LOTTO_TICKETS_NULL); + } + + if (lottos.size() < MIN_LOTTO_COUNT) { + throw new IllegalArgumentException(LOTTO_TICKETS_EMPTY); + } + + this.lottos = lottos; + } + + public int getLottoCount() { + return lottos.size(); + } + + public List getLottos() { + return Collections.unmodifiableList(lottos); + } + + public LottoResult compareWithWinningLotto(WinningLotto winningLotto, int purchaseAmount) { + // λͺ¨λ“  둜또의 당첨 λ“±μˆ˜λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€. + List ranks = lottos.stream() + .map(winningLotto::match) + .collect(Collectors.toList()); + + // LottoResult 객체λ₯Ό μƒμ„±ν•˜μ—¬ λ“±μˆ˜λ³„ 개수 및 수읡λ₯ μ„ κ΄€λ¦¬ν•©λ‹ˆλ‹€. + return new LottoResult(ranks, purchaseAmount); + } +} \ No newline at end of file diff --git a/src/main/java/lotto/domain/Rank.java b/src/main/java/lotto/domain/Rank.java new file mode 100644 index 0000000000..4bddef80ae --- /dev/null +++ b/src/main/java/lotto/domain/Rank.java @@ -0,0 +1,47 @@ +package lotto.domain; + +import java.util.Arrays; + +public enum Rank { + FIRST(6, false, 2_000_000_000, "6개 일치"), + SECOND(5, true, 30_000_000, "5개 일치, λ³΄λ„ˆμŠ€ λ³Ό 일치"), + THIRD(5, false, 1_500_000, "5개 일치"), + FOURTH(4, false, 50_000, "4개 일치"), + FIFTH(3, false, 5_000, "3개 일치"), + NONE(0, false, 0, ""); + + private final int matchCount; + private final boolean matchBonus; + private final int prize; + private final String description; + + Rank(int matchCount, boolean matchBonus, int prize, String description) { + this.matchCount = matchCount; + this.matchBonus = matchBonus; + this.prize = prize; + this.description = description; + } + + public static Rank valueOf(int matchCount, boolean matchBonus) { + if (matchCount == 5 && matchBonus) { + return SECOND; + } + return Arrays.stream(values()) + .filter(rank -> rank.matchCount == matchCount) + .filter(rank -> !rank.matchBonus) + .findFirst() + .orElse(NONE); + } + + public int getPrize() { + return prize; + } + + public String getDescription() { + return description; + } + + public boolean isWinning() { + return this != NONE; + } +} \ No newline at end of file diff --git a/src/main/java/lotto/domain/WinningLotto.java b/src/main/java/lotto/domain/WinningLotto.java new file mode 100644 index 0000000000..ddc15483d8 --- /dev/null +++ b/src/main/java/lotto/domain/WinningLotto.java @@ -0,0 +1,57 @@ +package lotto.domain; + +import static lotto.common.ErrorMessages.*; +import static lotto.common.LottoConstants.*; + +import java.util.List; +import java.util.Objects; + +public class WinningLotto { + private final Lotto winningNumbers; + private final int bonusNumber; + + public WinningLotto(Lotto winningNumbers, int bonusNumber) { + validate(winningNumbers, bonusNumber); + this.winningNumbers = winningNumbers; + this.bonusNumber = bonusNumber; + } + + private void validate(Lotto winningNumbers, int bonusNumber) { + validateNotNull(winningNumbers); + validateBonusNumberRange(bonusNumber); + validateDuplicateWithWinningNumbers(winningNumbers, bonusNumber); + } + + private void validateNotNull(Lotto winningNumbers) { + if (Objects.isNull(winningNumbers)) { + throw new IllegalArgumentException(LOTTO_NUMBERS_NULL); + } + } + + private void validateBonusNumberRange(int bonusNumber) { + if (bonusNumber < MIN_LOTTO_NUMBER || bonusNumber > MAX_LOTTO_NUMBER) { + throw new IllegalArgumentException(BONUS_NUMBER_RANGE); + } + } + + private void validateDuplicateWithWinningNumbers(Lotto winningNumbers, int bonusNumber) { + if (winningNumbers.contains(bonusNumber)) { + throw new IllegalArgumentException(BONUS_NUMBER_DUPLICATE); + } + } + + /** + * κ΅¬λ§€ν•œ λ‘œλ˜μ™€ 당첨 번호λ₯Ό λΉ„κ΅ν•˜μ—¬ λ“±μˆ˜λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. + */ + public Rank match(Lotto userLotto) { + // 1. μΌμΉ˜ν•˜λŠ” 번호 개수 계산 + List numbers = winningNumbers.getNumbers(); + int matchCount = userLotto.countMatches(numbers); + + // 2. λ³΄λ„ˆμŠ€ 번호 일치 μ—¬λΆ€ 확인 + boolean matchBonus = userLotto.contains(bonusNumber); + + // 3. Rank Enum을 톡해 λ“±μˆ˜ νŒλ³„ + return Rank.valueOf(matchCount, matchBonus); + } +} \ No newline at end of file diff --git a/src/main/java/lotto/parser/BonusNumberParser.java b/src/main/java/lotto/parser/BonusNumberParser.java new file mode 100644 index 0000000000..6bb392d378 --- /dev/null +++ b/src/main/java/lotto/parser/BonusNumberParser.java @@ -0,0 +1,20 @@ +package lotto.parser; + +import static lotto.common.ErrorMessages.*; + +public class BonusNumberParser { + + /** + * μ‚¬μš©μž μž…λ ₯ λ¬Έμžμ—΄(λ³΄λ„ˆμŠ€ 번호)을 μ •μˆ˜λ‘œ νŒŒμ‹±ν•˜μ—¬ λ°˜ν™˜ν•©λ‹ˆλ‹€. + * + * @param input μ‚¬μš©μžκ°€ μž…λ ₯ν•œ λ³΄λ„ˆμŠ€ 번호 λ¬Έμžμ—΄ + * @return νŒŒμ‹±λœ λ³΄λ„ˆμŠ€ 번호 (μ •μˆ˜) + * @throws IllegalArgumentException μž…λ ₯이 null λ˜λŠ” 빈 λ¬Έμžμ—΄μΈ 경우 + * @throws NumberFormatException μˆ«μžκ°€ μ•„λ‹Œ 값이 ν¬ν•¨λœ 경우 + */ + public static int parse(String input) { + ParserUtils.validateNotNullOrEmpty(input, BONUS_NUMBER_EMPTY); + String trimmedInput = input.trim(); + return ParserUtils.parseToInteger(trimmedInput, BONUS_NUMBER_INVALID_FORMAT); + } +} \ No newline at end of file diff --git a/src/main/java/lotto/parser/ParserUtils.java b/src/main/java/lotto/parser/ParserUtils.java new file mode 100644 index 0000000000..430a63b3ca --- /dev/null +++ b/src/main/java/lotto/parser/ParserUtils.java @@ -0,0 +1,42 @@ +package lotto.parser; + +import java.util.Objects; + +/** + * Parser듀이 κ³΅ν†΅μœΌλ‘œ μ‚¬μš©ν•˜λŠ” μœ ν‹Έλ¦¬ν‹° λ©”μ„œλ“œλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€. + */ +public class ParserUtils { + + private ParserUtils() { + // μœ ν‹Έλ¦¬ν‹° ν΄λž˜μŠ€λŠ” μΈμŠ€ν„΄μŠ€ν™” λ°©μ§€ + } + + /** + * μž…λ ₯이 nullμ΄κ±°λ‚˜ 빈 λ¬Έμžμ—΄(곡백 포함)인지 κ²€μ¦ν•©λ‹ˆλ‹€. + * + * @param input 검증할 μž…λ ₯ λ¬Έμžμ—΄ + * @param errorMessage μ˜ˆμ™Έ λ°œμƒ μ‹œ ν‘œμ‹œν•  μ—λŸ¬ λ©”μ‹œμ§€ + * @throws IllegalArgumentException μž…λ ₯이 nullμ΄κ±°λ‚˜ 빈 λ¬Έμžμ—΄μΈ 경우 + */ + public static void validateNotNullOrEmpty(String input, String errorMessage) { + if (Objects.isNull(input) || input.trim().isEmpty()) { + throw new IllegalArgumentException(errorMessage); + } + } + + /** + * λ¬Έμžμ—΄μ„ μ •μˆ˜λ‘œ νŒŒμ‹±ν•©λ‹ˆλ‹€. + * + * @param input νŒŒμ‹±ν•  λ¬Έμžμ—΄ + * @param errorMessage νŒŒμ‹± μ‹€νŒ¨ μ‹œ ν‘œμ‹œν•  μ—λŸ¬ λ©”μ‹œμ§€ + * @return νŒŒμ‹±λœ μ •μˆ˜ + * @throws NumberFormatException 숫자둜 λ³€ν™˜ν•  수 μ—†λŠ” 경우 + */ + public static int parseToInteger(String input, String errorMessage) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new NumberFormatException(errorMessage); + } + } +} \ No newline at end of file diff --git a/src/main/java/lotto/parser/PurchaseAmountParser.java b/src/main/java/lotto/parser/PurchaseAmountParser.java new file mode 100644 index 0000000000..9d62cc9c69 --- /dev/null +++ b/src/main/java/lotto/parser/PurchaseAmountParser.java @@ -0,0 +1,20 @@ +package lotto.parser; + +import static lotto.common.ErrorMessages.*; + +public class PurchaseAmountParser { + + /** + * μ‚¬μš©μž μž…λ ₯ λ¬Έμžμ—΄(κ΅¬μž… κΈˆμ•‘)을 μ •μˆ˜λ‘œ νŒŒμ‹±ν•˜μ—¬ λ°˜ν™˜ν•©λ‹ˆλ‹€. + * + * @param input μ‚¬μš©μžκ°€ μž…λ ₯ν•œ κ΅¬μž… κΈˆμ•‘ λ¬Έμžμ—΄ + * @return νŒŒμ‹±λœ κ΅¬μž… κΈˆμ•‘ (μ •μˆ˜) + * @throws IllegalArgumentException μž…λ ₯이 null λ˜λŠ” 빈 λ¬Έμžμ—΄μΈ 경우 + * @throws NumberFormatException μˆ«μžκ°€ μ•„λ‹Œ 값이 ν¬ν•¨λœ 경우 + */ + public static int parse(String input) { + ParserUtils.validateNotNullOrEmpty(input, PURCHASE_AMOUNT_EMPTY); + String trimmedInput = input.trim(); + return ParserUtils.parseToInteger(trimmedInput, PURCHASE_AMOUNT_INVALID_FORMAT); + } +} \ No newline at end of file diff --git a/src/main/java/lotto/parser/WinningNumbersParser.java b/src/main/java/lotto/parser/WinningNumbersParser.java new file mode 100644 index 0000000000..143d9f99b7 --- /dev/null +++ b/src/main/java/lotto/parser/WinningNumbersParser.java @@ -0,0 +1,47 @@ +package lotto.parser; + +import static lotto.common.ErrorMessages.*; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class WinningNumbersParser { + private static final String SEPARATOR = ","; + + /** + * μ‚¬μš©μž μž…λ ₯ λ¬Έμžμ—΄(당첨 번호)을 μ •μˆ˜ 리슀트둜 νŒŒμ‹±ν•˜μ—¬ λ°˜ν™˜ν•©λ‹ˆλ‹€. + * + * @param input μ‚¬μš©μžκ°€ μž…λ ₯ν•œ 당첨 번호 λ¬Έμžμ—΄ (μ‰Όν‘œλ‘œ ꡬ뢄됨) + * @return νŒŒμ‹±λœ 당첨 번호 리슀트 (μ •μˆ˜) + * @throws IllegalArgumentException μž…λ ₯이 null, 빈 λ¬Έμžμ—΄, λ˜λŠ” 빈 값이 ν¬ν•¨λœ 경우 + * @throws NumberFormatException μˆ«μžκ°€ μ•„λ‹Œ 값이 ν¬ν•¨λœ 경우 + */ + public static List parse(String input) { + ParserUtils.validateNotNullOrEmpty(input, WINNING_NUMBERS_EMPTY); + + List numberStrings = splitBySeparator(input); + return parseToIntegers(numberStrings); + } + + private static List splitBySeparator(String input) { + List parts = Arrays.stream(input.split(SEPARATOR, -1)) + .map(String::trim) + .collect(Collectors.toList()); + + validateNoEmptyValues(parts); + return parts; + } + + private static void validateNoEmptyValues(List parts) { + if (parts.stream().anyMatch(String::isEmpty)) { + throw new IllegalArgumentException(WINNING_NUMBERS_EMPTY_VALUE); + } + } + + private static List parseToIntegers(List numberStrings) { + return numberStrings.stream() + .map(str -> ParserUtils.parseToInteger(str, WINNING_NUMBERS_INVALID_FORMAT)) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/lotto/service/LottoGameService.java b/src/main/java/lotto/service/LottoGameService.java new file mode 100644 index 0000000000..7e92cb350e --- /dev/null +++ b/src/main/java/lotto/service/LottoGameService.java @@ -0,0 +1,110 @@ +package lotto.service; + +import lotto.domain.Lotto; +import lotto.domain.LottoResult; +import lotto.domain.LottoTickets; +import lotto.domain.WinningLotto; +import lotto.parser.BonusNumberParser; +import lotto.parser.PurchaseAmountParser; +import lotto.parser.WinningNumbersParser; +import lotto.util.LottoGenerator; +import lotto.validator.InputValidator; +import lotto.view.InputView; +import lotto.view.OutputView; + +import java.util.List; + +/** + * 둜또 κ²Œμž„μ˜ 전체 흐름을 κ΄€λ¦¬ν•˜λŠ” μ„œλΉ„μŠ€ ν΄λž˜μŠ€μž…λ‹ˆλ‹€. + */ +public class LottoGameService { + private final InputView inputView; + private final OutputView outputView; + + public LottoGameService() { + this.inputView = new InputView(); + this.outputView = new OutputView(); + } + + public LottoGameService(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + /** + * 둜또 κ²Œμž„μ„ μ‹€ν–‰ν•©λ‹ˆλ‹€. + */ + public void run() { + int purchaseAmount = inputPurchaseAmount(); + LottoTickets lottoTickets = purchaseLottos(purchaseAmount); + outputView.printLottoTickets(lottoTickets.getLottos()); + + Lotto winningNumbers = inputWinningNumbers(); + int bonusNumber = inputBonusNumber(winningNumbers); + + WinningLotto winningLotto = new WinningLotto(winningNumbers, bonusNumber); + LottoResult result = lottoTickets.compareWithWinningLotto(winningLotto, purchaseAmount); + + outputView.printWinningStatistics(result); + outputView.printProfitRate(result); + } + + /** + * ꡬ맀 κΈˆμ•‘μ„ μž…λ ₯λ°›κ³  κ²€μ¦ν•©λ‹ˆλ‹€. + * μ˜ˆμ™Έ λ°œμƒ μ‹œ μž¬μž…λ ₯을 μš”μ²­ν•©λ‹ˆλ‹€. + */ + private int inputPurchaseAmount() { + while (true) { + try { + String input = inputView.inputPurchaseAmount(); + int amount = PurchaseAmountParser.parse(input); + InputValidator.validatePurchaseAmount(amount); + return amount; + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } + + /** + * ꡬ맀 κΈˆμ•‘λ§ŒνΌ 둜또λ₯Ό μžλ™ μƒμ„±ν•©λ‹ˆλ‹€. + */ + private LottoTickets purchaseLottos(int purchaseAmount) { + List lottos = LottoGenerator.generateLottos(purchaseAmount); + return new LottoTickets(lottos); + } + + /** + * 당첨 번호λ₯Ό μž…λ ₯λ°›κ³  κ²€μ¦ν•©λ‹ˆλ‹€. + * μ˜ˆμ™Έ λ°œμƒ μ‹œ μž¬μž…λ ₯을 μš”μ²­ν•©λ‹ˆλ‹€. + */ + private Lotto inputWinningNumbers() { + while (true) { + try { + String input = inputView.inputWinningNumbers(); + List numbers = WinningNumbersParser.parse(input); + InputValidator.validateWinningNumbers(numbers); + return new Lotto(numbers); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } + + /** + * λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯λ°›κ³  κ²€μ¦ν•©λ‹ˆλ‹€. + * μ˜ˆμ™Έ λ°œμƒ μ‹œ μž¬μž…λ ₯을 μš”μ²­ν•©λ‹ˆλ‹€. + */ + private int inputBonusNumber(Lotto winningNumbers) { + while (true) { + try { + String input = inputView.inputBonusNumber(); + int bonusNumber = BonusNumberParser.parse(input); + InputValidator.validateBonusNumber(winningNumbers, bonusNumber); + return bonusNumber; + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/lotto/util/LottoGenerator.java b/src/main/java/lotto/util/LottoGenerator.java new file mode 100644 index 0000000000..4b1aa77bbf --- /dev/null +++ b/src/main/java/lotto/util/LottoGenerator.java @@ -0,0 +1,29 @@ +package lotto.util; + +import camp.nextstep.edu.missionutils.Randoms; +import lotto.domain.Lotto; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static lotto.common.LottoConstants.*; + +public class LottoGenerator { + + public static List generateLottos(int purchaseAmount) { + int lottoCount = purchaseAmount / LOTTO_PRICE; + return IntStream.range(0, lottoCount) + .mapToObj(i -> generateOneLotto()) + .collect(Collectors.toList()); + } + + private static Lotto generateOneLotto() { + List numbers = Randoms.pickUniqueNumbersInRange( + MIN_LOTTO_NUMBER, + MAX_LOTTO_NUMBER, + LOTTO_NUMBER_COUNT + ); + return new Lotto(numbers); + } +} diff --git a/src/main/java/lotto/validator/InputValidator.java b/src/main/java/lotto/validator/InputValidator.java new file mode 100644 index 0000000000..f359d409b6 --- /dev/null +++ b/src/main/java/lotto/validator/InputValidator.java @@ -0,0 +1,56 @@ +package lotto.validator; + +import java.util.List; + +import lotto.domain.Lotto; +import lotto.domain.WinningLotto; + +import static lotto.common.LottoConstants.*; +import static lotto.common.ErrorMessages.*; + +/** + * μ‚¬μš©μž μž…λ ₯값을 κ²€μ¦ν•˜λŠ” μœ ν‹Έλ¦¬ν‹° ν΄λž˜μŠ€μž…λ‹ˆλ‹€. + */ +public class InputValidator { + + /** + * ꡬ맀 κΈˆμ•‘μ΄ μœ νš¨ν•œμ§€ κ²€μ¦ν•©λ‹ˆλ‹€. + * + * @param amount ꡬ맀 κΈˆμ•‘ + * @throws IllegalArgumentException κΈˆμ•‘μ΄ 0 μ΄ν•˜, 1000원 미만, 1000원 λ‹¨μœ„κ°€ μ•„λ‹Œ 경우 + */ + public static void validatePurchaseAmount(int amount) { + if (amount <= 0) { + throw new IllegalArgumentException(PURCHASE_AMOUNT_ZERO); + } + if (amount < LOTTO_PRICE) { + throw new IllegalArgumentException(PURCHASE_AMOUNT_MINIMUM); + } + if (amount % LOTTO_PRICE != 0) { + throw new IllegalArgumentException(PURCHASE_AMOUNT_UNIT); + } + } + + /** + * 당첨 λ²ˆν˜Έκ°€ μœ νš¨ν•œμ§€ κ²€μ¦ν•©λ‹ˆλ‹€. + * Lotto 객체 생성을 톡해 null, 개수, λ²”μœ„, 쀑볡 검증을 μˆ˜ν–‰ν•©λ‹ˆλ‹€. + * + * @param numbers 당첨 번호 리슀트 + * @throws IllegalArgumentException Lotto 생성 쑰건을 λ§Œμ‘±ν•˜μ§€ μ•ŠλŠ” 경우 + */ + public static void validateWinningNumbers(List numbers) { + new Lotto(numbers); + } + + /** + * λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ μœ νš¨ν•œμ§€ κ²€μ¦ν•©λ‹ˆλ‹€. + * WinningLotto 객체 생성을 톡해 λ²”μœ„ 및 쀑볡 검증을 μˆ˜ν–‰ν•©λ‹ˆλ‹€. + * + * @param winningNumbers 당첨 번호 Lotto 객체 + * @param bonusNumber λ³΄λ„ˆμŠ€ 번호 + * @throws IllegalArgumentException WinningLotto 생성 쑰건을 λ§Œμ‘±ν•˜μ§€ μ•ŠλŠ” 경우 + */ + public static void validateBonusNumber(Lotto winningNumbers, int bonusNumber) { + new WinningLotto(winningNumbers, bonusNumber); + } +} \ No newline at end of file diff --git a/src/main/java/lotto/view/InputView.java b/src/main/java/lotto/view/InputView.java new file mode 100644 index 0000000000..827592492f --- /dev/null +++ b/src/main/java/lotto/view/InputView.java @@ -0,0 +1,43 @@ +package lotto.view; + +import camp.nextstep.edu.missionutils.Console; + +import static lotto.common.ViewMessages.*; + +/** + * μ‚¬μš©μž μž…λ ₯을 λ°›λŠ” View ν΄λž˜μŠ€μž…λ‹ˆλ‹€. + */ +public class InputView { + + /** + * ꡬ맀 κΈˆμ•‘μ„ μž…λ ₯λ°›μŠ΅λ‹ˆλ‹€. + * + * @return μ‚¬μš©μžκ°€ μž…λ ₯ν•œ ꡬ맀 κΈˆμ•‘ λ¬Έμžμ—΄ + */ + public String inputPurchaseAmount() { + System.out.println(INPUT_PURCHASE_AMOUNT); + return Console.readLine(); + } + + /** + * 당첨 번호λ₯Ό μž…λ ₯λ°›μŠ΅λ‹ˆλ‹€. + * + * @return μ‚¬μš©μžκ°€ μž…λ ₯ν•œ 당첨 번호 λ¬Έμžμ—΄ (μ‰Όν‘œλ‘œ ꡬ뢄) + */ + public String inputWinningNumbers() { + System.out.println(); + System.out.println(INPUT_WINNING_NUMBERS); + return Console.readLine(); + } + + /** + * λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯λ°›μŠ΅λ‹ˆλ‹€. + * + * @return μ‚¬μš©μžκ°€ μž…λ ₯ν•œ λ³΄λ„ˆμŠ€ 번호 λ¬Έμžμ—΄ + */ + public String inputBonusNumber() { + System.out.println(); + System.out.println(INPUT_BONUS_NUMBER); + return Console.readLine(); + } +} \ No newline at end of file diff --git a/src/main/java/lotto/view/OutputView.java b/src/main/java/lotto/view/OutputView.java new file mode 100644 index 0000000000..a5a509fd8b --- /dev/null +++ b/src/main/java/lotto/view/OutputView.java @@ -0,0 +1,76 @@ +package lotto.view; + +import lotto.domain.Lotto; +import lotto.domain.LottoResult; +import lotto.domain.Rank; + +import java.text.DecimalFormat; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +import static lotto.common.ViewMessages.*; + +/** + * κ²Œμž„ κ²°κ³Όλ₯Ό 좜λ ₯ν•˜λŠ” View ν΄λž˜μŠ€μž…λ‹ˆλ‹€. + */ +public class OutputView { + private static final DecimalFormat MONEY_FORMAT = new DecimalFormat("#,###"); + + /** + * κ΅¬λ§€ν•œ 둜또 λͺ©λ‘μ„ 좜λ ₯ν•©λ‹ˆλ‹€. + * 각 둜또 λ²ˆν˜ΈλŠ” μ˜€λ¦„μ°¨μˆœμœΌλ‘œ μ •λ ¬λ˜μ–΄ 좜λ ₯λ©λ‹ˆλ‹€. + * + * @param lottos κ΅¬λ§€ν•œ 둜또 리슀트 + */ + public void printLottoTickets(List lottos) { + System.out.println(); + System.out.printf(PURCHASED_LOTTO_COUNT_FORMAT + "%n", lottos.size()); + lottos.forEach(lotto -> System.out.println(lotto.toString())); + } + + /** + * 당첨 톡계λ₯Ό 좜λ ₯ν•©λ‹ˆλ‹€. + * 5λ“±λΆ€ν„° 1λ“±κΉŒμ§€ μˆœμ„œλŒ€λ‘œ 좜λ ₯ν•©λ‹ˆλ‹€. + * + * @param lottoResult 당첨 κ²°κ³Ό + */ + public void printWinningStatistics(LottoResult lottoResult) { + System.out.println(); + System.out.println(WINNING_STATISTICS_HEADER); + System.out.println(SEPARATOR); + + Stream.of(Rank.values()) + .filter(Rank::isWinning) + .sorted(Comparator.comparingInt(Rank::getPrize)) + .forEach(rank -> printRank(rank, lottoResult)); + } + + private void printRank(Rank rank, LottoResult lottoResult) { + String prize = MONEY_FORMAT.format(rank.getPrize()); + String description = rank.getDescription(); + int count = lottoResult.getPrizeCount(rank); + + System.out.printf(RANK_FORMAT + "%n", description, prize, count); + } + + /** + * 수읡λ₯ μ„ 좜λ ₯ν•©λ‹ˆλ‹€. + * μ†Œμˆ˜μ  첫째 μžλ¦¬κΉŒμ§€ ν‘œμ‹œν•˜λ©° λ°˜μ˜¬λ¦Όλ©λ‹ˆλ‹€. + * + * @param lottoResult 당첨 κ²°κ³Ό + */ + public void printProfitRate(LottoResult lottoResult) { + double profitRate = lottoResult.calculateProfitRate(); + System.out.printf(PROFIT_RATE_FORMAT + "%n", profitRate); + } + + /** + * μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό 좜λ ₯ν•©λ‹ˆλ‹€. + * + * @param message μ—λŸ¬ λ©”μ‹œμ§€ (이미 [ERROR] 접두사 포함) + */ + public void printErrorMessage(String message) { + System.out.println(message); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/ApplicationTest.java b/src/test/java/lotto/ApplicationTest.java index a15c7d1f52..a322c2dd20 100644 --- a/src/test/java/lotto/ApplicationTest.java +++ b/src/test/java/lotto/ApplicationTest.java @@ -1,6 +1,7 @@ package lotto; import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.List; @@ -10,52 +11,108 @@ import static org.assertj.core.api.Assertions.assertThat; class ApplicationTest extends NsTest { - private static final String ERROR_MESSAGE = "[ERROR]"; - - @Test - void κΈ°λŠ₯_ν…ŒμŠ€νŠΈ() { - assertRandomUniqueNumbersInRangeTest( - () -> { - run("8000", "1,2,3,4,5,6", "7"); - assertThat(output()).contains( - "8개λ₯Ό κ΅¬λ§€ν–ˆμŠ΅λ‹ˆλ‹€.", - "[8, 21, 23, 41, 42, 43]", - "[3, 5, 11, 16, 32, 38]", - "[7, 11, 16, 35, 36, 44]", - "[1, 8, 11, 31, 41, 42]", - "[13, 14, 16, 38, 42, 45]", - "[7, 11, 30, 40, 42, 43]", - "[2, 13, 22, 32, 38, 45]", - "[1, 3, 5, 14, 22, 45]", - "3개 일치 (5,000원) - 1개", - "4개 일치 (50,000원) - 0개", - "5개 일치 (1,500,000원) - 0개", - "5개 일치, λ³΄λ„ˆμŠ€ λ³Ό 일치 (30,000,000원) - 0개", - "6개 일치 (2,000,000,000원) - 0개", - "총 수읡λ₯ μ€ 62.5%μž…λ‹ˆλ‹€." - ); - }, - List.of(8, 21, 23, 41, 42, 43), - List.of(3, 5, 11, 16, 32, 38), - List.of(7, 11, 16, 35, 36, 44), - List.of(1, 8, 11, 31, 41, 42), - List.of(13, 14, 16, 38, 42, 45), - List.of(7, 11, 30, 40, 42, 43), - List.of(2, 13, 22, 32, 38, 45), - List.of(1, 3, 5, 14, 22, 45) - ); - } - - @Test - void μ˜ˆμ™Έ_ν…ŒμŠ€νŠΈ() { - assertSimpleTest(() -> { - runException("1000j"); - assertThat(output()).contains(ERROR_MESSAGE); - }); - } - - @Override - public void runMain() { - Application.main(new String[]{}); - } -} + private static final String ERROR_MESSAGE = "[ERROR]"; + + @DisplayName("κΈ°λŠ₯ ν…ŒμŠ€νŠΈ") + @Test + void κΈ°λŠ₯_ν…ŒμŠ€νŠΈ() { + assertRandomUniqueNumbersInRangeTest( + () -> { + run("8000", "1,2,3,4,5,6", "7"); + assertThat(output()).contains( + "8개λ₯Ό κ΅¬λ§€ν–ˆμŠ΅λ‹ˆλ‹€.", + "[8, 21, 23, 41, 42, 43]", + "[3, 5, 11, 16, 32, 38]", + "[7, 11, 16, 35, 36, 44]", + "[1, 8, 11, 31, 41, 42]", + "[13, 14, 16, 38, 42, 45]", + "[7, 11, 30, 40, 42, 43]", + "[2, 13, 22, 32, 38, 45]", + "[1, 3, 5, 14, 22, 45]", + "3개 일치 (5,000원) - 1개", + "4개 일치 (50,000원) - 0개", + "5개 일치 (1,500,000원) - 0개", + "5개 일치, λ³΄λ„ˆμŠ€ λ³Ό 일치 (30,000,000원) - 0개", + "6개 일치 (2,000,000,000원) - 0개", + "총 수읡λ₯ μ€ 62.5%μž…λ‹ˆλ‹€." + ); + }, + List.of(8, 21, 23, 41, 42, 43), + List.of(3, 5, 11, 16, 32, 38), + List.of(7, 11, 16, 35, 36, 44), + List.of(1, 8, 11, 31, 41, 42), + List.of(13, 14, 16, 38, 42, 45), + List.of(7, 11, 30, 40, 42, 43), + List.of(2, 13, 22, 32, 38, 45), + List.of(1, 3, 5, 14, 22, 45) + ); + } + + @DisplayName("μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ - ꡬ맀 κΈˆμ•‘μ΄ μˆ«μžκ°€ μ•„λ‹Œ 경우") + @Test + void μ˜ˆμ™Έ_ν…ŒμŠ€νŠΈ() { + assertSimpleTest(() -> { + runException("1000j"); + assertThat(output()).contains(ERROR_MESSAGE); + }); + } + + @DisplayName("μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ - ꡬ맀 κΈˆμ•‘μ΄ 1000원 λ‹¨μœ„κ°€ μ•„λ‹Œ 경우") + @Test + void μ˜ˆμ™Έ_ν…ŒμŠ€νŠΈ_κ΅¬λ§€κΈˆμ•‘_λ‹¨μœ„() { + assertSimpleTest(() -> { + runException("1500"); + assertThat(output()).contains(ERROR_MESSAGE); + }); + } + + @DisplayName("μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ - 당첨 λ²ˆν˜Έκ°€ 6κ°œκ°€ μ•„λ‹Œ 경우") + @Test + void μ˜ˆμ™Έ_ν…ŒμŠ€νŠΈ_λ‹Ήμ²¨λ²ˆν˜Έ_개수() { + assertSimpleTest(() -> { + runException("1000", "1,2,3,4,5"); + assertThat(output()).contains(ERROR_MESSAGE); + }); + } + + @DisplayName("μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ - 당첨 λ²ˆν˜Έμ— 쀑볡이 μžˆλŠ” 경우") + @Test + void μ˜ˆμ™Έ_ν…ŒμŠ€νŠΈ_λ‹Ήμ²¨λ²ˆν˜Έ_쀑볡() { + assertSimpleTest(() -> { + runException("1000", "1,2,3,4,5,5"); + assertThat(output()).contains(ERROR_MESSAGE); + }); + } + + @DisplayName("μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ - 당첨 λ²ˆν˜Έκ°€ λ²”μœ„λ₯Ό λ²—μ–΄λ‚œ 경우") + @Test + void μ˜ˆμ™Έ_ν…ŒμŠ€νŠΈ_λ‹Ήμ²¨λ²ˆν˜Έ_λ²”μœ„() { + assertSimpleTest(() -> { + runException("1000", "1,2,3,4,5,46"); + assertThat(output()).contains(ERROR_MESSAGE); + }); + } + + @DisplayName("μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ - λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 당첨 λ²ˆν˜Έμ™€ μ€‘λ³΅λœ 경우") + @Test + void μ˜ˆμ™Έ_ν…ŒμŠ€νŠΈ_λ³΄λ„ˆμŠ€λ²ˆν˜Έ_쀑볡() { + assertSimpleTest(() -> { + runException("1000", "1,2,3,4,5,6", "6"); + assertThat(output()).contains(ERROR_MESSAGE); + }); + } + + @DisplayName("μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ - λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ λ²”μœ„λ₯Ό λ²—μ–΄λ‚œ 경우") + @Test + void μ˜ˆμ™Έ_ν…ŒμŠ€νŠΈ_λ³΄λ„ˆμŠ€λ²ˆν˜Έ_λ²”μœ„() { + assertSimpleTest(() -> { + runException("1000", "1,2,3,4,5,6", "46"); + assertThat(output()).contains(ERROR_MESSAGE); + }); + } + + @Override + public void runMain() { + Application.main(new String[]{}); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/LottoTest.java b/src/test/java/lotto/LottoTest.java deleted file mode 100644 index 309f4e50ae..0000000000 --- a/src/test/java/lotto/LottoTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package lotto; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class LottoTest { - @Test - void 둜또_번호의_κ°œμˆ˜κ°€_6κ°œκ°€_λ„˜μ–΄κ°€λ©΄_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7))) - .isInstanceOf(IllegalArgumentException.class); - } - - @DisplayName("둜또 λ²ˆν˜Έμ— μ€‘λ³΅λœ μˆ«μžκ°€ 있으면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") - @Test - void 둜또_λ²ˆν˜Έμ—_μ€‘λ³΅λœ_μˆ«μžκ°€_있으면_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) - .isInstanceOf(IllegalArgumentException.class); - } - - // TODO: μΆ”κ°€ κΈ°λŠ₯ κ΅¬ν˜„μ— λ”°λ₯Έ ν…ŒμŠ€νŠΈ μ½”λ“œ μž‘μ„± -} diff --git a/src/test/java/lotto/domain/LottoResultTest.java b/src/test/java/lotto/domain/LottoResultTest.java new file mode 100644 index 0000000000..dd07841aff --- /dev/null +++ b/src/test/java/lotto/domain/LottoResultTest.java @@ -0,0 +1,70 @@ +package lotto.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LottoResultTest { + + @DisplayName("정상1: λ“±μˆ˜λ³„ 당첨 개수λ₯Ό μ •ν™•νžˆ μ €μž₯ν•œλ‹€") + @Test + void test1() { + // Given + List ranks = List.of(Rank.THIRD, Rank.THIRD, Rank.FIFTH, Rank.NONE); + + // When + LottoResult result = new LottoResult(ranks, 4000); + + // Then + assertThat(result.getPrizeCount(Rank.THIRD)).isEqualTo(2); + assertThat(result.getPrizeCount(Rank.FIFTH)).isEqualTo(1); + assertThat(result.getPrizeCount(Rank.FOURTH)).isEqualTo(0); + assertThat(result.getPrizeCount(Rank.NONE)).isEqualTo(0); + } + + @DisplayName("정상2: 총 μˆ˜μ΅κΈˆμ„ μ •ν™•ν•˜κ²Œ κ³„μ‚°ν•œλ‹€") + @Test + void test2() { + // 1λ“± 1개 (20μ–΅) + 3λ“± 1개 (150만) = 2,001,500,000 + LottoResult result = new LottoResult(List.of(Rank.FIRST, Rank.THIRD), 8000); + + long expectedProfit = 2_000_000_000L + 1_500_000L; + + assertThat(result.calculateTotalProfit()).isEqualTo(expectedProfit); + } + + @DisplayName("정상3: 수읡λ₯  계산 (반올림 ν…ŒμŠ€νŠΈ 1) - 66.666..% -> 66.7%") + @Test + void test3() { + // Given: 3000원 ꡬ맀, 2000원 당첨 + int amount = 3000; + int profit = 2000; + + // When + double profitRate = (double) profit * 100 / amount; + String formattedRate = String.format("%.1f", profitRate); + + // Then + assertThat(formattedRate).isEqualTo("66.7"); + } + + @DisplayName("μ˜ˆμ™Έ1: ꡬ맀 κΈˆμ•‘μ΄ 0이면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test6() { + assertThatThrownBy(() -> new LottoResult(List.of(Rank.FIFTH), 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] ꡬ맀 κΈˆμ•‘μ€ 0보닀 컀야 ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ2: ꡬ맀 κΈˆμ•‘μ΄ 음수이면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test7() { + assertThatThrownBy(() -> new LottoResult(List.of(Rank.FIFTH), -1000)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] ꡬ맀 κΈˆμ•‘μ€ 0보닀 컀야 ν•©λ‹ˆλ‹€."); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/domain/LottoTest.java b/src/test/java/lotto/domain/LottoTest.java new file mode 100644 index 0000000000..b13319038a --- /dev/null +++ b/src/test/java/lotto/domain/LottoTest.java @@ -0,0 +1,147 @@ +package lotto.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LottoTest { + + @DisplayName("정상1: 둜또 번호 6개λ₯Ό μ €μž₯ν•œλ‹€") + @Test + void test1() { + List numbers = List.of(1, 2, 3, 4, 5, 6); + + Lotto lotto = new Lotto(numbers); // 전체 경둜 제거 + assertThat(lotto.getNumbers()).hasSize(6); + assertThat(lotto.getNumbers()).containsExactly(1, 2, 3, 4, 5, 6); + } + + @DisplayName("정상2: 둜또 λ²ˆν˜ΈλŠ” 1~45 λ²”μœ„μ˜ μ€‘λ³΅λ˜μ§€ μ•ŠλŠ” μˆ«μžμ΄λ‹€") + @Test + void test2() { + List numbers = List.of(1, 15, 23, 32, 41, 45); + + Lotto lotto = new Lotto(numbers); + assertThat(lotto.getNumbers()).allMatch(num -> num >= 1 && num <= 45); + } + + @DisplayName("정상3: 둜또 번호λ₯Ό μ˜€λ¦„μ°¨μˆœμœΌλ‘œ μ •λ ¬ν•˜μ—¬ λ°˜ν™˜ν•œλ‹€") + @Test + void test3() { + List numbers = List.of(45, 1, 32, 15, 23, 8); + + Lotto lotto = new Lotto(numbers); + assertThat(lotto.getNumbers()).containsExactly(1, 8, 15, 23, 32, 45); + } + + @DisplayName("정상4: μ£Όμ–΄μ§„ 번호 λ¦¬μŠ€νŠΈμ™€ μΌμΉ˜ν•˜λŠ” 개수λ₯Ό λ°˜ν™˜ν•œλ‹€") + @Test + void test4() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + List winningNumbers = List.of(1, 2, 3, 7, 8, 9); + + int matchCount = lotto.countMatches(winningNumbers); + assertThat(matchCount).isEqualTo(3); + } + + @DisplayName("정상5: μΌμΉ˜ν•˜λŠ” λ²ˆν˜Έκ°€ 없을 λ•Œ 0을 λ°˜ν™˜ν•œλ‹€") + @Test + void test5() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + List winningNumbers = List.of(7, 8, 9, 10, 11, 12); + + int matchCount = lotto.countMatches(winningNumbers); + assertThat(matchCount).isEqualTo(0); + } + + @DisplayName("정상6: λͺ¨λ“  λ²ˆν˜Έκ°€ μΌμΉ˜ν•  λ•Œ 6을 λ°˜ν™˜ν•œλ‹€") + @Test + void test6() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + List winningNumbers = List.of(1, 2, 3, 4, 5, 6); + + int matchCount = lotto.countMatches(winningNumbers); + assertThat(matchCount).isEqualTo(6); + } + + @DisplayName("정상7: νŠΉμ • 번호 포함 μ—¬λΆ€λ₯Ό ν™•μΈν•œλ‹€ - ν¬ν•¨ν•˜λŠ” 경우") + @Test + void test7() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + + assertThat(lotto.contains(1)).isTrue(); + assertThat(lotto.contains(3)).isTrue(); + assertThat(lotto.contains(6)).isTrue(); + } + + @DisplayName("정상8: νŠΉμ • 번호 포함 μ—¬λΆ€λ₯Ό ν™•μΈν•œλ‹€ - ν¬ν•¨ν•˜μ§€ μ•ŠλŠ” 경우") + @Test + void test8() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + + assertThat(lotto.contains(7)).isFalse(); + assertThat(lotto.contains(45)).isFalse(); + } + + @DisplayName("μ˜ˆμ™Έ1: 둜또 λ²ˆν˜Έκ°€ null이면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test9() { + assertThatThrownBy(() -> new Lotto(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR]"); + } + + @DisplayName("μ˜ˆμ™Έ2: 둜또 번호의 κ°œμˆ˜κ°€ 6κ°œκ°€ λ„˜μ–΄κ°€λ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test10() { + assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5,6,7))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 6κ°œμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ3: 둜또 번호의 κ°œμˆ˜κ°€ 6개 미만이면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test11() { + assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 6κ°œμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + + @Tag("error") + @DisplayName("μ˜ˆμ™Έ4: 둜또 λ²ˆν˜Έμ— μ€‘λ³΅λœ μˆ«μžκ°€ 있으면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test12() { + assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5,5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ5: 둜또 λ²ˆν˜Έκ°€ 1보닀 μž‘μœΌλ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test13() { + assertThatThrownBy(() -> new Lotto(List.of(0,1,2,3,4,5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ6: 둜또 λ²ˆν˜Έκ°€ 45보닀 크면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test14() { + assertThatThrownBy(() -> new Lotto(List.of(1,2,3,4,5,46))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ7: 둜또 λ²ˆν˜Έκ°€ 음수이면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test15() { + assertThatThrownBy(() -> new Lotto(List.of(-1,1,2,3,4,5))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } +} diff --git a/src/test/java/lotto/domain/LottoTicketsTest.java b/src/test/java/lotto/domain/LottoTicketsTest.java new file mode 100644 index 0000000000..aeb4edd750 --- /dev/null +++ b/src/test/java/lotto/domain/LottoTicketsTest.java @@ -0,0 +1,72 @@ +package lotto.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LottoTicketsTest { + + private final Lotto lotto1 = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + private final Lotto lotto2 = new Lotto(List.of(7, 8, 9, 10, 11, 12)); + private final List validLottoList = List.of(lotto1, lotto2); + + @DisplayName("정상1: μ—¬λŸ¬ 개의 둜또λ₯Ό κ΄€λ¦¬ν•˜λŠ” 객체λ₯Ό μƒμ„±ν•œλ‹€") + @Test + void test1() { + // When + LottoTickets lottoTickets = new LottoTickets(validLottoList); + + // Then + assertThat(lottoTickets).isNotNull(); + assertThat(lottoTickets.getLottoCount()).isEqualTo(2); + assertThat(lottoTickets.getLottos()).containsExactly(lotto1, lotto2); + } + + @DisplayName("정상2: κ΅¬λ§€ν•œ 둜또 개수λ₯Ό μ •ν™•νžˆ λ°˜ν™˜ν•œλ‹€") + @Test + void test2() { + // When + LottoTickets lottoTickets = new LottoTickets(validLottoList); + + // Then + assertThat(lottoTickets.getLottoCount()).isEqualTo(2); + } + + @DisplayName("정상3: 당첨 λ²ˆν˜Έμ™€ λΉ„κ΅ν•˜μ—¬ 각 둜또의 λ“±μˆ˜λ₯Ό νŒλ³„ν•˜κ³  LottoResultλ₯Ό λ°˜ν™˜ν•œλ‹€") + @Test + void test3() { + // Given + // lotto1: 1, 2, 3, 4, 5, 6 (당첨 λ²ˆν˜Έμ™€ 6개 일치 -> 1λ“±) + // lotto2: 7, 8, 9, 10, 11, 12 (당첨 λ²ˆν˜Έμ™€ 0개 일치 -> 꽝) + LottoTickets lottoTickets = new LottoTickets(validLottoList); + WinningLotto winningLotto = new WinningLotto(lotto1, 7); // 1~6 당첨, λ³΄λ„ˆμŠ€ 7 + int purchaseAmount = 2000; + + // When + LottoResult result = lottoTickets.compareWithWinningLotto(winningLotto, purchaseAmount); + + assertThat(result.getPrizeCount(Rank.FIRST)).isEqualTo(1); + assertThat(result.getPrizeCount(Rank.NONE)).isEqualTo(0); + } + + @DisplayName("μ˜ˆμ™Έ1: 둜또 λ¦¬μŠ€νŠΈκ°€ null이면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test4() { + assertThatThrownBy(() -> new LottoTickets(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 λͺ©λ‘μ΄ μ—†μŠ΅λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ2: 둜또 λ¦¬μŠ€νŠΈκ°€ λΉ„μ–΄μžˆμœΌλ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€") + @Test + void test5() { + assertThatThrownBy(() -> new LottoTickets(Collections.emptyList())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] μ΅œμ†Œ 1개 μ΄μƒμ˜ 둜또λ₯Ό ꡬ맀해야 ν•©λ‹ˆλ‹€."); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/domain/RankTest.java b/src/test/java/lotto/domain/RankTest.java new file mode 100644 index 0000000000..3979fb8e3f --- /dev/null +++ b/src/test/java/lotto/domain/RankTest.java @@ -0,0 +1,77 @@ +package lotto.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class RankTest { + + @DisplayName("1: 6개 μΌμΉ˜ν•˜λ©΄ 1λ“±") + @Test + void test1() { + Rank rank = Rank.valueOf(6, false); + + assertThat(rank).isEqualTo(Rank.FIRST); + assertThat(rank.getPrize()).isEqualTo(2_000_000_000); + } + + @DisplayName("2: 5개 μΌμΉ˜ν•˜κ³  λ³΄λ„ˆμŠ€λ³Ό μΌμΉ˜ν•˜λ©΄ 2λ“±") + @Test + void test2() { + Rank rank = Rank.valueOf(5, true); + + assertThat(rank).isEqualTo(Rank.SECOND); + assertThat(rank.getPrize()).isEqualTo(30_000_000); + } + + @DisplayName("3: 5개 μΌμΉ˜ν•˜κ³  λ³΄λ„ˆμŠ€λ³Ό λΆˆμΌμΉ˜ν•˜λ©΄ 3λ“±") + @Test + void test3() { + Rank rank = Rank.valueOf(5, false); + + assertThat(rank).isEqualTo(Rank.THIRD); + assertThat(rank.getPrize()).isEqualTo(1_500_000); + } + + @DisplayName("4: 4개 μΌμΉ˜ν•˜λ©΄ 4λ“±") + @Test + void test4() { + Rank rank = Rank.valueOf(4, false); + + assertThat(rank).isEqualTo(Rank.FOURTH); + assertThat(rank.getPrize()).isEqualTo(50_000); + } + + @DisplayName("5: 3개 μΌμΉ˜ν•˜λ©΄ 5λ“±") + @Test + void test5() { + Rank rank = Rank.valueOf(3, false); + + assertThat(rank).isEqualTo(Rank.FIFTH); + assertThat(rank.getPrize()).isEqualTo(5_000); + } + + @DisplayName("6: 2개 μ΄ν•˜ μΌμΉ˜ν•˜λ©΄ NONE") + @ParameterizedTest + @CsvSource({"0, false", "1, false", "2, false"}) + void test6(int matchCount, boolean matchBonus) { + Rank rank = Rank.valueOf(matchCount, matchBonus); + + assertThat(rank).isEqualTo(Rank.NONE); + assertThat(rank.getPrize()).isEqualTo(0); + } + + @DisplayName("7: 당첨 μ—¬λΆ€ 확인") + @Test + void test7() { + assertThat(Rank.FIRST.isWinning()).isTrue(); + assertThat(Rank.SECOND.isWinning()).isTrue(); + assertThat(Rank.THIRD.isWinning()).isTrue(); + assertThat(Rank.FOURTH.isWinning()).isTrue(); + assertThat(Rank.FIFTH.isWinning()).isTrue(); + assertThat(Rank.NONE.isWinning()).isFalse(); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/domain/WinningLottoTest.java b/src/test/java/lotto/domain/WinningLottoTest.java new file mode 100644 index 0000000000..0b26ab4534 --- /dev/null +++ b/src/test/java/lotto/domain/WinningLottoTest.java @@ -0,0 +1,134 @@ +package lotto.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class WinningLottoTest { + + private final Lotto validWinningLottoNumbers = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + + @DisplayName("정상1: 당첨 번호 6κ°œμ™€ λ³΄λ„ˆμŠ€ 번호 1개λ₯Ό μ €μž₯ν•œλ‹€") + @Test + void test1() { + // Given + int bonusNumber = 7; + + // When + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, bonusNumber); + + // Then + assertThat(winningLotto).isNotNull(); + } + + @DisplayName("정상2: λ‘œλ˜μ™€ λΉ„κ΅ν•˜μ—¬ λ“±μˆ˜λ₯Ό νŒλ³„ν•œλ‹€ - 1λ“±") + @Test + void test2() { + // Given: 1, 2, 3, 4, 5, 6 (당첨 번호), 7 (λ³΄λ„ˆμŠ€) + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, 7); + // When: 1, 2, 3, 4, 5, 6 (6개 일치) + Lotto userLotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + + // Then + assertThat(winningLotto.match(userLotto)).isEqualTo(Rank.FIRST); + } + + @DisplayName("정상3: λ‘œλ˜μ™€ λΉ„κ΅ν•˜μ—¬ λ“±μˆ˜λ₯Ό νŒλ³„ν•œλ‹€ - 2λ“±") + @Test + void test3() { + // Given: 1, 2, 3, 4, 5, 6 (당첨 번호), 7 (λ³΄λ„ˆμŠ€) + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, 7); + // When: 1, 2, 3, 4, 5, 7 (5개 일치, λ³΄λ„ˆμŠ€ 일치) + Lotto userLotto = new Lotto(List.of(1, 2, 3, 4, 5, 7)); + + // Then + assertThat(winningLotto.match(userLotto)).isEqualTo(Rank.SECOND); + } + + @DisplayName("정상4: λ‘œλ˜μ™€ λΉ„κ΅ν•˜μ—¬ λ“±μˆ˜λ₯Ό νŒλ³„ν•œλ‹€ - 3λ“±") + @Test + void test4() { + // Given: 1, 2, 3, 4, 5, 6 (당첨 번호), 7 (λ³΄λ„ˆμŠ€) + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, 7); + // When: 1, 2, 3, 4, 5, 8 (5개 일치, λ³΄λ„ˆμŠ€ 뢈일치) + Lotto userLotto = new Lotto(List.of(1, 2, 3, 4, 5, 8)); + + // Then + assertThat(winningLotto.match(userLotto)).isEqualTo(Rank.THIRD); + } + + @DisplayName("정상5: λ‘œλ˜μ™€ λΉ„κ΅ν•˜μ—¬ λ“±μˆ˜λ₯Ό νŒλ³„ν•œλ‹€ - 5λ“±") + @Test + void test5() { + // Given: 1, 2, 3, 4, 5, 6 (당첨 번호), 7 (λ³΄λ„ˆμŠ€) + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, 7); + // When: 1, 2, 3, 8, 9, 10 (3개 일치) + Lotto userLotto = new Lotto(List.of(1, 2, 3, 8, 9, 10)); + + // Then + assertThat(winningLotto.match(userLotto)).isEqualTo(Rank.FIFTH); + } + + @DisplayName("정상6: λ‘œλ˜μ™€ λΉ„κ΅ν•˜μ—¬ λ“±μˆ˜λ₯Ό νŒλ³„ν•œλ‹€ - 꽝 (NONE)") + @ParameterizedTest(name = "{0}개 일치 μ‹œ NONE") + @CsvSource({"0", "1", "2"}) + void test6(int matchCount) { + // Given: 1, 2, 3, 4, 5, 6 (당첨 번호), 7 (λ³΄λ„ˆμŠ€) + WinningLotto winningLotto = new WinningLotto(validWinningLottoNumbers, 7); + + // When: matchCount에 맞게 둜또 생성 (λ³΄λ„ˆμŠ€ λ²ˆν˜Έμ™€λ„ λΆˆμΌμΉ˜ν•˜κ²Œ) + List lottoNumbers = List.of(1, 2, 3, 4, 5, 6).subList(0, matchCount); + lottoNumbers = new java.util.ArrayList<>(lottoNumbers); + // λ‚˜λ¨Έμ§€ 번호λ₯Ό 7, 8, ...둜 μ±„μ›Œμ„œ λ³΄λ„ˆμŠ€ 번호(7)와도 κ²ΉμΉ˜μ§€ μ•Šκ²Œ 함 + for (int i = 0; lottoNumbers.size() < 6; i++) { + lottoNumbers.add(8 + i); + } + Lotto userLotto = new Lotto(lottoNumbers); + + // Then + assertThat(winningLotto.match(userLotto)).isEqualTo(Rank.NONE); + } + + // μ˜ˆμ™Έ ν…ŒμŠ€νŠΈ + @DisplayName("μ˜ˆμ™Έ1: 당첨 λ²ˆν˜Έκ°€ null이면 IllegalArgumentException λ°œμƒ") + @Test + void test7() { + assertThatThrownBy(() -> new WinningLotto(null, 7)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] 둜또 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + + @DisplayName("μ˜ˆμ™Έ2: λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 당첨 λ²ˆν˜Έμ™€ μ€‘λ³΅λ˜λ©΄ IllegalArgumentException λ°œμƒ") + @Test + void test8() { + // 당첨 번호: 1, 2, 3, 4, 5, 6 + int bonusNumber = 6; + assertThatThrownBy(() -> new WinningLotto(validWinningLottoNumbers, bonusNumber)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 당첨 λ²ˆν˜Έμ™€ 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ3: λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 1보닀 μž‘μœΌλ©΄ IllegalArgumentException λ°œμƒ") + @Test + void test9() { + int bonusNumber = 0; + assertThatThrownBy(() -> new WinningLotto(validWinningLottoNumbers, bonusNumber)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ4: λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 45보닀 크면 IllegalArgumentException λ°œμƒ") + @Test + void test10() { + int bonusNumber = 46; + assertThatThrownBy(() -> new WinningLotto(validWinningLottoNumbers, bonusNumber)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/parser/BonusNumberParserTest.java b/src/test/java/lotto/parser/BonusNumberParserTest.java new file mode 100644 index 0000000000..ed3b70668f --- /dev/null +++ b/src/test/java/lotto/parser/BonusNumberParserTest.java @@ -0,0 +1,72 @@ +package lotto.parser; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BonusNumberParserTest { + + @Tag("normal") + @DisplayName("정상1: μœ νš¨ν•œ λ¬Έμžμ—΄ 번호λ₯Ό μ •μˆ˜λ‘œ νŒŒμ‹±ν•œλ‹€") + @Test + void test1() { + // Given + String input = "7"; + + // When + int result = BonusNumberParser.parse(input); + + // Then + assertThat(result).isEqualTo(7); + } + + @Tag("normal") + @DisplayName("정상2: μ•žλ’€ 곡백이 ν¬ν•¨λœ λ¬Έμžμ—΄λ„ μ •μˆ˜λ‘œ νŒŒμ‹±ν•œλ‹€") + @Test + void test2() { + // Given + String input = " 45 "; + + // When + int result = BonusNumberParser.parse(input); + + // Then + assertThat(result).isEqualTo(45); + } + + @Tag("error") + @DisplayName("μ˜ˆμ™Έ1: μž…λ ₯이 null이면 IllegalArgumentException λ°œμƒ") + @Test + void test3() { + String input = null; + + assertThatThrownBy(() -> BonusNumberParser.parse(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + + @Tag("error") + @DisplayName("μ˜ˆμ™Έ2: μž…λ ₯이 빈 λ¬Έμžμ—΄ λ˜λŠ” 곡백이면 IllegalArgumentException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: \"{0}\"") + @ValueSource(strings = {"", " ", " "}) + void test4(String input) { + assertThatThrownBy(() -> BonusNumberParser.parse(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + + @Tag("error") + @DisplayName("μ˜ˆμ™Έ3: μˆ«μžκ°€ μ•„λ‹Œ λ¬Έμžκ°€ ν¬ν•¨λ˜λ©΄ NumberFormatException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: \"{0}\"") + @ValueSource(strings = {"1a", "A", "7.0"}) + void test5(String input) { + assertThatThrownBy(() -> BonusNumberParser.parse(input)) + .isInstanceOf(NumberFormatException.class) + .hasMessage("[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/parser/ParserUtilsTest.java b/src/test/java/lotto/parser/ParserUtilsTest.java new file mode 100644 index 0000000000..9f5edebc33 --- /dev/null +++ b/src/test/java/lotto/parser/ParserUtilsTest.java @@ -0,0 +1,157 @@ +package lotto.parser; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ParserUtilsTest { + + @DisplayName("정상1: μœ νš¨ν•œ λ¬Έμžμ—΄μ€ 검증을 ν†΅κ³Όν•œλ‹€") + @Test + void test1() { + // Given + String input = "8000"; + String errorMessage = "[ERROR] ν…ŒμŠ€νŠΈ μ—λŸ¬"; + + // When & Then + ParserUtils.validateNotNullOrEmpty(input, errorMessage); + // μ˜ˆμ™Έκ°€ λ°œμƒν•˜μ§€ μ•ŠμœΌλ©΄ ν…ŒμŠ€νŠΈ 톡과 + } + + @DisplayName("정상2: 곡백이 ν¬ν•¨λœ λ¬Έμžμ—΄μ€ 검증을 ν†΅κ³Όν•œλ‹€") + @Test + void test2() { + // Given + String input = " 8000 "; + String errorMessage = "[ERROR] ν…ŒμŠ€νŠΈ μ—λŸ¬"; + + // When & Then + ParserUtils.validateNotNullOrEmpty(input, errorMessage); + // μ˜ˆμ™Έκ°€ λ°œμƒν•˜μ§€ μ•ŠμœΌλ©΄ ν…ŒμŠ€νŠΈ 톡과 + } + + @DisplayName("μ˜ˆμ™Έ1: μž…λ ₯이 null이면 IllegalArgumentException λ°œμƒ") + @Test + void test3() { + // Given + String input = null; + String errorMessage = "[ERROR] null μž…λ ₯ μ—λŸ¬"; + + // When & Then + assertThatThrownBy(() -> ParserUtils.validateNotNullOrEmpty(input, errorMessage)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(errorMessage); + } + + @DisplayName("μ˜ˆμ™Έ2: μž…λ ₯이 빈 λ¬Έμžμ—΄μ΄λ©΄ IllegalArgumentException λ°œμƒ") + @Test + void test4() { + // Given + String input = ""; + String errorMessage = "[ERROR] 빈 λ¬Έμžμ—΄ μ—λŸ¬"; + + // When & Then + assertThatThrownBy(() -> ParserUtils.validateNotNullOrEmpty(input, errorMessage)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(errorMessage); + } + + @DisplayName("μ˜ˆμ™Έ3: μž…λ ₯이 곡백만 있으면 IllegalArgumentException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: \"{0}\"") + @ValueSource(strings = {" ", " "}) + void test5(String input) { + // Given + String errorMessage = "[ERROR] 곡백 μ—λŸ¬"; + + // When & Then + assertThatThrownBy(() -> ParserUtils.validateNotNullOrEmpty(input, errorMessage)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(errorMessage); + } + + + @DisplayName("정상3: μœ νš¨ν•œ λ¬Έμžμ—΄μ„ μ •μˆ˜λ‘œ νŒŒμ‹±ν•œλ‹€") + @Test + void test6() { + // Given + String input = "8000"; + String errorMessage = "[ERROR] νŒŒμ‹± μ—λŸ¬"; + + // When + int result = ParserUtils.parseToInteger(input, errorMessage); + + // Then + assertThat(result).isEqualTo(8000); + } + + @DisplayName("정상4: 음수 λ¬Έμžμ—΄μ„ 음수 μ •μˆ˜λ‘œ νŒŒμ‹±ν•œλ‹€") + @Test + void test7() { + // Given + String input = "-1000"; + String errorMessage = "[ERROR] νŒŒμ‹± μ—λŸ¬"; + + // When + int result = ParserUtils.parseToInteger(input, errorMessage); + + // Then + assertThat(result).isEqualTo(-1000); + } + + @DisplayName("정상5: 0을 μ •μˆ˜λ‘œ νŒŒμ‹±ν•œλ‹€") + @Test + void test8() { + // Given + String input = "0"; + String errorMessage = "[ERROR] νŒŒμ‹± μ—λŸ¬"; + + // When + int result = ParserUtils.parseToInteger(input, errorMessage); + + // Then + assertThat(result).isEqualTo(0); + } + + @DisplayName("μ˜ˆμ™Έ4: μˆ«μžκ°€ μ•„λ‹Œ λ¬Έμžκ°€ ν¬ν•¨λ˜λ©΄ NumberFormatException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: \"{0}\"") + @ValueSource(strings = {"abc", "1000원", "1,000", "1.5"}) + void test9(String input) { + // Given + String errorMessage = "[ERROR] 숫자 ν˜•μ‹ μ—λŸ¬"; + + // When & Then + assertThatThrownBy(() -> ParserUtils.parseToInteger(input, errorMessage)) + .isInstanceOf(NumberFormatException.class) + .hasMessage(errorMessage); + } + + @DisplayName("μ˜ˆμ™Έ5: 빈 λ¬Έμžμ—΄μ€ NumberFormatException λ°œμƒ") + @Test + void test10() { + // Given + String input = ""; + String errorMessage = "[ERROR] 빈 λ¬Έμžμ—΄ νŒŒμ‹± μ—λŸ¬"; + + // When & Then + assertThatThrownBy(() -> ParserUtils.parseToInteger(input, errorMessage)) + .isInstanceOf(NumberFormatException.class) + .hasMessage(errorMessage); + } + + @DisplayName("μ˜ˆμ™Έ6: Integer λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ NumberFormatException λ°œμƒ") + @Test + void test11() { + // Given + String input = "9999999999999999999"; // Integer.MAX_VALUEλ₯Ό 초과 + String errorMessage = "[ERROR] λ²”μœ„ 초과 μ—λŸ¬"; + + // When & Then + assertThatThrownBy(() -> ParserUtils.parseToInteger(input, errorMessage)) + .isInstanceOf(NumberFormatException.class) + .hasMessage(errorMessage); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/parser/PurchaseAmountParserTest.java b/src/test/java/lotto/parser/PurchaseAmountParserTest.java new file mode 100644 index 0000000000..0400779ec1 --- /dev/null +++ b/src/test/java/lotto/parser/PurchaseAmountParserTest.java @@ -0,0 +1,66 @@ +package lotto.parser; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PurchaseAmountParserTest { + + @DisplayName("정상1: μœ νš¨ν•œ λ¬Έμžμ—΄ κΈˆμ•‘μ„ μ •μˆ˜λ‘œ νŒŒμ‹±ν•œλ‹€") + @Test + void test1() { + // Given + String input = "8000"; + + // When + int result = PurchaseAmountParser.parse(input); + + // Then + assertThat(result).isEqualTo(8000); + } + + @DisplayName("정상2: μ•žλ’€ 곡백이 ν¬ν•¨λœ λ¬Έμžμ—΄λ„ μ •μˆ˜λ‘œ νŒŒμ‹±ν•œλ‹€") + @Test + void test2() { + // Given + String input = " 8000 "; + + // When + int result = PurchaseAmountParser.parse(input); + + // Then + assertThat(result).isEqualTo(8000); + } + + @DisplayName("μ˜ˆμ™Έ1: μž…λ ₯이 null이면 IllegalArgumentException λ°œμƒ") + @Test + void test3() { + String input = null; + + assertThatThrownBy(() -> PurchaseAmountParser.parse(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] κ΅¬μž… κΈˆμ•‘μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + + @DisplayName("μ˜ˆμ™Έ2: μž…λ ₯이 빈 λ¬Έμžμ—΄ λ˜λŠ” 곡백이면 IllegalArgumentException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: \"{0}\"") + @ValueSource(strings = {"", " "}) + void test4(String input) { + assertThatThrownBy(() -> PurchaseAmountParser.parse(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] κ΅¬μž… κΈˆμ•‘μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + + @DisplayName("μ˜ˆμ™Έ3: μˆ«μžκ°€ μ•„λ‹Œ λ¬Έμžκ°€ ν¬ν•¨λ˜λ©΄ NumberFormatException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: \"{0}\"") + @ValueSource(strings = {"1000원", "a1000", "1,000"}) + void test5(String input) { + assertThatThrownBy(() -> PurchaseAmountParser.parse(input)) + .isInstanceOf(NumberFormatException.class) + .hasMessage("[ERROR] κ΅¬μž… κΈˆμ•‘μ€ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/parser/WinningNumbersParserTest.java b/src/test/java/lotto/parser/WinningNumbersParserTest.java new file mode 100644 index 0000000000..aee508835b --- /dev/null +++ b/src/test/java/lotto/parser/WinningNumbersParserTest.java @@ -0,0 +1,88 @@ +package lotto.parser; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class WinningNumbersParserTest { + + @DisplayName("정상1: μ‰Όν‘œλ‘œ κ΅¬λΆ„λœ λ¬Έμžμ—΄μ„ μ •μˆ˜ 리슀트둜 νŒŒμ‹±ν•œλ‹€") + @Test + void test1() { + // Given + String input = "1,2,3,4,5,6"; + + // When + List result = WinningNumbersParser.parse(input); + + // Then + assertThat(result).containsExactly(1, 2, 3, 4, 5, 6); + } + + @DisplayName("정상2: 곡백이 ν¬ν•¨λœ λ¬Έμžμ—΄λ„ μ •μˆ˜ 리슀트둜 νŒŒμ‹±ν•œλ‹€") + @Test + void test2() { + // Given + String input = " 1 , 2 , 3 , 4 , 5 , 6 "; + + // When + List result = WinningNumbersParser.parse(input); + + // Then + assertThat(result).containsExactly(1, 2, 3, 4, 5, 6); + } + + @DisplayName("μ˜ˆμ™Έ1: μž…λ ₯이 null이면 IllegalArgumentException λ°œμƒ") + @Test + void test3() { + String input = null; + + assertThatThrownBy(() -> WinningNumbersParser.parse(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 당첨 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + + @DisplayName("μ˜ˆμ™Έ2: μž…λ ₯이 빈 λ¬Έμžμ—΄ λ˜λŠ” 곡백이면 IllegalArgumentException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: \"{0}\"") + @ValueSource(strings = {"", " ", " "}) + void test4(String input) { + assertThatThrownBy(() -> WinningNumbersParser.parse(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 당첨 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + + @DisplayName("μ˜ˆμ™Έ3: μˆ«μžκ°€ μ•„λ‹Œ 값이 ν¬ν•¨λ˜λ©΄ NumberFormatException λ°œμƒ") + @Test + void test5() { + String input = "1,2,a,4,5,6"; + + assertThatThrownBy(() -> WinningNumbersParser.parse(input)) + .isInstanceOf(NumberFormatException.class) + .hasMessage("[ERROR] 당첨 λ²ˆν˜ΈλŠ” μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ4: μ‰Όν‘œλ‘œ κ΅¬λΆ„λœ ν•­λͺ©μ΄ 빈 값이면 IllegalArgumentException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: \"{0}\"") + @ValueSource(strings = {"1,,3,4,5,6", "1,2, ,4,5,6"}) + void test6(String input) { + assertThatThrownBy(() -> WinningNumbersParser.parse(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 당첨 λ²ˆν˜ΈλŠ” 빈 값일 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ5: λ¬Έμžμ—΄μ΄ μ‰Όν‘œλ‘œ λλ‚˜λŠ” 경우 IllegalArgumentException λ°œμƒ") + @Test + void test7() { + String input = "1,2,3,4,5,6,"; + + assertThatThrownBy(() -> WinningNumbersParser.parse(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 당첨 λ²ˆν˜ΈλŠ” 빈 값일 수 μ—†μŠ΅λ‹ˆλ‹€."); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/service/LottoGameServiceWithMockitoTest.java b/src/test/java/lotto/service/LottoGameServiceWithMockitoTest.java new file mode 100644 index 0000000000..f31393f399 --- /dev/null +++ b/src/test/java/lotto/service/LottoGameServiceWithMockitoTest.java @@ -0,0 +1,106 @@ +// package lotto.service; +// +// import lotto.view.InputView; +// import lotto.view.OutputView; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// +// import static org.assertj.core.api.Assertions.assertThat; +// import static org.mockito.Mockito.*; +// +// class LottoGameServiceWithMockitoTest { +// +// @DisplayName("정상1: κ²Œμž„μ΄ μ •μƒμ μœΌλ‘œ μ‹€ν–‰λœλ‹€") +// @Test +// void test1() { +// // Given +// InputView inputView = mock(InputView.class); +// OutputView outputView = mock(OutputView.class); +// +// when(inputView.inputPurchaseAmount()).thenReturn("1000"); +// when(inputView.inputWinningNumbers()).thenReturn("1,2,3,4,5,6"); +// when(inputView.inputBonusNumber()).thenReturn("7"); +// +// LottoGameService service = new LottoGameService(inputView, outputView); +// +// // When +// service.run(); +// +// // Then +// verify(inputView, times(1)).inputPurchaseAmount(); +// verify(inputView, times(1)).inputWinningNumbers(); +// verify(inputView, times(1)).inputBonusNumber(); +// verify(outputView, times(1)).printLottoTickets(any()); +// verify(outputView, times(1)).printWinningStatistics(any()); +// verify(outputView, times(1)).printProfitRate(any()); +// } +// +// @DisplayName("μ˜ˆμ™Έ1: 잘λͺ»λœ ꡬ맀 κΈˆμ•‘ μž…λ ₯ μ‹œ μž¬μž…λ ₯을 μš”μ²­ν•œλ‹€") +// @Test +// void test2() { +// // Given +// InputView inputView = mock(InputView.class); +// OutputView outputView = mock(OutputView.class); +// +// when(inputView.inputPurchaseAmount()) +// .thenReturn("1500") // 첫 번째: 잘λͺ»λœ μž…λ ₯ +// .thenReturn("1000"); // 두 번째: μ˜¬λ°”λ₯Έ μž…λ ₯ +// when(inputView.inputWinningNumbers()).thenReturn("1,2,3,4,5,6"); +// when(inputView.inputBonusNumber()).thenReturn("7"); +// +// LottoGameService service = new LottoGameService(inputView, outputView); +// +// // When +// service.run(); +// +// // Then +// verify(inputView, times(2)).inputPurchaseAmount(); // 2번 호좜 +// verify(outputView, times(1)).printErrorMessage(any()); // μ—λŸ¬ λ©”μ‹œμ§€ 1번 좜λ ₯ +// } +// +// @DisplayName("μ˜ˆμ™Έ2: 잘λͺ»λœ 당첨 번호 μž…λ ₯ μ‹œ μž¬μž…λ ₯을 μš”μ²­ν•œλ‹€") +// @Test +// void test3() { +// // Given +// InputView inputView = mock(InputView.class); +// OutputView outputView = mock(OutputView.class); +// +// when(inputView.inputPurchaseAmount()).thenReturn("1000"); +// when(inputView.inputWinningNumbers()) +// .thenReturn("1,2,3,4,5") // 첫 번째: 5개 μž…λ ₯ (였λ₯˜) +// .thenReturn("1,2,3,4,5,6"); // 두 번째: μ˜¬λ°”λ₯Έ μž…λ ₯ +// when(inputView.inputBonusNumber()).thenReturn("7"); +// +// LottoGameService service = new LottoGameService(inputView, outputView); +// +// // When +// service.run(); +// +// // Then +// verify(inputView, times(2)).inputWinningNumbers(); // 2번 호좜 +// verify(outputView, times(1)).printErrorMessage(any()); // μ—λŸ¬ λ©”μ‹œμ§€ 1번 좜λ ₯ +// } +// +// @DisplayName("μ˜ˆμ™Έ3: 잘λͺ»λœ λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ μ‹œ μž¬μž…λ ₯을 μš”μ²­ν•œλ‹€") +// @Test +// void test4() { +// // Given +// InputView inputView = mock(InputView.class); +// OutputView outputView = mock(OutputView.class); +// +// when(inputView.inputPurchaseAmount()).thenReturn("1000"); +// when(inputView.inputWinningNumbers()).thenReturn("1,2,3,4,5,6"); +// when(inputView.inputBonusNumber()) +// .thenReturn("6") // 첫 번째: 쀑볡 (였λ₯˜) +// .thenReturn("7"); // 두 번째: μ˜¬λ°”λ₯Έ μž…λ ₯ +// +// LottoGameService service = new LottoGameService(inputView, outputView); +// +// // When +// service.run(); +// +// // Then +// verify(inputView, times(2)).inputBonusNumber(); // 2번 호좜 +// verify(outputView, times(1)).printErrorMessage(any()); // μ—λŸ¬ λ©”μ‹œμ§€ 1번 좜λ ₯ +// } +// } \ No newline at end of file diff --git a/src/test/java/lotto/util/LottoGeneratorTest.java b/src/test/java/lotto/util/LottoGeneratorTest.java new file mode 100644 index 0000000000..b7ce1dc7fc --- /dev/null +++ b/src/test/java/lotto/util/LottoGeneratorTest.java @@ -0,0 +1,74 @@ +package lotto.util; + +import lotto.domain.Lotto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static lotto.common.LottoConstants.*; +import static org.assertj.core.api.Assertions.assertThat; + +class LottoGeneratorTest { + + @DisplayName("정상1: κ΅¬μž… κΈˆμ•‘μ— 따라 μ •ν™•ν•œ 개수의 둜또λ₯Ό μƒμ„±ν•œλ‹€") + @ParameterizedTest(name = "κ΅¬μž… κΈˆμ•‘: {0}원") + @ValueSource(ints = {1000, 5000, 10000}) + void test1(int purchaseAmount) { + // Given & When + List lottos = LottoGenerator.generateLottos(purchaseAmount); + int expectedCount = purchaseAmount / LOTTO_PRICE; + + // Then + assertThat(lottos).hasSize(expectedCount); + } + + @DisplayName("정상2: μƒμ„±λœ 각 λ‘œλ˜κ°€ μœ νš¨ν•œ 번호 6개λ₯Ό ν¬ν•¨ν•˜κ³  μžˆλŠ”μ§€ κ²€μ¦ν•œλ‹€") + @Test + void test2() { + // Given + int purchaseAmount = 3000; + + // When + List lottos = LottoGenerator.generateLottos(purchaseAmount); + + // Then + for (Lotto lotto : lottos) { + List numbers = lotto.getNumbers(); + + // 1. 번호 κ°œμˆ˜λŠ” 6κ°œμ—¬μ•Ό ν•œλ‹€. + assertThat(numbers).hasSize(LOTTO_NUMBER_COUNT); + + // 2. 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•œλ‹€. + assertThat(numbers).allMatch(number -> number >= MIN_LOTTO_NUMBER && number <= MAX_LOTTO_NUMBER); + + // 3. μ€‘λ³΅λœ μˆ«μžκ°€ μ—†μ–΄μ•Ό ν•œλ‹€. + Set uniqueNumbers = new HashSet<>(numbers); + assertThat(uniqueNumbers).hasSize(LOTTO_NUMBER_COUNT); + + // 4. μ˜€λ¦„μ°¨μˆœμœΌλ‘œ μ •λ ¬λ˜μ–΄ μžˆμ–΄μ•Ό ν•œλ‹€. + assertThat(numbers).isSorted(); + } + } + + @DisplayName("정상3: μ—¬λŸ¬ 둜또 생성 μ‹œ 각 λ‘œλ˜κ°€ μ„œλ‘œ λ‹€λ₯Έ 번호 쑰합을 κ°€μ§ˆ κ°€λŠ₯성이 μžˆλ‹€") + @Test + void test3() { + // Given: 100개 둜또 생성 + int purchaseAmount = 100_000; + + // When + List lottos = LottoGenerator.generateLottos(purchaseAmount); + + // Then + // 100개 λ‘œλ˜κ°€ λͺ¨λ‘ 같을 ν™•λ₯ μ€ 거의 0μ΄λ―€λ‘œ, μ΅œμ†Œ 2κ°œλŠ” λ‹€λ₯Ό κ²ƒμœΌλ‘œ κΈ°λŒ€ + Set> uniqueLottos = new HashSet<>(); + lottos.forEach(lotto -> uniqueLottos.add(lotto.getNumbers())); + + assertThat(uniqueLottos.size()).isGreaterThan(1); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/validator/InputValidatorTest.java b/src/test/java/lotto/validator/InputValidatorTest.java new file mode 100644 index 0000000000..c95ee4cfa2 --- /dev/null +++ b/src/test/java/lotto/validator/InputValidatorTest.java @@ -0,0 +1,137 @@ +package lotto.validator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static lotto.validator.InputValidator.validatePurchaseAmount; +import static lotto.validator.InputValidator.validateWinningNumbers; +import static lotto.validator.InputValidator.validateBonusNumber; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import lotto.domain.Lotto; + +class InputValidatorTest { + + @DisplayName("정상1: 1000원 λ‹¨μœ„μ˜ κΈˆμ•‘μ€ 검증을 ν†΅κ³Όν•œλ‹€") + @ParameterizedTest(name = "μž…λ ₯: {0}") + @ValueSource(ints = {1000, 8000, 10000}) + void test1(int amount) { + assertThatNoException().isThrownBy(() -> validatePurchaseAmount(amount)); + } + + @DisplayName("정상2: μœ νš¨ν•œ 6개의 당첨 λ²ˆν˜ΈλŠ” 검증을 ν†΅κ³Όν•œλ‹€") + @Test + void test2() { + List validNumbers = List.of(1, 2, 3, 4, 5, 6); + assertThatNoException().isThrownBy(() -> validateWinningNumbers(validNumbers)); + } + + @DisplayName("정상3: λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 1~45 λ²”μœ„ 내에 있고 당첨 λ²ˆν˜Έμ™€ μ€‘λ³΅λ˜μ§€ μ•ŠμœΌλ©΄ 검증을 ν†΅κ³Όν•œλ‹€") + @Test + void test3() { + Lotto validWinningLottoNumbers = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + int bonusNumber = 7; + + assertThatNoException().isThrownBy(() -> validateBonusNumber(validWinningLottoNumbers, bonusNumber)); + } + + @DisplayName("정상4: λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 경계값 1κ³Ό 45도 검증을 ν†΅κ³Όν•œλ‹€ (단, 쀑볡이 없을 λ•Œ)") + @Test + void test4() { + Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + + // 6이 당첨 λ²ˆν˜Έμ— ν¬ν•¨λ˜μ–΄ μžˆμœΌλ―€λ‘œ, λ³΄λ„ˆμŠ€ 번호 45λŠ” 톡과 + assertThatNoException().isThrownBy(() -> validateBonusNumber(lotto, 45)); + } + + @DisplayName("μ˜ˆμ™Έ1: 음수λ₯Ό μž…λ ₯ν•˜λ©΄ IllegalArgumentException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: {0}") + @ValueSource(ints = {-1, -1000, 0}) + void test5(int amount) { + assertThatThrownBy(() -> validatePurchaseAmount(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] κ΅¬μž… κΈˆμ•‘μ€ 0보닀 컀야 ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ3: 1,000원 미만(1~999)이면 IllegalArgumentException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: {0}") + @ValueSource(ints = {1, 500, 999}) + void test6(int amount) { + assertThatThrownBy(() -> validatePurchaseAmount(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] κ΅¬μž… κΈˆμ•‘μ€ 1,000원 이상이어야 ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ4: 1,000μ›μœΌλ‘œ λ‚˜λˆ„μ–΄λ–¨μ–΄μ§€μ§€ μ•ŠμœΌλ©΄ IllegalArgumentException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: {0}") + @ValueSource(ints = {1001, 1500, 8999}) + void test7(int amount) { + assertThatThrownBy(() -> validatePurchaseAmount(amount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] κ΅¬μž… κΈˆμ•‘μ€ 1,000원 λ‹¨μœ„μ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + + + @DisplayName("μ˜ˆμ™Έ5: 번호 λ¦¬μŠ€νŠΈκ°€ null이면 IllegalArgumentException λ°œμƒ") + @Test + void test8() { + assertThatThrownBy(() -> validateWinningNumbers(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 번호λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + + @DisplayName("μ˜ˆμ™Έ6: λ²ˆν˜Έκ°€ 6κ°œκ°€ μ•„λ‹ˆλ©΄ IllegalArgumentException λ°œμƒ") + @Test + void test9() { + List invalidSizeNumbers = List.of(1, 2, 3, 4, 5); + assertThatThrownBy(() -> validateWinningNumbers(invalidSizeNumbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 6κ°œμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ7: 1~45 λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ IllegalArgumentException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: {0}") + @ValueSource(ints = {0, 46}) + void test10(int invalidNumber) { + List outOfRangeNumbers = List.of(1, 2, 3, 4, 5, invalidNumber); + assertThatThrownBy(() -> validateWinningNumbers(outOfRangeNumbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ8 μ€‘λ³΅λœ λ²ˆν˜Έκ°€ 있으면 IllegalArgumentException λ°œμƒ") + @Test + void test11() { + List duplicateNumbers = List.of(1, 2, 3, 4, 5, 5); + assertThatThrownBy(() -> validateWinningNumbers(duplicateNumbers)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] 둜또 λ²ˆν˜ΈλŠ” 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ12: λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 1~45 λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ IllegalArgumentException λ°œμƒ") + @ParameterizedTest(name = "μž…λ ₯: {0}") + @ValueSource(ints = {0, 46}) + void test12(int invalidNumber) { + Lotto validWinningLottoNumbers = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + + assertThatThrownBy(() -> validateBonusNumber(validWinningLottoNumbers, invalidNumber)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 μ‚¬μ΄μ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ13: λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ 당첨 λ²ˆν˜Έμ™€ μ€‘λ³΅λ˜λ©΄ IllegalArgumentException λ°œμƒ") + @Test + void test13() { + Lotto validWinningLottoNumbers = new Lotto(List.of(1, 2, 3, 4, 5, 6)); + int duplicateNumber = 6; + + assertThatThrownBy(() -> validateBonusNumber(validWinningLottoNumbers, duplicateNumber)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("[ERROR] λ³΄λ„ˆμŠ€ λ²ˆν˜ΈλŠ” 당첨 λ²ˆν˜Έμ™€ 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€."); + } +} \ No newline at end of file diff --git a/src/test/java/lotto/view/InputViewTest.java b/src/test/java/lotto/view/InputViewTest.java new file mode 100644 index 0000000000..bd6f1d954b --- /dev/null +++ b/src/test/java/lotto/view/InputViewTest.java @@ -0,0 +1,4 @@ +package lotto.view; + +public class InputViewTest { +} diff --git a/src/test/java/lotto/view/OutputViewTest.java b/src/test/java/lotto/view/OutputViewTest.java new file mode 100644 index 0000000000..f069c57f58 --- /dev/null +++ b/src/test/java/lotto/view/OutputViewTest.java @@ -0,0 +1,102 @@ +package lotto.view; + +import lotto.domain.Lotto; +import lotto.domain.LottoResult; +import lotto.domain.Rank; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class OutputViewTest { + + private OutputView outputView; + private ByteArrayOutputStream outputStream; + private PrintStream originalOut; + + @BeforeEach + void setUp() { + outputView = new OutputView(); + outputStream = new ByteArrayOutputStream(); + originalOut = System.out; + System.setOut(new PrintStream(outputStream)); + } + + @AfterEach + void tearDown() { + System.setOut(originalOut); + } + + @DisplayName("정상1: κ΅¬λ§€ν•œ 둜또 λͺ©λ‘μ„ 좜λ ₯ν•œλ‹€") + @Test + void test1() { + // Given + List lottos = List.of( + new Lotto(List.of(1, 2, 3, 4, 5, 6)), + new Lotto(List.of(7, 8, 9, 10, 11, 12)) + ); + + // When + outputView.printLottoTickets(lottos); + + // Then + String output = outputStream.toString(); + assertThat(output).contains("2개λ₯Ό κ΅¬λ§€ν–ˆμŠ΅λ‹ˆλ‹€."); + assertThat(output).contains("[1, 2, 3, 4, 5, 6]"); + assertThat(output).contains("[7, 8, 9, 10, 11, 12]"); + } + + @DisplayName("정상2: 당첨 톡계λ₯Ό 좜λ ₯ν•œλ‹€") + @Test + void test2() { + // Given + List ranks = List.of(Rank.FIFTH, Rank.THIRD); + LottoResult lottoResult = new LottoResult(ranks, 8000); + + // When + outputView.printWinningStatistics(lottoResult); + + // Then + String output = outputStream.toString(); + assertThat(output).contains("당첨 톡계"); + assertThat(output).contains("---"); + assertThat(output).contains("3개 일치"); + assertThat(output).contains("5개 일치"); + } + + @DisplayName("정상3: 수읡λ₯ μ„ 좜λ ₯ν•œλ‹€") + @Test + void test3() { + // Given + List ranks = List.of(Rank.FIFTH); + LottoResult lottoResult = new LottoResult(ranks, 8000); + + // When + outputView.printProfitRate(lottoResult); + + // Then + String output = outputStream.toString(); + assertThat(output).contains("총 수읡λ₯ μ€"); + assertThat(output).contains("%μž…λ‹ˆλ‹€."); + } + + @DisplayName("μ˜ˆμ™Έ1: μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό 좜λ ₯ν•œλ‹€") + @Test + void test4() { + // Given + String errorMessage = "[ERROR] ν…ŒμŠ€νŠΈ μ—λŸ¬"; + + // When + outputView.printErrorMessage(errorMessage); + + // Then + String output = outputStream.toString(); + assertThat(output).contains("[ERROR] ν…ŒμŠ€νŠΈ μ—λŸ¬"); + } +} \ No newline at end of file