家計簿アプリを作る #72:レシート画像の添付(Cloudflare R2)
家計簿アプリ作成シリーズの第72回です。収支登録時に、レシートや領収書の写真をアップロードして取引に紐付けられるようにします。Cloudflare R2(オブジェクトストレージ)を使います。
R2へのアップロード
transactions テーブルに receiptKey カラムを追加し、アップロードされた画像はR2に保存、そのキーだけをDBに持たせる設計にしました。
let receiptKey = '';
if (file && file.size > 0) {
receiptKey = `receipts/${crypto.randomUUID()}`;
await platform!.env.RECEIPTS_BUCKET.put(receiptKey, await file.arrayBuffer(), {
httpMetadata: { contentType: file.type },
});
}
ハマりポイント:enctypeの付け忘れ
ファイルアップロード用のフォームに enctype="multipart/form-data" を付け忘れ、以下のエラーが出ました。CSVインポートの時と同じ、よくあるミスです。
Your form contains <input type="file"> fields, but is missing the necessary
`enctype="multipart/form-data"` attribute.
画像の表示は専用APIエンドポイント経由で
画像を表示する方法として、load でBase64エンコードして埋め込む方法と、専用のAPIエンドポイントで画像を返す方法を検討しました。前者はページの応答に画像データが常に含まれてペイロードが膨らむため、後者(/api/receipts/[...key]/+server.ts)を採用しました。
export const GET: RequestHandler = async ({ params, platform }) => {
const object = await platform!.env.RECEIPTS_BUCKET.get(params.key);
if (!object) {
return new Response('Not Found', { status: 404 });
}
return new Response(object.body, {
headers: { 'Content-Type': object.httpMetadata?.contentType ?? 'application/octet-stream' },
});
};
<img src={`/api/receipts/${data.transaction.receiptKey}`} alt="領収書" />
receiptKey の値(receipts/xxxx-xxxx)にスラッシュが含まれるため、動的ルートは [key] ではなく [...key](rest parameter)にする必要がありました。[key] は1つのパスセグメントにしかマッチしないため、スラッシュを含む値は正しく受け取れません。
ハマりポイント:ローカルのR2エミュレーションが共有されない
画像が404で返ってくる不具合がありましたが、原因はアップロード時と取得時で別々の開発サーバープロセスを起動していたことでした。ローカルのR2エミュレーションは .wrangler/state/v3/r2 にデータを保持しますが、プロセスが変わるとこのローカルストレージが引き継がれず、以前アップロードしたファイルが「存在しない」ものとして扱われてしまいます。
さらに、npm run build してから npx wrangler dev で確認していたため、ソースコードを修正しても自動リロードされない(ビルド済みの成果物を配信しているだけ)という別の落とし穴もありました。R2やD1などのCloudflare bindingsを試す際は、同じプロセスを維持しつつ、コードを直したら都度ビルドし直す、という手順が必要になる点を再確認しました。
一覧への添付有無の表示
一覧ページにも、領収書が添付されているかどうかが一目で分かるよう、アイコン列を追加しました。
<TableBodyCell>
{#if transaction.receiptKey}
<ReceiptSolid class="h-5 w-5 text-blue-500" />
{/if}
</TableBodyCell>
今回のスコープ
編集画面での領収書の差し替え・削除機能もあると親切ですが、既存のR2オブジェクトの削除処理なども必要になりスコープが広がるため、今回は「新規登録時のみ添付可能」という仕様で区切りとしました。
まとめ
| ポイント | 内容 |
|---|---|
| R2へのアップロード | キーだけをDBに保存し、実データはR2に置く |
| ファイルアップロードのフォーム | enctype="multipart/form-data"を忘れずに |
| 画像の表示方法 | Base64埋め込みより専用APIエンドポイント経由が軽量 |
| スラッシュを含むキー | 動的ルートは[key]ではなく[...key]を使う |
| ローカルR2エミュレーション | プロセスを揃え、build→wrangler devの手順を徹底する |