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