柴田AIクン、この記事でさ、LINEに通知する仕組みは作ったけど、結局そのあと承認するかっていうのはスプシを開かないといけないじゃない?



…デ?



そう!面倒なのです!簡単に承認できるシステム作っておくれよ!



…(毎回コイツハ…)
ということで、LINEでXの投稿下書きを通知したあと、「そのまま投稿していいか?」「訂正するのか?」「ボツにするのか?」を承認するのは普通だとスプレッドシートを開かないといけません。
ですが、Google Apps Script(GAS)のWebアプリ機能を使うと、スプシのデータをブラウザ上で操作できる管理画面を作ることができます。
承認フローの構築、在庫や注文の管理、タスクの進捗管理など、「スプレッドシートのデータをUIで操作したい」という場面で使える汎用的なパターンです。
この記事ではX自動投稿の承認UIを例に、実装手順を解説します。
作るもの
スプレッドシートの各行に対して「承認・訂正・ボツ」を選べるWebアプリUIです。




動作の流れはこんな感じです。
- URLにアクセスすると処理待ちの一覧が表示される
- 承認を押すとステータスが「承認済み」に更新される
- 訂正を押すと編集画面に移行して、テキストを修正してから承認できる
- ボツを押すとステータスが「ボツ」に更新される
スプレッドシートの準備
スプレッドシートの列構成はこんな感じ。
- A列:生成日時
- B列:本文(操作対象のテキスト)
- C列:ステータス(「下書き」「通知済み」「承認済み」「ボツ」「投稿済み」)
- D列:文字数
GASの実装
スクリプトエディタを開いて WebApp.gs というファイルを作成します。



