diff --git a/.github/workflows/check-no-external-libs.yml b/.github/workflows/check-no-external-libs.yml index fa0ceb9..e5a1d3e 100644 --- a/.github/workflows/check-no-external-libs.yml +++ b/.github/workflows/check-no-external-libs.yml @@ -43,4 +43,4 @@ jobs: for root, _, files in os.walk('src'): for file in files: if file.endswith('.py'): - check_imports(os.path.join(root, file))" + check_imports(os.path.join(root, file))" \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index e69de29..f37b3c3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,67 @@ +# 🎰 Lotto ν”„λ‘œκ·Έλž¨ + +## πŸ“œ **Lotto 클래슀 κΈ°λŠ₯ 정리** + +### 🎲 **1. 둜또 번호 처리** +βœ… `__init__(self, numbers: List[int])` + - 둜또 번호λ₯Ό μž…λ ₯λ°›μ•„ μ €μž₯ν•˜κ³  κ²€μ¦ν•©λ‹ˆλ‹€. + +βœ… `_validate(numbers: List[int])` + - 둜또 λ²ˆν˜Έκ°€ **6κ°œμΈμ§€, 쀑볡이 μ—†λŠ”μ§€, 1~45 λ²”μœ„ 내에 μžˆλŠ”μ§€** κ²€μ¦ν•©λ‹ˆλ‹€. + +--- + +### πŸ’° **2. 둜또 κ΅¬μž… κ΄€λ ¨** +βœ… `get_lotto_count()` + - μ‚¬μš©μžμ—κ²Œ **둜또 κ΅¬μž… κΈˆμ•‘μ„ μž…λ ₯λ°›κ³ ** κ΅¬μž…ν•  수 μžˆλŠ” 개수λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€. + - **1,000원 λ‹¨μœ„λ‘œλ§Œ κ΅¬μž… κ°€λŠ₯**ν•©λ‹ˆλ‹€. + +βœ… `_validate_amount(amount)` + - μž…λ ₯된 κΈˆμ•‘μ΄ **μˆ«μžμΈμ§€, 1,000원 이상인지, 1,000원 λ‹¨μœ„μΈμ§€** ν™•μΈν•©λ‹ˆλ‹€. + +--- + +### 🎟️ **3. 둜또 번호 생성** +βœ… `generate_random_lotto()` + - **1~45 μ‚¬μ΄μ˜ λžœλ€ν•œ 6개의 숫자**λ₯Ό μƒμ„±ν•˜μ—¬ 둜또 번호λ₯Ό λ§Œλ“­λ‹ˆλ‹€. + +βœ… `generate_lottos(count: int)` + - **μ‚¬μš©μžκ°€ κ΅¬μž…ν•œ 개수만큼** 둜또 번호λ₯Ό μžλ™ μƒμ„±ν•©λ‹ˆλ‹€. + +--- + +### 🎯 **4. 당첨 번호 μž…λ ₯** +βœ… `get_winning_numbers()` + - **μ‚¬μš©μžλ‘œλΆ€ν„° 당첨 λ²ˆν˜Έμ™€ λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯λ°›μŠ΅λ‹ˆλ‹€.** + - μž…λ ₯된 λ²ˆν˜Έκ°€ **μœ νš¨ν•œμ§€ κ²€μ¦ν•œ ν›„ λ°˜ν™˜ν•©λ‹ˆλ‹€.** + +βœ… `_get_valid_numbers(prompt, count)` + - **μ‚¬μš©μžκ°€ μž…λ ₯ν•œ 당첨 λ²ˆν˜Έκ°€ 6κ°œμΈμ§€, μœ νš¨ν•œ λ²”μœ„(1~45)인지 확인**ν•©λ‹ˆλ‹€. + +βœ… `_validate_numbers(numbers, count)` + - μž…λ ₯된 μˆ«μžκ°€ **개수 쑰건(6개) 및 λ²”μœ„(1~45)λ₯Ό λ§Œμ‘±ν•˜λŠ”μ§€ 검증**ν•©λ‹ˆλ‹€. + +βœ… `_get_valid_bonus(prompt, numbers)` + - **λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯λ°›μ•„ μœ νš¨μ„± 검증**을 μˆ˜ν–‰ν•©λ‹ˆλ‹€. + +βœ… `_is_valid_bonus(bonus, numbers)` + - λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ **1~45 사이이며, 당첨 λ²ˆν˜Έμ™€ μ€‘λ³΅λ˜μ§€ μ•ŠλŠ”μ§€** ν™•μΈν•©λ‹ˆλ‹€. + +--- + +### πŸ† **5. 당첨 κ²°κ³Ό 비ꡐ** +βœ… `check_results(purchased_lottos, winning_numbers, bonus_number)` + - **κ΅¬μž…ν•œ 둜또 λ²ˆν˜Έμ™€ 당첨 번호λ₯Ό 비ꡐ**ν•˜μ—¬ 당첨 λ“±μˆ˜λ₯Ό κ²°μ •ν•©λ‹ˆλ‹€. + +βœ… `_count_matches(lotto_numbers, winning_numbers, bonus_number)` + - 각 둜또 λ²ˆν˜Έκ°€ **당첨 λ²ˆν˜Έμ™€ λͺ‡ 개 μΌμΉ˜ν•˜λŠ”μ§€, λ³΄λ„ˆμŠ€ λ²ˆν˜Έκ°€ ν¬ν•¨λ˜μ–΄ μžˆλŠ”μ§€** ν™•μΈν•©λ‹ˆλ‹€. + +βœ… `_determine_prize_key(matched_count, has_bonus)` + - 일치 개수 및 λ³΄λ„ˆμŠ€ 번호 μ—¬λΆ€λ₯Ό λ°”νƒ•μœΌλ‘œ **당첨 λ“±μˆ˜λ₯Ό νŒλ³„**ν•©λ‹ˆλ‹€. + +--- + +### πŸ“Š **6. 당첨 κ²°κ³Ό 좜λ ₯** +βœ… `print_results(results, total_cost)` + - **각 당첨 λ“±μˆ˜λ³„ 개수λ₯Ό 좜λ ₯**ν•©λ‹ˆλ‹€. + - 총 당첨 κΈˆμ•‘κ³Ό **수읡λ₯ (%)을 κ³„μ‚°ν•˜μ—¬ 좜λ ₯**ν•©λ‹ˆλ‹€. diff --git a/pytest.ini b/pytest.ini index 58a1536..8a79834 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,4 +2,3 @@ pythonpath = src markers = custom_name: ν…ŒμŠ€νŠΈ μ„€λͺ…을 μœ„ν•œ μ»€μŠ€ν…€ 마컀 - \ No newline at end of file diff --git a/src/lotto/__init__.py b/src/lotto/__init__.py index 769b83e..b1fb13c 100644 --- a/src/lotto/__init__.py +++ b/src/lotto/__init__.py @@ -1,19 +1,5 @@ -# src/lotto/__init__.py +"""둜또 νŒ¨ν‚€μ§€ μ΄ˆκΈ°ν™” 파일""" -# πŸ“Œ 이 νŒ¨ν‚€μ§€λŠ” 둜또 κ΄€λ ¨ κΈ°λŠ₯을 μ œκ³΅ν•˜λŠ” λͺ¨λ“ˆμž…λ‹ˆλ‹€. -# μ™ΈλΆ€μ—μ„œ `from lotto import Lotto`와 같은 λ°©μ‹μœΌλ‘œ μ‚¬μš©ν•  수 μžˆλ„λ‘ -# ν•„μš”ν•œ λͺ¨λ“ˆμ„ 여기에 λ“±λ‘ν•˜μ„Έμš”. -# -# βœ… μƒˆλ‘œμš΄ λͺ¨λ“ˆμ„ μΆ”κ°€ν•  경우: -# - `from .[λͺ¨λ“ˆλͺ…] import [클래슀/ν•¨μˆ˜]` ν˜•μ‹μœΌλ‘œ μΆ”κ°€ν•˜μ„Έμš”. -# - ν•„μš”ν•œ 경우 `__all__`에 μΆ”κ°€ν•˜μ—¬ νŒ¨ν‚€μ§€ μ™ΈλΆ€μ—μ„œ λͺ…ν™•ν•˜κ²Œ μ‚¬μš©ν•  수 μžˆλ„λ‘ μ •μ˜ν•˜μ„Έμš”. -# - `flake8`의 F401 κ²½κ³ (`imported but unused`)κ°€ λ°œμƒν•˜λŠ” 경우, `__all__`을 ν™œμš©ν•΄ ν•΄κ²°ν•˜μ„Έμš”. +from .lotto import Lotto -from .lotto import Lotto # 🎲 둜또 번호 생성 및 검증을 μœ„ν•œ 클래슀 - -# νŒ¨ν‚€μ§€ μ™ΈλΆ€μ—μ„œ `from lotto import *` μ‚¬μš© μ‹œ μ œκ³΅ν•  λͺ¨λ“ˆμ„ λͺ…μ‹œμ μœΌλ‘œ μ •μ˜ν•©λ‹ˆλ‹€. __all__ = ["Lotto"] - -# πŸ’‘ μ˜ˆμ‹œ: μƒˆλ‘œμš΄ λͺ¨λ“ˆμ„ μΆ”κ°€ν•  λ•Œ -# from .other_module import OtherClass # πŸ†• 예: μƒˆλ‘œμš΄ 클래슀 μΆ”κ°€ μ‹œ -# __all__.append("OtherClass") # `__all__`에 μΆ”κ°€ν•˜μ—¬ μ™ΈλΆ€μ—μ„œ μ ‘κ·Ό κ°€λŠ₯ν•˜κ²Œ 함. diff --git a/src/lotto/lotto.py b/src/lotto/lotto.py index 9c8c935..46f5160 100644 --- a/src/lotto/lotto.py +++ b/src/lotto/lotto.py @@ -1,12 +1,214 @@ -from typing import List +"""lotto.py: 둜또 번호 생성 및 당첨 κ²°κ³Όλ₯Ό μ²˜λ¦¬ν•˜λŠ” λͺ¨λ“ˆ""" + +import random +from enum import Enum + + +class Prize(Enum): + """당첨 λ“±μˆ˜λ₯Ό Enum으둜 μ •μ˜""" + THREE_MATCH = 3 + FOUR_MATCH = 4 + FIVE_MATCH = 5 + FIVE_MATCH_BONUS = "5_bonus" + SIX_MATCH = 6 + class Lotto: - def __init__(self, numbers: List[int]): + """둜또 번호 및 당첨 κ²°κ³Όλ₯Ό μ²˜λ¦¬ν•˜λŠ” 클래슀""" + + def __init__(self, numbers: list[int]): self._validate(numbers) self._numbers = numbers - def _validate(self, numbers: List[int]): + @property + def numbers(self): + """둜또 번호λ₯Ό λ°˜ν™˜ν•˜λŠ” getter λ©”μ„œλ“œ""" + return self._numbers + + def _validate(self, numbers: list[int]): + """둜또 번호 검증: 개수, 쀑볡, λ²”μœ„""" if len(numbers) != 6: - raise ValueError + raise ValueError("둜또 λ²ˆν˜ΈλŠ” μ •ν™•νžˆ 6κ°œμ—¬μ•Ό ν•©λ‹ˆλ‹€.") + if len(set(numbers)) != 6: + raise ValueError("둜또 λ²ˆν˜Έμ— 쀑볡이 μžˆμ–΄μ„œλŠ” μ•ˆ λ©λ‹ˆλ‹€.") + if not all(1 <= num <= 45 for num in numbers): + raise ValueError("둜또 λ²ˆν˜ΈλŠ” 1λΆ€ν„° 45 사이여야 ν•©λ‹ˆλ‹€.") + + @staticmethod + def get_lotto_count(): + """둜또 κ΅¬μž… κΈˆμ•‘ μž…λ ₯ 및 μ˜ˆμ™Έ 처리""" + try: + amount = input("둜또 κ΅¬μž… κΈˆμ•‘μ„ μž…λ ₯ν•˜μ„Έμš”: ") + Lotto._validate_amount(amount) + + amount = int(amount) + lotto_count = amount // 1000 + print(f"{lotto_count}개의 둜또λ₯Ό κ΅¬μž…ν•©λ‹ˆλ‹€.") + return lotto_count + + except ValueError as e: + print(e) + raise + + @staticmethod + def _validate_amount(amount): + """둜또 κΈˆμ•‘ 검증""" + if not amount.isdigit(): + raise ValueError("[ERROR] 숫자λ₯Ό μž…λ ₯ν•˜μ„Έμš”.") + + amount = int(amount) + if amount < 1000 or amount % 1000 != 0: + raise ValueError("[ERROR] μ΅œμ†Œ 1,000원 이상, 1,000원 λ‹¨μœ„λ‘œ μž…λ ₯ν•΄μ•Ό ν•©λ‹ˆλ‹€.") + + @staticmethod + def generate_random_lotto(): + """λ¬΄μž‘μœ„ 둜또 번호 생성""" + return Lotto(sorted(random.sample(range(1, 46), 6))) + + @staticmethod + def generate_lottos(count: int): + """κ΅¬μž…ν•œ 둜또 개수만큼 번호 생성""" + return [Lotto.generate_random_lotto() for _ in range(count)] + + @staticmethod + def get_winning_numbers(): + """당첨 번호 및 λ³΄λ„ˆμŠ€ 번호 μž…λ ₯""" + numbers = Lotto._get_valid_numbers("당첨 번호λ₯Ό μž…λ ₯ν•΄ μ£Όμ„Έμš”: ", 6) + bonus = Lotto._get_valid_bonus("λ³΄λ„ˆμŠ€ 번호 1개λ₯Ό μž…λ ₯ν•˜μ„Έμš”: ", numbers) + return numbers, bonus + + @staticmethod + def _get_valid_numbers(prompt, count): + """μœ νš¨ν•œ 번호 μž…λ ₯ λ°›κΈ°""" + while True: + try: + numbers = list( + map(int, input(prompt).replace(',', ' ').split()) + ) + Lotto._validate_numbers(numbers, count) + return numbers + except ValueError as e: + print(e) + + @staticmethod + def _validate_numbers(numbers, count): + """μž…λ ₯된 숫자 검증""" + if len(numbers) != count or len(set(numbers)) != count: + raise ValueError(f"[ERROR] {count}개의 숫자λ₯Ό μž…λ ₯ν•΄μ•Ό ν•˜λ©°, 쀑볡이 μ—†μ–΄μ•Ό ν•©λ‹ˆλ‹€.") + if not all(1 <= num <= 45 for num in numbers): + raise ValueError("[ERROR] μˆ«μžλŠ” 1λΆ€ν„° 45 사이여야 ν•©λ‹ˆλ‹€.") + + @staticmethod + def _get_valid_bonus(prompt, numbers): + """λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ λ°›κΈ°""" + while True: + bonus = Lotto._safe_input_bonus(prompt) + if Lotto._is_valid_bonus(bonus, numbers): + return bonus + print("[ERROR] μ˜¬λ°”λ₯Έ λ³΄λ„ˆμŠ€ 번호λ₯Ό μž…λ ₯ν•˜μ„Έμš”.") + + @staticmethod + def _safe_input_bonus(prompt): + """λ³΄λ„ˆμŠ€ 번호 μ•ˆμ „ μž…λ ₯""" + try: + return int(input(prompt)) + except ValueError: + return -1 # 잘λͺ»λœ κ°’ λ°˜ν™˜ (κ²€μ¦μ—μ„œ 걸러짐) + + @staticmethod + def _is_valid_bonus(bonus, numbers): + """λ³΄λ„ˆμŠ€ 번호 검증""" + if bonus in numbers or not 1 <= bonus <= 45: + return False + return True + + @staticmethod + def check_results(purchased_lottos, winning_numbers, bonus_number): + """κ΅¬μž…ν•œ 둜또 λ²ˆν˜Έμ™€ 당첨 번호 비ꡐ""" + results = { + Prize.THREE_MATCH: 0, + Prize.FOUR_MATCH: 0, + Prize.FIVE_MATCH: 0, + Prize.FIVE_MATCH_BONUS: 0, + Prize.SIX_MATCH: 0, + } + + for lotto in purchased_lottos: + matched_count, has_bonus = Lotto._count_matches( + lotto.numbers, winning_numbers, bonus_number + ) + + Lotto._update_results(results, matched_count, has_bonus) + + return results + + @staticmethod + def _update_results(results, matched_count, has_bonus): + """당첨 κ²°κ³Ό λ”•μ…”λ„ˆλ¦¬λ₯Ό μ—…λ°μ΄νŠΈ""" + key = Lotto._determine_prize_key(matched_count, has_bonus) + if key: + results[key] += 1 + + @staticmethod + def _count_matches(lotto_numbers, winning_numbers, bonus_number): + """둜또 λ²ˆν˜Έμ™€ 당첨 번호 λΉ„κ΅ν•˜μ—¬ 일치 κ°œμˆ˜μ™€ λ³΄λ„ˆμŠ€ μ—¬λΆ€ λ°˜ν™˜""" + matched_count = len(set(lotto_numbers) & set(winning_numbers)) + has_bonus = bonus_number in lotto_numbers + return matched_count, has_bonus + + @staticmethod + def _determine_prize_key(matched_count, has_bonus): + """당첨 λ“±μˆ˜ νŒλ³„""" + if matched_count == 5 and has_bonus: + return Prize.FIVE_MATCH_BONUS + if matched_count == 6: + return Prize.SIX_MATCH + if matched_count == 5: + return Prize.FIVE_MATCH + if matched_count == 4: + return Prize.FOUR_MATCH + if matched_count == 3: + return Prize.THREE_MATCH + return None + + @staticmethod + def print_results(results, total_cost): + """당첨 톡계λ₯Ό 좜λ ₯ν•˜κ³  수읡λ₯ μ„ κ³„μ‚°ν•˜μ—¬ ν‘œμ‹œ""" + print("\n당첨 톡계") + print("---") + Lotto.print_winning_statistics(results) + + total_prize = Lotto.calculate_total_prize(results) + profit_ratio = Lotto.calculate_profit_ratio(total_prize, total_cost) + + print(f"총 수읡λ₯ μ€ {profit_ratio:.1f}%μž…λ‹ˆλ‹€.") + + @staticmethod + def print_winning_statistics(results): + """당첨 개수λ₯Ό 좜λ ₯""" + print( + f"3개 일치 (5,000원) - {results[Prize.THREE_MATCH]}개\n" + f"4개 일치 (50,000원) - {results[Prize.FOUR_MATCH]}개\n" + f"5개 일치 (1,500,000원) - {results[Prize.FIVE_MATCH]}개\n" + "5개 일치, λ³΄λ„ˆμŠ€ λ³Ό 일치 (30,000,000원) - " + f"{results[Prize.FIVE_MATCH_BONUS]}개\n" + f"6개 일치 (2,000,000,000원) - {results[Prize.SIX_MATCH]}개" + ) + + @staticmethod + def calculate_total_prize(results): + """총 당첨 κΈˆμ•‘ 계산""" + return ( + results[Prize.THREE_MATCH] * 5000 + + results[Prize.FOUR_MATCH] * 50000 + + results[Prize.FIVE_MATCH] * 1500000 + + results[Prize.FIVE_MATCH_BONUS] * 30000000 + + results[Prize.SIX_MATCH] * 2000000000 + ) - # TODO: μΆ”κ°€ κΈ°λŠ₯ κ΅¬ν˜„ + @staticmethod + def calculate_profit_ratio(total_prize, total_cost): + """수읡λ₯  계산""" + if total_cost > 0: + return (total_prize / total_cost) * 100 + return 0 diff --git a/src/lotto/main.py b/src/lotto/main.py index 5f270aa..5742256 100644 --- a/src/lotto/main.py +++ b/src/lotto/main.py @@ -1,6 +1,35 @@ +"""main.py: 둜또 ν”„λ‘œκ·Έλž¨μ˜ 싀행을 λ‹΄λ‹Ήν•˜λŠ” 메인 슀크립트""" + +from lotto import Lotto + + def main(): - # TODO: ν”„λ‘œκ·Έλž¨ κ΅¬ν˜„ - pass + """둜또 ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ ν•¨μˆ˜""" + # 1. 둜또 κ΅¬μž… 개수 μž…λ ₯ + lotto_count = Lotto.get_lotto_count() + + # 2. 둜또 번호 생성 + purchased_lottos = Lotto.generate_lottos(lotto_count) + + # πŸ“Œ μš”κ΅¬λœ ν˜•μ‹μœΌλ‘œ 좜λ ₯ + print(f"\n{lotto_count}개λ₯Ό κ΅¬λ§€ν–ˆμŠ΅λ‹ˆλ‹€.") + for lotto in purchased_lottos: + print(f"{lotto.numbers}") # βœ… 리슀트 ν˜•νƒœλ₯Ό κ·ΈλŒ€λ‘œ 좜λ ₯ + + # 3. 당첨 번호 및 λ³΄λ„ˆμŠ€ 번호 μž…λ ₯ + winning_numbers, bonus_number = Lotto.get_winning_numbers() + + # 4. 당첨 κ²°κ³Ό 확인 + results = Lotto.check_results( + purchased_lottos, + winning_numbers, + bonus_number + ) + + # 5. 당첨 톡계 및 수읡λ₯  좜λ ₯ + total_cost = lotto_count * 1000 # 총 κ΅¬μž… κΈˆμ•‘ + Lotto.print_results(results, total_cost) + if __name__ == "__main__": main() diff --git a/tests/lotto/test_main.py b/tests/lotto/test_main.py index cb89654..14d1ff0 100644 --- a/tests/lotto/test_main.py +++ b/tests/lotto/test_main.py @@ -2,8 +2,10 @@ from unittest.mock import patch from lotto.main import main + ERROR_MESSAGE = "[ERROR]" + # κΈ°λŠ₯ ν…ŒμŠ€νŠΈ def test_κΈ°λŠ₯_ν…ŒμŠ€νŠΈ(capsys): # random.sample λͺ¨μ˜ 처리: 둜또 번호 리슀트λ₯Ό κ³ μ •λœ κ°’μœΌλ‘œ μ„€μ •