import base64 import re import ast import nltk import pandas as pd from pathlib import Path from io import BytesIO from PIL import Image, ImageFile import streamlit as st from streamlit_extras.stylable_container import stylable_container import google.generativeai as genai from google.cloud import texttospeech from google.oauth2 import service_account from database import register_user, verify_user, save_learning_history, get_learning_history, update_username, find_username, reset_password import extra_streamlit_components as stx import time # Constants and directory setup wORK_DIR = Path(__file__).parent IMG_DIR, IN_DIR, OUT_DIR = wORK_DIR / "img", wORK_DIR / "input", wORK_DIR / "output" # Create directories if they don't exist IMG_DIR.mkdir(exist_ok=True) IN_DIR.mkdir(exist_ok=True) OUT_DIR.mkdir(exist_ok=True) def init_page(): st.set_page_config( page_title="앵무새 스쿨", layout="wide", page_icon="🦜" ) def show_auth_page(): st.markdown( """

🔊앵무새 스쿨

영어공인시험 학습 사이트

""", unsafe_allow_html=True) # 탭 생성 tab1, tab2, tab3 = st.tabs(["로그인", "회원가입", "아이디/비밀번호 찾기"]) with tab1: with st.form("login_form", border=True): st.markdown("""

로그인

""", unsafe_allow_html=True) username = st.text_input("아이디", placeholder="아이디를 입력하세요") password = st.text_input("비밀번호", type="password", placeholder="비밀번호를 입력하세요") submitted = st.form_submit_button("로그인", use_container_width=True) if submitted: if not username or not password: st.error("아이디와 비밀번호를 모두 입력해주세요.") else: success, user_id = verify_user(username, password) if success: # 닉네임(name) 가져오기 import database conn = database.sqlite3.connect(database.DB_PATH) c = conn.cursor() c.execute('SELECT name FROM users WHERE id = ?', (user_id,)) nickname = c.fetchone()[0] conn.close() st.session_state["authenticated"] = True st.session_state["username"] = username # 아이디 st.session_state["user_id"] = user_id st.session_state["nickname"] = nickname # 닉네임 st.success("로그인 성공!") st.rerun() else: st.error("아이디 또는 비밀번호가 올바르지 않습니다.") with tab2: with st.form("register_form", border=True): st.markdown("""

회원가입

""", unsafe_allow_html=True) name = st.text_input("닉네임", placeholder="닉네임을 입력하세요") new_username = st.text_input("사용할 아이디", placeholder="아이디를 입력하세요") new_password = st.text_input("사용할 비밀번호", type="password", placeholder="비밀번호를 입력하세요") confirm_password = st.text_input("비밀번호 확인", type="password", placeholder="비밀번호를 다시 입력하세요") email = st.text_input("이메일", placeholder="이메일을 입력하세요") submitted = st.form_submit_button("회원가입", use_container_width=True) if submitted: if not all([new_username, new_password, confirm_password, name, email]): st.error("모든 항목을 입력해주세요.") elif new_password != confirm_password: st.error("비밀번호가 일치하지 않습니다.") elif len(new_password) < 6: st.error("비밀번호는 최소 6자 이상이어야 합니다.") elif not re.match(r"[^@]+@[^@]+\.[^@]+", email): st.error("올바른 이메일 형식이 아닙니다.") else: if register_user(new_username, new_password, email, name): st.success("회원가입이 완료되었습니다! 이제 로그인해주세요.") else: st.error("이미 존재하는 아이디 또는 이메일입니다.") with tab3: st.markdown("""

아이디/비밀번호 찾기

