cert-study-app / docs /devlog /2026-07-04.md
Kentlo's picture
Sync from GitHub b098d17dbed1a6386c212d28b4bfa6138ad59d75
88c7275 verified
|
Raw
History Blame Contribute Delete
6.75 kB
# ๊ฐœ๋ฐœ์ผ์ง€: 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`๋ฅผ ๋ผ์šฐํŒ…๋งŒ ๋‚จ๊ธฐ๊ธฐ