1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LINE bot × GASで献立作成botを作る

Posted at

日々の献立考えるのめんどくさいな...と常に思っている民です。
そして子供が生まれてからは、離乳食作りも追加されたため、朝起きた瞬間から、今日の夜ご飯はどうしよう...ということを考えるようになりました。

もちろん便利な今は食材を入力すれば良い感じのレシピが検索できるサイトはたくさんありますが、個人的にはもはやググるのがめんどくさい!スクロールするのがめんどくさい!そしてそこから離乳食にも使い回せそうな料理を探すのがめんどくさい!

ということで、ある程度料理の設定をしたら、そこから残りの食材を教えたら献立を考えてくれるbotを作ってしまおう!という。

仕様

  1. 余っている食材を送る
  2. AIが設定を元に献立を送ってくれる
  3. 気に入らなかったら再作成する or 気に入ればOKを返すと、その日の献立を記録する

※献立作成時の設定などは、同じ生成AIママ部のゆかさんのnoteを参考にさせていただきました。

コード

index.js
// ========================================
// 設定情報(実際の値に置き換えてください)
// ========================================
const LINE_CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_CHANNEL_ACCESS_TOKEN");
const GEMINI_API_KEY = PropertiesService.getScriptProperties().getProperty("GEMINI_API_KEY");
const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty("SPREADSHEET_ID");

// ========================================
// ユーザー設定(固定)
// ========================================
const USER_PREFERENCES = {
  taste: "和風が好み",
  restrictions: "塩分控えめ",
  special: "赤ちゃんがいるので取り分けメニュー対応",
  family: "大人2人+赤ちゃん1人"
};

// ========================================
// LINE Webhook処理
// ========================================
function doPost(e) {
  try {
    const json = JSON.parse(e.postData.contents);
    const events = json.events;
    
    events.forEach(event => {
      if (event.type === 'message' && event.message.type === 'text') {
        handleTextMessage(event);
      } else if (event.type === 'postback') {
        handlePostback(event);
      }
    });
    
    return ContentService.createTextOutput(JSON.stringify({status: 'ok'}))
      .setMimeType(ContentService.MimeType.JSON);
  } catch (error) {
    console.error('Error in doPost:', error);
    return ContentService.createTextOutput(JSON.stringify({status: 'error'}))
      .setMimeType(ContentService.MimeType.JSON);
  }
}

// ========================================
// テキストメッセージ処理
// ========================================
function handleTextMessage(event) {
  const userId = event.source.userId;
  const userMessage = event.message.text;
  const replyToken = event.replyToken;
  
  // 食材リストとして処理
  const ingredients = userMessage;
  
  // 献立を生成
  const menu = generateMenu(ingredients);
  
  // 一時保存(postbackで使用)
  saveTemporaryData(userId, {
    ingredients: ingredients,
    menu: menu,
    timestamp: new Date().toISOString()
  });
  
  // 返信
  replyWithMenu(replyToken, menu);
}

// ========================================
// Postback処理
// ========================================
function handlePostback(event) {
  const userId = event.source.userId;
  const replyToken = event.replyToken;
  const data = event.postback.data;
  
  const tempData = getTemporaryData(userId);
  
  if (!tempData) {
    replyMessage(replyToken, '申し訳ありません。データが見つかりませんでした。もう一度食材を送信してください。');
    return;
  }
  
  if (data === 'regenerate') {
    // 再生成
    const newMenu = generateMenu(tempData.ingredients);
    
    // 一時保存を更新
    saveTemporaryData(userId, {
      ingredients: tempData.ingredients,
      menu: newMenu,
      timestamp: new Date().toISOString()
    });
    
    replyWithMenu(replyToken, newMenu);
    
  } else if (data === 'save') {
    // スプレッドシートに保存
    saveToSpreadsheet(tempData.ingredients, tempData.menu);
    
    // 保存完了メッセージ
    replyMessage(replyToken, '✅ 献立を記録しました!\n\n今日も美味しい食事を楽しんでくださいね🍚');
    
    // 一時データを削除
    deleteTemporaryData(userId);
  }
}