""", unsafe_allow_html=True) find_option = st.radio( "찾으실 항목을 선택하세요", ["아이디 찾기", "비밀번호 재설정"], horizontal=True ) if find_option == "아이디 찾기": with st.form("find_username_form", border=True): name = st.text_input("닉네임", placeholder="가입 시 등록한 닉네임을 입력하세요") email = st.text_input("이메일", placeholder="가입 시 등록한 이메일을 입력하세요") submitted = st.form_submit_button("아이디 찾기", use_container_width=True) if submitted: if not name or not email: st.error("닉네임과 이메일을 모두 입력해주세요.") else: username = find_username(name, email) if username: st.success(f"회원님의 아이디는 **{username}** 입니다.") else: st.error("입력하신 정보와 일치하는 계정이 없습니다.") else: # 비밀번호 재설정 with st.form("reset_password_form", border=True): username = st.text_input("아이디", placeholder="아이디를 입력하세요") email = st.text_input("이메일", placeholder="가입 시 등록한 이메일을 입력하세요") new_password = st.text_input("새 비밀번호", type="password", placeholder="새로운 비밀번호를 입력하세요") confirm_password = st.text_input("새 비밀번호 확인", type="password", placeholder="새로운 비밀번호를 다시 입력하세요") submitted = st.form_submit_button("비밀번호 재설정", use_container_width=True) if submitted: if not all([username, email, new_password, confirm_password]): st.error("모든 항목을 입력해주세요.") elif new_password != confirm_password: st.error("새 비밀번호가 일치하지 않습니다.") elif len(new_password) < 6: st.error("비밀번호는 최소 6자 이상이어야 합니다.") else: if reset_password(username, email, new_password): st.success("비밀번호가 성공적으로 변경되었습니다. 새로운 비밀번호로 로그인해주세요.") else: st.error("입력하신 정보와 일치하는 계정이 없습니다.") def init_session(initial_state: dict = None): if initial_state: for key, value in initial_state.items(): if key not in st.session_state: st.session_state[key] = value def init_score(): if "total_score" not in st.session_state: st.session_state["total_score"] = 0 if "quiz_data" not in st.session_state: st.session_state["quiz_data"] = [] if "answered_questions" not in st.session_state: st.session_state["answered_questions"] = set() if "correct_answers" not in st.session_state: st.session_state["correct_answers"] = 0 if "total_questions" not in st.session_state: st.session_state["total_questions"] = 0 if "learning_history" not in st.session_state: st.session_state["learning_history"] = [] # Initialize quiz-related session state variables if "quiz" not in st.session_state: st.session_state["quiz"] = [] if "answ" not in st.session_state: st.session_state["answ"] = [] if "audio" not in st.session_state: st.session_state["audio"] = [] if "choices" not in st.session_state: st.session_state["choices"] = [] if "img" not in st.session_state: st.session_state["img"] = None if "has_image" not in st.session_state: st.session_state["has_image"] = False if "img_bytes" not in st.session_state: st.session_state["img_bytes"] = None if "current_group" not in st.session_state: st.session_state["current_group"] = "default" if "voice" not in st.session_state: st.session_state["voice"] = "ko-KR-Standard-A" # 기본 음성 설정 def init_question_count(): if "question_count" not in st.session_state: st.session_state["question_count"] = 0 if "max_questions" not in st.session_state: st.session_state["max_questions"] = 10 # 최대 10문제 def can_generate_more_questions() -> bool: return st.session_state.get("question_count", 0) < st.session_state.get("max_questions", 10) def uploaded_image(on_change=None, args=None) -> Image.Image | None: with st.sidebar: st.markdown( "
이미지 업로드
", unsafe_allow_html=True ) # 안내 이미지 표시 (실패해도 계속 진행) try: guide_img = Image.open('img/angmose.jpg').resize((300, 300)) st.markdown( f"""
""", unsafe_allow_html=True ) except Exception as e: st.warning("안내 이미지를 불러올 수 없습니다.") st.markdown( """
이미지를 업로드 하시면
AI가 문장을 생성해 퀴즈를 출제합니다.
문장을 잘 듣고 퀴즈를 풀어보세요.
""", unsafe_allow_html=True ) # 이미지 상태 초기화 if "img_state" not in st.session_state: st.session_state["img_state"] = { "has_image": False, "img_bytes": None, "img": None } # 파일 업로더 uploaded = st.file_uploader( label="", label_visibility="collapsed", on_change=on_change, args=args, type=["jpg", "jpeg", "png", "gif", "bmp", "webp"] ) # 새로 업로드된 이미지가 있는 경우 if uploaded is not None: try: # 이미지 파일 크기 확인 if uploaded.size > 10 * 1024 * 1024: # 10MB 제한 st.error("이미지 파일 크기는 10MB 이하여야 합니다.") return None # 이미지 로드 및 변환 img = Image.open(uploaded) if img.format not in ["JPEG", "PNG", "GIF", "BMP", "WEBP"]: st.error("지원되지 않는 이미지 형식입니다.") return None img = img.convert("RGB") # 이미지를 세션 상태에 저장 buf = BytesIO() img.save(buf, format="PNG") img_bytes = buf.getvalue() # 이미지 상태 업데이트 st.session_state["img_state"] = { "has_image": True, "img_bytes": img_bytes, "img": img } with st.container(border=True): st.image(img, width=300) return img except Exception as e: st.error(f"이미지를 불러올 수 없습니다. 오류: {str(e)}") return None # 이전에 업로드된 이미지가 있는 경우 elif st.session_state["img_state"]["has_image"]: try: img = st.session_state["img_state"]["img"] if img: with st.container(border=True): st.image(img, width=300) return img except Exception as e: st.error("저장된 이미지를 불러올 수 없습니다. 새로운 이미지를 업로드해주세요.") st.session_state["img_state"] = { "has_image": False, "img_bytes": None, "img": None } return None return None # Utility functions def img_to_base64(img: Image.Image) -> str: buf = BytesIO() img.save(buf, format="PNG") return base64.b64encode(buf.getvalue()).decode() def get_prompt(group: str, difficulty: str = None) -> Path: # Map group to exam type exam_mapping = { "yle": "YLE", "toefl_junior": "TOEFL_JUNIOR", "toeic": "TOEIC", "toefl": "TOEFL" } # Map difficulty to exam level difficulty_mapping = { "easy": "easy", "normal": "medium", "hard": "hard" } exam_type = exam_mapping.get(group, "default") exam_level = difficulty_mapping.get(difficulty, "medium") # First try to get the specific exam type and difficulty prompt if difficulty: prompt_file = f"prompt_{exam_type.lower()}_{exam_level}.txt" path = IN_DIR / prompt_file if path.exists(): return path # If no specific prompt found or difficulty not provided, try exam-specific prompt path = IN_DIR / f"prompt_{exam_type.lower()}.txt" if path.exists(): return path # If no exam-specific prompt found, use default st.warning(f"⚠️ '{exam_type}' 시험 유형의 프롬프트가 존재하지 않아 기본값을 사용합니다.") return IN_DIR / "prompt_default.txt" def get_model() -> genai.GenerativeModel: GEMINI_KEY = st.secrets['GEMINI_KEY'] GEMINI_MODEL = "gemini-2.0-flash" genai.configure(api_key=GEMINI_KEY, transport="rest") return genai.GenerativeModel(GEMINI_MODEL) def generate_quiz(img: ImageFile.ImageFile, group: str, difficulty: str): if not can_generate_more_questions(): return None, None, None, None max_retries = 5 # 재시도 횟수 증가 retry_delay = 3 # 대기 시간 증가 for attempt in range(max_retries): try: with st.spinner(f"문제를 생성하는 중입니다... (시도 {attempt + 1}/{max_retries})"): prompt_desc = IN_DIR / "p1_desc.txt" sys_prompt_desc = prompt_desc.read_text(encoding="utf8") model_desc = get_model() resp_desc = model_desc.generate_content( [img, f"{sys_prompt_desc}\nDescribe this image"] ) description = resp_desc.text.strip() quiz_prompt_path = get_prompt(group, difficulty) sys_prompt_quiz = quiz_prompt_path.read_text(encoding="utf8") model_quiz = get_model() # Add exam-specific context to the prompt exam_context = { "elementary": "YLE 시험 형식에 맞춰", "middle": "TOEFL Junior 시험 형식에 맞춰", "high": "TOEIC 시험 형식에 맞춰", "adult": "TOEFL 시험 형식에 맞춰" } exam_type = exam_context.get(group, "") difficulty_context = { "easy": "기초 수준의", "normal": "중급 수준의", "hard": "고급 수준의" } level = difficulty_context.get(difficulty, "중급 수준의") resp_quiz = model_quiz.generate_content( f"{sys_prompt_quiz}\n{exam_type} {level} 문제를 생성해주세요.\n{description}" ) # Try different patterns to match the response quiz_text = resp_quiz.text.strip() # Pattern 1: Standard format with quotes quiz_match = re.search(r'Quiz:\s*["\'](.*?)["\']\s*$', quiz_text, re.MULTILINE) answer_match = re.search(r'Answer:\s*["\'](.*?)["\']\s*$', quiz_text, re.MULTILINE) choices_match = re.search(r'Choices:\s*(\[[^\]]+\](?:,\s*\[[^\]]+\])*)', quiz_text, re.MULTILINE | re.DOTALL) # Pattern 2: Format without quotes if not (quiz_match and answer_match and choices_match): quiz_match = re.search(r'Quiz:\s*(.*?)\s*$', quiz_text, re.MULTILINE) answer_match = re.search(r'Answer:\s*(.*?)\s*$', quiz_text, re.MULTILINE) choices_match = re.search(r'Choices:\s*\[(.*?)\]', quiz_text, re.MULTILINE | re.DOTALL) if choices_match: # Convert comma-separated choices to proper list format choices_str = choices_match.group(1) choices = [choice.strip().strip('"\'') for choice in choices_str.split(',')] choices = [f'"{choice}"' for choice in choices] choices_str = f"[{', '.join(choices)}]" choices_match = type('obj', (object,), {'group': lambda x: choices_str}) if quiz_match and answer_match and choices_match: quiz_sentence = quiz_match.group(1).strip().strip('"\'') answer_word = [answer_match.group(1).strip().strip('"\'')] try: choices = ast.literal_eval(choices_match.group(1)) if isinstance(choices, str): choices = [choice.strip().strip('"\'') for choice in choices.split(',')] except: # If parsing fails, try to extract choices manually choices_str = choices_match.group(1) choices = [choice.strip().strip('"\'') for choice in choices_str.split(',')] original_sentence = quiz_sentence.replace("_____", answer_word[0]) st.session_state["question_count"] = st.session_state.get("question_count", 0) + 1 return quiz_sentence, answer_word, choices, original_sentence # If all parsing attempts fail, raise error with the full response raise ValueError(f"AI 응답 파싱 실패! AI 응답 내용:\n{quiz_text}") except Exception as e: if attempt < max_retries - 1: st.warning(f"문제 생성 중 오류가 발생했습니다. {retry_delay}초 후 다시 시도합니다... ({attempt + 1}/{max_retries})") time.sleep(retry_delay) else: st.error(""" 문제 생성에 실패했습니다. 다음 중 하나를 시도해보세요: 1. 잠시 후 다시 시도 2. 다른 이미지로 시도 3. 페이지 새로고침 """) return None, None, None, None def synth_speech(text: str, voice: str, audio_encoding: str = None) -> bytes: lang_code = "-".join(voice.split("-")[:2]) MP3 = texttospeech.AudioEncoding.MP3 WAV = texttospeech.AudioEncoding.LINEAR16 audio_type = MP3 if audio_encoding == "mp3" else WAV client = tts_client() resp = client.synthesize_speech( input=texttospeech.SynthesisInput(text=text), voice=texttospeech.VoiceSelectionParams(language_code=lang_code, name=voice), audio_config=texttospeech.AudioConfig(audio_encoding=audio_type), ) return resp.audio_content def tts_client() -> texttospeech.TextToSpeechClient: cred = service_account.Credentials.from_service_account_info( st.secrets["gcp_service_account"] ) return texttospeech.TextToSpeechClient(credentials=cred) def tokenize_sent(text: str) -> list[str]: nltk.download(["punkt", "punkt_tab"], quiet=True) return nltk.tokenize.sent_tokenize(text) def set_quiz(img: ImageFile.ImageFile, group: str, difficulty: str): if img and not st.session_state["quiz"]: with st.spinner("이미지 퀴즈를 준비 중입니다...🦜"): # 이미지가 변경되었을 때 question_count 초기화 if "last_image" not in st.session_state or st.session_state["last_image"] != img: st.session_state["question_count"] = 0 st.session_state["last_image"] = img if not can_generate_more_questions(): st.warning(f"현재 {st.session_state['question_count']}문제를 풀었어요! 새로운 이미지를 업로드하면 새로운 문제를 풀 수 있습니다.") return quiz_sentence, answer_word, choices, full_desc = generate_quiz(img, group, difficulty) if not quiz_sentence: # If we've reached the question limit st.warning(f"현재 {st.session_state['question_count']}문제를 풀었어요! 새로운 이미지를 업로드하면 새로운 문제를 풀 수 있습니다.") return if isinstance(choices[0], list): choices = choices[0] answer_words = [answer_word] # Generate simple audio instruction question_audio = "Look at the image carefully." wav_file = synth_speech(question_audio, st.session_state["voice"], "wav") path = OUT_DIR / f"{Path(__file__).stem}.wav" with open(path, "wb") as fp: fp.write(wav_file) quiz_display = f"""이미지를 보고 설명을 잘 들은 후, 빈칸에 들어갈 알맞은 단어를 선택하세요. **{quiz_sentence}**""" st.session_state["img"] = img st.session_state["quiz"] = [quiz_display] st.session_state["answ"] = answer_words st.session_state["audio"] = [path.as_posix()] st.session_state["choices"] = [choices] st.session_state["quiz_data"] = [{ "question": quiz_display, "topic": "지문화", "difficulty": difficulty, "correct": False }] def show_quiz(difficulty="medium"): zipped = zip( range(len(st.session_state["quiz"])), st.session_state["quiz"], st.session_state["answ"], st.session_state["audio"], st.session_state["choices"], ) for idx, quiz, answ, audio, choices in zipped: key_feedback = f"feedback_{idx}" init_session({key_feedback: "", f"submitted_{idx}": False}) with st.form(f"form_question_{idx}", border=True): st.markdown("""

