본문으로 건너뛰기
강홍재/ James
← Work
MVP2026 · Solo Builder· Started(First Commit date)

Lecturenote AI

강의 슬라이드와 수업 전사록을 슬라이드 단위로 붙여 학습 노트를 만드는 웹앱. 3개월 만에 학기 관리 도구까지 커졌지만, "노트가 맞는지"를 재는 지표는 끝까지 만들지 못했다.

  • React
  • FastAPI
  • Whisper
  • Claude
  • RAG
  • QA

Setup

Problem

대학원 수업을 들으면서 매 학기 같은 일을 반복했다. 교수 슬라이드는 PDF나 PPT로 따로 있고, 수업에서 실제로 나온 설명은 녹음에만 있고, 내 필기는 또 다른 데 있다. 시험 기간이 되면 이 셋을 다시 손으로 붙이는 작업부터 시작하는데, 그게 공부가 아니라 정리 노동이었다. 그래서 노트의 물리적 단위를 먼저 못 박았다. "한 장 = 세로 A4, 위 1/3은 슬라이드 이미지, 아래 2/3은 줄 노트." 종이에 쓰던 포맷을 화면·인쇄·PDF 세 곳에서 똑같이 재현하고, 아래 줄 노트를 전사록에서 자동으로 채우는 것이 처음 세운 목표였다. 그런데 만들자마자 진짜 문제가 드러났다. 노트가 나오긴 나오는데, 그게 맞는 노트인지 내가 판단할 방법이 없었다. 슬라이드 하나에 붙은 문장이 실제로 그 슬라이드에서 나온 말인지, 모델이 지어낸 말인지를 매번 눈으로 확인해야 했다. 이 프로젝트의 미해결 관문은 처음부터 기능이 아니라 여기였다.

Context

혼자 만들었고, 내가 그 학기의 실사용자였다. 도그푸딩 밀도는 높았다 - 로컬 DB에 노트 64개, 슬라이드 1,891장, 실제 수강 과목 5개가 쌓였고, 그만큼 불편한 지점이 매주 새로 나왔다. 문제는 그 불편이 전부 "기능이 없어서" 생긴 것처럼 보였다는 점이다. 캘린더가 없어서, 시간표가 없어서, 예상문제가 없어서. 그래서 손이 계속 기능 쪽으로 갔다. 비용과 의존성은 처음부터 낮게 잡았다. LLM은 로컬 Ollama를 먼저 시도하고 실패하면 Claude로 폴백하도록 라우팅했고, AI 키가 아예 없으면 전체 파이프라인이 mock으로 끝까지 돌아가게 만들었다. 6월 초에는 페르소나 6인 라운드테이블 형식으로 제품 방향을 한 번 수렴시켜, "슬라이드-전사록 정렬 품질"과 "학기 단위 그래프" 중 후자를 축으로 택하고 전자는 코드는 유지하되 투자 동결로 처리했다. 지금 돌아보면 이 결정이 정렬 정확도를 측정하지 않아도 되는 명분을 만들어준 면이 있다.

Users

실사용자는 나 한 명이다. DB에 계정이 둘 있지만 하나는 내 계정이고 하나는 더미 테스트 계정이다. 초기 전략 문서에는 베타 사용자 모집 목표가 적혀 있지만, 가입 로그·인터뷰 노트·피드백 이슈 어느 쪽에서도 실행 흔적을 찾지 못했다. 즉 지금 시점에서 이 도구를 검증한 것은 내 한 학기치 사용뿐이다.

Hypothesis

슬라이드와 전사록을 슬라이드 단위로 정렬해 노트 초안을 자동으로 채워주면, 시험 전에 자료를 다시 붙이는 정리 노동이 사라지고 그 시간이 실제 공부로 넘어간다.

Build

What I did
  • PDF/PPT/PPTX 업로드 후 슬라이드를 이미지로 렌더하고 텍스트를 추출. 한 페이지에 2장이 들어간 인쇄본은 자동 감지해 위/아래로 분할하고, 바깥 흰 여백은 박스 기준으로 크롭
  • 전사록 입력 경로를 셋으로 - 오디오 업로드 후 Whisper STT, 전사록 파일 업로드, 텍스트 직접 붙여넣기. 녹음이 없어도 노트 생성이 되도록
  • 전사록을 청킹·임베딩해 슬라이드별로 매칭하고, 생성 전에 매칭 결과만 먼저 보는 미리보기를 둠. 표지·목차 슬라이드는 노트 대상에서 자동 제외
  • 노트 편집기 - 슬라이드 단위 페이저, 포커스 아웃 자동 저장, 슬라이드별 변경 이력을 diff 형식으로 보고 과거 버전으로 복원
  • 년도 > 학기 > 과목 트리와 캘린더 홈. 시험을 날짜가 아니라 "중간 8주차"처럼 주차로 입력받아 개강일에서 날짜를 역산
  • 아이패드 펜 필기 오버레이(펜·형광펜·지우개·필압)를 정규화 좌표로 저장하고, 필기를 이미지로 렌더해 비전 모델 OCR로 노트에 텍스트로 합류
  • PDF 내보내기(슬라이드 + 노트를 한 페이지에 합성), 과목 단위 치팅페이퍼 PDF, 예상문제 생성, 프로젝트 코퍼스 기반 Q&A. 그 위에 운영 골격으로 Google SSO + 관리자 승인, 계정별 데이터 분리, 플랜·쿼터 게이트, 읽기 전용 공유 링크, 7개 컴포넌트를 5분 주기로 샘플링하는 공개 상태 페이지
