PR

LINE家計簿Bot を作って使うまで ― 完全ガイド

やってみた

“LINE で送るだけ”
2 人用のシンプル家計簿
スプレッドシート連携 & 月次レポート付き で動かします。


1. なぜ LINE 家計簿 Bot?

  • アプリを開かない ⇒ 続かない
  • スクショ転送や手入力 ⇒ 面倒

そこで
“LINE で金額とメモを送るだけ”
“集計は Google Sheets が勝手にやる”
――そんな最小構成を作りました。


2. 全体アーキテクチャ

LINE Official Account                 毎月1日 自動Push
       │ POST (JSON)                       ▲
       ▼                                   │
Google Apps Script (Web App) ─→ Google スプレッドシート
       │ appendRow()                       ├ Transactions (生データ)
       └ reply / push                      └ ピボット or グラフ
  • 無料枠のみ
  • サーバーレス(GAS)
  • グループでも 1:1 でも使える

3. 10 分で終わるセットアップ

手順やること
スプレッドシートを作成シート名 Transactions列: Date / UserID / Name / Amount / Memo
LINE Official Account を開設 → Messaging API 有効化
スプレッドシート → 拡張機能 › Apps Script を開き、次章のコードを貼り付け
ウェブアプリとしてデプロイ実行者: 自分 / アクセス: 全員(匿名)発行URLの末尾 /exec を LINE Developers の Webhook URL に設定
OA Manager → 応答設定 を Bot モードに(あいさつ / 応答メッセージ OFF)
Bot を友だち追加して動作確認

4. Apps Script ― 全コード(最新版)

/**
 * LINE 家計簿 Bot v1.2 – 2ステップ入力・取消・月次集計
 * 2025-05-23
 * ① SPREADSHEET_ID ② ACCESS_TOKEN を設定してデプロイ
 */

// ★ 1) スプレッドシート ID
const SPREADSHEET_ID = 'YOUR_SHEET_ID';
// ★ 2) LINE Channel access token
const ACCESS_TOKEN   = 'YOUR_CHANNEL_ACCESS_TOKEN';

// (任意) ユーザー固定名マッピング
const USER_NAME_ALIAS = {
  // 'Uxxxxxxxxxxxx': 'Alice',
  // 'Uyyyyyyyyyyyy': 'Bob',
};

const CACHE_TTL = 300; // 5 分

/* ---------- Webhook ---------- */
function doPost(e) {
  // 手動実行時は e が無い
  if (!e || !e.postData) return HtmlService.createHtmlOutput('OK');
  try {
    (JSON.parse(e.postData.contents).events || []).forEach(handleEvent);
  } catch (err) {
    Logger.log(err);
  }
  return HtmlService.createHtmlOutput('OK'); // 200 OK
}

/* ---------- イベント ---------- */
function handleEvent(ev) {
  if (ev.type !== 'message' || ev.message.type !== 'text') return;

  const cache  = CacheService.getScriptCache();
  const uid    = ev.source.userId;
  const raw    = ev.message.text;
  const text   = raw.trim();
  const args   = text.split(/\s+/).map(s => s.toLowerCase());

  /* コマンド系 */
  if (text === '' || ['やめる','cancel'].includes(text)) {
    // メモ待ちキャンセル
    cache.remove(`pending-${uid}`);
    return reply(ev.replyToken,'🗑️ 入力をキャンセルしました');
  }
  if (['取り消し','undo'].includes(text)) {
    return reply(ev.replyToken,
      deleteLast(uid) ? '↩️ 取り消しました':'❌ 取り消す記録がありません');
  }
  if (args[0] === 'help') return reply(ev.replyToken, help());
  if (args[0] === 'sum') {
    const ym = args[1] || yyyymm(new Date());
    return reply(ev.replyToken, summary(ym));
  }

  /* 2 ステップ入力 */
  const pending = cache.get(`pending-${uid}`);
  if (pending) {                         // 今回はメモ
    store(uid, Number(pending), raw);
    cache.remove(`pending-${uid}`);
    return reply(ev.replyToken, `✅ 登録完了! ¥${Number(pending).toLocaleString()} : ${raw}`);
  }

  /* 金額判定 */
  const val = parseInt(args[0].replace(/[,円]/g,''),10);
  if (!isNaN(val)) {
    cache.put(`pending-${uid}`, String(val), CACHE_TTL);
    return reply(ev.replyToken,
      `💰 ¥${val.toLocaleString()} を受け取りました\n続けてメモを入力してください`);
  }

  /* ここまで来たら不明入力 */
  reply(ev.replyToken,'⚠️ 金額が読めませんでした。\n1500 昼ごはん\nまたは\n1500 → 昼ごはん\nの形式で送信してください');
}