문제

""", unsafe_allow_html=True) st.audio(audio) quiz_display = quiz.replace("**", "").replace( "_____", "_____" ) st.markdown(f"

{quiz_display}

", unsafe_allow_html=True) if not choices: st.error("선택지가 없습니다. 다시 문제를 생성하세요.") continue key_choice = f"choice_{idx}_0" init_session({key_choice: ""}) if st.session_state[key_choice] not in choices: st.session_state[key_choice] = choices[0] user_choice = st.radio( "보기 중 하나를 선택하세요👇", choices, key=key_choice ) submitted = st.form_submit_button("정답 제출 ✅", use_container_width=True) if submitted and not st.session_state.get(f"submitted_{idx}"): st.session_state[f"submitted_{idx}"] = True with st.spinner("채점 중입니다..."): is_correct = user_choice == answ[0] # 마지막 문제 정보 저장 st.session_state["last_question"] = quiz_display st.session_state["last_user_choice"] = user_choice st.session_state["last_correct_answer"] = answ[0] update_score(quiz_display, is_correct) if is_correct: feedback = "✅ 정답입니다! 🎉" else: feedback = f"❌ 오답입니다.\n\n{generate_feedback(user_choice, answ[0])}" st.session_state[key_feedback] = feedback feedback = st.session_state.get(key_feedback, "") if feedback: with st.expander("📚 해설 보기", expanded=True): st.markdown(f"**정답:** {answ[0]}") st.markdown(feedback, unsafe_allow_html=True) def update_score(question: str, is_correct: bool): init_score() # Only update if this question hasn't been answered before if question not in st.session_state["answered_questions"]: st.session_state["answered_questions"].add(question) st.session_state["total_questions"] += 1 # 모든 문제에 대해 문제 수 증가 if is_correct: st.session_state["correct_answers"] += 1 # 최대 100점까지만 점수 추가 if st.session_state["total_score"] < 100: st.session_state["total_score"] += 10 # 현재 문제의 피드백 생성 feedback = generate_feedback( st.session_state.get("last_user_choice", ""), st.session_state.get("last_correct_answer", "") ) if not is_correct else "정답입니다! 🎉" # Add to current quiz data st.session_state["quiz_data"].append({ "question": question, "correct": is_correct, "score": 10 if is_correct else 0, # 정답은 10점, 오답은 0점 "timestamp": pd.Timestamp.now(), "feedback": feedback, "question_content": st.session_state.get("last_question", ""), "user_choice": st.session_state.get("last_user_choice", ""), "correct_answer": st.session_state.get("last_correct_answer", "") }) # Save to database if user is authenticated if st.session_state.get("authenticated") and st.session_state.get("user_id"): # 현재까지 푼 문제 수 계산 current_question_count = len(st.session_state["answered_questions"]) save_learning_history( user_id=st.session_state["user_id"], group_code=st.session_state.get("current_group", "default"), score=10 if is_correct else 0, # 정답은 10점, 오답은 0점 total_questions=current_question_count, # 현재까지 푼 문제 수 question_content=st.session_state.get("last_question", ""), feedback=feedback, user_choice=st.session_state.get("last_user_choice", ""), correct_answer=st.session_state.get("last_correct_answer", "") ) def generate_feedback(user_input: str, answ: str) -> str: try: prompt_path = IN_DIR / "p3_feedback.txt" template = prompt_path.read_text(encoding="utf8") prompt = template.format(user=user_input, correct=answ) model = get_model() response = model.generate_content( f"""다음과 같은 형식으로 피드백을 제공해주세요: 1. 정답과 오답의 관계 설명 2. 간단한 학습 조언 3. 다음에 도움이 될 팁 정답: {answ} 학생 답변: {user_input} 위 형식에 맞춰 한국어로만 답변해주세요. 번역은 하지 마세요.""" ) return response.text.strip() if response and response.text else "(⚠️ 응답 없음)" except Exception as e: return f"(⚠️ 피드백 생성 중 오류: {e})" def show_score_summary(): if "quiz_data" not in st.session_state or not st.session_state["quiz_data"]: return # Get the latest counts from session state total = st.session_state["total_questions"] correct = st.session_state["correct_answers"] accuracy = round((correct / total) * 100, 1) if total else 0.0 score = st.session_state["total_score"] # Show current progress st.info(f"현재 {total}문제를 풀었어요! (정답률: {accuracy}%, 현재 점수: {score}점)") # Only show detailed summary when all 10 questions are answered if total < 10: return # Create a more visually appealing and accessible score display st.markdown("---") # Score header with emoji st.markdown("""

