Back to Portfolio

Case Study · 01

NextRep

電波の通らないジムの地下でも動く、RPE ドリブンなトレーニング記録アプリ

Problem

既存の記録アプリには、3 つの摩擦がある。

筋トレの記録は「重量 × 回数 × セット数」を高速に入れ続ける作業で、1 セット間の 数秒のひっかかり が集中力を切ってしまう。既存アプリを使い込むほど、摩擦の正体が 3 つあることが見えてきた。

入力摩擦 — OS キーボードが立ち上がるたびに画面がジャンプし、タップ位置がずれる。片手では操作しづらい。

電波依存 — ジムの地下や混雑時、サーバー依存のアプリは同期やログインで簡単に詰まる。

サブスク疲れ — 月額課金モデルが主流で、使用中に挟まる広告がストレスだった。

Approach

Local-First × BYOS × 入力経路の一元化。

IndexedDB を Dexie.js 経由でプライマリストレージに据え、ネットワークを前提から外した。

バックアップはユーザー自身の Google Drive に置き、BYOS (Bring Your Own Storage) として LWW + Tombstone で衝突解決する。運営側にデータが一切流れないまま、端末間同期が成立する構成。

入力側は OS キーボードを完全に排除し、カスタムテンキー (Numpad) を自前実装。全数値入力を uiStore.openNumpad() の 1 本に集約して、フォーカス喪失・ビューポート移動・二重送信をまとめて抑えた。

負荷提案は RPE (自覚的運動強度) を入力として、次セットの重量・回数を自動算出する Progressive Overload エンジンを内蔵。入力の手間を最小化するため、過去ログからプリフィルも行う。

カスタムテンキー (Numpad)

OS キーボードを完全排除する専用ボトムシート。aria-modal + フォーカストラップ付きで、汗ばんだ指でも取りこぼさない。

Smart Prefill

前回セッションの値をワンタップで再利用。新セット追加時は直前セットをコピーし、タップ数を限界まで削減。

Progressive Overload エンジン

RPE ベースで次セットの負荷を自動算出。「重量↑ / レップ↑ / 維持 / ディロード」の 4 状態で判定する。

アナリティクス

ContributionCalendar・MuscleHeatmap・VolumeChart で継続状況と部位別ボリュームを可視化。

Rest Timer

セット間の休憩を自動計測。バックグラウンド動作・通知対応で、タイマーを気にせず次セットへ。

BYOS 同期 (Google Drive)

任意機能。運営サーバーを経由せず、ユーザー自身の Drive に保存。LWW + Tombstone で競合解決する。

完全オフライン対応

PWA としてインストール可能。Service Worker + IndexedDB により、地下ジムでもゼロレイテンシで動作。

NextRep のホーム画面。Ready to Train? のタイトルと、ワークアウト開始ボタン、月別 Activity カレンダー、直近のワークアウト履歴を表示。
HomeActivity カレンダーで継続を可視化
ベンチプレスの種目カード。50kg × 11回という Smart Prefill の提案と、前回 RPE キーに基づく補足が表示されている。
Smart Prefill前回ログを元に初期値を提案
カスタムテンキー (Numpad) を展開した状態。±0.5 / 1.25 / 2.5 / 5kg の増減ボタンと、数字のテンキー、DONE ボタンが画面下部に固定表示されている。
Custom NumpadOS キーボードを完全排除
Analytics タブ。7日間のトータル、ボリューム、セット数と、週間ボリュームチャート、筋群別のヒートマップを表示。
Analytics週間ボリュームと筋群ヒートマップ
Frontend
React 19 + TypeScript + Vite 7
Routing
TanStack Router
Styling
Tailwind CSS v4
State
Zustand 5 + Dexie React Hooks
Local DB
Dexie.js 4 (IndexedDB)
PWA
vite-plugin-pwa (Workbox)
Sync
Google Drive API (BYOS)
Testing
Vitest + Playwright + Testing Library
Deploy
Cloudflare Pages

Result

電波ゼロでも "書ける・提案される" 状態へ。

ネットワーク依存を前提から外したことで、ジムでの体験が根本的に安定した。Google Drive バックアップはオプションのまま置かれ、使わない人でも一切困らない設計に。サーバー運用コストは引き続きゼロ。

最新版は nextrep.c12o.net で公開中。

Learnings

作ってみて分かったこと。

"オフライン対応" は機能名ではなく、前提

後から載せるのはほぼ不可能に近い。

最初にネットワーク依存をコードベースから追い出しておくと、その後の判断が簡単になる。

入力経路を 1 本に集約する

モバイル Web の最大ハマりどころは「入力」。

Numpad に一元化したことで、フォーカス喪失・ビューポート移動・二重送信といった細部のバグがまとめて消えた。

LWW は「弱い一貫性で十分」というドメイン判断とセット

筋トレ記録のように 1 ユーザー 1 デバイス同時使用が前提のドメインなら、CRDT を持ち込まずとも Tombstone + LWW で実用的に成立する。

どこまで割り切れるかの線引きが設計の要だった。

プライバシーをデフォルトにする設計

Google Drive を "自分が鍵を持つ保管庫" として扱う BYOS 構成は、SubCutter とも共通する思想。

アプリを跨いで同じ感覚で設計できるのは発見だった。