はじめに
前回、複数人で使えるアプリにするための認証・権限管理を実装しました。
今回は、作業マスタ機能を追加したところ、一括操作の実装で躓いた話です。
1. 作業登録の課題:毎回ポチポチするのが面倒
栽培管理アプリでは、日々の作業を記録する「作業登録」機能があります。現状では以下の項目が選択できます:
- 防除
- 灌水
- 施肥
- その他(テキスト入力)
問題点
実際に使ってみると、なんかしっくりこない:
- 「耕起」「定植」「収穫」など、防除・灌水・施肥以外の作業はすべて「その他」で毎回テキスト入力が必要
- 同じ作業を何度も入力するのが面倒
- 入力のたびに「前回なんて入力したっけ?」と迷う
- 表記ゆれ(「耕起」「耕うん」「耕運」など)が発生する
よく考えれば、農作業の種類は農園ごとにある程度決まっています。それなのに毎回手入力するのは非効率でした。


2. 結局、作業項目も別で設けることに
作業マスタを作る
作業マスタとは、よく使う作業をあらかじめ登録しておき、作業記録時に選択できるようにする機能です。
- 「耕起」「定植」「収穫」などを事前登録
- 作業登録時はプルダウンから選択するだけ
- 表記ゆれが防止できる
- 入力が圧倒的に楽になる
実装方針
作業マスタは以下の要件で実装することにしました:
- カテゴリ分け(圃場準備、定植・更新、栽培管理など)
- お気に入り機能(よく使う作業だけを表示)
- テンプレートから一括登録できるようにする

3. 初期データの一括登録機能を実装
workTypeTemplates.ts の作成
新規で農園を作る際に、ある程度の作業項目を一括登録できるようにしました。
色々検討しましたが、下記のような感じで77項目の作業内容に。
// workTypeTemplates.ts
export const workTypeTemplates = [
{ name: '耕起', category: 'field_prep' },
{ name: '畝立て', category: 'field_prep' },
{ name: '土壌改良', category: 'field_prep' },
{ name: '定植', category: 'planting' },
{ name: '播種', category: 'planting' },
{ name: '草刈り', category: 'management' },
// ... 全77件
];
これで新規農園でも、最初から77件の作業項目が使える状態になりました。
カテゴリ分けの実装
作業をカテゴリごとに整理することで、目的の作業を探しやすくしました:
- 圃場準備(11件):耕起、畝立て、マルチ敷設など
- 定植・更新(11件):定植、播種、株更新など
- 栽培管理(20件):灌水、施肥、摘芯など
- 施設管理(11件):ハウス開閉、換気扇操作など
- 保全作業(8件):点検・調整など
- 事務作業(5件):出荷準備、書類作成など
- その他(11件)

4. お気に入り機能で使いやすくする
77件もあると選ぶのが大変なので、お気に入り機能を追加しました。
実装内容
- よく使う作業だけを「お気に入り」に登録
- 作業登録画面では、お気に入りに登録した作業だけが表示される
- 作業マスタ画面で、いつでもお気に入りの追加・削除ができる
5. 問題発生:お気に入り一括登録が遅すぎる
発生した問題
初期設定で「よく使う作業をまとめてお気に入り登録したい」というケースがあります。
例えば、77件全てをチェックして一括お気に入り登録を実行したところ:
- 処理に30秒以上かかる
- 削除も同様に30秒以上
- 処理中は画面が固まったように見える
- 「本当に登録されているのか?」と不安になる
実装していたコード(問題のあるコード)
// 一括お気に入り登録(遅い実装)
const handleBulkAddFavorite = async () => {
let successCount = 0;
for (const id of selectedIds) {
try {
await toggleFavoriteWorkTypeAction(id, true); // 1件ずつ処理
successCount++;
} catch {
// エラーは無視
}
}
setMessage({
type: "success",
text: `${successCount}件をお気に入りに追加しました`
});
};
このコードは一見問題なさそうに見えますが、1件ずつ順番に処理しているのが問題でした。
6. 原因の特定:Chrome DevToolsで確認
Chrome DevToolsのNetworkタブで処理を確認してみました。