🏆 최종 점수

""", unsafe_allow_html=True) # Create two columns for better layout col1, col2 = st.columns(2) with col1: # Score card with large, clear numbers st.markdown(f"""

최종 점수

{score}점

""", unsafe_allow_html=True) with col2: # Progress card with clear statistics st.markdown(f"""

정답률

{accuracy}%

맞춘 문제: {correct} / {total}

""", unsafe_allow_html=True) # Progress bar with better visibility st.markdown(f"""
""", unsafe_allow_html=True) # Add encouraging message based on performance if total == 10 and correct == 10: st.success("🎉 축하합니다! 100점입니다! 완벽한 성적이에요!") elif accuracy >= 80: st.success("🎉 훌륭해요! 계속 이렇게 잘 해봐요!") elif accuracy >= 60: st.info("👍 잘하고 있어요! 조금만 더 노력해봐요!") else: st.warning("💪 조금 더 연습하면 더 잘할 수 있을 거예요!") clear_all_scores() def reset_quiz(): if st.session_state.get("quiz"): # Add some vertical space before the button st.markdown("
", unsafe_allow_html=True) if st.button("🔄 새로운 문제", type="primary"): # Keep authentication state auth_state = { "authenticated": st.session_state.get("authenticated", False), "username": st.session_state.get("username", ""), "user_id": st.session_state.get("user_id", None) } # Keep image state img_state = st.session_state.get("img_state", { "has_image": False, "img_bytes": None, "img": None }) # Keep score-related states score_state = { "total_score": st.session_state.get("total_score", 0), "quiz_data": st.session_state.get("quiz_data", []), "answered_questions": st.session_state.get("answered_questions", set()), "correct_answers": st.session_state.get("correct_answers", 0), "total_questions": st.session_state.get("total_questions", 0), "question_count": st.session_state.get("question_count", 0), "last_image": st.session_state.get("last_image", None) } # Clear only current quiz states keys_to_clear = ["quiz", "answ", "audio", "choices"] for key in keys_to_clear: if key in st.session_state: del st.session_state[key] # Clear form-related states for key in list(st.session_state.keys()): if key.startswith(("submitted_", "feedback_", "choice_", "form_question_")): del st.session_state[key] # Restore important states st.session_state.update(auth_state) st.session_state["img_state"] = img_state st.session_state.update(score_state) st.rerun() def show_learning_history(): if not st.session_state.get("authenticated") or not st.session_state.get("user_id"): return st.markdown("---") st.markdown("""

