diff --git a/docs/README.md b/docs/README.md index e69de29..be1d4a3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,75 @@ +## 기능 요구 사항 +- [X] 입력 기능 + - [X] 로또 구입 금액 입력 + - [X] 당첨 번호 입력. 번호는 쉽표(',')를 기준으로 구분 + - [X] 보너스 번호 입력 + +- [ ] 출력 기능 + - [X] 구입 금액 입력 요구 문구 "구입금액을 입력해 주세요." + - [X] 발행한 로또 수량 출력 + - [X] 발행한 로또 수량만큼 번호 출력. 번호는 오름차순으로 정렬하여 출력 + - [ ] 당첨 내역 출력 + - [ ] 수익률 출력, 수익률은 소수점 둘째 자리에서 반올림 + - [ ] 예외 상황 시 에러 문구 출력. 에러 문구는 "[ERROR]"로 시작 + +- [ ] 컴퓨터 기능 + - [X] 구입 금액에 해당하는 만큼 로또 발행 + - [X] 로또는 6자리이며, 범위는 1 ~ 45 사이 정수 + - [X] 사용자가 구매한 로또 번호와 당첨 번호 비교 + - [ ] 당첨 내역 및 수익률 계산 + +- [ ] 당첨 기준 및 금액 + - [ ] 1등: 6개 번호 일치 / 2,000,000,000원 + - [ ] 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 + - [ ] 3등: 5개 번호 일치 / 1,500,000원 + - [ ] 4등: 4개 번호 일치 / 50,000원 + - [ ] 5등: 3개 번호 일치 / 5,000원 + +- [X] 예외 처리 기능 + - [X] 사용자가 잘못된 값을 입력할 경우 ValueError를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받음 + - [X] 로또 구입 금액 입력 예외 사항 + - [X] int형이 아닌 경우 + - [X] 양의 정수가 아닌 경우 + - [X] 1,000원 단위가 아닌 경우 + - [X] 당첨 번호 입력 예외 사항 + - [X] 정수가 아닌 것이 포함된 경우 + - [X] 당첨 번호 길이가 6자리가 아닌 경우 + - [X] 번호 범위가 1 ~ 45를 벗어날 경우 + - [X] 당첨 번호가 중복될 경우 + - [X] 보너스 번호 입력 예외사항 + - [X] 정수가 아닌 경우 + - [X] 번호 범위가 1 ~ 45를 벗어날 경우 + - [X] 당첨 번호와 중복될 경우 + +## 기능 추가 +1. is_number() +> 입력 값이 정수인지 검증하는 함수 +2. validate_input_prchase_amount() +> 구매 금액 입력에 대한 검증 함수 +3. input_purchase_amount() +> 구매 금액을 입력받는 함수 +4. generate_lotto_quantity() +> 구매 가능한 로또 개수 계산 함수 +5. Lotto.issuance_lotto() +> 로또 발행 함수 +6. convert_to_list() +> 입력 값을 list 형식으로 변환하는 함수 +7. input_winning_numbers() +> 당첨 번호를 입력받는 함수 +8. Lotto._validate() +> 당첨 번호를 검증하는 함수 +9. input_bonus_number() +> 보너스 번호를 입력받는 함수 +10. Lotto.validate_bonus_number() +> 보너스 번호를 검증하는 함수 +11. Lotto.compare_winning_number() +> 당첨 번호와 발행 번호를 비교하는 함수 +12. Lotto.compare_bonus_number() +> 보너스 번호와 발행 번호를 비교하는 함수 +13. Lotto.calculate_result() +> 발행한 로또를 순서대로 당첨 번호, 보너스 번호 비교 함수를 실행하는 함수 + + +## 오류 수정 + +## 기능 수정 \ No newline at end of file diff --git a/src/lotto/__init__.py b/src/lotto/__init__.py index 769b83e..f2cc83a 100644 --- a/src/lotto/__init__.py +++ b/src/lotto/__init__.py @@ -1,7 +1,7 @@ # src/lotto/__init__.py # 📌 이 패키지는 로또 관련 기능을 제공하는 모듈입니다. -# 외부에서 `from lotto import Lotto`와 같은 방식으로 사용할 수 있도록 +# 외부에서 `from lotto import Lotto`와 같은 방식으로 사용할 수 있도록 # 필요한 모듈을 여기에 등록하세요. # # ✅ 새로운 모듈을 추가할 경우: @@ -10,9 +10,10 @@ # - `flake8`의 F401 경고(`imported but unused`)가 발생하는 경우, `__all__`을 활용해 해결하세요. from .lotto import Lotto # 🎲 로또 번호 생성 및 검증을 위한 클래스 +from .lotto import Score # 패키지 외부에서 `from lotto import *` 사용 시 제공할 모듈을 명시적으로 정의합니다. -__all__ = ["Lotto"] +__all__ = ["Lotto", "Score"] # 💡 예시: 새로운 모듈을 추가할 때 # from .other_module import OtherClass # 🆕 예: 새로운 클래스 추가 시 diff --git a/src/lotto/lotto.py b/src/lotto/lotto.py index 9c8c935..965933e 100644 --- a/src/lotto/lotto.py +++ b/src/lotto/lotto.py @@ -1,12 +1,95 @@ -from typing import List +from enum import Enum +import random + + +LOTTO_SIZE = 6 # 로또 길이 매직넘버상수 +LOTTO_NUMBER_RANGE = range(1, 46) # 로또 숫자 범위 매직넘버상수 + class Lotto: - def __init__(self, numbers: List[int]): + def __init__(self, numbers: list[int] = None): + if numbers is None: # numbers가 주어지지 않으면 자동으로 생성 + numbers = self.issuance_lotto() self._validate(numbers) self._numbers = numbers + self.bonus_number = int + self.numbers_list = list[list] + self.result_list = list[int] + self.statistics_list = list[list] + + def _validate(self, numbers: list[int]): + if len(numbers) != LOTTO_SIZE: + raise ValueError("[ERROR] 당첨 번호는 6자리입니다.") + elif not all(num in LOTTO_NUMBER_RANGE for num in numbers): + raise ValueError("[ERROR] 당첨 번호는 1 ~ 45 사이여야 합니다.") + elif not all(numbers.count(num) == 1 for num in numbers): + raise ValueError("[ERROR] 당첨 번호는 중복될 수 없습니다.") + + def validate_bonus_number(self, number: int): + self.bonus_number = number + if self.bonus_number in self._numbers: + raise ValueError("[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.") + elif self.bonus_number not in LOTTO_NUMBER_RANGE: + raise ValueError("[ERROR] 보너스 번호는 1 ~ 45 사이여야 합니다.") + + @staticmethod + def issuance_lotto(): + """랜덤한 6자리 로또 번호 생성 후 정렬하여 반환""" + value = sorted(random.sample(LOTTO_NUMBER_RANGE, LOTTO_SIZE)) + return value + + def __str__(self): + """str 형식으로 변환하여 반환""" + return str(self._numbers) + + def get_numbers(self): + """로또 번호 리스트 반환""" + return self._numbers + + def compare_winning_number(self, numbers: list[int]): + """당첨 번호와 발행 번호를 비교""" + count = 0 + for i in numbers: + if i in self._numbers: + count += 1 + return count + + def compare_bonus_number(self, numbers: list[int]): + """보너스 번호와 발행 번호를 비교""" + if self.bonus_number in numbers: + return 1 + return 0 + + def calculate_result(self, numbers_list: list[list]): + """발행한 로또를 순서대로 당첨 번호, 보너스 번호와 비교""" + self.numbers_list = numbers_list + self.result_list = [0 for _ in range(len(numbers_list))] + for i in range(len(self.result_list)): + count_winning = self.compare_winning_number(self.numbers_list[i]) + count_bonus = self.compare_bonus_number(self.numbers_list[i]) + self.result_list[i] = [count_winning, count_bonus] + return self.result_list + + +class Score(Enum): + FIRST = (6, 0, 2000000000) # 6개 일치, 보너스 X, 1등 + SECOND = (5, 1, 30000000) # 5개 일치, 보너스 O, 2등 + THIRD = (5, 0, 1500000) # 5개 일치, 보너스 X, 3등 + FOURTH = (4, 0, 50000) # 4개 일치, 보너스 X, 4등 + FIFTH = (3, 0, 5000) # 3개 일치, 보너스 X, 5등 + NONE = (0, 0, 0) # 당첨되지 않음 - def _validate(self, numbers: List[int]): - if len(numbers) != 6: - raise ValueError + def __init__(self, m_count, b_match, prize): + self.m_count = m_count # 맞춘 숫자 개수 + self.b_match = b_match # 보너스 번호 일치 여부 + self.prize = prize # 상금 - # TODO: 추가 기능 구현 + @classmethod + def get_score(cls, m_count, b_match): + """ + 당첨 번호 개수와 보너스 번호 여부를 받아 해당하는 Score 반환 + """ + for score in cls: + if score.m_count == m_count and score.b_match == b_match: + return score + return cls.NONE # 당첨되지 않은 경우 diff --git a/src/lotto/main.py b/src/lotto/main.py index 5f270aa..01258fd 100644 --- a/src/lotto/main.py +++ b/src/lotto/main.py @@ -1,6 +1,118 @@ +from lotto.lotto import Lotto +from lotto.lotto import Score + + +LOTTO_EACH_PRICE = 1000 # 로또 구입 금액 단위 매직넘버상수 + + +# data가 정수인지 검증 함수 +def is_number(data): + try: + return int(data) + except ValueError as e: + raise ValueError("[ERROR] 정수만 입력해주세요.") from e + + +# 구입 금액 입력 검증 함수 +def validate_purchase_amount(data): + purchase_amount = is_number(data) # 입력 값이 정수인지 검증 + if purchase_amount <= 0: # 입력 값이 양의 정수인지 검증 + raise ValueError("[ERROR] 구입 금액은 양의 정수 입니다.") + elif purchase_amount % LOTTO_EACH_PRICE != 0: # 입력 값이 1,000원 단위인지 검증 + raise ValueError("[ERROR] 구입 금액을 1,000원 단위로 입력해 주세요.") + return purchase_amount # 검증 통과 + + +def input_purchase_amount(): # 구입 금액 입력 함수 + print("구입금액을 입력해 주세요.") + purchase_amount = input() + purchase_amount = validate_purchase_amount(purchase_amount) # 구입 금액 검증 + return purchase_amount + + +# 구매 가능한 로또 개수 계산 +def generate_lotto_quantity(purcahse_amount): + lotto_quantity = purcahse_amount // LOTTO_EACH_PRICE + print("\n{0}개를 구매했습니다.".format(lotto_quantity)) + return lotto_quantity + + +# 입력 값을 list 형식으로 변환해주는 함수 +def convert_to_list(data): + try: + data = list(map(int, data.replace(" ", "").split(","))) + return data + except ValueError as e: + raise ValueError("[ERROR] 번호는 정수로 이루어져 있어야 합니다.") from e + + +# 당첨 번호를 입력받고 검증하는 함수 +def input_winning_numbers(): + print("\n당첨 번호를 입력해 주세요.") + winning_numbers = input() # 당첨 번호 입력 + winning_numbers = convert_to_list(winning_numbers) # 당첨 번호 list형으로 변경 + lotto = Lotto(winning_numbers) # 당첨 번호 검증 + return lotto + + +# 보너스 번호를 입력받고 검증하는 함수 +def input_bonus_number(lotto): + print("\n보너스 번호를 입력해 주세요.") + bouns_number = input() # 보너스 번호 입력 + bouns_number = is_number(bouns_number) # 보너스 번호를 int형으로 변환 + lotto.validate_bonus_number(bouns_number) # 보너스 번호 검증증 + + +def just_print(Score, score_count): + # 결과 출력 + for score in Score: + if score == Score.NONE: + continue # NONE 등급(낙첨)은 출력하지 않음 + description = f"{score.m_count}개 일치" + if score.b_match == 1: + description += ", 보너스 볼 일치" + print(f"{description} ({format(score.prize, ',d')}원)", end="") + print(f" - {score_count[score]}개") + + +def print_result(result_list, purchase_amount): + print("\n당첨 통계\n---") + + # 등수별 당첨 개수 저장 딕셔너리 초기화 + score_count = {score: 0 for score in Score if score != Score.NONE} + + # 당첨 개수 세기 + for match_count, bonus_match in result_list: + score = Score.get_score(match_count, bonus_match) + if score != Score.NONE: + score_count[score] += 1 + + just_print(Score, score_count) + + # 총 당첨 금액 계산 + total_prize = sum(score.prize * cnt for score, cnt in score_count.items()) + + # 수익률 계산 및 출력 + revenue_rate = (total_prize / purchase_amount) * 100 + print(f"총 수익률은 {revenue_rate:.1f}%입니다.") + + def main(): - # TODO: 프로그램 구현 - pass + purchase_amount = input_purchase_amount() # 구입 금액 입력 + l_quantity = generate_lotto_quantity(purchase_amount) # 로또 수량 계산산 + + issu_l_list = [Lotto.issuance_lotto() for _ in range(l_quantity)] # 로또 발행 + for lotto in issu_l_list: # 발행된 로또 출력 + print(lotto) + + lotto = input_winning_numbers() # 당첨 번호 입력 + + input_bonus_number(lotto) # 보너스 번호 입력 + + result_list = lotto.calculate_result(issu_l_list) + + print_result(result_list, purchase_amount) + if __name__ == "__main__": main() diff --git a/tests/lotto/test_lotto.py b/tests/lotto/test_lotto.py index e515562..98397e7 100644 --- a/tests/lotto/test_lotto.py +++ b/tests/lotto/test_lotto.py @@ -21,4 +21,63 @@ def test_create_lotto_by_duplicated_number(): Lotto([1, 2, 3, 4, 5, 5]) -# 추가 테스트 작성 가능 +# 로또 번호 범위 초과 예외 테스트 (예: 46이 포함된 경우) +@pytest.mark.custom_name("로또 번호가 1~45 범위를 벗어나면 예외가 발생한다.") +def test_create_lotto_by_out_of_range(): + with pytest.raises(ValueError): + Lotto([0, 1, 2, 3, 4, 5]) # 0 포함 + with pytest.raises(ValueError): + Lotto([1, 2, 3, 4, 5, 46]) # 46 포함 + + +# 보너스 번호 중복 예외 테스트 (보너스 번호가 당첨 번호와 중복) +@pytest.mark.custom_name("보너스 번호가 당첨 번호와 중복되면 예외가 발생한다.") +def test_validate_bonus_number_by_duplicate(): + lotto = Lotto([1, 2, 3, 4, 5, 6]) + with pytest.raises(ValueError): + lotto.validate_bonus_number(5) # 기존 당첨 번호와 중복된 보너스 번호 + + +# 보너스 번호 범위 초과 예외 테스트 +@pytest.mark.custom_name("보너스 번호가 1~45 범위를 벗어나면 예외가 발생한다.") +def test_validate_bonus_number_by_out_of_range(): + lotto = Lotto([1, 2, 3, 4, 5, 6]) + with pytest.raises(ValueError): + lotto.validate_bonus_number(0) # 0 포함 + with pytest.raises(ValueError): + lotto.validate_bonus_number(46) # 46 포함 + + +# 로또 자동 발행 시, 6개의 숫자가 포함되어 있는지 확인 +@pytest.mark.custom_name("로또 자동 발행 시, 6개의 숫자가 포함되어야 한다.") +def test_issuance_lotto_size(): + numbers = Lotto.issuance_lotto() + assert len(numbers) == 6 + + +# 로또 자동 발행 시, 숫자가 정렬되어 있는지 확인 +@pytest.mark.custom_name("로또 자동 발행 시, 번호가 오름차순 정렬되어야 한다.") +def test_issuance_lotto_sorted(): + numbers = Lotto.issuance_lotto() + assert numbers == sorted(numbers) + + +# 로또 번호 비교 테스트 (당첨 개수 확인) +@pytest.mark.custom_name("발행된 로또 번호와 당첨 번호를 비교하여 일치 개수를 확인한다.") +def test_compare_winning_number(): + winning_lotto = Lotto([1, 2, 3, 4, 5, 6]) + issued_numbers = [1, 2, 3, 10, 20, 30] # 3개 일치 + assert winning_lotto.compare_winning_number(issued_numbers) == 3 + + +# 보너스 번호 비교 테스트 +@pytest.mark.custom_name("발행된 로또 번호와 보너스 번호를 비교하여 일치 여부를 확인한다.") +def test_compare_bonus_number(): + winning_lotto = Lotto([1, 2, 3, 4, 5, 6]) + winning_lotto.validate_bonus_number(7) + + issued_numbers = [7, 8, 9, 10, 11, 12] # 보너스 번호(7) 포함 + assert winning_lotto.compare_bonus_number(issued_numbers) == 1 + + issued_numbers = [1, 2, 3, 4, 5, 6] # 보너스 번호 미포함 + assert winning_lotto.compare_bonus_number(issued_numbers) == 0 \ No newline at end of file