家計簿アプリを作る #69:よく使う取引のクイック登録

家計簿アプリ作成シリーズの第69回です。毎回同じ金額・カテゴリ・メモで登録することが多い取引(例:毎日のコーヒー代など)を「テンプレート」として保存し、収支一覧からワンクリックで登録できるようにします。

実装方針

transactionTemplates テーブルを新設し、これまでのカテゴリ・定期収支管理と同じCRUDパターンで一覧・登録・編集・削除ページを作りました。収支一覧ページには、登録済みテンプレートをボタンとして並べて表示し、クリックすると確認ダイアログを出した上で即座に取引を登録します。

ワンクリック登録はhidden inputでテンプレートIDだけ送る

フォーム入力画面を経由せず、<form> の hidden inputにテンプレートIDだけを埋め込んで送信する形にしました。

{#each data.transactionTemplates as template}
  <form method="POST" action="?/quickAdd" use:enhance>
    <input type="hidden" name="templateId" value={template.id} />
    <Button
      type="submit"
      onclick={(e: Event) => {
        if (!confirm(`${template.label}を登録しますか?`)) {
          e.preventDefault();
        }
      }}>{template.label}</Button
    >
  </form>
{/each}

サーバー側で日付を決定する

登録日は「今日」になりますが、クライアント側の時計は信用せず、サーバー側の new Date() を採用するようにしました。

quickAdd: async ({ request, platform, locals }) => {
  const formData = await request.formData();
  const templateId = formData.get('templateId');

  if (!templateId) {
    return fail(400, { error: 'Template ID is required.' });
  }

  const db = createDb(platform!.env.DB);
  const template = await db.select().from(transactionTemplates)
    .where(and(eq(transactionTemplates.userId, locals.user!.id), eq(transactionTemplates.id, String(templateId))))
    .get();

  if (!template) {
    return fail(404, { error: 'Template not found.' });
  }

  await db.insert(transactions).values({
    userId: locals.user!.id,
    amount: template.amount,
    type: template.type,
    category: template.category,
    memo: template.memo,
    date: formatLocalDate(new Date()), // 今日の日付を設定
  });

  redirect(303, /* ... */);
},

クライアントから送られてくるのは templateId だけで、金額やカテゴリなどの実際の値はサーバー側でDBから取得し直しています。仮にクライアント側で値を改ざんして送信しようとしても、サーバーが信用するのはIDだけなので影響を受けません。

まとめ

ポイント 内容
テンプレートのCRUD 既存のカテゴリ・定期収支管理と同じパターンをそのまま流用できる
ワンクリック登録 フォーム入力を経由せず、hidden inputでテンプレートIDだけを送信する
サーバー側での日付決定 クライアントの時計を信用せず、サーバーのnew Date()を採用する
クライアントからの入力の扱い IDだけを受け取り、実際の値はサーバー側でDBから取得し直すことで改ざんを防ぐ
← トップページに戻る