Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🧩 파이썬 콘솔 퀴즈 게임 프로젝트

프로젝트 개요

본 프로젝트는 터미널 환경에서 동작하는 파이썬 기반 콘솔 퀴즈 게임 구현을 통해 파이썬의 핵심 언어 메커니즘과 객체지향 프로그래밍 패러다임을 습득하는 것을 목적으로 합니다. 단순한 문법 학습을 넘어 대화형 콘솔 CLI 환경을 직접 구축해보며 아래의 핵심 역량을 종합적으로 내재화합니다:

  • -객체지향 설계: Quiz 및 QuizGame 클래스 분리를 통한 역할과 책임의 정의, 데이터와 메서드의 캡슐화 이해
  • -프로그램 제어 흐름: 예외 처리(Exception Handling), 사용자 입력 검증, 조건문 및 반복문을 통한 안정적인 게임 루프 구축
  • -데이터 영속성: JSON 기반 파일 입출력(I/O)을 활용한 퀴즈 데이터의 로드 및 동적 데이터 관리
  • -모듈화 및 재사용성: 유지보수와 기능 확장이 용이한 구조적인 코드 작성 및 모듈 분리 연습

파일 구조

project/
├── main.py # 진입점 - 초기화 및 게임 실행
├── gitignore # Git 추적 제외 설정
└── date/ # 데이터 지속성을 위한 JSON 저장소
├── state.json # 퀴즈 데이터 저장 파일 (자동 생성)
├── history.json # 탐험 기록 저장 파일 (자동 생성)
└── src/ # 핵심 비즈니스 로직 및 UI 모듈
├── quiz.py # 퀴즈 데이터 구조 및 개별 객체 행동 정의
├── quiz_manager.py # 퀴즈 목록 관리
├── quizgame.py # 게임 진행 상태 제어 및 UI
├── history_manager.py # 기록 저장 및 데이터 가공
├── exceptions.py # 커스텀 예외 클래스 정의
└── error_handler.py # 중앙집중식 예외 처리 및 방어적 로직
└── evidence/
└── tests/
├── test_quiz.py

퀴즈 주제 선정 이유

최근 인류 문명의 발전과 글로벌 흐름을 이해할 수 있는 '세계사 문명 교류'를 메인 주제로 선정하였습니다.

🌍 주제 흥미 및 사회적 가치 (Insight)

  • 대중적 오해 바로잡기: 역사 속 대중적으로 잘못 알려진 상식이나 편견을 퀴즈 형태로 제시함으로써, 사용자에게 신선한 흥미를 유발하고 바른 역사적 인사이트를 제공하기에 최적의 주제라고 판단했습니다.

💡 서비스 기획 관점 (Business & Value)

  • 실제 서비스 출시를 고려한 설계: 단순한 단발성 과제를 넘어, 실제 교육용 퀴즈 서비스로 확장 가능한 콘텐츠 밀도를 고려했습니다.
  • 지속 가능한 유저 참여: 사용자가 스스로 퀴즈를 풀고 새로운 문제까지 직접 추가하는 선순환 구조를 통해 지속적인 지식 학습이 가능한 환경을 기획했습니다.

💻 파이썬 학습 효과 (Technical Value)

  • 자연스러운 파이썬 핵심 개념 복습: 문제 데이터 구조화(JSON), 객체 지향 클래스 설계, 예외 처리 등 퀴즈 게임의 로직을 직접 구현해 보며 파이썬의 주요 문법과 개발 메커니즘을 자연스럽게 내재화할 수 있었습니다.