Product decisions
  • 노트의 단위를 "세로 A4 한 장, 위 1/3 슬라이드 + 아래 2/3 줄 노트"로 고정하고 화면·인쇄·PDF 세 곳에서 동일하게 유지 - 사용자가 결국 인쇄해서 보는 물건이라, 화면에서만 예쁜 레이아웃은 의미가 없다고 봤다
  • 비용이 드는 경로를 기본값에서 뺐다 - LLM은 로컬 Ollama를 먼저 시도하고 미연결이면 Claude로 폴백하며, AI 키가 아예 없으면 전체가 mock으로 끝까지 돈다. 업로드부터 노트 생성까지 파이프라인을 키 없이 돌려볼 수 있어야 개발 중에 매번 비용과 네트워크에 묶이지 않는다
  • 음성을 필수로 두지 않음 - 전사록 파일과 직접 입력을 나란히 두었고, 나중에는 전사록 직접 입력을 기본으로 올렸다. 결과적으로 이 결정이 원래 컨셉인 "슬라이드 + 녹음 한 쌍"을 조용히 옆으로 밀어냈다
  • 시험을 날짜가 아니라 주차로 입력받는 결정 - 개강일 + 요일 + 주차로 역산하므로 학기가 바뀌어도 개강일 하나만 고치면 전체가 따라온다. 대신 개강일 계산이 단일 실패점이 되고, 실제로 8월에 여기서 회귀가 났다
  • 쿼터 사용량을 별도 카운터가 아니라 "이번 달 생성된 프로젝트 수"로 계산 - 생성이 실패해 롤백되면 사용량도 같이 사라진다. 카운터 차감 누락이라는 버그 유형 자체를 구조에서 없앤 선택
  • 혼자 하는 프로젝트인데도 116개 PR을 전부 기능 브랜치에서 PR로 처리하고 main 직푸시를 하지 않음 - 되돌릴 단위를 남기기 위해서였고, 실제로 기능 하나를 넣었다가 3분 뒤 revert하고 30분 뒤 다른 형태로 다시 넣은 적이 있다
QA considerations
  • STT 전사록을 슬라이드 경계에 매칭할 때 오프셋이 밀려 노트가 엉뚱한 슬라이드에 붙지 않는가
  • Whisper 오인식·긴 무음·화자 겹침 같은 오디오 엣지 케이스에서 매칭이 견고한가
  • 모델이 전사록에 없는 내용을 지어내지 않는가 - 6월 15일 하루에 JSON 스캐폴딩 누출, 메타키 누출, 동일 내용 반복 생성, 원문 echo 회귀를 연속으로 잡아 방어 파서(_strip_json_scaffold, _is_meta_key, _dedupe_lines 등)로 굳혔다
  • 발화 없이 넘어간 슬라이드(빈 전사록 구간)에서 억지 노트를 만들지 않고 비워두는가
  • LLM 파라미터가 출력 스키마를 깨뜨리지 않는가 - repeat_penalty 1.3이 JSON 배열의 반복 키(note)까지 페널티해 모델이 다른 키로 드리프트했다. 페널티를 1.15로 낮추고 중복 제거 책임은 별도 함수로 분리해, 생성 파라미터와 스키마 안정성을 떼어놨다
  • 학기 주차 역산의 날짜 경계값 - 2학기 기본 개강일이 "9월 첫 월요일"이라 9월 1일이 화요일인 해에는 9월 첫 주가 캘린더에서 통째로 비었다. 재현 조건, 잘못된 값, 고친 값, 그리고 1학기는 영향 없음까지 커밋에 적어뒀다
  • 진행률과 남은 시간이 사용자에게 거짓말을 하지 않는가 - ETA가 시간이 갈수록 늘어나던 문제를 카운트다운으로 고쳤다. 백그라운드 작업에서는 실제 진행보다 화면에 뜬 숫자의 신뢰성이 먼저 깨진다
  • 슬라이드별 노트 정확도를 무엇으로 측정하고 회귀를 막을 것인가 - 지표 없이는 "만들기만 계속"이 되는 이 프로젝트의 미해결 관문이고, 3개월이 지난 지금도 답을 못 냈다

Outcome

