家計簿アプリを作る #23:収支一覧のキーワード検索

家計簿アプリ作成シリーズの第23回です。収支一覧にキーワード検索機能を追加します。月フィルタと組み合わせて、メモ・カテゴリを対象に絞り込みができるようにします。

実装方針

  • ?search=キーワード クエリパラメータで検索キーワードを管理
  • DBクエリは allTransactions の全件取得1本のみ
  • 月フィルタ・検索・ページネーションはすべてJavaScriptで処理
  • UIは flowbite-svelte の Search コンポーネントを使用

+page.server.ts の実装

// 検索キーワード
const search = url.searchParams.get('search') ?? '';

// 年月+検索で絞り込み
const monthTransactions = allTransactions.filter(t => {
  const matchMonth = t.date.startsWith(selectedMonth ?? '');
  const matchSearch = search
    ? t.memo.includes(search) || t.category.includes(search)
    : true;
  return matchMonth && matchSearch;
});

// ページ数計算・ページネーション
const totalPages = Math.ceil(monthTransactions.length / PAGE_SIZE);
const selectedTransactions = monthTransactions.slice(offset, offset + PAGE_SIZE);

// 集計はmonthTransactions(絞り込み後の全件)から計算
const income = monthTransactions
  .filter(t => t.type === 'income')
  .reduce((sum, t) => sum + t.amount, 0);
const expense = monthTransactions
  .filter(t => t.type === 'expense')
  .reduce((sum, t) => sum + t.amount, 0);

当初はDBクエリ(likeor)とJavaScriptフィルタの両方で検索を実装していましたが、allTransactions で全件取得済みなのでJavaScriptだけで統一しました。DBクエリを1本に減らすことで、集計・ページ数・表示データの整合性が取れます。

+page.svelte の実装

<script lang="ts">
  import { goto } from "$app/navigation";
  import { Search, Button } from "flowbite-svelte";

  let { data } = $props();

  let searchQuery = $state("");

  function handleSearch() {
    goto(`/?month=${data.selectedMonth}&page=1&search=${searchQuery}`);
  }

  function handlePageChange(page: number) {
    currentPage = page;
    goto(`/?month=${data.selectedMonth}&page=${currentPage}&search=${searchQuery}`);
  }
</script>

<Search bind:value={searchQuery}>
  <Button class="me-1" onclick={handleSearch}>検索</Button>
</Search>

Search コンポーネントに bind:value={searchQuery} で入力値を変数に同期します。Buttononclick からその変数を参照して goto で遷移します。

ページ切り替え時に検索キーワードを引き継ぐ

ページ切り替え時も search パラメータを URL に含めることで、検索状態を維持します。

function handlePageChange(page: number) {
  currentPage = page;
  goto(`/?month=${data.selectedMonth}&page=${currentPage}&search=${searchQuery}`);
}

月切り替え時は検索をリセット

月を切り替えた場合は検索キーワードをリセットして page=1 から表示します。

<Select
  onchange={(e) => goto(`/?month=${e.currentTarget.value}&page=1`)}
>

まとめ

ポイント 内容
検索の実装場所 DBクエリではなくJavaScriptの filter で統一
集計の計算対象 絞り込み後の全件(ページネーション前)から計算
ページ切り替え時 search パラメータを URL に引き継ぐ
月切り替え時 検索キーワードをリセットして page=1 から表示
← トップページに戻る