기능 목록

  1. 문명 탐험하기 (퀴즈 플레이) 등록된 퀴즈 중 원하는 수만큼 랜덤 출제 문제 수 선택: 5문제 / 전체 / 직접 입력 힌트 사용 가능 (힌트 사용 시 5점 / 미사용 시 10점) 정답·오답 해설 출력 게임 종료 후 결과 및 최고 점수 표시

  2. 새로운 문제 만들기 문제, 선택지 4개, 정답 번호 직접 입력 추가 즉시 JSON 파일에 자동 저장

  3. 문제 도감 보기 등록된 전체 퀴즈 목록 출력 문제, 선택지, 정답 번호 확인 가능

  4. 나의 탐험 기록 전체 탐험 기록 조회 (날짜, 점수, 정답률) 최고 점수 표시

  5. 문제 삭제 번호 선택으로 퀴즈 삭제 삭제 즉시 JSON 파일에 자동 반영

  6. 안전한 종료 Ctrl+C / EOF 입력 시 데이터 자동 저장 후 종료

실행 방법

python main.py

데이터 파일 설명

1. src/quiz.py (Quiz 모델)

  • 특징: 개별 퀴즈에 필요한 데이터(문제, 선택지, 정답, 힌트, 정답/오답 해설 등)와 **퀴즈 자체의 행동(출력, 힌트 제공, 정답 검증, 해설 출력)**을 완벽히 캡슐화한 핵심 도메인 클래스입니다.

    • 객체지향 캡슐화: 외부에서 객체 내부 속성을 직접 다루지 않고, is_correct(), display_hint(), print_explanation() 등의 전용 메서드를 통해 퀴즈의 동작을 안전하게 제어
    • enumerate 기반의 자동 선택지 번호 부여: JSON 저장소나 데이터 리스트에 번호가 별도로 없어도 enumerate(..., start=1)을 활용해 출력 시점에 동적으로 1번부터 선택지 번호를 자동 매핑
    • while 루프 기반의 유효성 검증(Robustness): 힌트 사용 입력 시 y/n 외의 잘못된 값을 입력받더라도 프로그램이 중단되지 않고 정정한 입력을 계속 유도하도록 설계
    • 정답/오답 입체적 해설 시스템: 단순히 정답 번호만 알려주는 것을 넘어 정답일 때의 상세 해설과 오답일 때의 오답 해설을 분리하여 학습 효과를 극대화

💡 핵심 코드 및 구현 상세

1) enumerate를 활용한 동적 선택지 번호 매기기 (display) JSON 데이터 구조를 단순하게 유지하면서 화면 출력 시 enumerate를 사용해 보기 번호를 1번부터 자동으로 매핑

def display(self):
    print("\n" + "─" * 45)
    print(f"(๑qᴗq๑). {self.question}❓")
    print("─" * 45)

    # start=1 속성을 활용하여 보기 번호를 동적으로 자동 생성
    for i, option in enumerate(self.options, start=1):
        print(f"   {i}. 👉 {option}")

    print("─" * 45)

2) 방어적 입력을 위한 무한 루프 힌트 시스템 (display_hint) 잘못된 사용자 입력을 예외 처리하고, 힌트 사용 여부에 따라 점수 차등 부여(5점/10점)를 위한 불리언(bool) 값을 안정적으로 반환

    """
    Returns:
        True  → 힌트 사용 (5점 획득)
        False → 힌트 미사용 (10점 획득)
    """
    while True:
        want_hint = input("\n💡 힌트가 필요한가요? (y/n): ").strip().lower()

        if want_hint in ["y", "yes", "예"]:
            hint_text = self.hint if self.hint else "등록된 힌트가 없습니다."
            print(f"👉 [힌트]: {hint_text}")
            print("⚠️ 힌트를 사용하여 맞히면 5점만 획득합니다. (미사용 시 10점)")
            return True

        if want_hint in ["n", "no", "아니오"]:
            return False

        # y/n 외 다른 입력값 입력 시 무한 루프 내에서 경고 후 재입력 요구
        print("⚠️ y 또는 n으로 입력해주세요.")

3) 학습 효과를 높이는 통합 해설 출력 (print_explanation) 사용자의 정답 여부에 따라 정답 텍스트와 전용 해설을 분기하여 출력함으로써 피드백의 품질을 높임

