Update combined_app.py
parent
ba7878220e
commit
59805cff64
312
combined_app.py
312
combined_app.py
|
|
@ -21,6 +21,7 @@ import difflib
|
|||
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)
|
||||
|
|
@ -34,6 +35,14 @@ PRESET_IMAGES = [
|
|||
"preset5.png"
|
||||
]
|
||||
|
||||
# 시험/난이도별 최소 음성 길이(초)
|
||||
MIN_AUDIO_SECONDS = {
|
||||
'yle': {'easy': 10, 'normal': 20, 'hard': 25},
|
||||
'toefl_junior': {'easy': 30, 'normal': 40, 'hard': 45},
|
||||
'toeic': {'easy': 50, 'normal': 55, 'hard': 60},
|
||||
'toefl': {'easy': 70, 'normal': 80, 'hard': 90}
|
||||
}
|
||||
|
||||
def init_page():
|
||||
st.set_page_config(
|
||||
page_title="앵무새 스쿨",
|
||||
|
|
@ -147,8 +156,8 @@ def show_auth_page():
|
|||
st.success(f"회원님의 아이디는 **{username}** 입니다.")
|
||||
else:
|
||||
st.error("입력하신 정보와 일치하는 계정이 없습니다.")
|
||||
# 비밀번호 재설정
|
||||
else:
|
||||
|
||||
else: # 비밀번호 재설정
|
||||
with st.form("reset_password_form", border=True):
|
||||
username = st.text_input("아이디", placeholder="아이디를 입력하세요")
|
||||
email = st.text_input("이메일", placeholder="가입 시 등록한 이메일을 입력하세요")
|
||||
|
|
@ -330,12 +339,14 @@ def uploaded_image(on_change=None, args=None) -> Image.Image | 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",
|
||||
|
|
@ -343,6 +354,7 @@ def get_prompt(group: str, difficulty: str = None) -> Path:
|
|||
"toefl": "TOEFL"
|
||||
}
|
||||
|
||||
# Map difficulty to exam level
|
||||
difficulty_mapping = {
|
||||
"easy": "easy",
|
||||
"normal": "medium",
|
||||
|
|
@ -351,6 +363,8 @@ def get_prompt(group: str, difficulty: str = None) -> Path:
|
|||
|
||||
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
|
||||
|
|
@ -381,6 +395,43 @@ def get_model() -> genai.GenerativeModel:
|
|||
)
|
||||
return model
|
||||
|
||||
def estimate_audio_seconds(text):
|
||||
# 한글/영어 혼합 기준: 1초 ≈ 7글자
|
||||
return max(len(text) // 7, 1)
|
||||
|
||||
def get_extra_sentence(group, difficulty):
|
||||
# 시험/난이도별로 난이도 차이 반영한 추가 문장
|
||||
if group == "yle":
|
||||
if difficulty == "easy":
|
||||
return "문제를 다시 한 번 잘 듣고, 보기 중에서 가장 알맞은 답을 골라보세요. "
|
||||
elif difficulty == "normal":
|
||||
return "각 선택지를 주의 깊게 듣고, 문제의 핵심 내용을 파악해 보세요. 정답을 고르기 전에 한 번 더 생각해 보세요. "
|
||||
else:
|
||||
return "문제와 선택지의 세부적인 차이점을 잘 구분해 보세요. 모든 정보를 종합적으로 고려해서 정답을 선택해 보세요. "
|
||||
elif group == "toefl_junior":
|
||||
if difficulty == "easy":
|
||||
return "문제를 잘 듣고, 각 선택지를 꼼꼼히 비교해 보세요. 정답을 찾기 위해 집중해서 들어주세요. "
|
||||
elif difficulty == "normal":
|
||||
return "문제와 선택지를 모두 주의 깊게 듣고, 정답을 고르기 전에 한 번 더 생각해 보세요. 각 선택지의 의미를 잘 파악해 보세요. "
|
||||
else:
|
||||
return "문제와 선택지를 집중해서 듣고, 세부적인 내용까지 신경 써서 정답을 골라보세요. 모든 정보를 종합적으로 고려해 보세요. "
|
||||
elif group == "toeic":
|
||||
if difficulty == "easy":
|
||||
return "문제를 잘 듣고, 각 선택지를 꼼꼼히 비교해 보세요. 정답을 찾기 위해 집중해서 들어주세요. "
|
||||
elif difficulty == "normal":
|
||||
return "문제와 선택지를 모두 주의 깊게 듣고, 정답을 고르기 전에 한 번 더 생각해 보세요. 각 선택지의 의미를 잘 파악해 보세요. "
|
||||
else:
|
||||
return "문제와 선택지를 집중해서 듣고, 세부적인 내용까지 신경 써서 정답을 골라보세요. 모든 정보를 종합적으로 고려해 보세요. "
|
||||
elif group == "toefl":
|
||||
if difficulty == "easy":
|
||||
return "문제를 잘 듣고, 각 선택지를 꼼꼼히 비교해 보세요. 정답을 찾기 위해 집중해서 들어주세요. 예시와 세부 설명을 참고하여 답을 선택해 보세요. "
|
||||
elif difficulty == "normal":
|
||||
return "문제와 선택지를 모두 주의 깊게 듣고, 정답을 고르기 전에 한 번 더 생각해 보세요. 각 선택지의 의미와 맥락을 잘 파악해 보세요. 추가적인 정보와 예시를 활용해 보세요. "
|
||||
else:
|
||||
return "문제와 선택지를 집중해서 듣고, 세부적인 내용까지 신경 써서 정답을 골라보세요. 고급 어휘와 복잡한 상황 설명을 이해하고, 모든 정보를 종합적으로 고려해 보세요. 예시와 추가 설명을 참고하여 답을 선택해 보세요. "
|
||||
else:
|
||||
return "Listen carefully to all the information and take your time to answer. "
|
||||
|
||||
def generate_quiz(img: ImageFile.ImageFile, group: str, difficulty: str):
|
||||
if not can_generate_more_questions():
|
||||
return None, None, None, None
|
||||
|
|
@ -563,7 +614,7 @@ Choices: [
|
|||
or len(correct_answer.strip().split()) == 1
|
||||
or any(len(c.strip().split()) == 1 for c in choices)
|
||||
):
|
||||
continue
|
||||
continue # 재생성
|
||||
|
||||
# 중복 문제 방지: 이미 푼 문제(question)가 있으면 예외 발생시켜 재생성
|
||||
if "answered_questions" in st.session_state and question in st.session_state["answered_questions"]:
|
||||
|
|
@ -629,33 +680,87 @@ def set_quiz(img: ImageFile.ImageFile, group: str, difficulty: str):
|
|||
return
|
||||
if isinstance(choices[0], list):
|
||||
choices = choices[0]
|
||||
# 시험 유형과 난이도에 따른 음성 길이/스타일 조절 (모든 조합 커버, 12가지)
|
||||
# 시험 유형과 난이도에 따른 프리앰블(더 길고 자연스럽게)
|
||||
if group == "yle" and difficulty == "easy":
|
||||
question_audio = f"Listen carefully. {question}"
|
||||
preamble = (
|
||||
"이제 곧 들려드릴 문제는 YLE 시험의 쉬운 난이도에 맞춰 출제되었습니다. "
|
||||
"문제를 주의 깊게 듣고, 각 선택지를 잘 비교해 보세요. "
|
||||
"정확한 답을 고르기 위해 모든 정보를 신중히 생각해 보세요. "
|
||||
)
|
||||
elif group == "yle" and difficulty == "normal":
|
||||
question_audio = f"Listen to the following question and think carefully before you answer. {question}"
|
||||
preamble = (
|
||||
"이제 YLE 시험의 중간 난이도 문제를 들려드리겠습니다. "
|
||||
"문제와 선택지를 모두 주의 깊게 듣고, 정답을 고르기 전에 한 번 더 생각해 보세요. "
|
||||
"각 선택지의 차이점을 잘 파악해 보세요. "
|
||||
)
|
||||
elif group == "yle" and difficulty == "hard":
|
||||
question_audio = f"Listen closely and try to understand all the details in the question. {question}"
|
||||
preamble = (
|
||||
"이제 YLE 시험의 어려운 난이도 문제입니다. "
|
||||
"문제와 선택지를 집중해서 듣고, 세부적인 내용까지 신경 써서 정답을 골라보세요. "
|
||||
"모든 정보를 종합적으로 고려해 보세요. "
|
||||
)
|
||||
elif group == "toefl_junior" and difficulty == "easy":
|
||||
question_audio = f"Please listen to the question and choose the best answer. {question}"
|
||||
preamble = (
|
||||
"TOEFL Junior 시험의 쉬운 난이도 문제입니다. "
|
||||
"문제를 잘 듣고, 각 선택지를 꼼꼼히 비교해 보세요. "
|
||||
"정확한 답을 찾기 위해 집중해서 들어주세요. "
|
||||
)
|
||||
elif group == "toefl_junior" and difficulty == "normal":
|
||||
question_audio = f"Listen carefully to the following question and take your time to answer. {question}"
|
||||
preamble = (
|
||||
"TOEFL Junior 시험의 중간 난이도 문제를 들려드리겠습니다. "
|
||||
"문제와 선택지를 모두 주의 깊게 듣고, 정답을 고르기 전에 한 번 더 생각해 보세요. "
|
||||
"각 선택지의 의미를 잘 파악해 보세요. "
|
||||
)
|
||||
elif group == "toefl_junior" and difficulty == "hard":
|
||||
question_audio = f"Listen very carefully and consider all the information before you answer. {question}"
|
||||
preamble = (
|
||||
"TOEFL Junior 시험의 어려운 난이도 문제입니다. "
|
||||
"문제와 선택지를 집중해서 듣고, 세부적인 내용까지 신경 써서 정답을 골라보세요. "
|
||||
"모든 정보를 종합적으로 고려해 보세요. "
|
||||
)
|
||||
elif group == "toeic" and difficulty == "easy":
|
||||
question_audio = f"Listen to the question and select the most appropriate answer. {question}"
|
||||
preamble = (
|
||||
"TOEIC 시험의 쉬운 난이도 문제입니다. "
|
||||
"문제를 잘 듣고, 각 선택지를 꼼꼼히 비교해 보세요. "
|
||||
"정확한 답을 찾기 위해 집중해서 들어주세요. "
|
||||
)
|
||||
elif group == "toeic" and difficulty == "normal":
|
||||
question_audio = f"Listen carefully and pay attention to the details in the question. {question}"
|
||||
preamble = (
|
||||
"TOEIC 시험의 중간 난이도 문제를 들려드리겠습니다. "
|
||||
"문제와 선택지를 모두 주의 깊게 듣고, 정답을 고르기 전에 한 번 더 생각해 보세요. "
|
||||
"각 선택지의 의미를 잘 파악해 보세요. "
|
||||
)
|
||||
elif group == "toeic" and difficulty == "hard":
|
||||
question_audio = f"Listen to the following question. Consider all the details and select the most accurate answer. {question}"
|
||||
preamble = (
|
||||
"TOEIC 시험의 어려운 난이도 문제입니다. "
|
||||
"문제와 선택지를 집중해서 듣고, 세부적인 내용까지 신경 써서 정답을 골라보세요. "
|
||||
"모든 정보를 종합적으로 고려해 보세요. "
|
||||
)
|
||||
elif group == "toefl" and difficulty == "easy":
|
||||
question_audio = f"Listen to the question and answer carefully. {question}"
|
||||
preamble = (
|
||||
"TOEFL 시험의 쉬운 난이도 문제입니다. "
|
||||
"문제를 잘 듣고, 각 선택지를 꼼꼼히 비교해 보세요. "
|
||||
"정확한 답을 찾기 위해 집중해서 들어주세요. 예시와 세부 설명을 참고하여 답을 선택해 보세요. "
|
||||
)
|
||||
elif group == "toefl" and difficulty == "normal":
|
||||
question_audio = f"Listen carefully to the following question. Think about all the information before answering. {question}"
|
||||
preamble = (
|
||||
"TOEFL 시험의 중간 난이도 문제를 들려드리겠습니다. "
|
||||
"문제와 선택지를 모두 주의 깊게 듣고, 정답을 고르기 전에 한 번 더 생각해 보세요. 각 선택지의 의미와 맥락을 잘 파악해 보세요. 추가적인 정보와 예시를 활용해 보세요. "
|
||||
)
|
||||
elif group == "toefl" and difficulty == "hard":
|
||||
question_audio = f"Listen very carefully. Consider all aspects and select the most accurate answer. {question}"
|
||||
preamble = (
|
||||
"TOEFL 시험의 어려운 난이도 문제입니다. "
|
||||
"문제와 선택지를 집중해서 듣고, 세부적인 내용까지 신경 써서 정답을 골라보세요. 고급 어휘와 복잡한 상황 설명을 이해하고, 모든 정보를 종합적으로 고려해 보세요. 예시와 추가 설명을 참고하여 답을 선택해 보세요. "
|
||||
)
|
||||
else:
|
||||
question_audio = question
|
||||
preamble = "Listen carefully to the following question and all the choices. Take your time to think before answering. "
|
||||
|
||||
question_audio = f"{preamble}{question}"
|
||||
|
||||
# 최소 길이 보장: 부족하면 추가 문장 반복
|
||||
min_sec = MIN_AUDIO_SECONDS.get(group, {}).get(difficulty, 10)
|
||||
while estimate_audio_seconds(question_audio) < min_sec:
|
||||
question_audio += get_extra_sentence(group, difficulty)
|
||||
|
||||
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:
|
||||
|
|
@ -714,16 +819,22 @@ def show_quiz(difficulty="medium"):
|
|||
st.error("선택지가 없습니다. 다시 문제를 생성하세요.")
|
||||
continue
|
||||
|
||||
# 보기 번호와 문장 함께 표시
|
||||
numbered_choices = [f"{i+1}. {c}" for i, c in enumerate(choices)]
|
||||
for nc in numbered_choices:
|
||||
st.markdown(nc)
|
||||
|
||||
key_choice = f"choice_{idx}_0_{quiz_run_id}"
|
||||
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
|
||||
init_session({key_choice: "1"}) # 기본값 1번
|
||||
user_choice_num = st.radio(
|
||||
"번호를 선택하세요👇",
|
||||
["1", "2", "3", "4"],
|
||||
key=key_choice,
|
||||
format_func=lambda x: f"{x}번"
|
||||
)
|
||||
|
||||
user_choice_idx = int(user_choice_num) - 1
|
||||
user_choice = choices[user_choice_idx]
|
||||
|
||||
submitted = st.form_submit_button("정답 제출 ✅", use_container_width=True)
|
||||
|
||||
if submitted and not st.session_state.get(f"submitted_{idx}_{quiz_run_id}"):
|
||||
|
|
@ -732,20 +843,18 @@ def show_quiz(difficulty="medium"):
|
|||
else:
|
||||
st.session_state[f"submitted_{idx}_{quiz_run_id}"] = True
|
||||
with st.spinner("채점 중입니다..."):
|
||||
# 정답 비교는 완전한 문장일 때만
|
||||
if is_valid_sentence(answ[idx]):
|
||||
is_correct = user_choice.strip().lower() == answ[idx].strip().lower()
|
||||
else:
|
||||
is_correct = False
|
||||
# 정답 인덱스
|
||||
correct_idx = choices.index(answ[idx]) if answ[idx] in choices else 0
|
||||
is_correct = (user_choice_idx == correct_idx)
|
||||
st.session_state["last_question"] = quiz
|
||||
st.session_state["last_user_choice"] = user_choice
|
||||
st.session_state["last_correct_answer"] = answ[idx]
|
||||
update_score(quiz, is_correct)
|
||||
|
||||
|
||||
if is_correct:
|
||||
feedback = "정답입니다."
|
||||
feedback = "정답입니다!"
|
||||
else:
|
||||
feedback = f"❌ 오답입니다.\n정답: {answ[idx]}\n\n{generate_feedback(user_choice, answ[idx])}"
|
||||
feedback = f"오답입니다.\n정답: {answ[idx]}\n\n{generate_feedback(user_choice, answ[idx])}"
|
||||
|
||||
st.session_state[key_feedback] = feedback
|
||||
|
||||
|
|
@ -761,48 +870,38 @@ def show_quiz(difficulty="medium"):
|
|||
|
||||
def update_score(question: str, is_correct: bool):
|
||||
init_score()
|
||||
|
||||
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 "정답입니다."
|
||||
|
||||
# Only update if this question hasn't been answered before
|
||||
if question not in [q["question"] for q in st.session_state["quiz_data"]]:
|
||||
st.session_state["quiz_data"].append({
|
||||
"question": question,
|
||||
"correct": is_correct,
|
||||
"score": 10 if is_correct else 0, # 정답은 10점, 오답은 0점
|
||||
"score": 10 if is_correct else 0,
|
||||
"timestamp": pd.Timestamp.now(),
|
||||
"feedback": feedback,
|
||||
"feedback": generate_feedback(
|
||||
st.session_state.get("last_user_choice", ""),
|
||||
st.session_state.get("last_correct_answer", "")
|
||||
) if not is_correct else "정답입니다.",
|
||||
"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"])
|
||||
|
||||
current_question_count = len(st.session_state["quiz_data"])
|
||||
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, # 현재까지 푼 문제 수
|
||||
score=10 if is_correct else 0,
|
||||
total_questions=current_question_count,
|
||||
question_content=st.session_state.get("last_question", ""),
|
||||
feedback=feedback,
|
||||
feedback=st.session_state["quiz_data"][-1]["feedback"],
|
||||
user_choice=st.session_state.get("last_user_choice", ""),
|
||||
correct_answer=st.session_state.get("last_correct_answer", "")
|
||||
)
|
||||
# 항상 동기화
|
||||
st.session_state["total_questions"] = len(st.session_state["quiz_data"])
|
||||
st.session_state["correct_answers"] = sum(1 for q in st.session_state["quiz_data"] if q["correct"])
|
||||
st.session_state["total_score"] = min(sum(q["score"] for q in st.session_state["quiz_data"]), 100)
|
||||
|
||||
def generate_feedback(user_input: str, answ: str) -> str:
|
||||
try:
|
||||
|
|
@ -826,14 +925,12 @@ def generate_feedback(user_input: str, answ: str) -> str:
|
|||
return f"(⚠️ 피드백 생성 중 오류: {e})"
|
||||
|
||||
def show_score_summary():
|
||||
total = st.session_state.get("question_count", 0)
|
||||
total = st.session_state.get("total_questions", 0)
|
||||
correct = st.session_state.get("correct_answers", 0)
|
||||
score = st.session_state.get("total_score", 0)
|
||||
accuracy = round((correct / total) * 100, 1) if total else 0.0
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown(f"<div style='text-align:center;'><b>현재 푼 문제 수:</b> {total} <b>정답률:</b> {accuracy}% <b>현재 점수:</b> {score}점</div>", unsafe_allow_html=True)
|
||||
|
||||
if total == 0:
|
||||
st.info("아직 문제를 풀지 않았어요! 문제를 풀고 점수를 확인해보세요.")
|
||||
elif accuracy == 100 and total > 0:
|
||||
|
|
@ -846,80 +943,45 @@ def show_score_summary():
|
|||
st.info("조금 더 연습하면 더 좋아질 거예요!")
|
||||
|
||||
def reset_quiz():
|
||||
if st.session_state.get("quiz"):
|
||||
st.markdown("<br>", unsafe_allow_html=True)
|
||||
if st.button("🔄 새로운 문제", type="primary"):
|
||||
auth_state = {
|
||||
"authenticated": st.session_state.get("authenticated", False),
|
||||
"username": st.session_state.get("username", ""),
|
||||
"user_id": st.session_state.get("user_id", None)
|
||||
}
|
||||
|
||||
img_state = {
|
||||
"has_image": st.session_state.get("has_image", False),
|
||||
"img_bytes": st.session_state.get("img_bytes", None),
|
||||
"img": st.session_state.get("img", None),
|
||||
"last_image": st.session_state.get("last_image", None),
|
||||
"sidebar_img_choice": st.session_state.get("sidebar_img_choice", None)
|
||||
}
|
||||
|
||||
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),
|
||||
"current_group": st.session_state.get("current_group", "default"),
|
||||
"global_difficulty": st.session_state.get("global_difficulty", "normal")
|
||||
}
|
||||
|
||||
keys_to_clear = ["quiz", "answ", "audio", "choices"]
|
||||
for key in keys_to_clear:
|
||||
if key in st.session_state:
|
||||
del st.session_state[key]
|
||||
|
||||
for key in list(st.session_state.keys()):
|
||||
if key.startswith(("submitted_", "feedback_", "choice_", "form_question_")):
|
||||
del st.session_state[key]
|
||||
|
||||
st.session_state.update(auth_state)
|
||||
st.session_state.update(img_state)
|
||||
st.session_state.update(score_state)
|
||||
|
||||
if st.session_state.get("img") and st.session_state.get("question_count", 0) < 10:
|
||||
set_quiz(
|
||||
st.session_state["img"],
|
||||
st.session_state.get("current_group", "default"),
|
||||
st.session_state.get("global_difficulty", "normal")
|
||||
)
|
||||
|
||||
st.rerun()
|
||||
# 문제 관련 세션 상태만 초기화 (키가 없으면 기본값으로 초기화)
|
||||
keys_defaults = {
|
||||
"quiz": [],
|
||||
"answ": [],
|
||||
"audio": [],
|
||||
"choices": []
|
||||
}
|
||||
for key, default in keys_defaults.items():
|
||||
st.session_state[key] = default
|
||||
# 폼 관련 상태 초기화
|
||||
for key in list(st.session_state.keys()):
|
||||
if key.startswith(("submitted_", "feedback_", "choice_", "form_question_")):
|
||||
del st.session_state[key]
|
||||
# quiz_run_id 갱신
|
||||
st.session_state["quiz_run_id"] = str(time.time())
|
||||
# 문제 생성은 메인 코드에서 처리
|
||||
|
||||
def show_learning_history():
|
||||
if not st.session_state.get("authenticated") or not st.session_state.get("user_id"):
|
||||
return
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown("""
|
||||
<div style='text-align: center; margin-bottom: 20px;'>
|
||||
<h2 style='color: #4B89DC;'>📚 학습 기록</h2>
|
||||
</div>
|
||||
""", 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'])
|
||||
|
||||
if 'score' not in history_df.columns:
|
||||
history_df['score'] = 0
|
||||
history_df['score'] = history_df['score'].fillna(0)
|
||||
# 날짜/시간 포맷 변경
|
||||
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",
|
||||
|
|
@ -928,13 +990,14 @@ def show_learning_history():
|
|||
"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점)
|
||||
# 누적 점수와 문제 수를 정확히 계산
|
||||
n = len(history_df)
|
||||
# 문제 수: 최근 기록이 1, 그 전이 2, ... 마지막이 n
|
||||
total_questions = list(range(n, 0, -1))
|
||||
cumulative_score = []
|
||||
current_score = 0
|
||||
for s in history_df['score']:
|
||||
|
|
@ -942,11 +1005,10 @@ def show_learning_history():
|
|||
current_score = min(current_score + 10, 100)
|
||||
cumulative_score.append(current_score)
|
||||
history_df['cumulative_score'] = cumulative_score
|
||||
|
||||
history_df['total_questions'] = total_questions
|
||||
# 표시할 컬럼만 선택
|
||||
display_df = history_df[['date', 'group_code', 'result', 'cumulative_score', 'total_questions']]
|
||||
display_df.columns = ['날짜', '시험 유형', '결과', '누적 점수', '문제 수']
|
||||
|
||||
if not display_df.empty:
|
||||
st.dataframe(
|
||||
display_df,
|
||||
|
|
@ -968,8 +1030,10 @@ def clear_all_scores():
|
|||
st.success("현재 점수가 초기화되었습니다.")
|
||||
st.rerun()
|
||||
|
||||
# Main application
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# Initialize page configuration first
|
||||
init_page()
|
||||
|
||||
|
||||
|
|
@ -1021,6 +1085,7 @@ if __name__ == "__main__":
|
|||
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()
|
||||
|
|
@ -1103,7 +1168,10 @@ if __name__ == "__main__":
|
|||
if st.session_state.get("question_count", 0) < 10:
|
||||
if st.button("새로운 문제", type="primary"):
|
||||
reset_quiz()
|
||||
st.session_state["quiz_run_id"] = str(time.time())
|
||||
# 새로운 문제 즉시 생성
|
||||
img_choice = st.session_state.get("sidebar_img_choice", PRESET_IMAGES[0])
|
||||
img = Image.open(f"img/{img_choice}")
|
||||
set_quiz(img, st.session_state.get("current_group", "default"), st.session_state.get("global_difficulty", "normal"))
|
||||
st.rerun()
|
||||
else:
|
||||
st.info("이 이미지로 풀 수 있는 최대 10문제를 모두 풀었습니다! 다른 이미지를 선택해 주세요.")
|
||||
|
|
|
|||
Loading…
Reference in New Issue