コストコのレシート取り込みを Cursor から切り離してアプリにした

Cursor のスキルと Supabase MCP で登録していたコストコのレシートを、Astro + Supabase の Web アプリに置き換えました。

ブラウザで画像を選ぶだけで、OCR から登録まで進められるようになりました。

できあがった画面と、作りの考え方を紹介します。

はじめに

以前は 📄Arrow icon of a page linkCursor 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 に書き込まない、という方針だけは最後まで崩さないようにしています。

今後は、エイリアスの管理画面、カテゴリ別の支出分析、価格変動アラートを足していく予定です。

同じようにレシートや家計のデータを自前で管理したい方の参考になれば幸いです。