Spaces:
Running
Running
| import streamlit as st | |
| from sqlalchemy import func | |
| from cert_study_app.config import DEFAULT_USER | |
| from cert_study_app.db import SessionLocal | |
| from cert_study_app.models import Question | |
| from cert_study_app.services.docs_source_service import doc_source_by_id | |
| from cert_study_app.services.learning_lab_service import ( | |
| PRACTICE_TASKS, | |
| active_tracks, | |
| certification_for_track, | |
| evaluate_lab_quiz_detail, | |
| evaluate_practice_detail, | |
| lessons_for_track, | |
| quizzes_for_track, | |
| roadmap_for_track, | |
| track_by_id, | |
| track_progress, | |
| ) | |
| from cert_study_app.services.learning_progress_service import ( | |
| lab_spaced_review_due_today, | |
| mark_learning_step, | |
| record_activity, | |
| save_completed_items, | |
| save_wrong_notes, | |
| study_units, | |
| update_lab_spaced_review, | |
| update_spaced_review, | |
| ) | |
| from cert_study_app.services.quiz_service import QuizService | |
| from cert_study_app.services.question_concept_service import concept_label | |
| from cert_study_app.ui.common import go_to, selected_lab_track | |
| def render_roadmap(): | |
| st.subheader("๋ก๋๋งต") | |
| track_id = selected_lab_track() | |
| track = track_by_id(track_id) | |
| certification = certification_for_track(track_id) | |
| st.caption(f"{track['name']} Track ยท {certification['name']} ๋๋น") | |
| steps = roadmap_for_track(track_id) | |
| if not steps: | |
| st.info("์์ง ์ค๋น ์ค์ธ Track์ ๋๋ค.") | |
| return | |
| for index, step in enumerate(steps, 1): | |
| with st.container(border=True): | |
| st.markdown(f"**{index}. {step.title}**") | |
| st.caption(step.level) | |
| st.write(step.description) | |
| def render_theory_learning(): | |
| st.subheader("์ด๋ก ํ์ต") | |
| track_id = selected_lab_track() | |
| track = track_by_id(track_id) | |
| certification = certification_for_track(track_id) | |
| st.caption(f"{track['name']} Track ยท {certification['name']} ๋๋น") | |
| all_lessons = lessons_for_track(track_id) | |
| if not all_lessons: | |
| st.info("์์ง ์น์ธ๋ ์ด๋ก ์นด๋๊ฐ ์์ต๋๋ค.") | |
| return | |
| search_col, level_col, filter_col = st.columns([3, 1, 1]) | |
| search_q = search_col.text_input("๋ ์จ ๊ฒ์", placeholder="ํค์๋ ๋๋ ์ ๋ชฉ ์ ๋ ฅโฆ", label_visibility="collapsed", key="lesson_search") | |
| level_filter = level_col.selectbox("๋ ๋ฒจ", ["์ ์ฒด", "์ ๋ฌธ", "์ค๊ธ", "๊ณ ๊ธ"], key="lesson_level_filter", label_visibility="collapsed") | |
| show_incomplete = filter_col.checkbox("๋ฏธ์๋ฃ๋ง", key="lesson_incomplete_only") | |
| completed_lessons = st.session_state.lab_completed_lessons | |
| lessons = all_lessons | |
| if search_q.strip(): | |
| q = search_q.strip().lower() | |
| lessons = [l for l in lessons if q in l.title.lower() or any(q in kw.lower() for kw in l.keywords) or q in l.summary.lower()] | |
| if level_filter != "์ ์ฒด": | |
| lessons = [l for l in lessons if l.level == level_filter] | |
| if show_incomplete: | |
| lessons = [l for l in lessons if l.id not in completed_lessons] | |
| if not lessons: | |
| st.info("๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.") | |
| return | |
| raw_index = st.session_state.lab_lesson_index | |
| index = min(raw_index, len(lessons) - 1) | |
| lesson = lessons[index] | |
| with st.container(border=True): | |
| _level_badge = {"์ ๋ฌธ": "๐ข ์ ๋ฌธ", "์ค๊ธ": "๐ก ์ค๊ธ", "๊ณ ๊ธ": "๐ด ๊ณ ๊ธ"}.get(lesson.level, lesson.level) | |
| done_chip = "<span class='cert-chip cert-chip-done'>โ ์๋ฃ</span>" if lesson.id in completed_lessons else "" | |
| st.caption(_level_badge) | |
| st.markdown(f"### {done_chip} {lesson.title}", unsafe_allow_html=True) | |
| st.caption(f"์นด๋ {index + 1}/{len(lessons)} ยท {lesson.track}") | |
| st.write(lesson.summary) | |
| if lesson.details: | |
| with st.expander("์์ธํ ๋ณด๊ธฐ", expanded=False): | |
| for detail in lesson.details: | |
| st.write(detail) | |
| if lesson.example: | |
| with st.expander("์์", expanded=False): | |
| st.code(lesson.example) | |
| if lesson.common_mistake: | |
| with st.expander("์์ฃผ ํ๋ฆฌ๋ ํฌ์ธํธ", expanded=False): | |
| st.warning(lesson.common_mistake) | |
| if lesson.keywords: | |
| st.caption("ํค์๋: " + " ยท ".join(lesson.keywords)) | |
| if st.button("์ด ๋ ์จ ์๋ฃ ์ฒดํฌ", type="primary", use_container_width=True): | |
| st.session_state.lab_completed_lessons.add(lesson.id) | |
| save_completed_items( | |
| st.session_state.lab_completed_lessons, | |
| st.session_state.lab_completed_quizzes, | |
| st.session_state.lab_completed_practices, | |
| ) | |
| mark_learning_step(track_id, "lesson") | |
| record_activity(track_id, "lesson", 1) | |
| st.session_state.lab_lesson_just_completed = lesson.id | |
| st.success(f"์๋ฃ ์ฒดํฌ! ์ค๋ ํ๋ {study_units():.1f}๋จ์") | |
| if st.session_state.get("lab_lesson_just_completed") == lesson.id and lesson.related_practices: | |
| all_tasks = [t for t in PRACTICE_TASKS if t.track == track_id and t.status == "approved"] | |
| task_ids = [t.id for t in all_tasks] | |
| related_task_ids = [pid for pid in lesson.related_practices if pid in task_ids] | |
| if related_task_ids: | |
| st.markdown("**๊ด๋ จ ์ค์ต ๋ฐ๋ก ๊ฐ๊ธฐ**") | |
| for pid in related_task_ids: | |
| task_idx = task_ids.index(pid) | |
| task_title = all_tasks[task_idx].title | |
| if st.button(f"์ค์ต: {task_title}", key=f"goto_practice_{pid}", use_container_width=True): | |
| st.session_state.lab_practice_index = task_idx | |
| st.session_state.lab_lesson_just_completed = None | |
| go_to("์ค์ตํ๊ธฐ") | |
| prev_col, next_col = st.columns(2) | |
| if prev_col.button("์ด์ ๋ ์จ", use_container_width=True, disabled=index == 0): | |
| st.session_state.lab_lesson_index = max(0, index - 1) | |
| st.rerun() | |
| if next_col.button("๋ค์ ๋ ์จ", use_container_width=True, disabled=index >= len(lessons) - 1): | |
| st.session_state.lab_lesson_index = min(len(lessons) - 1, index + 1) | |
| st.rerun() | |
| if index == len(lessons) - 1 and len(lessons) > 1: | |
| if st.button("ํ์ธ ํด์ฆ๋ก ์ด๋", use_container_width=True): | |
| go_to("ํ์ธ ํด์ฆ") | |
| def render_learning_quiz(): | |
| st.subheader("ํ์ธ ํด์ฆ") | |
| track_id = selected_lab_track() | |
| track = track_by_id(track_id) | |
| certification = certification_for_track(track_id) | |
| st.caption(f"{track['name']} Track ยท {certification['name']} ๋๋น") | |
| quizzes = quizzes_for_track(track_id) | |
| if not quizzes: | |
| st.info("์์ง ์ค๋น๋ ํ์ธ ํด์ฆ๊ฐ ์์ต๋๋ค.") | |
| return | |
| due_ids = set(lab_spaced_review_due_today()) | |
| due_quizzes = [q for q in quizzes if q.id in due_ids] | |
| other_quizzes = [q for q in quizzes if q.id not in due_ids] | |
| ordered_quizzes = due_quizzes + other_quizzes | |
| if due_quizzes: | |
| st.info(f"์ค๋ ๋ณต์ต ์์ ํด์ฆ {len(due_quizzes)}๊ฐ๊ฐ ์์ ๋ฐฐ์น๋์์ต๋๋ค.") | |
| index = min(st.session_state.lab_quiz_index, len(ordered_quizzes) - 1) | |
| quiz = ordered_quizzes[index] | |
| is_due = quiz.id in due_ids | |
| with st.container(border=True): | |
| badge = "๐ ๋ณต์ต" if is_due else quiz.difficulty | |
| st.caption(f"{quiz.track} ยท {quiz.question_type} ยท {badge}") | |
| st.markdown(f"### ๋ฌธ์ {index + 1}/{len(ordered_quizzes)}") | |
| st.write(quiz.question) | |
| if quiz.question_type == "multiple_choice": | |
| answer = st.radio("๋ต", quiz.options, key=f"lab_quiz_answer_{quiz.id}") | |
| else: | |
| answer = st.text_input("๋ช ๋ น์ด ์ ๋ ฅ", key=f"lab_quiz_answer_{quiz.id}", placeholder="๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํ์ธ์") | |
| source = doc_source_by_id(quiz.source_id) | |
| if source: | |
| st.markdown(f"์ถ์ฒ: [{source.provider} ยท {source.title}]({source.url})") | |
| if st.button("์ ๋ต ํ์ธ", type="primary", use_container_width=True): | |
| record_activity(track_id, "quiz", 1) | |
| correct, detail_tokens = evaluate_lab_quiz_detail(quiz, answer) | |
| if correct: | |
| st.session_state.lab_completed_quizzes.add(quiz.id) | |
| save_completed_items( | |
| st.session_state.lab_completed_lessons, | |
| st.session_state.lab_completed_quizzes, | |
| st.session_state.lab_completed_practices, | |
| ) | |
| mark_learning_step(track_id, "quiz") | |
| st.success("์ ๋ต์ ๋๋ค.") | |
| else: | |
| if quiz.question_type == "command" and len(detail_tokens) > 1: | |
| parts_html = " ".join( | |
| f'<span style="color:{"green" if ok else "red"};font-weight:bold">{tok}</span>' | |
| for tok, ok in detail_tokens | |
| ) | |
| st.error("์ค๋ต์ ๋๋ค.") | |
| st.markdown(f"**์ ๋ต ๋ถ์:** {parts_html} ", unsafe_allow_html=True) | |
| st.caption("์ด๋ก์ = ์ ๋ ฅ๋จ / ๋นจ๊ฐ์ = ๋๋ฝ ๋๋ ์ค๋ฅ") | |
| else: | |
| st.error(f"์ค๋ต์ ๋๋ค. ์ ๋ต: `{quiz.answer}`") | |
| wrong_ids = {item["id"] for item in st.session_state.lab_wrong_notes} | |
| if quiz.id not in wrong_ids: | |
| st.session_state.lab_wrong_notes.append({ | |
| "id": quiz.id, | |
| "item_type": "quiz", | |
| "track": quiz.track, | |
| "question": quiz.question, | |
| "user_answer": str(answer), | |
| "correct_answer": quiz.answer, | |
| "explanation": quiz.explanation, | |
| }) | |
| save_wrong_notes(st.session_state.lab_wrong_notes) | |
| update_lab_spaced_review(quiz.id, correct) | |
| if quiz.question_type == "multiple_choice" and quiz.options: | |
| try: | |
| _db = SessionLocal() | |
| try: | |
| db_q = _db.query(Question).filter(Question.chunk_key == quiz.id).first() | |
| if db_q: | |
| try: | |
| opts = list(quiz.options) | |
| chosen_letter = chr(ord("A") + opts.index(str(answer))) if str(answer) in opts else None | |
| if chosen_letter: | |
| QuizService(_db).answer(db_q.id, chosen_letter, DEFAULT_USER) | |
| except Exception: | |
| pass | |
| update_spaced_review(db_q.id, correct) | |
| finally: | |
| _db.close() | |
| except Exception: | |
| pass | |
| st.markdown('<div class="answer-explanation">', unsafe_allow_html=True) | |
| st.markdown(quiz.explanation) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| if not correct and quiz.lesson_id: | |
| all_lessons = lessons_for_track(track_id) | |
| lesson_ids = [l.id for l in all_lessons] | |
| if quiz.lesson_id in lesson_ids: | |
| if st.button("์ด ๋ ์จ ๋ค์ ๋ณด๊ธฐ", key=f"goto_lesson_{quiz.id}"): | |
| st.session_state.lab_lesson_index = lesson_ids.index(quiz.lesson_id) | |
| go_to("์ด๋ก ํ์ต") | |
| prev_col, next_col = st.columns(2) | |
| if prev_col.button("์ด์ ํด์ฆ", use_container_width=True, disabled=index == 0): | |
| st.session_state.lab_quiz_index = max(0, index - 1) | |
| st.rerun() | |
| if next_col.button("๋ค์ ํด์ฆ", use_container_width=True, disabled=index >= len(ordered_quizzes) - 1): | |
| st.session_state.lab_quiz_index = min(len(ordered_quizzes) - 1, index + 1) | |
| st.rerun() | |
| def render_lab_practice(): | |
| st.subheader("์ค์ตํ๊ธฐ") | |
| st.caption("ํ์ฌ๋ Docker ํฐ๋ฏธ๋์ด ์๋๋ผ fake terminal simulator์ ๋๋ค.") | |
| track_id = selected_lab_track() | |
| tasks = [task for task in PRACTICE_TASKS if task.track == track_id and task.status == "approved"] | |
| if not tasks: | |
| st.info("์ด Track์ ์ค์ต์ ์์ง ์ค๋น ์ค์ ๋๋ค. ํ์ฌ fake terminal ์ค์ต์ Linux / LFCS ์ค์ฌ์ผ๋ก ์ ๊ณตํฉ๋๋ค.") | |
| return | |
| index = min(st.session_state.lab_practice_index, len(tasks) - 1) | |
| task = tasks[index] | |
| with st.container(border=True): | |
| st.caption(f"{task.track} ยท {task.difficulty} ยท {task.status}") | |
| st.markdown(f"### {task.title}") | |
| st.write(task.task_description) | |
| command = st.text_input("ํฐ๋ฏธ๋ ์ ๋ ฅ", key=f"practice_command_{task.id}", placeholder=task.expected_command) | |
| with st.expander("ํํธ", expanded=False): | |
| st.write(task.hint) | |
| if st.button("์ค์ต ์ฑ์ ", type="primary", use_container_width=True): | |
| all_pass, condition_results = evaluate_practice_detail(task, command) | |
| if all_pass: | |
| st.session_state.lab_completed_practices.add(task.id) | |
| save_completed_items( | |
| st.session_state.lab_completed_lessons, | |
| st.session_state.lab_completed_quizzes, | |
| st.session_state.lab_completed_practices, | |
| ) | |
| mark_learning_step(track_id, "apply") | |
| record_activity(track_id, "practice", 1) | |
| st.success(f"์ ๋ต์ ๋๋ค. ์ค๋ ํ๋ {study_units():.1f}๋จ์") | |
| if task.takeaway: | |
| st.info(f"ํต์ฌ ํฌ์ธํธ: {task.takeaway}") | |
| else: | |
| st.error("์์ง ์กฐ๊ฑด์ ๋ง์กฑํ์ง ๋ชปํ์ต๋๋ค.") | |
| for cond, ok in condition_results: | |
| icon = "โ " if ok else "โ" | |
| st.markdown(f"{icon} `{cond}`") | |
| st.markdown('<div class="answer-explanation">', unsafe_allow_html=True) | |
| st.markdown(task.explanation) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| prev_col, next_col = st.columns(2) | |
| if prev_col.button("์ด์ ์ค์ต", use_container_width=True, disabled=index == 0): | |
| st.session_state.lab_practice_index = max(0, index - 1) | |
| st.rerun() | |
| if next_col.button("๋ค์ ์ค์ต", use_container_width=True, disabled=index >= len(tasks) - 1): | |
| st.session_state.lab_practice_index = min(len(tasks) - 1, index + 1) | |
| st.rerun() | |
| def render_progress(): | |
| st.subheader("์ง๋์จ") | |
| completed_lessons = set(st.session_state.lab_completed_lessons) | |
| completed_quizzes = set(st.session_state.lab_completed_quizzes) | |
| completed_practices = set(st.session_state.lab_completed_practices) | |
| for track in active_tracks(): | |
| certification = certification_for_track(track["id"]) | |
| progress = track_progress(track["id"], completed_lessons, completed_quizzes, completed_practices) | |
| with st.container(border=True): | |
| st.markdown(f"**{track['name']}**") | |
| st.caption(f"{track['description']} ยท ๋ชฉํ ์๊ฒฉ์ฆ: {certification['name']}") | |
| st.progress(progress["percent"] / 100 if progress["total"] else 0, text=f"{progress['completed']}/{progress['total']} ์๋ฃ") | |
| st.metric("์ค๋ ์๋ฃํ ํ์ต", len(completed_lessons) + len(completed_quizzes) + len(completed_practices)) | |
| st.markdown("#### AZ-104 ๋ฌธ์ ์ํ ์์ญ ๋ถํฌ") | |
| db = SessionLocal() | |
| try: | |
| rows = ( | |
| db.query(Question.category, func.count(Question.id)) | |
| .filter(Question.source == "AZ-104") | |
| .group_by(Question.category) | |
| .order_by(func.count(Question.id).desc()) | |
| .all() | |
| ) | |
| if not rows: | |
| st.caption("์์ง AZ-104 ๋ฌธ์ ๋ถ๋ฅ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.") | |
| else: | |
| total = sum(count for _category, count in rows) | |
| for category, count in rows: | |
| ratio = count / total if total else 0 | |
| st.progress(ratio, text=f"{concept_label(category)} ยท {count}๋ฌธํญ") | |
| finally: | |
| db.close() | |