Metrics
  • 커밋 246개 (2026-06-02 ~ 2026-08-14). 월별로 6월 236개, 7월 0개, 8월 10개
  • 머지된 PR 116개. 전부 기능 브랜치에서 PR로, main 직푸시 0회
  • API 엔드포인트 70개, 추적 소스 17,462줄, DB 테이블 14개, Alembic 마이그레이션 18개
  • 자동화 테스트 0개. 테스트 파일 0개, pytest/vitest/jest/playwright 의존성 0개, CI 설정 0개
  • 로컬 도그푸딩 데이터 - 노트 64개, 슬라이드 1,891장, 과목 5개, 계정 2개(본인 + 더미 테스트 계정)
  • 배포 0회. 공개 URL 없음, 저장소 private, 결제 연동 없음(플랜·쿼터 스키마만 존재)
  • AI 노트 편집율(NSM) 엔드포인트는 만들어져 있으나 실제로 돌린 수치가 없다 - 측정 전. 전사록 테이블 0건, 예상문제·치팅페이퍼·공유링크 저장 기록 0건 - 만든 기능 중 로컬 데이터상 한 번도 쌓이지 않은 것들
Result / Learning

결과부터 정직하게 쓰면, 제품은 커졌고 관문은 그대로다. 6월에 시작한 두 기능짜리 노트 생성기가 3개월 만에 년도·학기·과목 트리와 캘린더, 시간표, 시험 주차 역산, 예상문제, 치팅페이퍼, 펜 필기와 OCR, 계정과 쿼터, 공유 링크, 공개 상태 페이지까지 붙은 학습 관리 도구가 됐다. 커밋 246개, PR 116개, 엔드포인트 70개. 내 저장소 중 가장 큰 제품이다. 그런데 이 프로젝트를 처음 정리할 때 적어둔 미해결 관문 - "슬라이드별 노트 정확도를 무엇으로 측정할 것인가" - 에는 3개월 동안 한 줄도 답하지 못했다. 편집율 엔드포인트를 만들어두기까지 했는데 한 번도 돌려서 수치를 남기지 않았다. 지표가 없으니 "노트가 좋아졌는가"라는 질문에 답할 수 없었고, 답할 수 없는 질문 대신 답할 수 있는 질문(다음에 뭘 만들까)으로 계속 옮겨갔다. 이게 정확히 내가 스스로 경고해뒀던 "만들기만 계속"이다. 더 아픈 신호도 있다. 로컬 DB의 전사록 테이블이 비어 있어서, 최근 노트 64개는 사실상 슬라이드 텍스트만으로 만들어진 것으로 보인다. 원래 가설의 핵심이던 "슬라이드 + 녹음 한 쌍"이 정작 내 실사용에서는 거의 쓰이지 않은 셈이다. 가설의 중심부를 검증하지 않은 채 주변부를 3개월간 넓혔다는 뜻이고, 규모 숫자는 그걸 가려주지 못한다.

Retrospective
  • 측정을 미룬 게 아니라 결정을 미룬 것이었다. 노트 정확도를 못 재니 "지금 것이 충분히 좋은가"를 판정할 수 없었고, 판정을 못 하니 접거나 고치는 대신 계속 붙이는 쪽으로만 손이 갔다. 다음에는 노트를 100장 만들기 전에 20장에 대한 채점 기준부터 만들겠다.
  • 테스트 0개를 속도로 정당화했고, 8월에 날짜 회귀로 대가를 치렀다. 다른 프로젝트에서는 시간 계산 로직을 순수 함수로 빼서 경계값을 테스트로 고정했는데 여기서는 안 했다. 그 결과 개강일 회귀도 테스트 없이 코드만 고쳤고, 같은 결함이 다시 안 난다는 보장이 지금도 없다. 최소한 주차 역산과 슬라이드 분할 판정만이라도 순수 함수로 빼서 테스트를 붙였어야 했다.
  • 6월 라운드테이블에서 "정렬 품질" 대신 "학기 그래프"를 축으로 택한 결정은 방향으로는 납득되지만, 부작용으로 정렬 정확도를 측정하지 않을 명분이 됐다. 축을 옮기더라도 옮기기 전 축의 품질 기준은 남겨뒀어야 했다.
Tech stack
  • React 18 + TypeScript + Vite 6
  • Tailwind CSS v4
  • FastAPI + Python
  • SQLAlchemy 2.0 + Pydantic v2
  • PostgreSQL 16 / SQLite + Alembic
  • PyMuPDF + Pillow + LibreOffice (슬라이드 렌더)
  • OpenAI Whisper API / whisper CLI (STT)
  • Anthropic Claude (노트·문제·OCR·Q&A)
  • Ollama 로컬 LLM/VLM/임베딩 라우팅
  • 자체 RAG (전사록 청킹 + 임베딩 + 코사인 검색)
  • PyJWT + google-auth (Google SSO)
  • Docker Compose (postgres + backend + nginx)