# 개발일지: 2026-07-04 ## 이번 세션에서 한 일 크게 두 가지였다. 1. **앱 실행 중 발생하는 API 에러 처리 강화** 2. **학생 시각으로 본 UX 개선 6종** --- ## 1. API 에러 처리 강화 ### 문제 앱이 켜져 있는 상태에서 특정 기능을 쓸 때 화면 전체가 에러로 깨지는 현상이 있었다. 원인은 세 곳이었다. **① ChromaDB 빈 컬렉션 crash** `QuestionVectorStore.search(k=5)`를 호출할 때, 컬렉션에 문서가 0개이면 ChromaDB가 `n_results > n_documents` 에러를 던진다. 문제 색인을 한 번도 안 한 상태에서 AI 검색 버튼을 누르면 페이지 전체가 깨졌다. ```python # 수정 전 — 그냥 query 호출 result = collection.query(query_texts=[query], n_results=k) # 수정 후 — count 체크 후 min(k, count) 사용 count = collection.count() if count == 0: return [] kwargs["n_results"] = min(k, count) result = collection.query(**kwargs) ``` `cert_study_app/services/vector_service.py`의 `search()` 메서드에서 수정했다. **② Ollama 스트리밍 에러 전파** `study_assistant_service.py`의 `stream_chunks()` 제너레이터가 Ollama API 오류를 그냥 올려보냈다. UI(`notes.py`, `streamlit_app.py`)에서 `for chunk in result["stream"]` 루프를 돌다가 예외가 터지면 페이지가 깨졌다. ```python # 수정 후 — 제너레이터 내부에서 catch def stream_chunks(): try: for chunk in chain.stream(...): yield chunk except Exception as exc: yield f"\n\n*(Ollama 응답 오류: {exc})*" ``` **③ render_quiz_assistant 에러 전파** `StudyAssistantService` 초기화(임베딩 모델 로드)나 `ask_stream` 중 오류가 나도 사용자에게 아무 안내 없이 페이지가 깨졌다. ```python # 수정 후 — 초기화 실패는 조기 반환, ask_stream 실패는 st.error() try: service = StudyAssistantService(...) except Exception as exc: st.error(f"AI 검색 서비스 초기화 실패: {exc}") return try: result = service.ask_stream(...) for chunk in result["stream"]: ... except Exception as exc: st.error(f"AI 질문 처리 중 오류: {exc}") st.caption(f"Ollama URL이 실행 중인지, 모델이 설치되어 있는지 확인하세요.") ``` ### 수정 파일 요약 | 파일 | 수정 내용 | |------|-----------| | `cert_study_app/services/vector_service.py` | `search()` — 빈 컬렉션 안전 처리, `n_results = min(k, count)` | | `cert_study_app/services/study_assistant_service.py` | `stream_chunks()` — Ollama 오류 yield로 처리 | | `cert_study_app/services/concept_note_service.py` | `requests.post` — ConnectionError를 RuntimeError로 래핑 | | `cert_study_app/services/visual_question_service.py` | 동일 패턴 | | `streamlit_app.py` | `render_quiz_assistant()` — 초기화·스트리밍 에러 표시 | | `cert_study_app/ui/notes.py` | 동일 패턴 (모듈 분리본) | --- ## 2. UX 개선 6종 학생 시각으로 앱을 쓰면서 불편한 점 6개를 찾고 바로 수정했다. ### 문제 1: "다음에 뭘 해야 하는지 모른다" 홈의 Smart CTA(파란 버튼)가 "개념공부" 랜딩 페이지로 가서 또 버튼을 눌러야 했다. **수정:** 단계별 직접 이동으로 변경 - 이론 카드 안 본 경우 → "이론 카드 시작하기 →" → `이론 학습` 직행 - 이론 카드는 봤고 퀴즈가 남은 경우 → "확인 퀴즈 이어가기 →" → `확인 퀴즈` 직행 ### 문제 2: "오늘 몇 단계인지 숫자만 보인다" Hero 카드에 "오늘 1/3 단계" 텍스트만 있어서 구체적으로 뭘 해야 하는지 몰랐다. **수정:** Hero 카드 아래에 3단계 표시기 추가 ``` ①개념 → ②실습 → ③복습 (완료 시 ✓ 표시) ``` ### 문제 3: "→ 버튼이 너무 작다" 모드 카드(개념공부 / 실습 / 시험준비) 오른쪽에 작은 "→" 버튼만 있어서 모바일에서 누르기 어려웠다. **수정:** 카드 전체를 full-width 버튼으로 교체. 제목·설명·상태를 한 줄에 담고 전체 영역 클릭 가능. ### 문제 4: "이론 카드 완료 후 다음을 모른다" "학습 완료" 버튼을 누르면 성공 메시지가 잠깐 나왔다가 사라지고, 다음 단계 안내가 없었다. **수정:** 완료 직후 "이 레슨 확인 퀴즈 N개 바로 풀기 →" 버튼 표시. 관련 퀴즈가 없으면 "확인 퀴즈 전체 이어가기 →" 표시. ### 문제 5: "퀴즈 정답 후 다음 문제까지 스크롤해야 한다" 설명이 길면 "다음 퀴즈" 버튼이 화면 밖으로 내려가 있었다. **수정:** 설명 바로 아래 "다음 퀴즈 →" 버튼 추가. 마지막 문제면 안내 메시지 표시. ### 문제 6: "실습 완료 후 다음을 모른다" 실습 채점 정답 후 "다음 실습" 버튼이 아래에만 있었다. **수정:** 채점 결과 바로 아래 "다음 실습 →" 버튼 추가. 모든 실습 완료 시 "홈에서 다음 단계로" 안내. --- ## 배운 것 ### ChromaDB count check 패턴 ```python count = collection.count() if count == 0: return [] n_results = min(k, count) # k가 count보다 크면 에러 ``` ChromaDB는 `n_results > n_documents`이면 예외를 던진다. 빈 컬렉션에서 search를 호출하는 것은 실제로 자주 발생하는 상황(첫 실행, 색인 전)이므로 반드시 방어해야 한다. ### Streamlit에서 제너레이터 에러 처리 Streamlit 스트리밍은 `for chunk in generator:` 루프에서 예외가 발생하면 그 시점에 페이지가 깨진다. 제너레이터 내부에서 try/except로 처리하고, 에러를 yield하는 방식이 가장 안전하다. ### UX: "다음 단계 안내"가 학습 흐름의 핵심 공부 앱에서 학생이 가장 자주 겪는 불편은 "지금 뭘 해야 하는지 모른다"이다. 각 화면의 끝에 "다음에 할 것 →" 버튼 하나가 있으면 이탈률이 줄어든다. 완료 후 안내, 채점 후 다음 이동은 모두 이 원칙에서 나온다. --- ## 다음 작업 후보 - **Alembic 마이그레이션 도입** — 현재 `db.py`는 앱 시작마다 `ALTER TABLE`을 실행한다. Alembic으로 교체하면 안전하다. - **Progress 저장 atomic write** — `save_progress()`가 JSON 파일을 통째로 덮어쓴다. `tempfile + os.replace()`로 교체하면 crash 시 데이터 손실을 막을 수 있다. - **Spaced Repetition 통합** — `spaced_review`(int ID)와 `lab_spaced_review`(str ID) 두 구현이 거의 동일하다. 제네릭 클래스 하나로 통합할 수 있다. - **cert_study_app/ui/ 모듈화 완성** — `quiz.py`, `admin.py` 생성 후 `streamlit_app.py`를 라우팅만 남기기