📚 학습 기록

""", unsafe_allow_html=True) # Get learning history from database history = get_learning_history(st.session_state["user_id"]) if not history: st.info("아직 학습 기록이 없습니다. 퀴즈를 풀어보세요!") return # 데이터프레임 생성 (모든 컬럼 포함) history_df = pd.DataFrame(history, columns=['group_code', 'score', 'total_questions', 'timestamp', 'question_content', 'feedback', 'user_choice', 'correct_answer']) # 날짜/시간 포맷 변경 history_df['timestamp'] = pd.to_datetime(history_df['timestamp']) history_df['date'] = history_df['timestamp'].dt.strftime('%Y-%m-%d %H:%M') # 시험 유형 이름 매핑 group_name_mapping = { "yle": "YLE", "toefl_junior": "TOEFL JUNIOR", "toeic": "TOEIC", "toefl": "TOEFL" } history_df['group_code'] = history_df['group_code'].map(group_name_mapping) # 정답 여부 표시를 위한 함수 def get_result_icon(row): return "✅" if row['score'] > 0 else "❌" history_df['result'] = history_df.apply(get_result_icon, axis=1) # 누적 점수 계산 (최대 100점) cumulative_score = [] current_score = 0 for s in history_df['score']: if s > 0 and current_score < 100: current_score = min(current_score + 10, 100) cumulative_score.append(current_score) history_df['cumulative_score'] = cumulative_score # 표시할 컬럼만 선택 display_df = history_df[['date', 'group_code', 'result', 'cumulative_score', 'total_questions']] display_df.columns = ['날짜', '시험 유형', '결과', '누적 점수', '문제 수'] if not display_df.empty: st.dataframe( display_df, use_container_width=True, hide_index=True ) else: st.info("학습 기록이 없습니다.") def clear_all_scores(): if st.button("🗑️ 현재 점수 초기화", type="secondary"): # Only clear current score-related data, not learning history st.session_state["total_score"] = 0 st.session_state["quiz_data"] = [] st.session_state["answered_questions"] = set() st.session_state["correct_answers"] = 0 st.session_state["total_questions"] = 0 st.session_state["question_count"] = 0 # Reset question count st.success("현재 점수가 초기화되었습니다.") st.rerun() # Main application if __name__ == "__main__": try: # Initialize page configuration first init_page() # Initialize session state init_score() init_question_count() # 로그인 상태 확인 if not st.session_state.get("authenticated", False): show_auth_page() else: # 메인 페이지 타이틀 st.markdown( """