結果、以下のことが分かりました:
- リクエストが77回、順番に実行されている
- 各リクエストが前のリクエストの完了を待ってから開始(直列処理)
- 1件あたり約400ms かかるため、77件 × 400ms = 約30秒
つまり、for...awaitループで1件ずつ順番に処理していたのが原因でした。
7. 最初の改善案:Promise.allで並列化
「並列処理にすれば速くなるはず!」と思い、まずはPromise.allを使って並列化してみました。
修正したコード
// Promise.allで並列化(でも速くならなかった...)
const handleBulkAddFavorite = async () => {
const results = await Promise.allSettled(
Array.from(selectedIds).map((id) =>
toggleFavoriteWorkTypeAction(id, true)
)
);
const successCount = results.filter(
(r) => r.status === "fulfilled"
).length;
setMessage({
type: "success",
text: `${successCount}件をお気に入りに追加しました`
});
};
結果:体感は変わらず…
コードを修正して再度実行してみましたが、体感の速度はほとんど変わりませんでした。
Networkタブでもリクエストが順番に実行されていました。
原因:Next.jsのServer Actionsは並列実行されない
調べてみると、Next.jsのServer Actionsには以下の制約がありました:
- Supabaseの認証(Row Level Security)が絡むと、セッション管理の都合で順次実行になる
- クライアント側で
Promise.allを使っても、サーバー側で並列実行されるとは限らない
つまり、1件ずつServer Actionを呼び出す方式では、並列化できないということでした。
8. 本当の解決策:バックエンドで一括処理APIを作成
問題の本質は「77回のServer Action呼び出し」にあります。
解決策は、バックエンドに一括処理用のServer Actionを作り、1回の呼び出しで全件処理することです。
一括お気に入り登録APIの実装
actions.tsに以下の関数を追加しました:
// actions.ts
export async function bulkToggleFavoriteWorkTypesAction(
ids: number[],
isFavorite: boolean
) {
const supabase = await createClient();
const { error } = await supabase
.from('work_types')
.update({ is_favorite: isFavorite })
.in('id', ids); // SupabaseのIN句で一括更新
if (error) throw error;
}
Supabaseの.in('id', ids)を使うことで、1回のクエリで複数件を更新できます。
一括削除APIの実装
同様に、一括削除用の関数も追加しました:
// actions.ts
export async function bulkDeleteWorkTypesAction(ids: number[]) {
const supabase = await createClient();
const { error } = await supabase
.from('work_types')
.delete()
.in('id', ids); // SupabaseのIN句で一括削除
if (error) throw error;
}
クライアント側の修正
// Client.tsx
const handleBulkAddFavorite = async () => {
if (selectedIds.size === 0) return;
setMessage({ type: "success", text: "処理中..." });
try {
// 一括処理APIを1回だけ呼び出す
await bulkToggleFavoriteWorkTypesAction(
Array.from(selectedIds),
true
);
setMessage({
type: "success",
text: `${selectedIds.size}件をお気に入りに追加しました`
});
setSelectedIds(new Set());
router.refresh();
} catch {
setMessage({ type: "error", text: "処理に失敗しました" });
}
};
結果:77件が一瞬で処理完了!

修正後、77件の処理が一瞬(約1秒)で完了するようになりました。
Networkタブでもリクエストが1回だけ実行されていることが確認できます。
9. ついでにUX改善も実装
速度改善のついでに、ユーザー体験も向上させました。
9-1. 処理中のメッセージ表示
処理開始時に「処理中…」、完了時に「○件を処理しました」と表示するようにしました。
これでユーザーが「ちゃんと動いている」と安心できます。
9-2. ボタン配置の最適化
一括操作バーのボタンが増えたため、モバイルでは2行レイアウトにしました:
- モバイル:1行目に[★ 追加][☆ 解除]、2行目に[キャンセル][削除]
- PC:1行に全ボタンを配置
また、通常表示では「追加」「解除」両方のボタンを表示し、お気に入り表示時は「解除」のみ表示するようにしました。
まとめ

作業マスタを導入したことで、日々の作業登録が格段に楽になりました。