def print_explanation(self, is_correct_user: bool):
    if is_correct_user:
        print("⭕ 정답입니다!")
        if self.answer_explanation:
            print(f"💡 [정답 해설] {self.answer_explanation}")
    else:
        # 인덱스 계산(self.answer - 1)으로 정답 번호와 텍스트를 함께 출력
        answer_text = self.options[self.answer - 1]
        print(f"❌ 틀렸습니다! (정답: {self.answer}번 - {answer_text})")
        if self.wrong_explanations:
            print(f"💡 [오답 해설] {self.wrong_explanations}")

2. src/quiz_manager.py (퀴즈 데이터 관리)

  • 특징: 퀴즈 목록의 추가, 삭제, 전체 조회, 개수 확인 등 데이터 집합 관리(CRUD)만 전담하는 매니저 클래스임

  • 잘된 점:

  • 단일 책임 원칙 준수: UI 및 파일 저장 로직을 분리하고 순수 데이터 컬렉션 관리 기능만 담당하게 하여 독립적인 단위 테스트와 재사용성을 극대화

  • 안전한 인덱스 처리 방어: 사용자 입력 번호(1-based index)를 파이썬 리스트 인덱스(0-based index)로 변환할 때, 범위를 벗어나는 접근(IndexError)을 내부에서 완벽히 차단

  • 데이터 보호: 전체 목록 조회 시 데이터가 외부에서 무단 수정되는 것을 방지하도록 고려됨

💡 핵심 코드 및 구현 상세

방어적 에러 처리 및 예외 전환

  • 예외 처리 및 프로그램 안정성 확보: json.JSONDecodeError나 OSError 같은 어려운 시스템 에러가 발생했을 때 프로그램이 비정상 종료되는 것을 방지
  • 시스템 에러를 읽기 쉬운 메시지로 변환: 시스템 에러를 프로젝트 전용 예외(DataCorruptedError, DataLoadError)로 전환하여 상위 모듈에서 에러 원인을 직관적으로 파악하고 가독성 높게 처리할 수 있게 함
  • 원본 에러와 연결 유지 (from e): from e 구문을 사용해 원본 에러의 발생 원인(Traceback)을 잃어버리지 않고 추적할 수 있도록 안전하게 연결함
try:
    # JSON 파일 로드 로직
    ...
except json.JSONDecodeError as e:
    raise DataCorruptedError("퀴즈 파일 손상") from e
except OSError as e:
    raise DataLoadError("퀴즈 파일 읽기 실패") from e

3. src/quiz_manager.py (퀴즈 데이터 관리)

  • 특징: 사용자 메뉴 입력을 받고, QuizManager 및 HistoryManager 등의 백엔드 모듈과 상호작용하여 전체 게임 흐름 및 사용자 인터페이스(UI)를 매끄럽게 제어함.
  • 잘된 점 (Good Point):
    • 사용자 맞춤형 게임 플레이 및 무작위성: 전체 문제 중에서 풀어볼 문제 수를 사용자가 직접 설정 가능하며, random.shuffle을 사용해 게임마다 문제 순서를 무작위로 섞어 단조로움을 방지함.
    • 철저한 단계별 입력 방어 (Robust Validation): 빈값 체크, 숫자 형태 검증(isdigit), 범위 체크(InputRangeError) 등 정답 및 메뉴 입력 시 예외를 3중으로 차단하여 프로그램 튕김을 완벽히 방지함.
    • 실시간 데이터 영속성 결합: 문제 추가/삭제, 게임 종료 및 최고 점수 갱신 시점에 맞춰 _save_data()를 즉시 호출함으로써 데이터 손실을 원천 차단함.
    • 단일 책임 원칙(SRP) 기반의 UI 전담 설계: 데이터 저장 및 컬렉션 관리 알고리즘은 각 Manager 클래스에 위임하고, QuizGame은 사용자 입출력(UI)과 게임 제어 흐름에만 집중함.

💡 핵심 코드 및 구현 상세

1) 문제 수 지정 및 무작위 출제 (_select_question_count & random.shuffle) 등록된 전체 문제 데이터를 그대로 쓰지 않고 random.shuffle()로 무작위 섞기를 수행하며, 사용자가 원하는 문제 수만큼 유연하게 게임을 진행

