hirojinblog
記事一覧へ

一人用カードゲームのカウンターと、PocketBaseを使った家計簿を作った

「アリスの時間迷宮」で使うCounterと、支出だけを素早く記録するKakeiboを作ったときの実装メモです。現在の保存方法と、まだ認証を付けていない状態もコードと一緒に残します。

個人開発 WebアプリViteTypeScriptReactPocketBase

小さいWebアプリを2つ作りました。

1つ目は、一人用カードゲーム「アリスの時間迷宮」を遊ぶときに使うCounterです。プレイ中の数値を手元で動かせればよかったので、機能は増減、リセット、カウンターの追加と削除に絞りました。

2つ目はKakeiboです。市販の家計簿アプリほど多機能でなくていいので、日付、カテゴリ、金額、メモだけを入れられるものが欲しかったのと、PocketBaseを実際に触ってみたかったのがきっかけです。

「アリスの時間迷宮」用のCounter

CounterはViteとVanilla TypeScriptで作りました。Reactは使わず、CounterクラスがDOMを組み立て、値もクラスの中で持っています。

「アリスの時間迷宮」で使うために作ったCounterの画面

最初の1個を表示したあと、ADD COUNTERを押すたびに同じクラスのインスタンスを追加します。

const initialCounter = new Counter('カウンター', 0);
countersContainer.appendChild(initialCounter.render());
addButton.addEventListener('click', () => {
const newCounter = new Counter('カウンター', 0);
countersContainer.appendChild(newCounter.render());
});

Counterが持つ状態は名前と数値の2つだけです。

export class Counter {
private label: string;
private value: number;
constructor(label: string, initialValue: number = 0) {
this.label = label;
this.value = initialValue;
}
private updateDisplay() {
this.displayEl.textContent = this.value.toString();
}
}

画面には1ずつ増減する大きなボタンに加え、-5、-2、+2、+5を置きました。各ボタンはvalueを変えてから表示を更新します。

this.containerEl.querySelector('.minus-btn')
?.addEventListener('click', () => {
this.value -= 1;
this.updateDisplay();
});
this.containerEl.querySelector('.adjust-plus-5')
?.addEventListener('click', () => {
this.value += 5;
this.updateDisplay();
});
this.containerEl.querySelector('.reset-btn')
?.addEventListener('click', () => {
this.value = 0;
this.updateDisplay();
});

カウンター名はカード上部の入力欄で変更できます。不要になったカードは左上のボタンでDOMから削除します。画面幅に応じて1〜4列へ変わるグリッドにしたので、複数の数値を並べても同じ画面で確認できます。

Counterの実装の流れ

Counter側の実装は次の5段階に分かれています。

  1. Vite、TypeScript、Tailwind CSSだけの画面を用意する
  2. Counterクラスにlabelとvalueを持たせる
  3. 増減、リセット、名前変更、削除のイベントを付ける
  4. main.tsで初期カウンターと追加ボタンを配置する
  5. CSS Gridの列数を画面幅で切り替える

Counterはまだ保存しない

現在のCounterにはlocalStorageもデータベースもありません。数値と名前はCounterインスタンスの中だけにあるため、ページを再読み込みすると初期状態へ戻ります。ログイン機能もありません。

「アリスの時間迷宮」を遊んでいる間だけ使う道具としてはこれで足りますが、途中状態から再開する用途にはまだ対応していません。

PocketBaseを試すために作ったKakeibo

KakeiboはReactのフロントエンドとPocketBaseのバックエンドに分けました。画面から登録した支出はPocketBaseへ保存し、表示中の月を変えると、その月のデータを取り直します。

PocketBaseへ保存した支出を月別に表示するKakeiboの画面

プロジェクトの構成は次のようになっています。

kakeibo/
├── compose.yaml
├── pb_migrations/
│ └── 1690000000_init_transactions.js
├── pb_data/ # 起動後に作られる保存先
└── frontend/
└── src/
├── App.tsx
├── components/
└── lib/pocketbase.ts

PocketBaseは0.22.10を起動時に取得し、データをホスト側のpb_dataへ保存しています。フロントエンドから接続先を指定しなかった場合は、ブラウザで開いているホスト名の8090番ポートへ接続します。

const baseUrl = import.meta.env.VITE_POCKETBASE_URL
|| `${window.location.protocol}//${window.location.hostname}:8090`;
export const pb = new PocketBase(baseUrl);

transactionsコレクション

最初の起動時にmigrationからtransactionsコレクションを作ります。フィールドは次の5つです。

フィールド型現在の使い方
typeselectexpenseまたはincome
datedate支出した日
categorytext食費、日用品、交通費、住居費、交際費、その他
amountnumber0以上の整数
memotext任意の短いメモ

モデル上は収入も持てますが、現在の登録画面はexpenseを固定で送っています。収入入力はまだ作っていません。

await pb.collection('transactions').create({
type: 'expense',
date: date + ' 00:00:00.000Z',
category,
amount: Number(amount),
memo,
});

保存に成功したら金額とメモを空にし、一覧を再取得します。履歴側には削除ボタンもあり、確認ダイアログのあとにレコードIDを指定して削除します。

await pb.collection('transactions').delete(id);
onDelete(); // 一覧を再取得

月ごとの取得と集計

App.tsxでは表示中の月の開始日時と終了日時を作り、PocketBaseのfilterへ渡しています。日付の新しい順で最大100件を取得する実装です。

const records = await pb.collection('transactions')
.getList<Transaction>(1, 100, {
sort: '-date',
filter: `date >= "${startOfMonth.replace('T', ' ')}" && date <= "${endOfMonth.replace('T', ' ')}"`,
});
setTransactions(records.items);

取得したレコードは日付ごとにまとめて履歴へ表示します。今月の総支出とカテゴリ別の金額は、React側でtransactionsをreduceして計算しています。PocketBase側に集計用のAPIは追加していません。

100件を超えた分のページングも未実装なので、ひと月の登録数が多い場合は続きを取得できません。

Kakeiboの実装の流れ

Kakeibo側の実装は次の6段階に分かれています。

  1. PocketBaseのmigrationでtransactionsを定義する
  2. React側にPocketBaseクライアントを用意する
  3. 支出フォームからcreateする
  4. 月の範囲を指定して一覧を取得する
  5. 履歴の削除と、カテゴリ別集計を追加する
  6. pb_dataをホストへマウントして再起動後も記録を残す

保存と認証の現在地

2つのアプリで保存方法ははっきり分かれています。

  • Counterはブラウザのメモリだけを使い、再読み込みで消える
  • KakeiboはPocketBaseへ保存し、pb_dataにデータベースを残す

Kakeiboには、まだユーザーログインを付けていません。フロントエンドにも認証処理はなく、migrationのlistRule、viewRule、createRule、updateRule、deleteRuleはすべて空文字です。現在は未認証アクセスを許可する設定なので、そのままインターネットへ公開する構成ではありません。

"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": "",
"deleteRule": "",

つまり、今のKakeiboは自分の端末や閉じた開発環境で試す段階です。外部公開するなら、先にPocketBaseのユーザー認証とコレクションルールを追加し、各レコードを所有者へ結び付ける必要があります。

Counterは目的を満たすところまで、KakeiboはPocketBaseへの登録・月別取得・削除・集計まで作りました。次に触るなら、Counterは途中状態の保存、Kakeiboは認証と100件以降のページングから手を入れます。