교육 · 웹앱
수업노트
워크시트 생성부터 자동 채점, 학부모 월간 리포트, 녹음 숙제 전사와 발음 평가까지. 학원 강사의 하루를 그대로 옮긴 관리 도구.
- 기간
- 2026.06 —
- 역할
- 기획·설계·구현 단독
- 상태
- 운영 중
- 규모
- 6,639줄
문제
워크시트 제작, 채점, 학부모 보고가 전부 수작업이었다. 수업보다 수업 준비와 뒤처리에 시간이 더 들었다.
해결
강사의 하루 동선을 그대로 화면으로 옮기고, AI가 감당할 수 있는 구간(생성·채점·요약)만 자동화했다. 판단이 필요한 곳에는 사람을 남겼다.
130MB
누적 실사용 데이터
숙제 사진 103MB 포함
실사용 중
상태
문제
학원 강사의 하루는 수업 두 시간, 준비와 뒤처리 네 시간입니다.
- 워크시트를 교재에서 직접 뽑아 만들고
- 걷어서 채점하고
- 누가 뭘 틀렸는지 기억해 뒀다가
- 월말에 학부모에게 보낼 말을 다시 씁니다
각 단계가 어렵지는 않습니다. 다만 매주 반복되고, 기억에 의존하며, 사람이 지치면 품질이 떨어집니다.
설계 결정
| 결정 | 이유 |
|---|---|
| 단일 파일 HTML | 빌드 파이프라인이 없으면 배포가 파일 복사 하나다. 혼자 만들고 혼자 고치는 도구에서 빌드 설정은 순수 비용 |
| AI 키는 브라우저 우선, 워커는 폴백 | 워커 경유가 막혀도 선생님 화면은 계속 돈다. 학생 화면과 무인 크론만 워커가 필요하다 |
| 채점은 서버, 오답 복습은 클라이언트 | 재채점에 AI를 다시 부르면 비용이 계속 든다. 오답 복습은 이미 아는 정답과 비교만 하면 되므로 AI 비용 0 |
| 약점을 사람이 아니라 데이터가 기억 | “이 학생 관계대명사 약해요”가 강사 머릿속에만 있으면 인수인계가 안 된다 |
아키텍처
[선생님 브라우저] 단일 HTML (localStorage: 학생·출결·수업메모)
│
├── Anthropic API 직접 호출 (브라우저 키)
│
▼
┌────────────────────────────┐
│ Cloudflare Worker (프록시) │ 키 없는 학생 화면 · 무인 크론
│ + D1 (제출·채점·약점·교재) │
└────────────────────────────┘
▲
[학생 브라우저] 반 링크로 접속, 워크시트 제출
단일 HTML 5,125줄 + 워커 1,514줄.
핵심 기능
- 워크시트 생성 — 자연어로 요청하거나 반별 진도 버튼을 누르면, 적재된 교재 42권을 근거로 문제를 만듭니다. 제출 기한이 D-day로 붙습니다.
- 약점 학습 루프 — 개념카드(덮고 확인) → 문제 → 서버 자동채점 → 오답 복습(직접 다시 풀기). 틀린 확인요소가 학생별 약점으로 누적됩니다.
- 제출 관리 — 반별 제출 현황, 문항별 오답률, 재제출·오답학습 배지.
- 월간 학부모 보고서 — 출결·숙제·단어·진도에 확인학습 횟수, 평균, 보완 개념, 오답 복습 성과를 붙여 씁니다. “보완할 개념은 긍정 톤으로, 오답을 다시 맞힌 건 칭찬으로”를 프롬프트 규칙으로 고정했습니다.
- 녹음 숙제와 발음 평가 — 학생이 읽은 것을 Whisper가 받아 적고, 발음 점수를 붙입니다.
어려웠던 문제
워커에서만 나는 403
어느 날 워커에서 Anthropic 호출이 전부 403으로 막혔습니다. 반환된 에러가 Anthropic의 정상 에러 형식이 아니었습니다. 정상이라면 {"type":"error","error":{…}}인데 {"error":{"type":"forbidden"}}이 왔습니다.
형식이 다르다는 건 Anthropic이 아니라 그 앞단이 막았다는 뜻이었습니다. 키나 결제 문제가 아니었습니다.
진단법을 만들어 뒀습니다. 브라우저에서 키로 직접 부르고(200이면 키 정상), 워커를 경유해 부릅니다(403이면 경로 차단). 어느 쪽이 문제인지 1분 안에 갈립니다.
대응은 구조로 했습니다. AI 호출이 브라우저 키를 먼저 쓰고 워커는 키가 없을 때만 탑니다. 그래서 워커가 막혀도 선생님 앱은 계속 돕니다.
다른 기기에서 채점이 빈 모범답안으로 돌던 버그
/api/exam/{id} GET이 항상 정답을 제거한 버전을 반환하고 있었습니다. 학생에게 정답이 새지 않게 하려던 조치였는데, 선생님이 다른 기기에서 제출 현황을 열면 그 응답으로 채점이 돌아 모범답안이 비어 있었습니다.
토큰 인증이 붙은 요청이면 정답을 포함한 전체를 반환하도록 갈랐습니다. 같은 엔드포인트가 호출자에 따라 다른 것을 돌려줘야 했던 케이스입니다.
용량을 걱정하기 전에 재보기
“사진 때문에 곧 터지지 않나”라는 걱정이 있었습니다. 재보니 130MB / 5GB, 그중 숙제 사진이 103MB, 증가율은 주당 10MB였습니다. 연 500MB, 몇 년 여유.
그래서 지금은 아무것도 하지 않기로 했습니다. 2~3GB에 도달하면 90일 지난 사진 blob만 지우거나 R2로 옮기면 됩니다. 최적화 시점을 데이터로 정한 셈입니다.
알고 있는 리스크
실사용 중인 도구라 남은 문제도 적어 둡니다.
- 가장 큰 것: localStorage 단일 기기 의존. 학생·출결·수업메모가 선생님 브라우저에만 있습니다. 캐시를 지우거나 기기를 바꾸면 유실됩니다. 다음 작업 1순위는 하루 한 번 D1로 자동 스냅샷을 보내는 백업입니다.
- 학생 이름 자유 입력. 제출·약점·보고서가 학생이 타이핑한 이름으로 묶입니다. 띄어쓰기나 오타가 나면 한 사람의 데이터가 갈라집니다. 명단 선택으로 바꾸거나 서버에서 정규화해야 합니다.
- 오답 판정이 관대함. 부분 문자열 포함도 정답 처리합니다. 기록용이라 치명적이진 않지만, 절반만 써도 “다시 맞힘”이 됩니다.
배운 것
- 에러의 형식이 원인을 말해줍니다. 응답 스키마가 그 서비스의 것이 아니면, 그 서비스가 아니라 앞단을 봐야 합니다.
- 폴백은 기능이 아니라 구조에 넣습니다. “막히면 다른 걸 쓴다”를 호출 지점마다 쓰면 빠뜨립니다. 호출 함수 하나에 넣으면 전부 커버됩니다.
- 최적화는 재보고 정합니다. 용량 걱정에 며칠 쓸 뻔했는데 계산 10분으로 “몇 년 여유”가 나왔습니다.
English Summary
A single-file web app for cram-school teachers, in daily production use. It generates worksheets from a 42-book corpus, auto-grades submissions, accumulates per-student weakness data, transcribes recorded homework with Whisper, scores pronunciation, and drafts monthly parent reports grounded in that data.
Stack: single-file HTML frontend, Cloudflare Workers + D1, Claude API, Whisper, Azure pronunciation assessment. ~6.6k LOC, solo design and build. ~130MB of real production data.
Notable problems solved: diagnosing a 403 that came from an upstream edge, not the API (the error envelope didn’t match the provider’s schema), and a same-endpoint-different-caller bug where answer-stripped payloads silently broke cross-device grading.
학생·학부모 정보는 데모에서 전부 샘플 데이터로 치환