# 전체 퀴즈 목록을 무작위로 섞어 매판 새로운 느낌 전달
random.shuffle(quizzes)

# 사용자가 플레이할 문제 수 커스텀 선택
question_count = self._select_question_count(total_available)

2) 커스텀 예외 기반의 다단계 정답 입력 방어 (_get_answer) 입력값 공백 검사 ➔ 숫자 변환 가능 여부 검사 ➔ InputRangeError 커스텀 예외 발생을 통한 범위 검사의 3단계 안전장치를 적용

    """정답 번호 입력 처리 및 방어적 유효성 검증"""
    while True:
        raw = input(f"\n👉 정답 번호를 입력하세요 (1~{max_num}): ").strip()

        # 1. 빈 입력 검증
        if raw == "":
            print("⚠️ 입력이 비어있습니다. 다시 입력해주세요.")
            continue

        # 2. 숫자 형태 검증
        if not raw.isdigit():
            print("⚠️ 숫자만 입력할 수 있습니다.")
            continue

        num = int(raw)

        # 3. 커스텀 예외를 활용한 범주 검증
        try:
            if not 1 <= num <= max_num:
                raise InputRangeError(f"1~{max_num} 사이의 번호를 입력해주세요.")
            return num

        except InputRangeError as e:
            print(f"⚠️ {e}")  # 예외 메시지 출력 후 다시 입력 유도

3) 탐험 결과 집계, 최고 기록 갱신 및 자동 저장 (_show_result) 게임 종료 후 정답률 산출, 히스토리 저장 및 최고 점수 갱신 판정을 진행하고 곧바로 파일 저장을 수행

    """게임 종료 후 결과 산출, 최고 기록 갱신 및 데이터 저장"""
    if total == 0:
        return

    percentage = int((correct_count / total) * 100)
    
    # ... (결과 텍스트 출력 생략) ...

    # 히스토리 매니저 연동 및 최고 점수 판단
    if self.history_manager:
        self.history_manager.add_record({
            "score": score,
            "total": total,
            "correct_count": correct_count,
            "percentage": percentage
        })
        self.history_manager.update_best_score(score)
        best = self.history_manager.get_best_score()

    # 결과 도출 직후 즉시 파일로 저장하여 영속성 확보
    self._save_data()

4) Manager 클래스 위임을 통한 단순 입출력(UI) 구현 (add_quiz, show_quizzes, delete_quiz) QuizGame 내부에서 데이터를 직접 조작하지 않고 self.manager의 메서드를 호출함으로써 비즈니스 로직과 UI 인터페이스를 명확히 분리

    """UI를 통해 입력받고 QuizManager에 데이터 추가 위임"""
    # ... 사용자 입력 검증 및 UI 구성 ...
    
    # 실제 데이터 컬렉션 추가는 매니저가 전담
    self.manager.add_quiz(question, options, answer)
    self._save_data()

def delete_quiz(self):
    """UI 삭제 요청 받아서 QuizManager의 삭제 로직으로 전달"""
    # ... 번호 입력 처리 ...
    
    try:
        deleted = self.manager.delete_quiz(index) # 삭제 위임
        print(f"\n✅ '{deleted.question}' 문제가 삭제되었습니다.")
        self._save_data()
    except IndexError as e:
        print(f"⚠️ {e}")

4. src/exceptions.py & src/error_handler.py (예외 처리 구조)

  • 특징: 커스텀 예외 계층 구조(QuizAppError 상속)를 정의하고, 발생한 예외를 중앙에서 일관되게 가공·복구·처리하는 에러 핸들러 모듈
  • 잘된 점:
    • 중앙집중식 예외-메시지 매핑 (USER_MESSAGES): 시스템 에러가 발생했을 때 난해한 파이썬 예외 객체를 사용자 친화적인 안내 메시지(str)로 일괄 변환하여 출력
    • 명확한 복구 가능 여부 판별 (is_recoverable): 발생한 에러가 시스템을 재시도/초기화하여 계속 진행할 수 있는 복구 가능 에러인지, 프로그램 종료가 필요한 에러인지 런타임에 동적으로 판별
    • 시스템 강제 종료 시 데이터 보호 (Safe Shutdown): Ctrl+C(KeyboardInterrupt)나 입력 스트림 종료(EOFError) 발생 시, save_callback을 실행하여 현재까지의 진행 상황을 안전하게 저장한 후 종료하도록 방어

