Degree Planner
A degree-requirement calculator that moves the SKKU EMBA graduation worksheet to the web - enter your courses and it computes requirement fulfillment and the attainable degree.
- React 19
- TypeScript
- Supabase
- EMBA
Setup
- Problem
EMBA members had to check graduation requirements (total credits, Core/Advanced, international-language, convergence/global) one by one on a paper worksheet. A miscalculation pushes graduation back.
- Context
An internal tool for SKKU EMBA members. It moves the school's official worksheet to the web, but draws a line as an "assistive tool" - the final call belongs to the registrar.
- Users
EMBA members computing their degree requirements, and an admin managing course offerings per semester.
Build
- What I did
- A requirement engine - computes total credits, requirement fulfillment, and the attainable degree from undergrad major status and per-semester courses
- An admin CMS - manages course offerings by semester (atomic-write RPCs)
- Google login + roster-based approval - reads for approved users only, writes for admins only (RLS)
- Excel import for bulk course entry (SheetJS) and result/timetable image export
- Product decisions
- Pinning it as an "assistive tool" - it must not be mistaken for replacing the degree decision, so the boundary (the registrar confirms) is stated in the copy
- Roster-based approval - the trust unit is not the school domain but "a person on this cohort's roster"; personal Gmail can log in, but reads are blocked until approved
- Built to embed as a web view in the Flutter app (wonwoo-sucheop-app) - mobile-first layout, HashRouter, and relative-path builds set up in advance
- QA considerations
- Same input, same result - the requirement calc is a side-effect-free pure function with constants, pinned by unit tests on the tricky boundary combos (exactly 45 credits, business 6 / non-business 12, Advanced 18, etc.)
- Owning the false-positive cost - a wrong "you can graduate" slips a real graduation, so instead of an automated verdict a Disclaimer states the registrar has the final say
- Do the rules match the school's standard - is the rule data separated from the logic so regressions can be caught when per-semester rules change?
- Access control - reads for approved users only (
is_approved), writes admin-only, and status always starts pending so it can't be self-elevated - Atomic-write RPC - a semester's offering replace (delete then insert) is wrapped in one transaction so a mid-write failure can't wipe it wholesale
Outcome
- Metrics
In internal use for this cohort. No public metrics, as it sits behind a login and approval gate.
Process (planning → build)
The same template from problem and hypothesis through scope, spec (SSOT), build, the QA gate, and release - how this was planned and driven.
Process (planning → build)
Problem & hypothesis
EMBA members had to cross-check total credits, Core, Advanced, international-language, and convergence/global requirements by hand on an Excel worksheet - matching course codes and areas themselves - and a miscalculation pushes graduation back. The hypothesis: base the calculation on the user's actually-entered courses (UserCourse) rather than a semester's offering list, so the same input yields the same result even when a past course is no longer offered (docs/ssot.md section 5 core principle).
Skills · Problem framing · hypothesis
Scope & priority
Scope was pinned first in docs/ssot.md section 4 - the requirement engine, result dashboard, and semester/course/offering management are core; school-system integration, automated grade collection, course registration, and replacing the official graduation review are explicitly out. Following the section 20 Phase 1-6 order, the calculator came first and Admin, Excel import, and ops features followed; login-based storage was originally out of MVP but was later actually added as Google login, roster approval, and per-account storage.
Skills · Scoping · prioritization
Spec-first (SSOT)
docs/ssot.md is a literal Single Source of Truth, defining the requirement rules (section 9), calculation logic (section 13), data model (section 11), and screens (section 7) ahead of the code. The implementation visibly tracks it - constants.ts cites SSOT section 9, each step in calculateRequirements.ts cites its 13.x subsection in comments, and the test describe blocks carry the same SSOT section numbers.
Skills · Spec · documentation
Build & iteration
A React 19 + Vite + TypeScript + Tailwind v4 + Radix (shadcn) front end over a Supabase backend, with the requirement calculation pulled into a side-effect-free pure function (calculateRequirements.ts) and its thresholds isolated in constants.ts. The key DB moves are atomic-write RPCs (0004 replace_semester_courses wraps delete-then-insert in one transaction so a mid-write failure can't wipe a semester's offerings), roster-based approval RLS (0005), and a SheetJS Excel import that skips title/blank rows and normalizes display labels to canonical areas.
Skills · Execution · delivery
Verification (QA gate)
Trust was defined as "same input, same result" and "rules match the school's standard," so the requirement logic was isolated into pure functions and constants and pinned by unit tests (calculateRequirements.test.ts) covering the tricky crossovers - exactly 45 credits, business 6 vs non-business 12, Advanced totaling 18 but bunched into one area so the four-area rule fails, and conversely four areas met but the total falling short of 18. CI (ci.yml) gates every push and PR with the full 30-test suite plus typecheck, lint, and build, and the DB layer adds its own gates - reads only for approved users (is_approved), writes admin-only, and a status that claim_identity always starts at pending so it can't be self-elevated. The most important QA decision was owning the cost of a false positive: a wrong "you can graduate" would actually slip a member's graduation, so instead of an automated verdict the Disclaimer keeps reminding that the registrar has the final say, and a partially-met state is softened via isPartiallyMet rather than overclaimed.
Skills · Quality · risk
Release
It ships as a static SPA auto-deployed on every main push via Cloudflare Pages' Git integration - HashRouter, relative-path builds (base:"./"), and _headers cache/security headers make it run anywhere, and the GitHub Actions deploy (deploy.yml) was removed to avoid double-deploying while ci.yml stays purely as a verification gate. It was built to embed as a flutter_inappwebview view in the Flutter app wonwoo-sucheop-app, and it currently sits in internal use behind a login and roster-approval gate (0.1.0), so there are no public metrics.
Skills · Release
- Tech stack
- React 19
- TypeScript
- Vite
- Tailwind v4
- Supabase
- Cloudflare Pages