“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 行派:
金額 メモ
- 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 123 で deleteRow(123) |
カテゴリ選択 UI | LIFF でフォームを作り Bot に送信 |
3 人以上・複数グループ | そのまま使えます(UserID 列で判別) |
月次 PDF レポート | Apps Script で Google Slide をテンプレ化 & メール送信 |
おわりに
「LINE に打つだけ」→「スプレッドシートが育つ」→「月初にレポート届く」
これだけで家計共有は驚くほどラクになります。
ぜひ試してみて、改善アイデアがあれば Pull Request / コメントお待ちしています!
コメント