ちょっと長いですが…一応全体のコードを載せておきます
それぞれの役割は後ほど説明します。
コード全体
// =============================
// Webアプリのエントリーポイント
// =============================
function doGet(e) {
const row = e.parameter.row ? parseInt(e.parameter.row) : null;
if (row) {
return createEditView(row);
}
return createListView();
}
// =============================
// 一覧画面
// =============================
function createListView() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const rows = sheet.getDataRange().getValues();
const webAppUrl = ScriptApp.getService().getUrl();
let tableRows = '';
rows.forEach((row, index) => {
if (index === 0) return;
const rowNumber = index + 1;
const date = row[0];
const text = row[1];
const status = row[2];
const editUrl = webAppUrl + '?row=' + rowNumber;
const buttons = (status === '下書き' || status === '通知済み')
? `<div class="actions" id="actions-${rowNumber}">
<button class="btn-approve" onclick="approve(${rowNumber})">✅ 承認</button>
<a class="btn-edit" href="${editUrl}" target="_top">✏️ 訂正</a>
<button class="btn-reject" onclick="reject(${rowNumber})">🗑 ボツ</button>
</div>`
: `<div id="actions-${rowNumber}"><span class="done">${status}</span></div>`;
tableRows += `
<div class="card">
<div class="date">${date}</div>
<div class="tweet">${text}</div>
${buttons}
</div>`;
});
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>投稿承認UI</title>
<style>
html { font-size: 16px; }
body { font-family: sans-serif; padding: 12px; background: #f5f5f5; }
h2 { font-size: 20px; margin-bottom: 16px; }
.card { background: white; border-radius: 8px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); }
.date { font-size: 12px; color: #aaa; margin-bottom: 8px; }
.tweet { font-size: 15px; white-space: pre-wrap; line-height: 1.6; margin-bottom: 16px; }
.actions { display: flex; gap: 8px; }
.actions button, .actions a { flex: 1; padding: 10px; font-size: 14px; border: none; border-radius: 6px; cursor: pointer; text-align: center; text-decoration: none; }
.btn-approve { background: #4CAF50; color: white; }
.btn-edit { background: #2196F3; color: white; }
.btn-reject { background: #f44336; color: white; }
.done { font-size: 13px; color: #aaa; }
</style>
</head>
<body>
<h2>📋 投稿承認リスト</h2>
${tableRows}
<script>
function approve(rowNumber) {
google.script.run
.withSuccessHandler(function() {
document.getElementById('actions-' + rowNumber).innerHTML = '<span class="done">承認済み ✅</span>';
})
.updateStatus(rowNumber, '承認済み');
}
function reject(rowNumber) {
if (!confirm('ボツにしますか?')) return;
google.script.run
.withSuccessHandler(function() {
document.getElementById('actions-' + rowNumber).innerHTML = '<span class="done">ボツ 🗑</span>';
})
.updateStatus(rowNumber, 'ボツ');
}
</script>
</body>
</html>`;
return HtmlService.createHtmlOutput(html);
}
// =============================
// 訂正画面(?row=N でアクセス)
// =============================
function createEditView(rowNumber) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const text = sheet.getRange(rowNumber, 2).getValue();
const webAppUrl = ScriptApp.getService().getUrl();
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>訂正・承認</title>
<style>
body { font-family: sans-serif; padding: 16px; max-width: 600px; margin: 0 auto; }
h2 { font-size: 18px; }
textarea { width: 100%; height: 160px; padding: 10px; font-size: 14px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; resize: vertical; }
.char-count { font-size: 12px; color: #888; text-align: right; margin-top: 4px; }
button { margin-top: 12px; padding: 12px 24px; font-size: 15px; border: none; border-radius: 4px; cursor: pointer; width: 100%; }
.btn-save { background: #4CAF50; color: white; }
.btn-back { background: #888; color: white; margin-top: 8px; }
#message { margin-top: 12px; padding: 10px; background: #e8f5e9; color: #2e7d32; border-radius: 4px; display: none; }
</style>
</head>
<body>
<h2>✏️ 訂正・承認(行${rowNumber})</h2>
<textarea id="text">${text}</textarea>
<div class="char-count"><span id="count">${text.length}</span> 文字</div>
<button class="btn-save" onclick="save()">✅ 訂正して承認</button>
<button class="btn-back" onclick="window.top.location.href='${webAppUrl}'">← 一覧に戻る</button>
<div id="message">承認しました!</div>
<script>
const textarea = document.getElementById('text');
const count = document.getElementById('count');
textarea.addEventListener('input', function() {
count.textContent = textarea.value.length;
});
function save() {
const newText = textarea.value.trim();
if (!newText) { alert('本文が空です'); return; }
const btn = document.querySelector('.btn-save');
btn.disabled = true;
google.script.run
.withSuccessHandler(function() {
document.getElementById('message').style.display = 'block';
window.top.location.href = '${webAppUrl}';
})
.updateTextAndApprove(${rowNumber}, newText);
}
</script>
</body>
</html>`;
return HtmlService.createHtmlOutput(html);
}
// =============================
// スプシ更新関数
// =============================
function updateStatus(rowNumber, status) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
sheet.getRange(rowNumber, 3).setValue(status);
}
function updateTextAndApprove(rowNumber, newText) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
sheet.getRange(rowNumber, 2).setValue(newText);
sheet.getRange(rowNumber, 3).setValue('承認済み');
}doGet:URLにアクセスしたときの処理
これがURLにアクセスしてきたときに最初に処理される部分です。
このWebアプリでは、「とりあえず一覧を表示する画面」と「特定の投稿を訂正する画面」の着地地点が2つあります。



ここで「一覧を表示するか」「投稿の訂正画面を表示するか」を振り分けます
function doGet(e) {
const action = e.parameter.action;
const row = e.parameter.row;
if (action === "edit") {
return createEditView(Number(row));
}
return createListView();
}
URLのパラメータで action=edit&row=N が渡ってきたら編集画面を、それ以外は一覧画面を返します。
createListView:一覧画面
長いので省略しますが、この部分です。
function createListView() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const rows = sheet.getDataRange().getValues();
const webAppUrl = ScriptApp.getService().getUrl();
…
</body>
</html>`;
return HtmlService.createHtmlOutput(html);
}
ここは結構調整しました。
基本的にスマホで操作することをイメージして、リストは横幅いっぱい、それぞれの項目の下部に「承認」「訂正」「ボツ」のボタンをつけています。
createEditView:訂正画面
こちらも一部抜粋。この部分です。
function createEditView(rowNumber) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
const text = sheet.getRange(rowNumber, 2).getValue();
const webAppUrl = ScriptApp.getService().getUrl();
…
</body>
</html>`;
return HtmlService.createHtmlOutput(html);
}
スプシから該当行の投稿下書きを取り出して、書き換え→「訂正して承認」ボタンでスプシに上書きしつつ、ステータスを「承認済み」にします。



メインはここまで。あとの関数はこの中で使っているスプシ更新系です
updateStatus:ステータス更新
ステータスを変更(「下書き」→「承認済み」「ボツ」)する関数です。
色んなところで使うので関数化しています。
function updateStatus(row, status) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
sheet.getRange(row, 3).setValue(status);
}
updateTextAndApprove:テキスト修正して承認
訂正して承認する関数を別途作っていて、先程の「訂正画面」の中で使っています。
function updateTextAndApprove(row, newText) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
sheet.getRange(row, 2).setValue(newText);
sheet.getRange(row, 3).setValue("承認済み");
}



長いけど、これでバッチリ承認Webアプリの完成です
デプロイ方法
GASエディタの上部メニューから「デプロイ」→「新しいデプロイ」を選択します。




設定はこんな感じ。
- 種類:ウェブアプリ
- 実行ユーザー:自分
- アクセスできるユーザー:自分のみ(または全員)
デプロイするとURLが発行されます。このURLがWebアプリの入口になります。
まとめ
GASのWebアプリ機能を使うと、スプレッドシートをそのまま管理画面として使えます。コードの構造はシンプルで、doGet でリクエストを受け取り、HTMLを返すだけです。
ステータスの種類やボタンの種類は用途に合わせて自由に変えられるので、承認フロー以外にもいろいろな場面で使えます。



…とはいえ、結構複雑なので別の用途にそのまま使い回すのは難しいかもしれません。流れはマネできるかな、と思いますが。なにかあったら、ぜひ公式ラインに登録して相談してみてください
この記事と、コチラの記事を部品として実装しつつ、次の記事の流れに組み込んでXの自動投稿を実現してます。全体像は次の記事をご覧ください。










