Spaces:
Running
๊ฐ๋ฐ์ผ์ง: 2026-07-04
์ด๋ฒ ์ธ์ ์์ ํ ์ผ
ํฌ๊ฒ ๋ ๊ฐ์ง์๋ค.
- ์ฑ ์คํ ์ค ๋ฐ์ํ๋ API ์๋ฌ ์ฒ๋ฆฌ ๊ฐํ
- ํ์ ์๊ฐ์ผ๋ก ๋ณธ UX ๊ฐ์ 6์ข
1. API ์๋ฌ ์ฒ๋ฆฌ ๊ฐํ
๋ฌธ์
์ฑ์ด ์ผ์ ธ ์๋ ์ํ์์ ํน์ ๊ธฐ๋ฅ์ ์ธ ๋ ํ๋ฉด ์ ์ฒด๊ฐ ์๋ฌ๋ก ๊นจ์ง๋ ํ์์ด ์์๋ค.
์์ธ์ ์ธ ๊ณณ์ด์๋ค.
โ ChromaDB ๋น ์ปฌ๋ ์ crash
QuestionVectorStore.search(k=5)๋ฅผ ํธ์ถํ ๋, ์ปฌ๋ ์
์ ๋ฌธ์๊ฐ 0๊ฐ์ด๋ฉด ChromaDB๊ฐ n_results > n_documents ์๋ฌ๋ฅผ ๋์ง๋ค.
๋ฌธ์ ์์ธ์ ํ ๋ฒ๋ ์ ํ ์ํ์์ AI ๊ฒ์ ๋ฒํผ์ ๋๋ฅด๋ฉด ํ์ด์ง ์ ์ฒด๊ฐ ๊นจ์ก๋ค.
# ์์ ์ โ ๊ทธ๋ฅ 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"] ๋ฃจํ๋ฅผ ๋๋ค๊ฐ ์์ธ๊ฐ ํฐ์ง๋ฉด ํ์ด์ง๊ฐ ๊นจ์ก๋ค.
# ์์ ํ โ ์ ๋๋ ์ดํฐ ๋ด๋ถ์์ 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 ์ค ์ค๋ฅ๊ฐ ๋๋ ์ฌ์ฉ์์๊ฒ ์๋ฌด ์๋ด ์์ด ํ์ด์ง๊ฐ ๊นจ์ก๋ค.
# ์์ ํ โ ์ด๊ธฐํ ์คํจ๋ ์กฐ๊ธฐ ๋ฐํ, 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 ํจํด
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๋ฅผ ๋ผ์ฐํ ๋ง ๋จ๊ธฐ๊ธฐ