
왜 역할을 나눴나
피아노 연습이라는 같은 목적을 두고도, 사람마다 관계가 다릅니다. 선생님에게 배우는 사람이 있고, 혼자 하는 사람이 있고, 가르치는 사람이 있습니다. 이들을 하나의 화면에 욱여넣으면 누구에게도 맞지 않는 앱이 됩니다.
그래서 LevelUp Piano는 처음부터 역할(role) 을 1급 개념으로 두었습니다. 로그인하면 자신이 누구인지 고르고, 이후의 홈 화면·권한·데이터가 그에 맞게 흐릅니다.
네 역할
- 독립 학습자(Learner) — 선생님 없이 혼자 배우는 일반 사용자. 구독으로 커리큘럼과 카탈로그를 이용하고, 게이미피케이션이 습관을 만듭니다.
- 학생(Student) — 선생님에게 배정받은 곡을 연습합니다. 선생님이 진도와 약점을 봅니다.
- 선생님(Teacher) — MIDI로 곡을 만들어 학생에게 배정하고, 학생 목록·레슨·코칭을 관리합니다.
- 운영자(Operator) — 모두가 공유하는 공식 곡 카탈로그를 관리하는 내부 스태프 역할입니다.
역할마다 접근 권한이 다릅니다. 이 경계는 화면에 흩뿌린 조건문이 아니라 서버가 라우트마다 역할을 확인해 강제합니다(권한이 없으면 403).
| 권한 | Learner | Student | Teacher | Operator |
|---|---|---|---|---|
| 곡 조회 | 카탈로그 | 배정 곡 | 자기 곡 | 전체 |
| 곡 업로드 | — | — | ✓ | ✓ |
| 학생 진도 조회 | — | — | 배정 학생 | — |
| 공식 카탈로그 게시 | — | — | — | ✓ |
| 점수 제출 | ✓ | ✓ | 테스트(미제출) | — |
학생 vs 독립 학습자 — 닮았지만 다른

가장 흥미로운 지점은 학생과 독립 학습자의 관계였습니다. 둘 다 "연주하고 점수를 받는" 플레이어지만, 곡이 오는 곳이 다릅니다.
- 학생의 곡은 선생님이 배정한 곡에서 옵니다.
- 독립 학습자의 곡은 공식 카탈로그의 커리큘럼에서 옵니다.
연습 화면과 판정 엔진, 결과·셀러브레이션은 완전히 같아야 하고, 곡의 출처만 달라야 합니다. 그래서 공용 플레이어 코어(진행 읽기·게임플레이·결과)를 두고, 그 위에 역할별 데이터 소스와 홈 셸을 얹는 구조로 잡았습니다. 코어는 곡이 어디서 오는지 모릅니다 — 배정 곡이든 카탈로그든 같은 계약을 구현할 뿐입니다(구조를 단순화하면 이렇습니다).
// 플레이어 코어는 곡의 출처를 모른다 — 이 계약만 안다.
abstract interface class SongSource {
Future<List<PlayableSong>> load(); // Student → 배정 곡, Learner → 카탈로그
}두 역할이 같은 엔진을 공유하니, 게임플레이를 개선하면 양쪽이 동시에 좋아집니다.
역할별 셸
각 역할은 자기만의 셸(홈·탭·설정)을 가집니다.
- 학습자 셸: 커리큘럼 경로, 리그, 친구, 상점
- 학생 셸: 배정 곡, 선생님의 응원, 진도
- 선생님 셸: 대시보드, 곡 관리, 학생 관리, 초대 코드, 레슨
- 운영자 셸: 공식 카탈로그 관리(게시/숨김·테스트 플레이·업로드)
라우팅은 로그인 상태와 역할에 따라 적절한 셸로 보냅니다. 구독이 필요한데 없으면 결제 화면으로, MIDI 연결이 필요한 흐름이면 연결 화면으로 — 역할 중립적인 화면(연습·MIDI 연결)은 여러 역할이 공유합니다.
역할 전환
운영자는 별도로 가입하는 계정이 아니라, 자격이 있는 스태프가 자신의 원래 역할과 운영자 모드를 오갈 수 있게 설계했습니다. 전환은 서버가 승인하며, 언제든 원래 역할로 안전하게 복귀합니다. 전환 중에는 전체 화면 로딩으로 끊기지 않도록 매끄럽게 처리하고, 전환이 실패해도 현재 역할을 그대로 유지합니다.
학생과 독립 학습자는 같은 게임플레이 엔진을 공유하고 곡의 출처만 다르다. 공용 코어 + 역할별 셸 — 사용자층이 완전히 달라도 핵심 경험은 한 번만 잘 만들면 됐다.
설계에서 배운 것
역할을 데이터 모델·라우팅·서버 인가의 공통 축으로 두니 "누가 무엇을 볼 수 있고 수정할 수 있는가"가 화면 조건문에 흩어지지 않고 한 곳에서 결정됩니다. 그리고 공용 코어 + 역할별 셸이라는 분리 덕분에, 사용자층이 완전히 달라도 핵심 경험(연주와 판정)은 한 번만 잘 만들면 됐습니다. 권한이 서버로 모이니 역할별 라우팅·권한 API·운영자 전환을 각각 테스트로 고정할 수도 있었습니다. 여러 페르소나를 담는 앱에서 가장 크게 남은 교훈입니다.

