HRK Tech Blog
© 2026 HRK Tech Blog. All rights reserved.
一覧に戻る

⚠️ 免責事項

本記事の内容は筆者の個人的な見解や検証結果に基づいています。記載されたコードや設定を利用したことによる不具合・損害について、筆者は一切の責任を負いかねますので、自己責任でご利用ください。

React + Spring Bootの自作家計簿アプリに、Gemini APIでレシート自動入力機能を実装してみた
#Java#AI#React#個人開発#GeminiAPI

React + Spring Bootの自作家計簿アプリに、Gemini APIでレシート自動入力機能を実装してみた

📅2026年6月7日

僕だけが利用する家計簿アプリを開発しているのですが、毎回レシートを見ながら「食費何円、日用品何円...」と手動で入力するのがだんだん面倒くさくなり、記録するのを怠けてしまうことがありました。
そこで、
「レシート画像をアップロードして、AIに自動で日付・商品・金額・カテゴリを判別してもらい自動入力してもらえばいいやん!」
と思いGemini APIを家計簿内に実装してみましたので、その実装手順やプロンプトの工夫などを備忘録としてこの記事に残したいと思います。

ソースコード

本記事で解説する家計簿アプリのソースコードは、以下のGitHubリポジトリで公開しています。
公開用のリポジトリを別で作成したので、記事の内容と少し差分があるかもしれません。

https://github.com/HRK-098/my-kakeibo.git

なぜGemini APIなのか?

OCRライブラリを使用することも考えましたが、実装がめんどくさいのと、そもそもAIがうまいこと全部やってくれるだろうと思いGemini APIを使用しました。
また、個人で利用する家計簿アプリなので、1日のレシート読み込み回数は多くないです。
上位モデルを使用しなければ基本無料で利用できる点も大きいです。
今回はgemini-2.5-flashを使用しました。レート制限は下記です。
・1分あたりのリクエスト数(RPM):5
・1分あたりのトークン数(入力)(TPM):250K
・1日あたりのリクエスト数(RPD):20
十分です。

【準備】Gemini APIキーの作成

Gemini APIを使用するために、Google AI Studioにアクセスし、API Keyを作成する必要があります。

  1. Google AI Studioにアクセスし、ログインします
  2. 画面左下の方にGet API keyがあるので、そちらをクリック
  3. APIキー画面に遷移したら、画面右上の方にAPIキーを作成ボタンがあるので、そちらをクリック
  4. 後は画面の指示通りにキー名とプロジェクトを選択して作成する

環境変数にAPIキーの設定

コードにAPIキーをハードコードしないために、環境変数から読み込むように設定します。
今回の環境はWindowsなので、システムの環境変数に設定しました。

  1. 変数名をGEMINI_API_KEYと指定
  2. 上記で作成したGemini APIキーを変数値として指定

システム構成と処理フロー

処理の流れはシンプルに以下のように設計しました。

  1. フロントエンド(React):レシート画像をドラッグ&ドロップでアップロードする
  2. バックエンド(Spring Boot/Java):画像を受け取り、Base64形式にエンコード。DBから現在のカテゴリ一覧を取得する
  3. AI(Gemini API):画像とカテゴリ一覧をGeminiに渡し、JSON形式で解析データを取得する
  4. フロントエンド(React):解析結果を確認・修正できるモーダルを表示し、OKなら家計簿に登録する

Javaの実装

API キーが取得できたら、Spring Boot でレシート画像を受け取って Gemini API にリクエストを送るコントローラーを実装します。

レシート解析用 DTO の作成

Gemini から JSON 形式で結果を受け取るため、データ構造(DTO)を定義します。

// ReceiptScanItem.java (明細)
public class ReceiptScanItem {
    private String category;    // カテゴリ名
    private Integer amount;     // 金額
    private String description; // 店舗名や品名
    // getter/setter/constructor 省略
}

// ReceiptScanResult.java (全体の解析結果)
public class ReceiptScanResult {
    private String date; // YYYY-MM-DD
    private List<ReceiptScanItem> items;
    private Integer totalAmount;
    // getter/setter/constructor 省略
}

コントローラーの実装

Java21のHttpClientを使いgemini-2.5-flashモデルへ画像データとテキストプロンプトを送信します。
プロンプトでは、DBに登録されているカテゴリ名の一覧を動的に埋め込み、AIにその中から分類させます。

@RestController
@RequestMapping("/api/receipt")
public class ReceiptScannerController {

  @Value("${gemini.api.key}")
  private String geminiApiKey;

  @Autowired
  private CategoryDefRepository categoryRepository;

  @Autowired
  private ObjectMapper objectMapper;

  private final HttpClient httpClient = HttpClient.newHttpClient();