/* ---------- 主要ロジック ---------- */
function store(uid, amount, memo) {
  const sh = sheet();
  sh.appendRow([new Date(), uid, name(uid), amount, memo]);
}
function deleteLast(uid) {
  const sh = sheet(), vals = sh.getDataRange().getValues();
  for (let i = vals.length-1; i>=1; i--) {
    if (vals[i][1]===uid){ sh.deleteRow(i+1); return true; }
  }
  return false;
}
function summary(ym) {
  const data = monthlyTotals(ym);
  if (!Object.keys(data).length) return `📅 ${ym} は記録なし`;

  const total = Object.values(data).reduce((a,b)=>a+b,0);
  const lines = [`📅 ${ym} の支出`,`──────────────`];
  Object.entries(data).sort((a,b)=>b[1]-a[1])
        .forEach(([n,v])=>lines.push(`${n.padEnd(8)} : ¥${v.toLocaleString()}`));
  lines.push('──────────────',`合計        : ¥${total.toLocaleString()}`);
  return lines.join('\n');
}
function monthlyTotals(ym){
  const [y,m]=ym.split('/').map(Number), res={}, vals=sheet().getDataRange().getValues();
  vals.slice(1).forEach(r=>{
    const d=new Date(r[0]);
    if(d.getFullYear()===y&&d.getMonth()===m-1){
      res[r[2]]=(res[r[2]]||0)+Number(r[3]);
    }
  }); return res;
}

/* ---------- ユーティリティ ---------- */
function sheet(){return SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('Transactions');}
function yyyymm(d){return `${d.getFullYear()}/${d.getMonth()+1}`;}
function name(uid){
  if (USER_NAME_ALIAS[uid]) return USER_NAME_ALIAS[uid];
  try{
    const r=UrlFetchApp.fetch(`https://api.line.me/v2/bot/profile/${uid}`,{
      headers:{Authorization:`Bearer ${ACCESS_TOKEN}`},muteHttpExceptions:true});
    return JSON.parse(r).displayName||'Unknown';
  }catch(e){return'Unknown';}
}
function reply(tok,txt){
  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply',{
    method:'post', headers:{Authorization:`Bearer ${ACCESS_TOKEN}`},
    contentType:'application/json', payload:JSON.stringify({replyToken:tok,messages:[{type:'text',text:txt}]}),
  });
}
function help(){return`📗 コマンド一覧
----------------
支出入力   1500 昼ごはん
           1500 → 昼ごはん
取消入力   やめる / cancel
直近削除   取り消し / undo
合計今月   sum
合計指定   sum 2025/4
ヘルプ     help`;}

5. 使い方早見表 🌟

やりたいこと送るメッセージ
支出を記録1500 昼ごはん or1500昼ごはん
入力をキャンセルやめる / cancel
直前 1 件削除取り消し / undo
今月合計sum
特定月合計sum 2024/12
ヘルプhelp

6. コマンド詳細

6-1. 支出登録

  1. 1 行派金額 メモ
  2. 2 行派金額 → Bot が促す → メモ

金額は 3,980円 のようにカンマ・円付きでも OK。

6-2. 取消 & 削除

  • やめる / cancel … メモ入力前をキャンセル
  • 取り消し / undo … 確定後でも自分の最新 1 行を削除

6-3. 月次合計

sum          ← 今月
sum 2024/12  ← 指定月

Bot がユーザー別+合計を返します。

6-4. ヘルプ

help

7. よくある質問 & トラブル

症状/質問対処
OA 定型メッセージが出るOA Manager → 応答モード Bot、あいさつ/応答 OFF
「Unknown」と表示初回だけ出ることあり。気になる場合は USER_NAME_ALIAS に手動登録
シートに書き込まれないシート名 Transactions か確認 / Apps Script 権限を承認
302 Found エラーWebhook URL が /exec か確認 / アクセス権「全員(匿名)」に

8. もっと便利に

機能ひとこと実装
ID 指定削除追加時に行番号を返信 → del 123deleteRow(123)
カテゴリ選択 UILIFF でフォームを作り Bot に送信
3 人以上・複数グループそのまま使えます(UserID 列で判別)
月次 PDF レポートApps Script で Google Slide をテンプレ化 & メール送信

おわりに

「LINE に打つだけ」→「スプレッドシートが育つ」→「月初にレポート届く」
これだけで家計共有は驚くほどラクになります。
ぜひ試してみて、改善アイデアがあれば Pull Request / コメントお待ちしています!

コメント