ハコヅミを作る #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)にした話です。

← トップページに戻る