Cursor のスキルと Supabase MCP で登録していたコストコのレシートを、Astro + Supabase の Web アプリに置き換えました。
ブラウザで画像を選ぶだけで、OCR から登録まで進められるようになりました。
できあがった画面と、作りの考え方を紹介します。
はじめに
以前は 📄Cursor Pro+Supabaseでコストコレシートを自動OCR&DB登録する手順 で書いたとおり、Cursor のチャットにレシート画像を渡して OCR し、Supabase に流し込んでいました。
その後、値引きや表記ゆれのルールが増えて、costco-receipt-import というスキルにまとめました。
ただ、レシートを1枚登録するたびに Cursor を立ち上げるのが、じわじわ面倒になってきました。
やりたいのは「画像を渡して、確認して、登録する」だけなんですよね。
そこで、Cursor に依存しない localhost 専用のアプリを作りました。
作りたかったもの
要件は次の4つです。
- レシートの取り込みを、Cursor のチャットから切り離す
- 登録前に、行の内容と表記ゆれを必ず人が確認する
- 商品ごとの価格推移と値引き履歴を、グラフで見られる
- 家計簿連携用に CSV を出力できる
設計方針は、OCR 直後や表記ゆれが未解決の状態では Supabase に書き込まないことです。
構成
| レイヤ | 選定 | 理由 |
|---|---|---|
| フレームワーク | Astro 7 + @astrojs/node | 画面と API ルートを localhost で完結させるため |
| UI | React Islands | 取り込みとグラフなど、状態を持つ部分だけ React にするため |
| スタイル | Tailwind CSS v4 | 最小限の実用的な UI にするため |
| DB | Supabase Postgres | これまで使っていたプロジェクトをそのまま使うため |
| OCR | Gemini Vision | 画像をそのまま渡せて、コストも抑えやすいため |
| グラフ・検証 | Recharts / zod + Vitest | 価格の比較と、API の入力チェック・テストのため |
今回は service role キーを使うので、Node アダプターで API ルートを持たせて、キーはサーバー側の .env にだけ置いています。
OCR は Gemini Vision を既定にして、OpenAI と Anthropic にも環境変数で切り替えられる作りです。
flowchart LR
Img["レシート画像"] --> UI["React Islands"]
UI -->|"POST /api/ocr"| API["Astro API ルート"]
API --> Vision["Gemini Vision"]
Vision --> API
API -->|"下書き JSON"| UI
UI -->|"POST /api/import"| API
API --> SB[("Supabase")] DB に書き込む API は /api/import だけで、confirmed: true が付いていないと受け付けません。
画面の紹介
1. ダッシュボード
冒頭の画像がダッシュボードで、購入件数、実質支出合計、値引き合計と、直近の購入が並ぶトップ画面です。
実質支出合計は、値引き後の金額で集計しています。
この画面では 473 件、実質支出合計 ¥871,283、値引き合計 ¥6,050 と表示されています。
先頭の 6 行は、次に紹介する取り込み画面で登録した 9/18 のレシートです。
2. 取り込み(行の確認)
取り込みは「画像 → 行確認 → 表記ゆれ → 完了」の4ステップです。
画像はドラッグ&ドロップかクリックで選びます。
OCR が終わると、購入日と、商品名・単価・値引きが編集できる表で出てきます。
行の追加や削除もここでできます。
同じレシート内の同名・同単価は1件にまとめていて、この画面では5件を集約したと表示されています。
CPN や IRC などの値引き行は、直前の商品の値引きとして割り当てます。
この時点では、まだ DB には何も書き込んでいません。
3. 表記ゆれの確認
OCR は同じ商品でも、スペースの有無などで名前が少しずれます。
既存のエイリアスに一致した行は、確定名が自動で入ります。
この画面では OCR 名「シーフードアヒージョ」が、既存のエイリアス経由で「ファイブ シーフードアヒージョ」に紐づいています。
未マッピングの行は、既存商品を検索して紐づけるか、新しい正式名称を入力して確定させます。
下の「薩州 麦 2.7L」は、既存の「薩州麦2.7L」を探している途中の画面です。
未解決が1件でも残っていると「登録する」ボタンは押せません。
押したあとも、確認ダイアログで同意しないと登録されません。
固定の表記ゆれ表は自動では適用せず、提案として出して、採用か却下を選ぶ形にしています。
面倒ではありますが、スペース1つで別商品になってしまうので、ここだけはあえて人の目を挟むことにしました。
4. 商品検索と価格推移
商品名やエイリアスで部分一致検索して、最大5商品を同じグラフに重ねて比較できます。
単価と、値引きを引いた実質価格は、ボタンで切り替えます。
画面では CAFFITALY 140PK、KS BATH TISSUE 30、KS ペーパータオル 12RL を並べています。
CAFFITALY が 4,000円台から 5,000円台へ上がっている様子は、グラフにするとはっきり見えます。
値引きがあった商品は、グラフの下の「値引き履歴」に表で出ます。
このほか、期間を指定して「日付,商品名,単価,値引き額」の CSV を出力するページと、エイリアスを確認するページもあります。
データの持ち方
正規化した3つのテーブルで持っています。
products_master に正式な商品名、product_aliases に OCR の表記と商品の対応、purchase_history に購入日・単価・値引き額を入れています。
OCR の生ログを残す costco_receipts は任意です。
業務ルールは、Cursor のスキルと同じものをそのままコードに移しました。
E / T は税率の区分で、値引きではありません。
CPN / IRC / マイナス行は、直前の商品の値引き額として扱います。
OCR が返す 2.398 のような小数は、1,000倍して整数の円にします。
最後のルールは、以前の記事で ¥2,398 が ¥2.398 と読まれて、後から直す必要があった件への対応でもあります。
これらの判定は、価格・値引き・提案・集約ごとにファイルを分けて、Vitest で10テストを書いています。
OCR 自体は毎回ゆれますが、その後の判定はテストで固定できるので。
Cursor 運用との違い
| Cursor + スキル | 今回のアプリ | |
|---|---|---|
| 起動 | Cursor を立ち上げてチャットで指示 | ブラウザで localhost を開く |
| 登録前の確認 | チャット上のやり取り | 行確認と表記ゆれの二重確認。未解決だと登録不可 |
| 価格の確認 | Supabase のコンソールで確認 | 商品検索画面で価格推移をグラフ表示 |
Cursor のスキルは、緊急用として残しています。
注意点
公開デプロイはしない
service role キーは RLS を通らずに書き込めるため、インターネットに公開する使い方は想定していません。
localhost で動かす前提です。
キーは Supabase の Project Settings → API にある service_role で、anon ではありません。
OCR は間違えることがある
二重確認は、OCR の読み間違いを拾う意味でも手放せないと思っています。
まとめ
Cursor 上で回していたレシート登録を、Astro + Supabase の localhost 専用アプリにして、ブラウザだけで完結するようにしました。
OCR の直後や表記ゆれが未解決の状態では DB に書き込まない、という方針だけは最後まで崩さないようにしています。
今後は、エイリアスの管理画面、カテゴリ別の支出分析、価格変動アラートを足していく予定です。
同じようにレシートや家計のデータを自前で管理したい方の参考になれば幸いです。