  @PostMapping("/scan")
  public ResponseEntity<?> scanReceipt(@RequestParam("file") MultipartFile file) {
    String apiKey = geminiApiKey;
    if (apiKey == null || apiKey.trim().isEmpty()) {
      apiKey = System.getenv("GEMINI_API_KEY");
    }

    try {
      // カテゴリ一覧を取得してプロンプトに動的に埋め込む
      List<String> categories = categoryRepository.findAll().stream()
          .map(CategoryDef::getName)
          .toList();
      String categoryListStr = String.join(", ", categories);

      // プロンプトの構築
      String prompt = "あなたは優秀な家計簿アシスタントです。アップロードされたレシート画像を解析し、以下のカテゴリリストに当てはまる支出項目を抽出してください。\n" +
              "カテゴリリスト: [" + categoryListStr + "]\n\n" +
              "【レシート解析および計算の汎用ルール】\n" +
              "1. 最終支払合計金額の特定 (これを totalAmount とする)\n" +
              "2. 各商品の最終税込・割引後価格の算出 (金額の総和が totalAmount と1円単位で完全に一致するように調整すること)\n\n" +
              "以下のJSONフォーマットのみで返答してください。\n" +
              "{\n" +
              "  \"date\": \"YYYY-MM-DD\",\n" +
              "  \"items\": [\n" +
              "    { \"category\": \"カテゴリ名\", \"amount\": 100, \"description\": \"品名\" }\n" +
              "  ],\n" +
              "  \"totalAmount\": 100\n" +
              "}";

      // 画像を Base64 エンコード
      String base64Image = Base64.getEncoder().encodeToString(file.getBytes());
      String contentType = file.getContentType() != null ? file.getContentType() : "image/jpeg";

      // リクエストボディの組み立て
      Map<String, Object> inlineData = Map.of("mimeType", contentType, "data", base64Image);
      Map<String, Object> textPart = Map.of("text", prompt);
      Map<String, Object> imagePart = Map.of("inlineData", inlineData);
      Map<String, Object> partContainer = Map.of("parts", List.of(textPart, imagePart));
      Map<String, Object> generationConfig = Map.of("responseMimeType", "application/json");

      Map<String, Object> requestBodyMap = Map.of(
          "contents", List.of(partContainer),
          "generationConfig", generationConfig
      );
      String requestBodyJson = objectMapper.writeValueAsString(requestBodyMap);

      // API 呼び出し
      String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=" + apiKey;
      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(url))
          .header("Content-Type", "application/json")
          .POST(HttpRequest.BodyPublishers.ofString(requestBodyJson))
          .build();

      HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

      // JSONレスポンスのパース
      JsonNode root = objectMapper.readTree(response.body());
      String responseText = root.path("candidates").path(0).path("content").path("parts").path(0).path("text").asText();
      ReceiptScanResult result = objectMapper.readValue(responseText, ReceiptScanResult.class);

      return ResponseEntity.ok(result);
    } catch (Exception e) {
      return ResponseEntity.internalServerError().body(Map.of("message", e.getMessage()));
    }
  }
}

フロントエンドの実装 (React + TypeScript)

フロントエンドでは、画像をアップロードするドラッグ&ドロップ領域を持つモーダル UI を用意します。
APIからの返り値である「スキャン結果(日付・品目リスト)」を一度フォームに反映させ、画面上で1品ずつ微調整できるようにしています。

データの登録処理(カテゴリ毎の集計)

AIが正しく認識しているか画面上で1品ずつ確認・編集できるようにしつつ、登録ボタンを押したタイミングで、フロントエンド側でカテゴリ毎に自動で集計(合算)して登録します。
これにより、履歴が商品ごとに細切れになってしまうのを防ぎつつ、カテゴリ別の正確な家計簿データを登録できます。

const handleRegister = async () => {
  if (formItems.length === 0) return;
  setIsSaving(true);
  try {
    // カテゴリごとに金額を集計し、商品名をカンマ区切りで結合する
    const grouped: { [category: string]: { amount: number; descList: string[] } } = {};
    
    for (const item of formItems) {
      const cat = item.category || "未分類";
      if (!grouped[cat]) {
        grouped[cat] = { amount: 0, descList: [] };
      }
      grouped[cat].amount += Number(item.amount || 0);
      if (item.description && item.description.trim()) {
        grouped[cat].descList.push(item.description.trim());
      }
    }

    // 集計した結果を登録用の取引リストに変換
    const newTxList = Object.keys(grouped).map((cat) => {
      const group = grouped[cat];
      const uniqueDescs = Array.from(new Set(group.descList));
      const combinedDesc = uniqueDescs.join(", ");
      
      return {
        date: scanDate || defaultDate,
        amount: group.amount,
        category: cat,
        description: combinedDesc || "レシート一括登録",
        paidBy,
        isSettlementTarget,
      };
    });

    // 一括登録
    await onAddTransactions(newTxList);
    onSuccess(`${newTxList.length}件の支出をカテゴリ毎にまとめて登録しました!`);
    onClose();
    // クリーンアップ処理
    setSelectedFile(null);
    setPreviewUrl(null);
    setScanResult(null);
  } catch (err) {
    onError("家計簿への登録に失敗しました");
  } finally {
    setIsSaving(false);
  }
};

動かしてみた

AIスキャンボタン

AIスキャンボタン

レシートアップロード初期画面
レシートアップロード初期画面

レシートアップロード
レシートアップロード

AI解析中
AI解析中

解析結果
解析結果

履歴に反映
履歴に反映

まとめ

Google AI StudioのGemini APIを活用することで、わずか数十行のプロンプトとJava標準のHttpClientだけで、実用的なレシートスキャン機能を自作家計簿に実装することができました。
特に、税込み・税抜き・割引が複雑に絡む日本のレシートの計算ズレを「合計金額と一致させる」プロンプトの工夫だけで解消できたのは、LLMの賢さを実感しました。
AIしゅごい

参考URL

Gemini API 公式ドキュメント

https://ai.google.dev/gemini-api/docs?hl=ja

関連記事

MavenアプリのDocker化
#Java#Docker

MavenアプリのDocker化

2026/4/7
WindowsのNeovim (LazyVim) でJava開発環境を構築するまでの完全トラシューガイド
#Java#vim

WindowsのNeovim (LazyVim) でJava開発環境を構築するまでの完全トラシューガイド

2026/3/2