ハコヅミを作る #16:ガス缶や食材などの消耗品を、ギアとは別に管理する

前回(#15)でキャンプの情報がそろったので、今回は持ち物の側を広げます。ガス缶・薪・調味料・食材など、キャンプのたびに減ったり、用意し直したりするもの(消耗品)です。

Notion の「今後の機能強化」の「消耗品の管理」にあたり、ステップ4の「消耗品の購入フォロー」(残りがなくなったら買い足しを知らせる)の土台にもなります。


3つの Issue に分けた

一度に全部やると大きくなるので、3つに分けて、1つずつ受入確認をしながら進めました。

Issue 内容
#75 消耗品の登録・編集・削除(一覧、種類、残り、コンテナ)
#77 数量と、コンテナの中身の画面に消耗品を出す
#78 プランと持ち物チェックに、消耗品を入れる

#75 で「対象外」に書いたものを、#77・#78 で拾っていく形です。最初から対象外を書いておくと、「何をまだやっていないか」が Issue に残るので、次の Issue の下書きがそのまま作れます。


ギアとは別のテーブルにした

消耗品を gears の1行として、「種類:消耗品」のように扱う案もありました。それでも、別のテーブル(supplies)にしています。

  • 製品で表すものではない:ギアはメーカーと製品名で表すが、「薪」「卵」にメーカーはない
  • 大事なのは残りがあるかどうか:ギアには、おすすめ度や画像、「欲しい」「廃棄済」の状態がある。消耗品に要るのは、残りと数量
  • ギアの画面や処理を変えずに済む:同じテーブルにすると、ギアの一覧・プラン・持ち物チェックのあちこちに「消耗品なら」という分かれ道が入る

一方で、コンテナとの紐づけはギアと同じ形にしました。ガス缶をカーゴに入れっぱなしにしておく、という使い方はギアと変わらないからです。

「残りを管理する」と「毎回用意する」

消耗品には、性質の違う2種類があります。

種類 例 残り
残りを管理する(stock) ガス缶、薪、調味料、ゴミ袋 「残りあり」「残りなし」を持つ
毎回用意する(every_time) 食パン、卵などの食料品 持たない(null)

ガス缶は、使い切るまで何回か持っていきます。食料品は、キャンプのたびに買うので、残りを気にしません。この違いを kind の列で表し、残り(in_stock)は stock のときだけ使います。

消耗品一覧。食料(食パン 毎回用意、卵 ×6 毎回用意)、調味料(オリーブオイル 残りなし、塩・こしょう 残りあり)、日用品、燃料(OD缶 110 残りなし、OD缶 250 ×2 残りあり、薪 ×2 毎回用意、着火剤 残りあり)。残りなしは赤字で、コンテナはバッジ

消耗品の一覧。「残りなし」は目立つ色にした

画面は、この記事の時点のコードを手元で動かして撮ったものです。ギアとコンテナは#7で Notion から取り込んだ実際のもので、消耗品は記事用に登録したものです。

「残りなし」は、削除やエラーと同じ色(danger)にしました。一覧を開いたときに、買い足すものがすぐ目に入るようにするためです。


数量(#77)

#75 では数量を持たせていませんでしたが、使ってみると「ガス缶は2本」「卵は6個」のように、持っていく数を残したくなります。

supplies に quantity(1〜99、既定値 1)を足しました。一覧や持ち物チェックでは、2以上のときだけ「×2」と出します。1のときに「×1」と出すと、ほとんどの行に付いてうるさいためです。

消耗品を編集する画面。名前「OD缶 250」、カテゴリ「燃料」、数量「2」、種類「残りを管理する」、残り「残りあり」、コンテナ「カーゴ 30L-1」

消耗品の編集画面。種類が「毎回用意する」のときは、残りの欄は出ない

あわせて、コンテナの編集画面の「中身」にも消耗品を出しました。コンテナに何が入っているかを見るときに、ギアしか出ないのは不便だったためです。


プランと持ち物チェックに入れる(#78)

ここが本番です。消耗品をプランに入れ、キャンプの持ち物チェックで、ギアと一緒にチェックできるようにしました。

プランの編集画面の「持っていく消耗品」。カテゴリごとに、チェックボックス付きで消耗品が並ぶ。数量とコンテナのバッジ付き

プランの編集画面に足した「持っていく消耗品」

項目のテーブルも、ギアとは別にした

持ち物チェックの項目は、ギアなら camp_items(キャンプ × ギア)です。ここに消耗品も入れられれば楽ですが、camp_items はギアへの外部キーを持っています。消耗品も入れるには、外部キーを外したり列を足したりするテーブルの作り直しが要ります(#5で苦労したものです)。

そこで、消耗品用に camp_supply_items を新しく作りました。プランとの紐づけも、同じ理由で plan_supplies を別に作っています。

plan_gears        (plan_id, gear_id)
plan_supplies     (plan_id, supply_id)    ← 新しく作った
camp_items        (camp_id, gear_id, checked, not_needed)
camp_supply_items (camp_id, supply_id, checked, not_needed)    ← 新しく作った

画面では、ひとつにまとめて扱う

テーブルは分かれていても、持ち物チェックの画面では、ギアと消耗品を同じように並べたいところです。コンテナごとにまとめる、進み具合に数える、コンテナの一括チェックで両方をチェックする、などです。

そこで、2種類の項目を1つの形にそろえる関数(toCampViewItems())を作り、そのあとの処理(コンテナごとのまとめ、進み具合、コンテナのチェック状態)は、これまでの関数をそのまま使えるようにしました。項目には gear:〜・supply:〜 のキーを付けて、ギアと消耗品で ID が重なっても区別できるようにしています。

return [
	...gearItems.map((item) => ({
		key: `gear:${item.gearId}`,
		kind: 'gear' as const,
		// ...
	})),
	...supplyItems.map((item) => ({
		key: `supply:${item.supplyId}`,
		kind: 'supply' as const,
		// ...
	}))
];
持ち物チェックの「カーゴ 30L-1」のまとまり。コンテナのチェックボックスがチェック済みで、中に「OD缶 110 残りなし」「OD缶 250 ×2」と、ギア(ギガパワーBFストーブなど)が並び、すべてチェック済み

持ち物チェック。カーゴの中のガス缶(消耗品)も、ギアと一緒に並ぶ

残りがない消耗品には、持ち物チェックでも「残りなし」と出します。積み込みの途中で、「OD缶 110 は残りがないから、買ってこないと」と気づけるようにするためです。

「中身をすべて見る」にも、消耗品を出しています。

「カーゴ 30L-2」のまとまりで「中身をすべて見る」を開いたところ。オリーブオイル、コンフィマットレス180 などのギアと、塩・こしょうが並ぶ

「中身をすべて見る」。ギアと消耗品をまとめて名前順に出す

CI でだけ落ちた E2E

#78 の PR では、CI の E2E が1件だけ落ちました。手元では何度流しても通るテストです。

落ちたのは、「ギアをチェックしたら、コンテナのチェックボックスが『一部』の表示になる」ことを確かめるテストでした。チェックの送信が終わるのは待っていましたが、そのあと画面が描き直されるのは待たずに、「一部」かどうかを読んでいました。手元のマシンは速いので描き直しが間に合い、CI では間に合わなかった、というわけです。

// 「一部」の表示になるまで、何度か読み直して待つ
await expect
	.poll(() => containerBox.evaluate((el) => el instanceof HTMLInputElement && el.indeterminate))
	.toBe(true);

「一部」の表示(indeterminate)は HTML の属性ではなく JavaScript のプロパティなので、Playwright の toBeChecked() のような「待ってくれる」確かめ方が使えません。expect.poll で、値が変わるまで読み直すようにしました。#6で書いた「待つべきものを待っていない」の、また1つの例です。

直したあとの CI は、今度はテストではなく、15分の時間の上限で止められました。E2E が増えて、CI 全体が15分近くかかるようになっていたためです。この話は次回にまとめます。


おわりに

  • 消耗品は、性質が違うのでギアとは別のテーブルに。コンテナとの紐づけだけはギアと同じ形
  • 「残りを管理する」と「毎回用意する」の2種類。残りなしは目立たせる
  • 持ち物チェックの項目も別のテーブルにして、テーブルの作り直しを避けた。画面では1つの形にそろえて、これまでの処理を使い回す
  • 「一部」の表示は待ってくれないので、expect.poll で待つ

3つの Issue とも、本番にリリースしました。残りがなくなった消耗品を「次のキャンプまでに買い足す」ように知らせる機能(購入フォロー)も、これで作れる下地ができました。

← トップページに戻る