🔊앵무새 스쿨

영어공인시험 학습 프로그램

""", unsafe_allow_html=True ) # 사이드바에 사용자 정보 표시 with st.sidebar: st.markdown(f"""

👤 닉네임: {st.session_state.get('nickname', '')}

""", unsafe_allow_html=True) # 닉네임 변경 폼 with st.expander("✏️ 닉네임 변경", expanded=False): with st.form("change_nickname_form"): new_nickname = st.text_input( "새로운 닉네임", placeholder="변경할 닉네임을 입력하세요", value=st.session_state.get('nickname', '') ) submitted = st.form_submit_button("변경하기", use_container_width=True) if submitted: if not new_nickname: st.error("닉네임을 입력해주세요.") elif new_nickname == st.session_state.get('nickname'): st.info("현재 사용 중인 닉네임과 동일합니다.") else: if update_username(st.session_state.get('user_id'), new_nickname): st.session_state["nickname"] = new_nickname st.success("닉네임이 변경되었습니다!") st.rerun() else: st.error("이미 사용 중인 닉네임입니다.") if st.button("로그아웃", use_container_width=True): # Clear all session state for key in list(st.session_state.keys()): del st.session_state[key] st.rerun() # 메인 컨텐츠 init_score() init_question_count() # 1. 시험 종류 선택 st.markdown("### 📚 시험 종류 선택") group_display = st.selectbox( "시험 종류를 선택하세요.", ["YLE", "TOEFL JUNIOR", "TOEIC", "TOEFL"], help="선택한 시험 유형에 맞는 퀴즈가 출제됩니다." ) group_mapping = { "YLE": "yle", "TOEFL JUNIOR": "toefl_junior", "TOEIC": "toeic", "TOEFL": "toefl" } group_code = group_mapping.get(group_display, "default") st.session_state["current_group"] = group_code # 2. 난이도 선택 st.markdown("### 🎯 난이도 선택") difficulty_display = st.selectbox( "문제 난이도를 선택하세요.", ["쉬움", "중간", "어려움"], help="선택한 난이도에 따라 문제의 복잡도가 달라집니다." ) difficulty_mapping = { "쉬움": "easy", "중간": "normal", "어려움": "hard" } global_difficulty = difficulty_mapping.get(difficulty_display, "normal") # 3. 이미지 업로드 or 복원 st.markdown("### 🖼️ 이미지 업로드") img = uploaded_image() if img: # 새로운 퀴즈 생성이 필요한 경우 if not st.session_state.get("quiz"): set_quiz(img, group_code, global_difficulty) show_quiz(global_difficulty) if st.session_state.get("quiz_data"): show_score_summary() show_learning_history() reset_quiz() else: st.info("이미지를 업로드하면 퀴즈가 시작됩니다!") except Exception as e: st.error(f"오류가 발생했습니다: {str(e)}") st.info("페이지를 새로고침하거나 다시 시도해주세요.")