食材管理を自動化するためのアプリ
まじでずっと作りたくて、何より自分が欲しいから実装する
🍳 食材管理 & レシピ提案アプリ 設計ドキュメント(MVP版)
1. 開発環境・技術選定
フロントエンド: Webアプリ (Next.js + TypeScript)
バックエンド: Next.js API Routes (TS)
DB: Supabase(無料枠利用)
OCR: Tesseract(MVPは無料、精度が必要ならGoogle Vision API)
レシピデータ: 楽天レシピAPI(日本語対応)
通知: Discord Webhook(専用サーバーのチャンネルに自動投稿)
2. ユースケース
1. ユーザーがレシートを撮影し、OCRで食材を登録
2. 食材は 名前・数量・賞味期限 を持つ
3. 毎日決まった時間(例: JST 8:00)、期限が近い食材をもとにレシピを検索
4. 検索結果(3件程度)をDiscordに自動通知
5. 家族全員がDiscordでレシピを閲覧可能
3. 機能一覧
🥬 食材管理
入力方法:
OCR
手動入力
項目:
食材名
数量(オプション)
単位(個, g, ml など)
賞味期限
⏰ 賞味期限管理
毎日定時にチェック
「期限切れ」や「あと3日以内の食材」を優先
📖 レシピ提案
楽天レシピAPIで「食材名」をキーに検索
3件をランダム or スコア順で選定
レシピ名+URLを通知
🔔 通知
Discord Webhookで専用サーバーのチャンネルに投稿
投稿形式(例):
code:ex.json
🍳 今日のおすすめレシピ 🍳
🥢 親子丼
🥗 野菜炒め
🍲 シチュー
4. データモデル(Supabase想定)
items(食材)
https://scrapbox.io/files/68dd7fde209bb263859ead2e.png
households(家族)
https://scrapbox.io/files/68dd8003a44403c08b6ca2e3.png
daily_runs(通知ログ)
https://scrapbox.io/files/68dd80185d761d097c1d0986.png
5. APIインターフェース(Next.js API Routes)
/api/items
POST: 食材登録
Body: { name, quantity?, unit?, expires_on }
GET: 登録済み食材一覧を取得
DELETE: 食材削除
/api/recipes/search
GET: 指定された食材でレシピ検索(楽天レシピAPIを呼ぶ)
Query: ?ingredients=玉ねぎ,鶏肉
/api/cron/daily-notify
GET: 毎日ジョブで叩かれるエンドポイント
処理内容:
食材DBから期限が近いものを抽出
レシピ検索
Discordに通知
daily_runs に記録
6. 通知設計
トリガー: Vercel Cron or Supabase Scheduled Function
タイミング: 毎日 8:00 JST
宛先: households.discord_webhook_url
形式: Markdownでレシピリストを整形
code:example.mmd
erDiagram
households {
uuid id PK
text name
text discord_webhook_url
}
items {
uuid id PK
uuid household_id FK
text name
numeric quantity
text unit
date expires_on
timestamptz created_at
}
daily_runs {
date run_date PK
uuid household_id FK
timestamptz sent_at
int recipe_count
}
households ||--o{ items : "has many"
households ||--o{ daily_runs : "logs"
API設計書
1. /api/ocr/parse
概要
レシート画像をOCRで解析し、食材名を抽出する。
Method: POST
code: Request.json
{
"imageBase64": "data:image/png;base64,iVBORw0K..."
}
table:
field 型 説明
imageBase64 string Base64形式のレシート画像データ
code:responce.json
{
"items": [
{ "name": "牛乳" },
{ "name": "玉ねぎ" },
{ "name": "鶏もも肉" }
]
}
2. /api/items
概要:
冷蔵庫内の食材データを管理する(登録・取得・更新・削除)
POST /api/items
code:request.json
{
"name": "牛乳",
"quantity": 1,
"unit": "本",
"expires_on": "2025-10-13"
}
table:
field 型 説明
name string 食材名
quantity number 数量
unit string 単位(個, g, mlなど)
expires_on string (ISO8601) 賞味期限
Response (201 Created)
code:responce.json
{
"id": "e8e612fc-78a0-4b21-921b-2d80f2386b44",
"name": "牛乳",
"quantity": 1,
"unit": "本",
"expires_on": "2025-10-13",
"created_at": "2025-10-09T08:00:00Z"
}
GET/api/items
説明:
登録済みの食材一覧を取得
code:response.json
[
{
"id": "e8e612fc-78a0-4b21-921b-2d80f2386b44",
"name": "牛乳",
"quantity": 1,
"unit": "本",
"expires_on": "2025-10-13"
},
{
"id": "f9a623ad-12b0-4d8b-a833-32fef33c9d31",
"name": "玉ねぎ",
"quantity": 3,
"unit": "個",
"expires_on": "2025-10-12"
}
]
PATCH /api/items/:id
説明:
食材の数量や賞味期限を更新する
code:request.json
{
"quantity": 2
}
code:responce.json
{
"id": "e8e612fc-78a0-4b21-921b-2d80f2386b44",
"quantity": 2
}
DELETE/api/items/:id
説明:
食材を削除する
Response (204 No Content)
3. /api/recipes/search
概要:
指定された食材からレシピを検索(楽天レシピAPI利用)
Method: GET
Auth: 不要(MVPでは公開)
code:query
/api/recipes/search?ingredients=牛乳,玉ねぎ,鶏もも肉
table:
field 型 説明
ingredients string 食材名
code:responce.json
[
{
"title": "親子丼(簡単5分)",
},
{
"title": "鶏肉のクリーム煮",
}
]
4. /api/cron/daily-notify
概要:
毎朝定時(JST 8:00)にレシピを検索し、Discordへ自動通知。
(Vercel CronまたはSupabase Schedulerで実行)
処理フロー
1. Supabaseから賞味期限が近い食材を取得
2. /api/recipes/search を内部的に呼び出してレシピ取得
3. households.discord_webhook_url に通知内容をPOST
4. daily_runsに記録を追加
code:response.json
{
"date": "2025-10-09",
"recipes_sent": 3,
"status": "notified"
}
型定義:
code:type.ts
// ==============================
// 🧊 Item(食材)
// ==============================
export interface Item {
id: string;
name: string;
quantity: number;
unit: string;
expires_on: string; // ISO date string (例: "2025-10-13")
created_at: string;
}
// POST /api/items
export interface CreateItemRequest {
name: string;
quantity: number;
unit: string;
expires_on: string;
}
export interface CreateItemResponse extends Item {}
// GET /api/items
export type GetItemsResponse = Item[];
// PATCH /api/items/:id
export interface UpdateItemRequest {
quantity?: number;
unit?: string;
expires_on?: string;
}
export interface UpdateItemResponse {
id: string;
quantity?: number;
unit?: string;
expires_on?: string;
}
// DELETE /api/items/:id
// → Response: 204 No Content (no body)
// ==============================
// 🧠 OCR(レシート解析)
// ==============================
export interface OcrParseRequest {
imageBase64: string;
}
export interface OcrItem {
name: string;
}
export interface OcrParseResponse {
items: OcrItem[];
}
// ==============================
// 🍳 Recipe(レシピ)
// ==============================
export interface Recipe {
title: string;
url: string;
ingredients: string[];
}
// GET /api/recipes/search
export interface RecipeSearchResponse extends Array<Recipe> {}
// ==============================
// 🔔 Daily Notify(定時通知)
// ==============================
export interface DailyNotifyResponse {
date: string;
recipes_sent: number;
status: "notified" | "skipped" | "error";
}
export interface DailyNotifyError {
error: string;
}
code:zod.ts
import { z } from "zod";
export const createItemSchema = z.object({
name: z.string().min(1, "必須です"),
quantity: z.number().min(0.1),
unit: z.string().min(1),
expires_on: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
export type CreateItemSchema = z.infer<typeof createItemSchema>;
構成イメージ:
code:json
src/
├── pages/
│ ├── api/
│ │ ├── ocr/parse.ts
│ │ ├── items.ts
│ │ ├── recipes/search.ts
│ │ └── cron/daily-notify.ts
│ └── index.tsx
├── types/
│ └── api.ts
└── schema/
└── itemSchema.ts ← Zodスキーマ