// ========================================
// Gemini APIで献立生成
// ========================================
function generateMenu(ingredients) {
  const prompt = `
あなたは家庭料理の献立アドバイザーです。以下の条件で時短献立を提案してください。

【ユーザー設定】
- 味付け: ${USER_PREFERENCES.taste}
- 制約: ${USER_PREFERENCES.restrictions}
- 特記事項: ${USER_PREFERENCES.special}
- 家族構成: ${USER_PREFERENCES.family}

【使用できる食材】
${ingredients}

【献立の条件】
- 調理時間は全体で20分以内
- メイン1品、副菜1〜2品を提案
- 赤ちゃんの取り分けができる料理を優先
- 各料理に簡単な調理手順を添える
- 親しみやすく、わかりやすい表現で

【出力形式】
以下の形式で出力してください:

🍳 **メイン: [料理名]**
[調理手順を2-3行で]
👶 取り分け: [赤ちゃん用の取り分け方法]

🥗 **副菜: [料理名]**
[調理手順を2-3行で]

💡 **ポイント**
[時短のコツや栄養面でのアドバイスを1-2行で]
`;

  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${GEMINI_API_KEY}`;
  
  const payload = {
    contents: [{
      parts: [{
        text: prompt
      }]
    }],
    generationConfig: {
      temperature: 0.9,
      maxOutputTokens: 2000
    }
  };
  
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };
  
  try {
    const response = UrlFetchApp.fetch(url, options);
    const json = JSON.parse(response.getContentText());
    
    if (json.candidates && json.candidates[0] && json.candidates[0].content) {
      return json.candidates[0].content.parts[0].text;
    } else {
      console.error('Unexpected Gemini API response:', json);
      return '献立の生成中にエラーが発生しました。もう一度お試しください。';
    }
  } catch (error) {
    console.error('Error calling Gemini API:', error);
    return '献立の生成中にエラーが発生しました。もう一度お試しください。';
  }
}

// ========================================
// LINE返信(献立+ボタン)
// ========================================
function replyWithMenu(replyToken, menu) {
  const url = 'https://api.line.me/v2/bot/message/reply';
  
  const message = {
    replyToken: replyToken,
    messages: [
      {
        type: 'text',
        text: menu,
        quickReply: {
          items: [
            {
              type: 'action',
              action: {
                type: 'postback',
                label: '🔄 もう一回',
                data: 'regenerate',
                displayText: 'もう一回'
              }
            },
            {
              type: 'action',
              action: {
                type: 'postback',
                label: '✅ OK',
                data: 'save',
                displayText: 'OK'
              }
            }
          ]
        }
      }
    ]
  };
  
  const options = {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
    },
    payload: JSON.stringify(message)
  };
  
  UrlFetchApp.fetch(url, options);
}

// ========================================
// LINE返信(テキストのみ)
// ========================================
function replyMessage(replyToken, text) {
  const url = 'https://api.line.me/v2/bot/message/reply';
  
  const message = {
    replyToken: replyToken,
    messages: [
      {
        type: 'text',
        text: text
      }
    ]
  };
  
  const options = {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
    },
    payload: JSON.stringify(message)
  };
  
  UrlFetchApp.fetch(url, options);
}

// ========================================
// 一時データ管理(PropertiesServiceを使用)
// ========================================
function saveTemporaryData(userId, data) {
  const properties = PropertiesService.getScriptProperties();
  properties.setProperty('temp_' + userId, JSON.stringify(data));
}

function getTemporaryData(userId) {
  const properties = PropertiesService.getScriptProperties();
  const data = properties.getProperty('temp_' + userId);
  return data ? JSON.parse(data) : null;
}

function deleteTemporaryData(userId) {
  const properties = PropertiesService.getScriptProperties();
  properties.deleteProperty('temp_' + userId);
}

// ========================================
// スプレッドシートに保存
// ========================================
function saveToSpreadsheet(ingredients, menu) {
  try {
    const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
    let sheet = ss.getSheetByName('献立履歴');
    
    // シートが存在しない場合は作成
    if (!sheet) {
      sheet = ss.insertSheet('献立履歴');
      sheet.appendRow(['日付', '食材', '献立内容', 'メモ']);
      sheet.getRange(1, 1, 1, 4).setFontWeight('bold').setBackground('#f3f3f3');
    }
    
    // データを追加
    const date = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd');
    sheet.appendRow([date, ingredients, menu, '']);
    
    // 自動リサイズ
    sheet.autoResizeColumns(1, 4);
    
  } catch (error) {
    console.error('Error saving to spreadsheet:', error);
    throw error;
  }
}

// ========================================
// 動作確認用テスト関数
// ========================================
function testGenerateMenu() {
  const testIngredients = "鶏もも肉、豚バラ、ほうれん草、大根、トマト";
  const menu = generateMenu(testIngredients);
  console.log(menu);
}

function testSaveToSpreadsheet() {
  const testIngredients = "鶏もも肉、ほうれん草、大根";
  const testMenu = "テスト献立\n\nメイン: 鶏の照り焼き\n副菜: ほうれん草のおひたし";
  saveToSpreadsheet(testIngredients, testMenu);
  console.log('保存完了');
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?