ハコヅミを作る #3:最初の機能「ギアの登録」で、開発の流れを一周した
前回(#2)で開発の土台がそろったので、いよいよ最初の機能を作りました。 ギアの一覧と登録です。
機能としては小さいのですが、決めた開発の流れを初めて最後まで通したことで、流れそのものにも直すところが見つかりました。今回はその記録です。
作ったもの
- ギアのテーブル(
gears)と、最初のマイグレーション - ギアの一覧:「所有」しているギアを、カテゴリごと・製品名順に表示
- ギアの登録:製品名(必須)・メーカー・カテゴリ・状態(欲しい/所有/廃棄済)

ギアの一覧(上のほうを切り出したもの)。カテゴリごとに、製品名順で並ぶ
画面は、この記事の時点のコードを手元で動かして撮ったものです。データは、あとで#7でNotionから取り込んだ、実際のギアを入れています。 見た目はまだ、機能を確かめるための最低限のものです。
1. Issueは、Claude Codeが下書きして自分が決める
まず、Claude Codeに Issue の下書きを作ってもらいました。 下書きには、仕様とACのほかに、Claude Codeが仮に決めたことが「決めてほしい点」として並びます。
- 編集・削除は次の Issue に分ける(1つのPRを小さく保つため)
- 持ち主を表す
user_idは、当面は固定値にする - カテゴリは自由入力にして、登録済みの値を候補に出す
- 一覧は「所有」しているギアだけを出す
このうち、カテゴリは最初「分類」と書かれていました。Notionのデータベースの項目名をそのまま使ったためです。ここは「カテゴリ」に直してもらいました。 また、「スマホでの入力の考慮」は現時点では不要と判断して、対象外にしました。
開発プロセスの記事で「ACは人が最終的に決める」と書いたとおり、ここは自分で決める部分です。候補が並んでいると、判断はずっと楽でした。
持ち主(user_id)を決める場所は1か所に
今は自分しか使わないので、user_id は固定値です。
ただ、将来ログインを入れたときに置き換えやすいよう、決める場所をサーバーの入り口(hooks.server.ts)の1か所だけにしました。画面やDBの処理は、そこで決まった値を使うだけです。
// src/hooks.server.ts
export const handle: Handle = async ({ event, resolve }) => {
event.locals.userId = OWNER_USER_ID; // 今は固定値。認証を入れるときはここだけ変える
return resolve(event);
};
2. レビューで仕様が2回変わった
PRをレビューしていて、2か所気になりました。
「名前」と「製品名」の両方は要らなかった
最初の設計では、ギアに「名前」(例:テント)と「製品名」(例:アメニティドーム)の両方がありました。 実際に入力画面を見ると、両方を入れるのは手間です。製品名だけで十分だと気づいて、名前の項目をなくしてもらいました。
まだどの環境にもマイグレーションを適用していなかったので、テーブルの定義ごと作り直せました。 合わせて、Issueの本文とPRの説明も直してもらい、Issueの末尾には「変更履歴」を残しています。後から「なぜ名前がないのか」を追えるようにするためです。
メーカーも、カテゴリと同じく候補から選びたい
カテゴリは、過去に登録した値が入力欄の候補に出るようにしていました。 メーカーも、同じメーカーの道具をいくつも持っているので、同じようにしてもらいました。

ギアの登録画面。メーカーとカテゴリは自由に入力でき、登録済みの値が候補に出る
画面を触ってみて初めて気づくことは、やはりあります。レビューの段階で直せたのはよかったです。
3. テストはACごとに振り分けた
ACは、次のようにテストにしました。
| AC | テスト |
|---|---|
| 製品名が空のときは保存されない | ユニット(入力の検証)+E2E(エラー表示) |
| 一覧はカテゴリごと・製品名順 | ユニット(並べ方)+E2E |
| 状態の初期値は「所有」 | E2E |
同じ user_id のギアだけが一覧に出る |
E2E(他人のギアをDBに直接入れて確かめる) |

製品名が空のまま保存すると、保存されずにエラーが出る。入力したカテゴリは消えずに残る
最後の user_id のACは、画面を触るだけでは確かめられません。今は自分しか使っていないので、「他人のギア」がないからです。
そこでE2Eテストの中で、wrangler d1 execute --local を使ってローカルのD1に他人のギアを直接入れ、それが一覧に出ないことを確かめています。
4. PRで Closes をやめて Refs にした
ここが、流れを一周して一番大きく変えたところです。
PRの本文に Closes #8 と書くと、GitHubはマージした時点でIssue #8を自動で閉じます。
ところが、決めた開発の流れでは、受入判断はマージしたあと、開発環境で触って行います。
develop にマージ → 開発環境に自動デプロイ ← ここで Issue が閉じてしまう
開発環境で触って AC を確認する(受入判断) ← 本当はここで閉じたい
このままだと、IssueのACが未チェックのまま閉じてしまいます。
そこで、次のように変えました。
- PRとコミットには、Issueを閉じない
Refs #8を書く - 開発環境で触って「受入OK」となったら、Claude CodeにIssueのACへチェックを付けて閉じてもらう
こうすると、IssueのACのチェック=自分が受入判断をした印になります。
1つ落とし穴がありました。GitHubは、コミットのメッセージに書かれた Closes でもIssueを閉じます。PRの本文だけ直しても足りず、作業ブランチのコミットのメッセージも書き換えました。
5. 初めてのマイグレーションを、開発と本番で確かめた
ギアのテーブルは、最初のマイグレーションでもあります。 #2で作った自動デプロイでは、デプロイの前にそのブランチの環境のD1へマイグレーションを適用します。これが本当に正しい環境に適用されるかを、ここで初めて確かめました。
開発環境
develop にマージすると、デプロイのログに次のように出ました。
Migrations to be applied:
│ 0000_create_gears.sql │
...
│ 0000_create_gears.sql │ ✅ │
さらに、開発用のD1に gears テーブルができていることを、手元から読み取りだけのSQLで確かめました。
ここで1つルールを足しています。もともと「手元から --remote を付けたコマンドは実行しない」と決めていました。読み取りだけとはいえ、確認のためにこれを破ってしまったので、開発用のD1に対する読み取りだけの SELECT は例外として許可し、本番は読み取りでも触らない、とはっきりさせました。
DBの中身をGUIで見る
リモートのD1の中身は、Cloudflareのダッシュボードで見られます。D1のデータベースを開くと、テーブルを表の形で見る画面と、SQLを実行するコンソールがあります。 ただし、この画面からはデータの書き換えもできてしまいます。本番は見るだけにしています。
本番環境
develop → main のPRをマージすると、本番用のD1にも同じマイグレーションが適用されました。
本番のD1は手元から触らない方針なので、テーブルができたことは、ダッシュボードで自分で確かめました。これで最後のACにチェックを付けて、Issueを閉じています。
小ネタ:Cloudflareのアカウントを、ログアウトせずに切り替える
開発用と本番用でCloudflareのアカウントを分けているため、ダッシュボードで両方を見るたびに、ログアウトとログインを繰り返していました。
ダッシュボードの左上では、ログイン中のユーザーがメンバーになっているアカウントを切り替えられます。そこで、片方のユーザーを、もう一方のアカウントにメンバーとして招待しました。
大事なのは招待の向きです。
- 本番用のユーザーを、開発用のアカウントに招待する:ダッシュボードは本番用のユーザーでログインし、左上から両方を切り替える
- 手元の
wranglerは、今までどおり開発用のユーザーでログインしておく。このユーザーは開発用のアカウントにしか入れないので、手元から本番を操作できない守りはそのまま
逆向き(開発用のユーザーを本番用のアカウントに招待)にすると、手元の wrangler からも本番が見えてしまいます。
おわりに
機能としては「ギアを登録して一覧に出す」だけですが、流れを一周したことで、次の点を固められました。
- Issueの下書きはClaude Code、ACを決めるのは自分
- PRには
Refsを書き、ACのチェックは受入判断の印にする - マイグレーションは、開発・本番それぞれで適用されたことを確かめる
次回は、この一周で固まった手順を、Claude Codeのスキル(/issue と /implement)にした話です。