Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e701b83
docs: 구현할 기능 목록
akran123 Feb 13, 2025
a241007
feat: 구입 금액 입력 구현
akran123 Feb 13, 2025
a9cd686
feat: 예외처리 구현
akran123 Feb 13, 2025
3846436
feat: 보너스 번호 입력 구현
akran123 Feb 13, 2025
e87c55e
feat: 예외처리 기능 구현
akran123 Feb 13, 2025
a3b6564
feat: 로또 번호 생성 구현
akran123 Feb 13, 2025
f7819e1
feat: 로또 번호 출력 기능 구현
akran123 Feb 13, 2025
15ac1d9
feat: 로또 당첨 번호 비교 기능
akran123 Feb 13, 2025
030e2f5
feat: 로또 당첨 번호 비교
akran123 Feb 13, 2025
55710c2
feat: 당첨금 목록
akran123 Feb 13, 2025
bbd658f
fix: 비교 버그 수정
akran123 Feb 13, 2025
6e100a2
fix: 비교 버그 수정
akran123 Feb 13, 2025
e1bf619
feat: 당첨 통계 및 수익률 출력 구현
akran123 Feb 13, 2025
46b25e1
feat: 버그 수정 및 클래스 변경
akran123 Feb 13, 2025
8ff95bc
feat: 완성본
akran123 Feb 13, 2025
4824cc1
fix: 버그 수정
akran123 Feb 15, 2025
fc4ccdb
fix: 버그 수정
akran123 Feb 15, 2025
197bb9f
[#0] rename `requirement.txt` -> `requirements.txt` to make it usual …
swthewhite Feb 13, 2025
c379455
[#0] add custom marker for test descriptions in pytest configuration
swthewhite Feb 13, 2025
1c15719
[#0] add enum to `check-no-external-libs.yml`
swthewhite Feb 13, 2025
557bfd2
[#0] make `check-function-length.yml` to work only for function length
swthewhite Feb 13, 2025
651f1c8
[#0] enhance `check-commit-convention.yml` to avoid `[#0]` commit
swthewhite Feb 13, 2025
9431b48
[#0] add lost comma in `check-no-external-libs.yml`
swthewhite Feb 13, 2025
68e121a
[#0] enhance `__init__.py` to pass PEP8
swthewhite Feb 13, 2025
1c2ba7f
[#0] enhance `check-no-external-libs.yml` to recognize internal library
swthewhite Feb 13, 2025
04a77d0
fix: PEP8 규격 수정
akran123 Feb 16, 2025
7356e54
fix: docs 복구
akran123 Feb 16, 2025
9420105
fix: PEP8 규격 수정
akran123 Feb 16, 2025
2b5c9ce
Merge branch 'main' into akran123
swthewhite Feb 16, 2025
1ca86d0
fix: 버그 수정
akran123 Feb 16, 2025
06c557c
Merge branch 'akran123' of https://github.com/akran123/python-lotto i…
akran123 Feb 16, 2025
7d72664
style: PEP8 수정
akran123 Feb 16, 2025
cb0e757
style: yml 수정
akran123 Feb 16, 2025
063e41e
style: PEP8
akran123 Feb 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check-no-external-libs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

> [!NOTE]
> 이 코드는 원래 [java-lotto-6](https://github.com/woowacourse-precourse/java-lotto-6)에서 제공된 **Java 기반의 로또 게임**을 **Python**에 맞게 변환한 과제입니다. 프로젝트 구조, 요구 사항, 기능 구현 방식은 원본 저장소를 바탕으로 Python 환경에 맞추어 수정하였습니다.

---

## 🔍 진행 방식
Expand Down Expand Up @@ -137,7 +136,6 @@ tests/lotto/test_lotto.py .. [100%]
```
구입금액을 입력해 주세요.
8000

8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
Expand All @@ -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개
Expand Down
46 changes: 46 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -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 범위를 벗어날 경우 예외 처리
보너스 번호가 당첨 번호와 중복될 경우 예외 처리
1 change: 0 additions & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@
pythonpath = src
markers =
custom_name: 테스트 설명을 위한 커스텀 마커

19 changes: 2 additions & 17 deletions src/lotto/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
116 changes: 110 additions & 6 deletions src/lotto/lotto.py
Original file line number Diff line number Diff line change
@@ -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 사이여야 합니다.")

Check warning on line 88 in src/lotto/lotto.py

View check run for this annotation

Codecov / codecov/patch

src/lotto/lotto.py#L88

Added line #L88 was not covered by tests

@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)
120 changes: 118 additions & 2 deletions src/lotto/main.py
Original file line number Diff line number Diff line change
@@ -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원으로 나누어 떨어져야 합니다.")

Check warning on line 11 in src/lotto/main.py

View check run for this annotation

Codecov / codecov/patch

src/lotto/main.py#L11

Added line #L11 was not covered by tests
if amount < 1000:
raise ValueError("구입 금액은 1,000원 이상이어야 합니다.")

Check warning on line 13 in src/lotto/main.py

View check run for this annotation

Codecov / codecov/patch

src/lotto/main.py#L13

Added line #L13 was not covered by tests

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}")

Check warning on line 40 in src/lotto/main.py

View check run for this annotation

Codecov / codecov/patch

src/lotto/main.py#L39-L40

Added lines #L39 - L40 were not covered by tests

Comment on lines +32 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

예외 처리 및 테스트 케이스 보완이 필요합니다.

당첨 번호 입력 처리에서 다음 개선사항이 필요합니다:

  1. 예외 처리가 테스트되지 않았습니다
  2. 잘못된 입력 형식(예: 쉼표 없는 입력)에 대한 구체적인 에러 메시지가 필요합니다

다음과 같이 수정해보세요:

     try:
         winning_numbers = list(map(int, input().split(",")))
         return Lotto(winning_numbers).get_numbers()
     except ValueError as error:
-        print(f"[ERROR] {error}")
+        if "split" in str(error):
+            print("[ERROR] 쉼표(,)로 구분된 숫자를 입력해 주세요.")
+        else:
+            print(f"[ERROR] {error}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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:
if "split" in str(error):
print("[ERROR] 쉼표(,)로 구분된 숫자를 입력해 주세요.")
else:
print(f"[ERROR] {error}")
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 39-40: src/lotto/main.py#L39-L40
Added lines #L39 - L40 were not covered by tests


def check_bonus_number(bonus_num, winning_numbers):
"""보너스 번호 검증"""
if not bonus_num.isdigit():
raise ValueError("숫자를 입력해 주세요.")

Check warning on line 46 in src/lotto/main.py

View check run for this annotation

Codecov / codecov/patch

src/lotto/main.py#L46

Added line #L46 was not covered by tests

bonus_num = int(bonus_num)
if bonus_num in winning_numbers:
raise ValueError("보너스 숫자와 입력한 당첨 번호는 중복되지 않아야 합니다.")

Check warning on line 50 in src/lotto/main.py

View check run for this annotation

Codecov / codecov/patch

src/lotto/main.py#L50

Added line #L50 was not covered by tests
if bonus_num > 45 or bonus_num < 1:
raise ValueError("로또 번호의 숫자 범위는 1~45까지입니다.")

Check warning on line 52 in src/lotto/main.py

View check run for this annotation

Codecov / codecov/patch

src/lotto/main.py#L52

Added line #L52 was not covered by tests

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}")

Check warning on line 64 in src/lotto/main.py

View check run for this annotation

Codecov / codecov/patch

src/lotto/main.py#L63-L64

Added lines #L63 - L64 were not covered by tests


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}%입니다.")
Comment on lines +84 to +103
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

결과 출력 로직의 중복을 제거해야 합니다.

현재 구현에서 결과 출력 문자열 포맷팅 로직이 중복되어 있습니다.

다음과 같이 리팩토링하는 것을 제안합니다:

     def print_results(results, total_prize, amount):
         profit_percentage = round((total_prize / (amount * 1000)) * 100, 2)
+        
+        def format_result(rank):
+            bonus_text = ", 보너스 볼 일치" if rank == Rank.SECOND else ""
+            return f"{rank.match_cnt}개 일치{bonus_text} ({rank.prize:,}원) - {results[rank]}개"
 
         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]}개"
-                )
+            if rank != Rank.NONE:
+                print(format_result(rank))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 print_results(results, total_prize, amount):
"""당첨 결과 및 수익률 출력"""
profit_percentage = round((total_prize / (amount * 1000)) * 100, 2)
def format_result(rank):
bonus_text = ", 보너스 볼 일치" if rank == Rank.SECOND else ""
return f"{rank.match_cnt}개 일치{bonus_text} ({rank.prize:,}원) - {results[rank]}개"
print("\n당첨 통계")
print("---")
for rank in Rank:
if rank != Rank.NONE:
print(format_result(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()
2 changes: 2 additions & 0 deletions tests/lotto/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@ def test_예외_테스트():
# 잘못된 금액 입력
with patch("builtins.input", side_effect=["1000j"]):
main()


Loading