🧠 概要:
概要
この記事では、LINEを活用して簡単に食事記録ができるボットの作成方法が紹介されています。初心者でも短時間で設定が可能で、面倒な食事記録から解放される仕組みが解説されています。ボットは食事の写真を送るだけで栄養素を分析し、データは自動的にGoogleスプレッドシートに保存されます。
要約ポイント
-
このボットの特徴:
- LINEを使用し、専用アプリは不要。
- 簡単な設定で、プログラミング初心者でも30分で開始可能。
-
ボットの機能:
- 写真を送るとAIが食事内容を分析。
- テキストで食事名を送るだけで記録できる。
- 栄養素(カロリー、糖質、タンパク質、脂質など)を自動計算。
- Googleスプレッドシートにデータを自動保存。
-
おすすめな人:
- 食事記録のアプリを続けられない人。
- 細かい入力が面倒な人。
- 簡単に健康管理をしたい人。
-
今後のアップデート予定:
- AIによる食事バランスのアドバイス機能。
- 体重変化や運動記録に基づくフィードバック。
-
必要なもの:
- Googleアカウント、LINEアカウント、OpenAIアカウント、クレジットカード。
-
セットアップ手順:
- LINE公式アカウントの作成。
- Googleスプレッドシートの準備。
- Googleドライブにフォルダ作成。
- OpenAI APIキーの取得。
- Google Apps Scriptの設定。
- LINEのWebhookと連携。
-
使用方法:
- 食事写真の送信で記録、またはテキストでの食事名記録。
- 簡単に体重や運動記録も可能。
- 注意点:
- スプレッドシートに記録が反映されない場合のチェックリストが提示。
この記事では、手軽に続けられる食事記録の方法が示されており、特に忙しい人にとって便利なツールが提供されています。
📱 今後の機能拡張予定:・「最近の食事バランスは?」と質問するとAIがアドバイス・体重変化や運動記録に基づいたパーソナルなフィードバック
・食生活改善のための具体的なヒントを提供
この記事を見れば、あなたも今すぐ作れます♪
💡 お知らせ:今後AIアドバイス機能を追加するなど、さらに便利な機能を開発中です。
今のうちに試してみるのがおすすめです!
🍽 食事記録をラクにするLINEボット
このボットでできること:
📸 写真を撮って送るだけでAIが食事内容を分析
✏️ 「fカレーライス」と一言送るだけでも記録OK
🔢 カロリー・糖質・タンパク質・脂質・野菜量を自動計算
⚖️ 「60kg」や「ex1」など短い言葉で体重・運動も記録
📊 すべての記録がGoogleスプレッドシートに自動保存
プログラミングの知識は必要ありません!
この説明書どおりに設定するだけで、誰でも作れます。
「ちゃんと記録しなきゃ」というプレッシャーなし。「送っただけで記録されてた」くらいがちょうどいい。
それが、このボットの考え方です。
👍 こんな人におすすめ
-
食事記録アプリを入れても使わなくなってしまう
-
毎回料理名を考えるのが面倒くさい
-
細かい計算より、食生活の傾向を知りたい
-
とにかく簡単に健康管理をしたい
-
LINEなら毎日使うから続けられそう
🥗 栄養計算について
このボットでは:
-
食事内容からおおよその栄養素を推定します
-
正確な栄養計算ではなく参考程度の目安です
-
分量や調理法までは把握できません
でも、スプレッドシートに写真も表示されるので、「最近揚げ物多いかも」「野菜が足りていないかも」など、
食事の傾向に気づくきっかけになります。
🌈 大事なのは「続けること」
このLINEボットは:
-
続かない人のために作りました
-
完璧な記録より続けやすさを重視しています
-
送るだけで記録できる手軽さが魅力です
それでは、作り方を見ていきましょう!
📋 必要なもの
-
Googleアカウント
-
LINEアカウント
-
OpenAIアカウント(AIの機能に使います)
-
クレジットカード(OpenAI APIの登録に必要、使用料は月に数十円~数百円程度)
🔧 セットアップ手順
STEP1:LINE公式アカウント(Bot)を作る
• LINE Developersにログイン個人用LINEアカウントでログイン(QRコード推奨)
普段使っているLINEアカウント、または新規のメールアドレスを使ってLINEビジネスIDを作成
• プロバイダーを作成
「新しいプロバイダーを作成する」→名前入力→「作成」
• Messaging APIチャネルの作成「Messaging APIを作成する」→「LINE公式アカウントを作成」電話番号認証→基本情報入力(名前・メール・業種[個人]など)「設定」→「Messaging API」→プロバイダー選択
チャネル名を設定→規約欄は空欄でOK
• アクセストークン取得左メニューから「Messaging API設定」へ移動「チャネルアクセストークン(長期)」の「発行」ボタンをクリック
表示されたトークンをコピーして安全な場所に保存(プログラムの認証に必要)
• ボット友だち追加とWebhook設定「Messaging API設定」ページ下部のQRコードをスキャンして友だち追加同ページの「応答設定」で「応答メッセージ」を「オフ」に設定
「Webhookの利用」を「オン」に設定(後でURLを設定)
※詳細な画面写真や設定方法はこちらの記事で確認できます。
STEP2:スプレッドシートをコピーして準備する
• 以下のリンクをクリックしてスプレッドシートテンプレートをコピー:
食事記録テンプレートをコピー
• 「コピーを作成」をクリックしてご自分のGoogleドライブにコピー
• コピーしたスプレッドシートのURLから「ID」をメモ
(`https://docs.google.com/spreadsheets/d/【ここがID】/edit…`)
STEP3:Googleドライブにフォルダを作る
• Googleドライブに「LINE食事画像」などの名前でフォルダ作成• フォルダのURLから「ID」をメモ
(`https://drive.google.com/drive/folders/【ここがID】`)
-
フォルダの共有設定を変更
-
フォルダの右にある「︙」→「共有」をクリック
-
「リンクを知っている人」を選択
-
アクセス権は「閲覧者」に設定
-
※共有設定が必要な理由:スプレッドシートで食事の写真を表示するために必要な設定です閲覧権限のみの設定なので、写真の編集や削除はできません
URLを知らない人はアクセスできないため、安全です
STEP4:OpenAI APIキーを取得する
• OpenAIでアカウント作成• 「API keys」→「Create new secret key」でキーを作成しメモ※APIキーは後から再表示できませんので、取得時に必ずコピーして安全な場所に保存してください。
• 支払い設定を行う(クレジットカード登録)
STEP5:Google Apps Script(GAS)を設定する
• STEP2で作ったスプレッドシートを開く
• メニュー「拡張機能」→「Apps Script」をクリック 🖼️
• エディタに表示されているコードを確認
• 初期設定の部分に、IDやAPIキーを入力する
const CONFIG = { SPREADSHEET_ID: "あなたのスプレッドシートID", SHEET_NAME: "シート1", ACCESS_TOKEN: "あなたのLINEアクセストークン", FOLDER_ID: "あなたのGoogleドライブフォルダID", GPT_API_KEY: "あなたのOpenAI APIキー", GPT_MODEL: "gpt-4o-mini" };
•画面右上の青色の「デプロイ」ボタンをクリック メニューから「新しいデプロイ」を選択「種類の選択」という画面が出たら「ウェブアプリ」を選択「新しい説明文」には以下を入力:
「LINE食事記録ボット」など
• 公開の設定をします「次のユーザーとして実行」は「自分」を選択 「アクセスできるユーザー」は「全員」を選択(※LINEボットがプログラムを使えるようにするために必要な設定です)
最後に青色の「デプロイ」ボタンをクリック
• 権限の確認:Googleアカウントでの認証が求められます
下記の「初回認証手順」に従って進めてください
• デプロイ完了後:「アプリをデプロイしました」という画面が表示されます「ウェブアプリ」のURLが表示されるので、必ずメモしてください
「完了」をクリックして設定を終了します
このURLは次のSTEP6で必要になりますので、必ず保存してください。
初回認証手順(セキュリティ警告について)
※初回は以下の警告画面が表示されますが、問題ありません:
-
「アプリを検証していません」という警告画面が表示される
-
画面は青色の警告バナーで表示されます
-
「このアプリはGoogleで確認されていません」または「Google hasn’t verified this app」という警告が表示されます
(ブラウザの言語設定により異なります)
-
-
左下の「詳細」(または「Advanced」)をクリック
-
小さな灰色のリンクテキストです
-
画面の左下に表示されています
-
-
「安全でないページに移動」(または「Go to … (unsafe)」)をクリック
-
「許可を確認」画面で自分のGoogleアカウントを選択
-
普段使っているGoogleアカウントを選んでください
-
複数のアカウントがある場合は、スプレッドシートを作成したアカウントを選択
-
-
「詳細」をクリック
-
一番下までスクロールして「(安全でないページ)に移動」をクリック
-
最後に「アクセスを許可」をクリック
-
アプリが要求する権限の一覧が表示されます
-
すべての権限にチェックが入っていることを確認してください
-
※これはGoogleの標準的なセキュリティ対策です。
このアプリは安全に進めることができます。
デプロイ完了後の手順
-
「アプリをデプロイしました」という画面が表示されます
-
「ウェブアプリ」のURLが表示されるので、必ずメモしてください
-
「完了」をクリックして設定を終了します
⚠️ 重要:次に進む前の確認事項
必ず以下の項目を確認してから次に進んでください:
-
ウェブアプリのURLをメモしましたか?
-
「完了」ボタンをクリックしましたか?
-
スプレッドシートの画面に戻りましたか?
以上でSTEP5は完了です!
次のSTEP6「LINE WebhookとGASを連携」では、メモしたURLを使用します。
STEP6:LINE WebhookとGASを連携
• LINE Developersコンソールに戻る• 「Messaging API設定」を選択• 「Webhook URL」に STEP5でメモしたURLを貼り付け
•「Webhookの利用」を「オン」に
📱 使い方
食事を記録する方法
写真で記録: 🖼️
-
食事の写真を撮ってLINE Botに送信するだけ!
-
AIが自動で分析して栄養素を計算します
テキストで記録:
-
`f食事名` と送信(例: `fカレーライス`)
-
または `f@トースト、目玉焼き` のように送信
体重を記録
運動記録には2つの方法があります:
-
強度による記録
-
`ex0`: 運動なし(座る、寝る、テレビ視聴など)
-
`ex1`: 運動軽め(ストレッチ、ゆっくり歩行、軽い家事)
-
`ex2`: 運動中程度(早歩き、自転車、水泳(ゆっくり)、ヨガ)
-
`ex3`: 運動重め(ジョギング、ランニング、テニス、サッカー)
-
-
具体的な運動内容の記録
-
`ex@運動内容` の形式で記録
-
例:`ex@ジョギング30分`
-
例:`ex@ヨガ教室1時間`
-
例:`ex@散歩20分`
-
⚠️ 注意点と設定確認
-
スプレッドシートに記録されない場合:
-
Webhook URLが正しいか確認
-
GASの設定が正しいか見直す
-
-
AIの分析結果が表示されない場合:
-
OpenAI APIキーが正しいか確認
-
課金設定が済んでいるか確認
-
-
GPTモデルについて:
-
デフォルトは「gpt-4o-mini」(コスパ良好)
-
他のモデルに変更したい場合はCONFIGの`GPT_MODEL`を変更
-
-
利用頻度について:
-
通常の個人利用であれば問題なく動作します
-
1日数回〜数十回程度の利用を想定しています
-
🚀 カスタマイズのヒント
-
AIモデルの変更
-
より高精度なモデル(GPT-4oなど)に変更したい場合:
-
CONFIGの`GPT_MODEL`の値を変更(例: `”gpt-4o”`)
-
※料金が変わるので注意
-
これでLINEを使った食事管理AIボットの完成です!
食事の写真を送るだけで栄養管理ができる便利なツールをぜひ活用してください。
コピペ用完全コード
基本の使いかた
STEP2のスプレッドシートをコピーすると、GASのプログラムコードも一緒についてきます
バックアップ用コード
もしもの時のために、プログラムコードのコピーを以下に保存しています。
普段は使う必要はありません
更新情報
※このコードは非常時のバックアップ用です。
const CONFIG = { SPREADSHEET_ID: "あなたのスプレッドシートID", SHEET_NAME: "シート1", ACCESS_TOKEN: "あなたのLINEアクセストークン", FOLDER_ID: "あなたのGoogleドライブフォルダID", GPT_API_KEY: "あなたのOpenAI APIキー", GPT_MODEL: "gpt-4o-mini" }; const SYSTEM_CONFIG = { DUPLICATE_CHECK_MS: 20000 }; const COLUMN = { TIMESTAMP: 1, EVENT_TYPE: 2, IMAGE_URL: 3, IMAGE_DISPLAY: 4, MEAL_TIME: 5, MEAL_NAME: 6, CALORIES: 7, CARBS: 8, PROTEIN: 9, FAT: 10, VEGETABLES: 11, FIBER: 12, WEIGHT: 13, EXERCISE: 14, DETAILS: 15 }; const REGEX = { FOOD: /^f[@::]?s*(.+)/i, WEIGHT: [ /^w[@::]?s*(d+.?d*)s*kg/i, /^w[@::]?s*(d+.?d*)/i, /(d+.?d*)s*kg/i ], EXERCISE: [ /^ex[@::]?s*(.+)/i, /^e[@::]?s*(.+)/i ], MEAL_TIME: /朝食|昼食|夕食|間食/, CALORIES: /カロリー[@::]s*(?:約)?(d+)/, CARBS: /糖質g?[@::]s*(?:約)?(d+)/, PROTEIN: /タンパク質g?[@::]s*(?:約)?(d+)/, FAT: /脂質g?[@::]s*(?:約)?(d+)/ }; function doPost(e) { Logger.log("✅ doPost 実行されました!"); const sheet = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID).getSheetByName(CONFIG.SHEET_NAME); const json = JSON.parse(e.postData.contents); const event = json.events[0]; const replyToken = event.replyToken; const userText = event.message.type === 'text' ? event.message.text : ''; const imageId = event.message.type === 'image' ? event.message.id : ''; const timestamp = new Date(event.timestamp); const eventType = event.message.type; if (isDuplicate(sheet, timestamp, eventType, imageId, userText)) { Logger.log("重複メッセージと判断されました。処理をスキップします。"); return ContentService.createTextOutput("OK"); } const rowData = Array(COLUMN.DETAILS).fill(''); rowData[COLUMN.TIMESTAMP - 1] = timestamp; rowData[COLUMN.EVENT_TYPE - 1] = eventType; if (eventType === 'image') { handleImageMessage(imageId, rowData, sheet); return ContentService.createTextOutput("OK"); } else if (eventType === 'text') { if (handleTextMessage(userText, rowData, sheet)) { return ContentService.createTextOutput("OK"); } const gptResponse = askGPTForGeneralQuery(userText); if (gptResponse && replyToken) { replyToLine(gptResponse, replyToken); return ContentService.createTextOutput("OK"); } } sheet.appendRow(rowData); Logger.log(`eventType: ${event.message.type}, userText: ${userText}, imageId: ${imageId}`); return ContentService.createTextOutput("OK"); } function handleTextMessage(userText, rowData, sheet) { const foodMatch = matchAny([REGEX.FOOD], userText); if (foodMatch) { const mealName = foodMatch[1].trim(); rowData[COLUMN.MEAL_NAME - 1] = mealName; sheet.appendRow(rowData); const newRow = sheet.getLastRow(); estimateNutritionFromMealName(mealName, newRow); return true; } let hasWeight = false; let hasExercise = false; let weightValue = null; let exerciseValue = null; const weightMatches = userText.match(/(d+.?d*)s*kg/) || userText.match(/w[@::]?s*(d+.?d*)s*kg/i) || userText.match(/w[@::]?s*(d+.?d*)/i); if (weightMatches) { weightValue = weightMatches[1] ? weightMatches[1].trim() : weightMatches[0].trim(); if (!weightValue.endsWith('kg')) weightValue = weightValue + 'kg'; rowData[COLUMN.WEIGHT - 1] = weightValue; hasWeight = true; } const exerciseMatches = userText.match(/ex[@::]?s*([0-3])/i) || userText.match(/ex[@::]?s*([^s]+)/i) || userText.match(/e[@::]?s*([0-3])/i) || userText.match(/e[@::]?s*([^s]+)/i) || matchAny(REGEX.EXERCISE, userText); if (exerciseMatches) { exerciseValue = exerciseMatches[1] ? exerciseMatches[1].trim() : exerciseMatches[0].trim(); if (exerciseValue === '0') { exerciseValue = '運動なし'; } else if (exerciseValue === '1') { exerciseValue = '運動軽め'; } else if (exerciseValue === '2') { exerciseValue = '運動中程度'; } else if (exerciseValue === '3') { exerciseValue = '運動重め'; } rowData[COLUMN.EXERCISE - 1] = exerciseValue; hasExercise = true; } if (hasWeight || hasExercise) { sheet.appendRow(rowData); return true; } const mealTimeMatch = userText.match(REGEX.MEAL_TIME); if (mealTimeMatch) { rowData[COLUMN.MEAL_TIME - 1] = mealTimeMatch[0]; } extractNutritionValues(userText, rowData); return false; } function matchAny(patterns, text) { if (!Array.isArray(patterns)) { return text.match(patterns); } for (const pattern of patterns) { const match = text.match(pattern); if (match) return match; } return null; } function handleImageMessage(imageId, rowData, sheet) { const imageData = getImageFromLine(imageId); rowData[COLUMN.IMAGE_URL - 1] = imageData.url; sheet.appendRow(rowData); const newRow = sheet.getLastRow(); if (imageData.url) { const fileIdMatch = imageData.url.match(//d/([^/]+)/); if (fileIdMatch && fileIdMatch[1]) { const fileId = fileIdMatch[1]; const imageUrl = `https://drive.google.com/uc?export=view&id=${fileId}`; sheet.getRange(newRow, COLUMN.IMAGE_DISPLAY).setFormula(`=IMAGE("${imageUrl}")`); } } if (imageData.blob) { askChatGPT(imageData.blob, newRow, COLUMN.DETAILS); } } function extractNutritionValues(text, rowData) { const extractValue = (regex, text) => { const match = text.match(regex); return match ? match[1] : null; }; const calories = extractValue(REGEX.CALORIES, text); const carbs = extractValue(REGEX.CARBS, text); const protein = extractValue(REGEX.PROTEIN, text); const fat = extractValue(REGEX.FAT, text); if (calories) rowData[COLUMN.CALORIES - 1] = calories; if (carbs) rowData[COLUMN.CARBS - 1] = carbs; if (protein) rowData[COLUMN.PROTEIN - 1] = protein; if (fat) rowData[COLUMN.FAT - 1] = fat; } function getImageFromLine(messageId) { const url = `https://api-data.line.me/v2/bot/message/${messageId}/content`; const headers = { "Authorization": `Bearer ${CONFIG.ACCESS_TOKEN}` }; try { const response = UrlFetchApp.fetch(url, { headers: headers, method: "get" }); const fileName = generateUniqueFileName(); const blob = response.getBlob().setName(fileName); const folder = DriveApp.getFolderById(CONFIG.FOLDER_ID); const file = folder.createFile(blob); file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); return { url: file.getUrl(), blob: blob }; } catch (error) { Logger.log(`画像の取得エラー: ${error.message}`); return { url: '', blob: null }; } } function generateUniqueFileName() { const today = new Date(); const dateStr = today.getFullYear() % 100 + ("0" + (today.getMonth() + 1)).slice(-2) + ("0" + today.getDate()).slice(-2); const randomStr = Math.random().toString(36).substring(2, 6); return `food_${dateStr}_${randomStr}.jpg`; } function askChatGPT(imageBlob, row, column) { try { const base64Image = Utilities.base64Encode(imageBlob.getBytes()); const timeHint = getTimeHint(); const payload = createGptPayloadForImage(base64Image, timeHint); const options = createGptRequestOptions(payload); const response = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", options); const responseCode = response.getResponseCode(); if (responseCode !== 200) { throw new Error(`API応答コード: ${responseCode}, レスポンス: ${response.getContentText()}`); } const json = JSON.parse(response.getContentText()); const replyText = json.choices[0].message.content; const sheet = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID).getSheetByName(CONFIG.SHEET_NAME); sheet.getRange(row, column).setValue(replyText); extractAndWriteNutritionData(replyText, row, sheet); Logger.log("ChatGPTの応答: " + replyText); return replyText; } catch (error) { Logger.log("ChatGPTリクエスト失敗: " + error); const sheet = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID).getSheetByName(CONFIG.SHEET_NAME); sheet.getRange(row, column).setValue("ChatGPTリクエスト失敗: " + error.message); return null; } } function getTimeHint() { const now = new Date(); const hour = now.getHours(); if (hour >= 5 && hour < 10) { return "(現在は朝の時間帯です)"; } else if (hour >= 10 && hour < 15) { return "(現在は昼の時間帯です)"; } else if (hour >= 17 && hour < 22) { return "(現在は夕方~夜の時間帯です)"; } return ""; } function createGptPayloadForImage(base64Image, timeHint) { return { model: CONFIG.GPT_MODEL, messages: [ { role: "user", content: [ { type: "text", text: `これは食事の写真です。以下の形式で簡潔に回答してください。説明文は不要です。数値は数字のみで記入してください: 食事名: [料理名] 時間帯: [朝食/昼食/夕食/間食のいずれか] ${timeHint} カロリー: [推定カロリー値] 糖質g: [推定糖質量] タンパク質g: [推定タンパク質量] 脂質g: [推定脂質量] 野菜摂取量g: [推定総野菜量] 食物繊維g: [推定食物繊維量] 時間帯の判断は、料理の種類や構成や時間から推測してください。 野菜量は写真から見える野菜の総量を推定してください。 食物繊維は、野菜、穀物、豆類などから推定してください。` }, { type: "image_url", image_url: { url: `data:image/jpeg;base64,${base64Image}` } } ] } ], temperature: 0.3, max_tokens: 300 }; } function createGptRequestOptions(payload) { return { method: "post", contentType: "application/json", headers: { Authorization: `Bearer ${CONFIG.GPT_API_KEY}` }, payload: JSON.stringify(payload), muteHttpExceptions: true }; } function extractAndWriteNutritionData(responseText, row, sheet) { try { const patterns = { mealName: /食事名[@::]s*(.+)/, mealTime: /時間帯[@::]s*(.+)/, calories: /カロリー[@::]s*(?:約)?(d+)/, carbs: /糖質g[@::]s*(?:約)?(d+)/, protein: /タンパク質g[@::]s*(?:約)?(d+)/, fat: /脂質g[@::]s*(?:約)?(d+)/, vegetables: /野菜摂取量g[@::]s*(?:約)?(d+)/, fiber: /食物繊維g[@::]s*(?:約)?(d+)/ }; const columnMap = { mealName: COLUMN.MEAL_NAME, mealTime: COLUMN.MEAL_TIME, calories: COLUMN.CALORIES, carbs: COLUMN.CARBS, protein: COLUMN.PROTEIN, fat: COLUMN.FAT, vegetables: COLUMN.VEGETABLES, fiber: COLUMN.FIBER }; const extractedData = {}; for (const [key, pattern] of Object.entries(patterns)) { const match = responseText.match(pattern); if (match) { const value = key === 'mealName' || key === 'mealTime' ? match[1].trim() : match[1]; extractedData[key] = value; sheet.getRange(row, columnMap[key]).setValue(value); } } Logger.log(`栄養素データを抽出しました: ${JSON.stringify(extractedData)}`); } catch (error) { Logger.log("栄養素データの抽出と書き込みに失敗しました: " + error); } } function estimateNutritionFromMealName(mealName, row, timeHint) { try { const payload = { model: CONFIG.GPT_MODEL, messages: [ { role: "user", content: `以下の食事の一般的な栄養素を推定してください。数値のみ簡潔に回答してください: 食事名: ${mealName} 回答は以下の形式で簡潔に記載してください。説明文は不要です: 時間帯: [朝食/昼食/夕食/間食のいずれか] ${timeHint} カロリー: [推定カロリー値] 糖質g: [推定糖質量] タンパク質g: [推定タンパク質量] 脂質g: [推定脂質量] 野菜摂取量g: [推定野菜量] 食物繊維g: [推定食物繊維量] 時間帯の判断は、料理の種類や構成や時間から推測してください。` } ], temperature: 0.3, max_tokens: 200 }; const options = createGptRequestOptions(payload); const response = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", options); const responseCode = response.getResponseCode(); if (responseCode !== 200) { throw new Error(`API応答コード: ${responseCode}, レスポンス: ${response.getContentText()}`); } const json = JSON.parse(response.getContentText()); const replyText = json.choices[0].message.content; const sheet = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID).getSheetByName(CONFIG.SHEET_NAME); sheet.getRange(row, COLUMN.DETAILS).setValue(replyText); extractAndWriteNutritionData(replyText, row, sheet); Logger.log("食事名からの栄養素推定: " + replyText); return replyText; } catch (error) { Logger.log("栄養素推定リクエスト失敗: " + error); const sheet = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID).getSheetByName(CONFIG.SHEET_NAME); sheet.getRange(row, COLUMN.DETAILS).setValue("栄養素推定リクエスト失敗: " + error.message); return null; } } function isDuplicate(sheet, currentTimestamp, eventType, imageId, userText = '') { try { if (eventType === 'text') { const isSpecificCommand = matchAny([REGEX.FOOD], userText) || matchAny(REGEX.WEIGHT, userText) || userText.match(/ex[0-3]/i) || matchAny(REGEX.EXERCISE, userText); if (!isSpecificCommand) { return false; } } const lastRow = sheet.getLastRow(); if (lastRow <= 1) return false; const lastTimestamp = sheet.getRange(lastRow, COLUMN.TIMESTAMP).getValue(); const lastEventType = sheet.getRange(lastRow, COLUMN.EVENT_TYPE).getValue(); if (lastEventType === eventType && lastTimestamp instanceof Date && (currentTimestamp - lastTimestamp) < SYSTEM_CONFIG.DUPLICATE_CHECK_MS) { if (eventType === 'image') { const lastImageUrl = sheet.getRange(lastRow, COLUMN.IMAGE_URL).getValue(); if (lastImageUrl && lastImageUrl.includes(imageId)) { return true; } } if (eventType === 'text') { return true; } } return false; } catch (error) { Logger.log("重複チェック中にエラーが発生しました: " + error); return false; } } function getRecentData(days = 4) { try { const sheet = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID).getSheetByName(CONFIG.SHEET_NAME); const lastRow = sheet.getLastRow(); if (lastRow <= 1) return { meals: [], weights: [], exercises: [] }; const today = new Date(); const startDate = new Date(); startDate.setDate(today.getDate() - days); const maxRows = Math.min(lastRow, 100); const dataRange = sheet.getRange(Math.max(2, lastRow - maxRows + 1), 1, maxRows, COLUMN.DETAILS); const data = dataRange.getValues(); const result = { meals: [], weights: [], exercises: [] }; for (let i = data.length - 1; i >= 0; i--) { const row = data[i]; const timestamp = row[COLUMN.TIMESTAMP - 1]; if (timestamp instanceof Date && timestamp >= startDate) { const mealName = row[COLUMN.MEAL_NAME - 1]; const weight = row[COLUMN.WEIGHT - 1]; const exercise = row[COLUMN.EXERCISE - 1]; if (mealName) { const mealData = { date: formatDate(timestamp), mealName: mealName, mealTime: row[COLUMN.MEAL_TIME - 1] || "", calories: row[COLUMN.CALORIES - 1] || "", carbs: row[COLUMN.CARBS - 1] || "", protein: row[COLUMN.PROTEIN - 1] || "", fat: row[COLUMN.FAT - 1] || "", vegetables: row[COLUMN.VEGETABLES - 1] || "" }; result.meals.push(mealData); } if (weight) { result.weights.push({ date: formatDate(timestamp), weight: weight }); } if (exercise) { result.exercises.push({ date: formatDate(timestamp), exercise: exercise }); } } } return result; } catch (error) { Logger.log("データ取得エラー: " + error); return { meals: [], weights: [], exercises: [] }; } } function formatDate(date) { return `${date.getMonth() + 1}月${date.getDate()}日`; } function replyToLine(message, replyToken) { const url = 'https://api.line.me/v2/bot/message/reply'; const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.ACCESS_TOKEN}` }; const payload = { 'replyToken': replyToken, 'messages': [ { 'type': 'text', 'text': message } ] }; const options = { 'method': 'post', 'headers': headers, 'payload': JSON.stringify(payload), 'muteHttpExceptions': true }; try { const response = UrlFetchApp.fetch(url, options); Logger.log("LINE返信結果: " + response.getContentText()); return response; } catch (error) { Logger.log("LINE返信エラー: " + error); return null; } }
Views: 0