Why split by role
Even with the same goal — practicing piano — people stand in different relationships. Some learn from a teacher, some go it alone, some do the teaching. Cram them all into one screen and you get an app that fits no one.
So LevelUp Piano treated role as a first-class concept from the start. When you sign in, you choose who you are, and from there the home screen, permissions, and data flow accordingly.
The four roles
- Independent learner — a general user learning alone. A subscription unlocks the curriculum and catalog, and gamification builds the habit.
- Student — practices songs assigned by a teacher, who tracks their progress and weak spots.
- Teacher — creates songs from MIDI, assigns them to students, and manages their roster, lessons, and coaching.
- Operator — an internal staff role that curates the official song catalog everyone shares.
Each role has different access. This boundary isn't scattered across screen conditionals — the server checks the role on every route and enforces it (a 403 without permission).
| Capability | Learner | Student | Teacher | Operator |
|---|---|---|---|---|
| View songs | catalog | assigned | own | all |
| Upload songs | — | — | ✓ | ✓ |
| View student progress | — | — | assigned | — |
| Publish official catalog | — | — | — | ✓ |
| Submit scores | ✓ | ✓ | test (not saved) | — |
Student vs. independent learner — alike but not the same

The most interesting relationship was between student and independent learner. Both are players who "perform and get scored," but their songs come from different places.
- A student's songs come from what the teacher assigned.
- An independent learner's songs come from the official catalog's curriculum.
The practice screen, judgment engine, and results/celebration must be identical; only the song's source differs. So I built a shared player core (progress reads, gameplay, results) and layered per-role data sources and home shells on top. The core doesn't know where songs come from — assigned or catalog, both just implement the same contract (simplified, it looks like this).
// The player core doesn't know the song's source — only this contract.
abstract interface class SongSource {
Future<List<PlayableSong>> load(); // Student → assigned, Learner → catalog
}Because both roles share the same engine, improving gameplay improves both at once.
Per-role shells
Each role gets its own shell (home, tabs, settings):
- Learner shell: the curriculum path, leagues, friends, shop
- Student shell: assigned songs, teacher's cheers, progress
- Teacher shell: dashboard, song management, roster, invite codes, lessons
- Operator shell: official-catalog management (publish/unpublish, test play, upload)
Routing sends you to the right shell based on auth state and role. If a subscription is required but missing, it routes to the paywall; if a flow needs MIDI, to the connection screen — while role-neutral screens (practice, MIDI connection) are shared across roles.
Switching roles
The operator isn't a separately registered account; eligible staff can move between their base role and operator mode. The switch is approved server-side, and you can safely return to your original role at any time. During the switch there's no jarring full-screen loading, and if a switch fails, you simply stay in your current role.
Student and independent learner share the same gameplay engine; only the song source differs. Shared core + per-role shells — even with completely different audiences, the core experience only had to be built well once.
What the design taught me
Making role a shared axis across the data model, routing, and server authorization means "who can see and change what" is decided in one place instead of scattered across screen conditionals. And the shared core + per-role shells split meant that even with completely different audiences, the core experience — playing and being judged — only had to be built well once. With permissions funneled to the server, per-role routing, the permission API, and the operator switch could each be pinned down with tests. That's the biggest lesson from an app that holds several personas.