diff --git a/.github/workflows/check-no-external-libs.yml b/.github/workflows/check-no-external-libs.yml index fa0ceb9..6ba6d6e 100644 --- a/.github/workflows/check-no-external-libs.yml +++ b/.github/workflows/check-no-external-libs.yml @@ -20,8 +20,8 @@ jobs: import os import ast - allowed_modules = {'sys', 'os', 'math', 'random', 'datetime', 're', 'enum'} + allowed_modules = {'sys', 'os', 'math', 'random', 'datetime', 're', 'enum'} def is_internal_module(module_name): \"\"\" 내부 모듈(`src/` 폴더 내 Python 파일)인지 확인 \"\"\" module_path = os.path.join('src', module_name.replace('.', '/') + '.py') diff --git a/README.md b/README.md index 867338a..928ae25 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ > [!NOTE] > 이 코드는 원래 [java-lotto-6](https://github.com/woowacourse-precourse/java-lotto-6)에서 제공된 **Java 기반의 로또 게임**을 **Python**에 맞게 변환한 과제입니다. 프로젝트 구조, 요구 사항, 기능 구현 방식은 원본 저장소를 바탕으로 Python 환경에 맞추어 수정하였습니다. - --- ## 🔍 진행 방식 @@ -137,7 +136,6 @@ tests/lotto/test_lotto.py .. [100%] ``` 구입금액을 입력해 주세요. 8000 - 8개를 구매했습니다. [8, 21, 23, 41, 42, 43] [3, 5, 11, 16, 32, 38] @@ -147,13 +145,10 @@ tests/lotto/test_lotto.py .. [100%] [7, 11, 30, 40, 42, 43] [2, 13, 22, 32, 38, 45] [1, 3, 5, 14, 22, 45] - 당첨 번호를 입력해 주세요. 1,2,3,4,5,6 - 보너스 번호를 입력해 주세요. 7 - 당첨 통계 --- 3개 일치 (5,000원) - 1개 diff --git a/docs/README.md b/docs/README.md index e69de29..654e834 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,46 @@ +구현 기능 목록 + +입력 처리 + 구입 금액 입력 받기 + 1,000원 단위로 입력 확인 + 잘못된 입력 처리 (ValueError 발생 및 [ERROR] 메시지 출력) + + 당첨 번호 입력 받기 + 쉼표(,)로 구분된 6개의 숫자 입력 + 숫자는 1~45 범위 내에 있어야 함 + 중복되지 않도록 검사 + 잘못된 입력 처리 (ValueError 발생 및 [ERROR] 메시지 출력) + +보너스 번호 입력 받기 + 숫자는 1~45 범위 내에 있어야 함 + 당첨 번호와 중복되지 않도록 검사 + 잘못된 입력 처리 (ValueError 발생 및 [ERROR] 메시지 출력) + + +로또 발행 + 입력된 금액에 따라 로또 발행 + 1,000원당 1개씩 생성 + 각 로또는 1~45 범위의 숫자 6개 (중복 없이) 랜덤 생성 + 생성된 로또 번호는 오름차순 정렬 + 발행된 로또 번호 출력 + + 당첨 결과 계산 + 각 로또 번호와 당첨 번호 비교 + 일치하는 번호 개수 계산 + 보너스 번호 일치 여부 확인 + + 당첨 내역 계산 + 각 당첨 등수 개수 출력 + +수익률 계산 + 총 당첨 금액 계산 + 수익률 계산 ((총 당첨 금액 / 구입 금액) * 100) + 소수점 둘째 자리에서 반올림하여 출력 + +예외 처리 + 입력값이 숫자가 아닐 경우 예외 처리 + 구입 금액이 1,000원 단위가 아닐 경우 예외 처리 + 당첨 번호가 6개가 아닐 경우 예외 처리 + 당첨 번호가 1~45 범위를 벗어날 경우 예외 처리 + 보너스 번호가 1~45 범위를 벗어날 경우 예외 처리 + 보너스 번호가 당첨 번호와 중복될 경우 예외 처리 \ No newline at end of file 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..51e868d 100644 --- a/src/lotto/__init__.py +++ b/src/lotto/__init__.py @@ -1,19 +1,4 @@ -# src/lotto/__init__.py +from .lotto import Lotto, Rank -# 📌 이 패키지는 로또 관련 기능을 제공하는 모듈입니다. -# 외부에서 `from lotto import Lotto`와 같은 방식으로 사용할 수 있도록 -# 필요한 모듈을 여기에 등록하세요. -# -# ✅ 새로운 모듈을 추가할 경우: -# - `from .[모듈명] import [클래스/함수]` 형식으로 추가하세요. -# - 필요한 경우 `__all__`에 추가하여 패키지 외부에서 명확하게 사용할 수 있도록 정의하세요. -# - `flake8`의 F401 경고(`imported but unused`)가 발생하는 경우, `__all__`을 활용해 해결하세요. -from .lotto import Lotto # 🎲 로또 번호 생성 및 검증을 위한 클래스 - -# 패키지 외부에서 `from lotto import *` 사용 시 제공할 모듈을 명시적으로 정의합니다. -__all__ = ["Lotto"] - -# 💡 예시: 새로운 모듈을 추가할 때 -# from .other_module import OtherClass # 🆕 예: 새로운 클래스 추가 시 -# __all__.append("OtherClass") # `__all__`에 추가하여 외부에서 접근 가능하게 함. +__all__ = ["Lotto", "Rank"] diff --git a/src/lotto/lotto.py b/src/lotto/lotto.py index 9c8c935..0b3c02a 100644 --- a/src/lotto/lotto.py +++ b/src/lotto/lotto.py @@ -1,12 +1,116 @@ -from typing import List +import random +from enum import Enum + + +class Rank(Enum): + """ + 로또 당첨 순위를 정의하는 클래스. + + - FIFTH: 3개 일치 (5,000원) + - FOURTH: 4개 일치 (50,000원) + - THIRD: 5개 일치 (1,500,000원) + - SECOND: 5개 + 보너스 번호 일치 (30,000,000원) + - FIRST: 6개 일치 (2,000,000,000원) + - NONE: 0개 일치 (당첨 없음) + """ + FIFTH = (3, False, 5_000) + FOURTH = (4, False, 50_000) + THIRD = (5, False, 1_500_000) + SECOND = (5, True, 30_000_000) + FIRST = (6, False, 2_000_000_000) + NONE = (0, False, 0) + + def __init__(self, match_cnt, bonus_match, prize): + """ + Rank 객체 초기화. + + Args: + match_cnt (int): 일치하는 번호 개수 + bonus_match (bool): 보너스 번호 일치 여부 + prize (int): 당첨 금액 + """ + self.match_cnt = match_cnt + self.bonus_match = bonus_match + self.prize = prize + + @classmethod + def get_rank(cls, match_cnt, bonus): + """ + 일치 개수와 보너스 번호 여부를 기반으로 당첨 순위 반환. + + Args: + match_cnt (int): 일치하는 번호 개수 + bonus (bool): 보너스 번호 일치 여부 + + Returns: + Rank: 해당하는 당첨 순위 + """ + for rank in cls: + if rank.match_cnt == match_cnt and rank.bonus_match == bonus: + return rank + return cls.NONE + class Lotto: - def __init__(self, numbers: List[int]): + """ + 로또 번호 및 당첨 결과를 처리하는 클래스. + + - 1~45 사이의 서로 다른 6개의 숫자를 가짐. + - 로또 번호 검증 및 생성 기능 포함. + """ + ERROR_MESSAGE = "[ERROR] 구입 금액이 잘못되었습니다." + + def __init__(self, numbers: list[int]): + """ + Lotto 객체 초기화. + + Args: + numbers (list[int]): 1~45 사이의 6개 정수 리스트 + """ self._validate(numbers) - self._numbers = numbers + self._numbers = sorted(numbers) + + def _validate(self, numbers: list[int]): + """ + 로또 번호 검증: 개수, 중복, 범위 확인. - def _validate(self, numbers: List[int]): + Args: + numbers (list[int]): 1~45 사이의 6개 정수 리스트 + + Raises: + ValueError: 유효하지 않은 로또 번호일 경우 예외 발생 + """ 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 사이여야 합니다.") + + @classmethod + def generate_randomlotto(cls): + """ + 무작위 로또 번호 생성. + + Returns: + Lotto: 생성된 로또 객체 + """ + return cls(random.sample(range(1, 46), 6)) + + def get_numbers(self): + """ + 로또 번호 반환. + + Returns: + list[int]: 정렬된 로또 번호 리스트 + """ + return self._numbers + + def __str__(self): + """ + 문자열 변환. - # TODO: 추가 기능 구현 + Returns: + str: 로또 번호 리스트를 문자열로 반환 + """ + return str(self._numbers) diff --git a/src/lotto/main.py b/src/lotto/main.py index 5f270aa..e338a95 100644 --- a/src/lotto/main.py +++ b/src/lotto/main.py @@ -1,6 +1,122 @@ +from lotto import Rank, Lotto + + +def check_amount(input_amount): + """로또 구입 금액 검증""" + if not input_amount.isdigit(): + raise ValueError("[ERROR] 숫자를 입력해 주세요.") + + amount = int(input_amount) + if amount % 1000 != 0: + raise ValueError("구입 금액은 1,000원으로 나누어 떨어져야 합니다.") + if amount < 1000: + raise ValueError("구입 금액은 1,000원 이상이어야 합니다.") + + return amount // 1000 + + +def prompt_purchase_amount(): + """로또 구입 금액 입력""" + print("구입금액을 입력해 주세요.") + amount = input() + return check_amount(amount) + + +def print_lotto_tickets(tickets): + """구매한 로또 번호 출력""" + print(f"\n{len(tickets)}개를 구매했습니다.") + for ticket in tickets: + print(ticket) # ✅ `__str__()` 사용하여 출력 + + +def prompt_winning_numbers(): + """당첨 번호 입력""" + while True: + print("\n당첨 번호를 입력해 주세요.") + try: + winning_numbers = list(map(int, input().split(","))) + return Lotto(winning_numbers).get_numbers() + except ValueError as error: + print(f"[ERROR] {error}") + + +def check_bonus_number(bonus_num, winning_numbers): + """보너스 번호 검증""" + if not bonus_num.isdigit(): + raise ValueError("숫자를 입력해 주세요.") + + bonus_num = int(bonus_num) + if bonus_num in winning_numbers: + raise ValueError("보너스 숫자와 입력한 당첨 번호는 중복되지 않아야 합니다.") + if bonus_num > 45 or bonus_num < 1: + raise ValueError("로또 번호의 숫자 범위는 1~45까지입니다.") + + return bonus_num + + +def prompt_bonus_number(winning_numbers): + """보너스 번호 입력""" + while True: + try: + bonus_num = input("\n보너스 번호를 입력해 주세요.\n") + return check_bonus_number(bonus_num, winning_numbers) + except ValueError as error: + print(f"[ERROR] {error}") + + +def evaluate_tickets(tickets, winning_numbers, bonus_num): + """구입한 로또 번호와 당첨 번호 비교""" + results = {rank: 0 for rank in Rank} + total_prize = 0 + + for ticket in tickets: + ticket_numbers = ticket.get_numbers() + match_count = len(set(winning_numbers) & set(ticket_numbers)) + bonus = bonus_num in ticket_numbers + + rank = Rank.get_rank(match_count, bonus) + results[rank] += 1 + total_prize += rank.prize + + return results, total_prize + + +def print_results(results, total_prize, amount): + """당첨 결과 및 수익률 출력""" + profit_percentage = round((total_prize / (amount * 1000)) * 100, 2) + + print("\n당첨 통계") + print("---") + for rank in Rank: + if rank == Rank.SECOND: + print( + f"{rank.match_cnt}개 일치, 보너스 볼 일치 ({rank.prize:,}원) - " + f"{results[rank]}개" + ) + + if rank != Rank.NONE and rank != Rank.SECOND: + print( + f"{rank.match_cnt}개 일치 ({rank.prize:,}원) - " + f"{results[rank]}개" + ) + + print(f"총 수익률은 {profit_percentage}%입니다.") + + def main(): - # TODO: 프로그램 구현 - pass + """로또 게임 실행""" + amount = prompt_purchase_amount() + tickets = [Lotto.generate_randomlotto() for _ in range(amount)] + print_lotto_tickets(tickets) + + winning_numbers = prompt_winning_numbers() + bonus_num = prompt_bonus_number(winning_numbers) + + results, total_prize = evaluate_tickets( + tickets, winning_numbers, bonus_num + ) + print_results(results, total_prize, amount) + if __name__ == "__main__": main() diff --git a/tests/lotto/test_main.py b/tests/lotto/test_main.py index cb89654..7df2e0f 100644 --- a/tests/lotto/test_main.py +++ b/tests/lotto/test_main.py @@ -57,3 +57,5 @@ def test_예외_테스트(): # 잘못된 금액 입력 with patch("builtins.input", side_effect=["1000j"]): main() + +