Spaces:
Running
Running
| # ๊ฐ๋ฐ์ผ์ง: 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`๋ฅผ ๋ผ์ฐํ ๋ง ๋จ๊ธฐ๊ธฐ | |