"80점"은 다음 행동을 알려주지 않는다
연습을 끝내고 "80점"이 뜨면, 기분은 알 수 있지만 무엇을 다시 해야 할지는 모릅니다. 점수는 결과이지 처방이 아닙니다. 좋은 연습 도구라면 "그래서 지금 뭘 하면 되는데?"에 답해야 합니다.
그래서 LevelUp Piano는 채점을 곡 단위가 아니라 마디·손 단위로 잘게 기록합니다.
마디와 손으로 쪼갠 데이터
한 세션의 판정 결과를, 각 음이 속한 마디와 손(왼손·오른손)으로 집계합니다. 그러면 "이 곡 80점"이 아니라 "5마디 왼손 정확도가 특히 낮음" 같은 구체적 지도가 나옵니다.
약점은 어떻게 계산하나
각 (마디, 손) 버킷마다 Perfect·Good·Bad·Miss 개수를 세고, 정확도를 이렇게 매깁니다 — 애매하게 맞은 Good은 절반만 인정합니다.
// (마디, 손) 버킷의 정확도 — Good은 0.5로 가중
fun measureAccuracy(s: MeasureStat): Double =
if (s.total <= 0) 0.0 else (s.perfect + s.good * 0.5) / s.total이 정확도가 낮은 버킷이 곧 약점입니다. 연속된 약한 마디를 하나로 묶어 "3~7마디 왼손" 같은 타겟 재연습 후보로 제안합니다.
약점 → 타겟 재연습

이 지도에서 곧바로 행동을 제안합니다. 사용자는 곡 전체를 처음부터 반복하는 대신, 필요한 부분만 다시 칠 수 있습니다.
- 마디 범위 선택 — 예: 3~7마디만
- 손 선택 — 왼손만 / 오른손만 / 양손
- 템포 조절 — 느리게 시작해 올리기
- 루프 — 그 구간을 반복
"곡 전체를 열 번"보다 "약한 5마디를 왼손으로 느리게 열 번"이 훨씬 효율적입니다. 진단이 곧 재연습 설정으로 이어지도록 설계해, 발견과 실행 사이의 마찰을 없앴습니다.
간격 반복 — 잊을 때쯤 다시
한 번 잘 쳤다고 끝이 아닙니다. 사람은 잊습니다. 그래서 간격 반복을 붙였습니다. 어학 앱의 복습 카드와 같은 원리(SM-2 계열)를 피아노 마디에 적용한 것입니다.
마디마다 복습 스케줄을 두고, 정확도가 통과선(80%) 이상이면 다음 간격으로 넘어가고, 미달이면 처음으로 되돌립니다. 간격은 1일 → 3일 → 7일 → 14일로 늘어납니다.
private val reviewIntervals = listOf(1, 3, 7, 14) // days
// 통과(정확도 ≥ 0.8)하면 reps 전진, 실패하면 0으로 리셋 → 1일부터 다시
val nextReps = if (accuracy >= 0.8) reps + 1 else 0
val dueAt = now.plusDays(reviewIntervals[nextReps.coerceIn(0, 3)])기한이 된 마디는 한 번에 최대 5개까지 꺼내 "오늘 복습할 마디"로 제안합니다.
서사가 되는 진도: 마스터리와 주간 리캡
낱낱의 데이터는 서사로 묶여야 동기가 됩니다.
- 마스터리 뷰 — 곡·구간별로 얼마나 익었는지를 한눈에.
- 주간 리캡 — 이번 주 무엇을 얼마나 연습했는지 요약해, "내가 나아가고 있다"는 감각을 줍니다.
데이터는 가볍게
마디 단위 통계는 세션마다 쌓이면 금세 방대해집니다. 그래서 (마디, 손) 통계는 곡·학생별로 최근 10세션만 보존하고 그보다 오래된 세션은 지웁니다. 진단에는 오래된 전체 이력보다 최근 상태가 중요하니까요. 적응형 학습은 "모든 걸 영원히 저장"이 아니라 "지금 결정에 필요한 만큼"이면 충분합니다.
출시 초기라 "타겟 재연습이 실제로 정확도를 얼마나 올렸는가"는 아직 측정하지 못했습니다. 검증할 지표는 명확합니다 — 같은 (마디, 손)의 재연습 전후 정확도 상승, 복습 재방문율, 전체곡 재시도 통과율.
점수는 결과이지 처방이 아니다. 마디·손 해상도로 잡은 데이터라야 "여기를, 이렇게 다시"라는 실행 가능한 처방으로 바뀐다.
마치며
적응형 학습의 핵심은 화려한 AI가 아니라 올바른 입도(granularity) 였습니다. 곡이 아니라 마디, 통짜가 아니라 손 — 이 해상도로 데이터를 잡으니, 진단이 곧 실행 가능한 처방이 되고, 간격 반복이 그 처방을 습관으로 굳힙니다. "다시 하라"가 아니라 "여기를, 이렇게 다시 하라"고 말해주는 것이 목표였습니다.
"80 points" doesn't tell you your next move
Finish practicing and see "80 points," and you know how you feel — but not what to redo. A score is an outcome, not a prescription. A good practice tool has to answer "so what do I do now?"
So LevelUp Piano records grading not per song but finely, at the measure and hand level.
Data split by measure and hand
A session's judgment results are aggregated by the measure each note belongs to and by hand (left/right). Instead of "80 on this song," you get a concrete map: "measure 5, left hand accuracy is especially low."
How is weakness computed?
For each (measure, hand) bucket, count the Perfect/Good/Bad/Miss hits and score accuracy like this — a marginal Good only counts half.
// Accuracy of a (measure, hand) bucket — Good is weighted 0.5
fun measureAccuracy(s: MeasureStat): Double =
if (s.total <= 0) 0.0 else (s.perfect + s.good * 0.5) / s.totalLow-accuracy buckets are the weak spots. Consecutive weak measures are merged into a single targeted-practice candidate like "measures 3–7, left hand."
Weakness → targeted re-practice

