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 ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํŽ˜์ด์ง€ ์ „์ฒด๊ฐ€ ๊นจ์กŒ๋‹ค.

# ์ˆ˜์ • ์ „ โ€” ๊ทธ๋ƒฅ 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๋ฅผ ๋ผ์šฐํŒ…๋งŒ ๋‚จ๊ธฐ๊ธฐ