💡 핵심 코드 및 구현 상세

1) 예외 타입별 사용자 친화적 메시지 변환 (USER_MESSAGES & get_user_message) 딕셔너리와 isinstance 검사를 활용하여 에러 종류별로 명확하고 다정한 사용자 안내 문구를 매핑함.

예외 계층 구조

QuizAppError ← 최상위 커스텀 예외
├── DataLoadError ← 파일 불러오기 실패
├── DataSaveError ← 파일 저장 실패
├── DataCorruptedError ← 파일 손상
├── InvalidInputError ← 잘못된 입력
│ └── InputRangeError ← 범위 벗어난 숫자 입력
├── QuizNotFoundError ← 퀴즈 없음
└── QuizValidationError ← 퀴즈 유효성 오류

# 예외 타입 → 사용자 메시지 중앙 집중 매핑
USER_MESSAGES = {
    DataLoadError:      "📂 데이터를 불러오지 못했습니다. 기본 데이터로 시작합니다.",
    DataSaveError:      "💾 저장에 실패했습니다. 잠시 후 다시 시도해주세요.",
    DataCorruptedError: "⚠️ 데이터 파일이 손상되었습니다. 초기화합니다.",
    InvalidInputError:  "⚠️ 올바른 값을 입력해주세요.",
}

@staticmethod
def get_user_message(error: Exception) -> str:
    """발생한 예외 객체를 사용자가 이해하기 쉬운 메시지로 변환"""
    for error_type, message in ErrorHandler.USER_MESSAGES.items():
        if isinstance(error, error_type):
            return message

    # 매핑 목록에 없는 미정의 예외 발생 시 기본 메시지 반환
    return "⚠️ 오류가 발생했습니다. 잠시 후 다시 시도해주세요."

2) 시스템 복구 가능 여부 판별 로직 (is_recoverable) 에러 발생 시 무조건 종료하지 않고, 기본값 사용이나 재시도로 시스템을 살릴 수 있는지 튜플 타입 검사로 빠르게 판단

@staticmethod
def is_recoverable(error: Exception) -> bool:
    """
    복구 가능한 예외인지 판별
    - 복구 가능 (True)  → 재시도 또는 기본 데이터 사용
    - 복구 불가능 (False) → 에러 로그 기록 후 종료
    """
    recoverable = (
        DataLoadError,
        DataCorruptedError,
        InvalidInputError,
    )
    return isinstance(error, recoverable)

3) 비상 종료 시 데이터 안전 저장 콜백 (handle_keyboard_interrupt & handle_eof_error) 사용자가 프로그램 실행 중 Ctrl+C 등으로 강제 종료를 시도하더라도, 저장을 담당하는 콜백 함수(save_callback)를 유연하게 전달받아 유실 없이 데이터를 파일에 쓴 뒤 안전하게 종료

def handle_keyboard_interrupt(save_callback=None):
    """Ctrl+C 입력 시 안전 종료 및 데이터 자동 저장"""
    print("\n\n⚠️ 탐험을 중단했습니다.")

    # 전달받은 데이터 저장 콜백 함수가 있다면 실행
    if save_callback:
        print("💾 데이터를 저장합니다...")
        save_callback()

    print("👋 안전하게 종료합니다.")