From that map, the app suggests action immediately. Instead of repeating the whole song from the top, the user can redo only what's needed.
- Measure range — e.g., just measures 3–7
- Hand — left only / right only / both
- Tempo — start slow, ramp up
- Loop — repeat that section
"The weak 5 measures, left hand, slow, ten times" beats "the whole song, ten times." By wiring diagnosis directly into re-practice settings, we removed the friction between discovering a problem and acting on it.
Spaced repetition — again, just as you'd forget
Playing it well once isn't the end. People forget. So there's spaced repetition — the review-card principle of language apps (SM-2 style), applied to piano measures.
Each measure carries a review schedule: if accuracy clears the pass line (80%) it advances to the next interval; if not, it resets to the start. The intervals grow 1 → 3 → 7 → 14 days.
private val reviewIntervals = listOf(1, 3, 7, 14) // days
// pass (accuracy ≥ 0.8) advances reps; a fail resets to 0 → back to 1 day
val nextReps = if (accuracy >= 0.8) reps + 1 else 0
val dueAt = now.plusDays(reviewIntervals[nextReps.coerceIn(0, 3)])Measures that come due are pulled up to five at a time as "measures to review today."
Progress as a story: mastery and weekly recap
Individual data points only motivate once bound into a narrative.
- Mastery view — how well you've learned each song and section, at a glance.
- Weekly recap — a summary of what and how much you practiced this week, giving the feeling of "I'm moving forward."
Keep the data light
Measure-level stats pile up fast across sessions. So per song and student, (measure, hand) stats keep only the most recent 10 sessions and older ones are pruned — for diagnosis, recent state matters more than the full history. Adaptive learning isn't "store everything forever" — "as much as this decision needs" is enough.
It's early post-launch, so "how much did targeted re-practice actually raise accuracy?" isn't measured yet. The metrics to verify are clear — before/after accuracy gains on the same (measure, hand), review return rate, and full-song retry pass rate.
A score is an outcome, not a prescription. Only data captured at measure-and-hand resolution turns into an actionable "redo this, this way."
Closing
The heart of adaptive learning wasn't fancy AI but the right granularity. Measures, not songs; per-hand, not lumped together — capturing data at that resolution turns diagnosis into an actionable prescription, and spaced repetition hardens that prescription into a habit. The goal was to say not "do it again," but "redo this, this way."