From dfac980a6e5bca8af5fab6b8b08260680ed1d36e Mon Sep 17 00:00:00 2001 From: no Date: Thu, 15 May 2025 06:44:53 +0000 Subject: [PATCH] Delete combined_app.py --- combined_app.py | 968 ------------------------------------------------ 1 file changed, 968 deletions(-) delete mode 100644 combined_app.py diff --git a/combined_app.py b/combined_app.py deleted file mode 100644 index e268da7..0000000 --- a/combined_app.py +++ /dev/null @@ -1,968 +0,0 @@ -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("페이지를 새로고침하거나 다시 시도해주세요.") \ No newline at end of file