5. src/history_manager.py (탐험 기록 및 최고 점수 관리)

  • 특징: 게임 플레이 결과(점수, 맞힌 개수, 정답률) 보관 및 최고 점수 갱신, history.json 파일 영속성을 담당하는 히스토리 전담 클래스
  • 잘된 점:
    • 타임스탬프 동적 생성 (datetime): 외부에서 기록을 넘겨받을 때 datetime.now()를 활용해 기록 작성 일시(YYYY-MM-DD HH:MM)를 시스템에서 자동으로 매핑함.
    • 방어적 데이터 타입 검증 (isinstance): add_record 및 update_best_score 호출 시 전달된 데이터의 타입(Dict, Int)을 검증하여 오염된 데이터가 저장되는 것을 원천 차단함.
    • 유연한 초기화 및 파일 예외 처리: 기록 파일이 존재하지 않거나(FileNotFound), 손상된 경우(JSONDecodeError)에도 시스템이 비정상 종료되지 않고 기본값(0점, 빈 리스트)으로 안전하게 초기화함.

💡 핵심 코드 및 구현 상세

1) 타임스탬프 자동 매핑 및 타입 방어 (add_record)

def add_record(self, record):
    """기록 형식을 검증하고 현재 시각을 자동으로 추가하여 저장"""
    if not isinstance(record, dict):
        print("⚠️ 기록 형식이 올바르지 않습니다.")
        return

    # 기록 생성 시점의 현재 날짜/시간을 자동으로 포맷팅하여 추가
    record["date"] = datetime.now().strftime("%Y-%m-%d %H:%M")
    self.history.append(record)

2) 안전한 최고 점수 판정 (update_best_score)

    """정수 형태의 점수만 검증하여 최고 점수 비교 및 갱신"""
    if not isinstance(score, int):
        return

    if score > self.best_score:
        self.best_score = score

📂 data/ 디렉터리 (state.json, history.json)

  • 특징: state.json과 history.json을 통해 프로그램이 종료되어도 문제 목록, 최고 점수, 플레이 이력이 유지되는 영속성(Persistence) 데이터 레이어임.
  • 잘된 점 (Good Point):
    • 입체적 오답 해설 데이터 구조: 단순 정답 번호에 그치지 않고, 선택한 보기 번호별 전용 오답 해설(wrong_explanations)을 딕셔너리 형태로 분리 정의하여 정교한 피드백 시스템을 구축함.
    • 자동 초기화 매커니즘 (Self-Healing): 최초 실행 시 파일이 없거나, 파일 손상(JSONDecodeError) 시 기본 퀴즈 데이터(세계사 문명 문제)로 자동 복구해 시스템이 멈추지 않도록 안전장치를 제공함.
    • 학습 유도용 힌트 및 정답 해설 포함: 퀴즈 객체가 요구하는 데이터(question, options, answer, hint, answer_explanation)를 표준 규격화하여 관리함.

💡 데이터 구조 예시 (data/state.json)
{
  "best_score": 0,
  "quizzes": [
    {
      "question": "문명의 발전 방식으로 가장 적절한 것은",
      "options": [
        "1. 한 국가가 독자적으로 발전한다",
        "2. 여러 문명이 서로 영향을 주고받는다",
        "3. 강한 문명이 약한 문명을 이끈다",
        "4. 문명은 서로 분리되어 발전한다"
      ],
      "answer": 2,
      "hint": "인류 문명의 발전은 하나의 지역에서만 이루어졌을까, 아니면 여러 문명의 만남 속에서 이루어졌을까?",
      "answer_explanation": "문명은 고립된 발전보다 서로 다른 문명의 지식, 기술, 사상이 교류하면서 새로운 형태로 발전한다.",
      "wrong_explanations": {
        "1": "문명은 내부 발전도 있지만, 대부분 다른 문명과의 교류 속에서 발전해왔다.",
        "3": "강한 문명이 영향을 주기도 하지만 문명 발전을 단순한 지배 관계로만 설명할 수 없다.",
        "4": "문명은 서로 연결되고 영향을 주고받으며 변화한다."
      }
    }
  ]
}

과제 수행 증거

  • 세팅

  • 단일 퀴즈 테스트

  • 퀴즈 메인 화면

  • 퀴즈 추가 화면

  • git log

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages