スプレッドシート業務アプリに画面キャプチャ付き不具合報告フォームを付ける(GAS)
GASのOverGridImageにgetBlob()が無くシート貼付画像は読めない。HTMLダイアログでCtrl+Vを受ける正しい設計・記録列マイグレーション・read-back検証・画像を捨てない後片付けまでの実装指示書
約10.5万トークンの節約 (API料金換算で約160円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「スプレッドシート業務アプリに画面キャプチャ付き不具合報告フォームを付ける(GAS)」は、Google WorkspaceカテゴリのAI指示書(MDファイル)です。GASのOverGridImageにgetBlob()が無くシート貼付画像は読めない。HTMLダイアログでCtrl+Vを受ける正しい設計・記録列マイグレーション・read-back検証・画像を捨てない後片付けまでの実装指示書この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約10.5万トークン(API料金換算で約160円)・88%のトークンを節約できます。
- カテゴリ
- Google Workspace
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約12万トークン
- この巻物使用時
- 約1.5万トークン
- 節約量
- 約10.5万トークン (約160円)
- 更新日
- 2026-08-25
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/gas-2/raw を読み込んで、この指示書どおりに実装して"
中身
Google スプレッドシート業務アプリに「画面キャプチャ付き 不具合・要望フォーム」を付ける(GAS)
対象: Apps Script(GAS) で作った社内ツールが Google スプレッドシート上で動いていて、 利用者から「不具合と一緒にスクショを送りたい」と言われたとき。
結論を先に: シートに画像を貼らせる設計は原理的に動かない。HTML のモーダルダイアログを開き、
その中で Ctrl+V を受け取る。ここを知らないまま「シートに貼って▶」を実装すると、実機で画像が0枚になる。
1. 最初に潰すべき落とし穴(実測)
Apps Script の Sheet.getImages() は取得できるが、返る OverGridImage に getBlob() が存在しない。
TypeError: images[k].getBlob is not a function // typeof img.getBlob === 'undefined'
- 使えるのは
getAnchorCell()/remove()/getWidth()など。getUrl()は URL 挿入した画像以外 null。 - Sheets API v4 にも浮動画像(drawing)は出てこない。
- 裏技として「スプレッドシートを XLSX で書き出し
Utilities.unzip()してxl/media/*を取る」は可能だが、 シート↔画像の対応付けがxl/drawings/*.xml解析頼みで脆く、ファイル全体を落とすので実用にしない。 - セル内画像(
CellImage.getContentUrl())は取得できる可能性があるが、Ctrl+Vでは作られない (挿入メニューからの操作が必要)ので利用者の手数が増える。
→ 画像の入口は HtmlService のダイアログ一本にする。
2. 構成
[スプレッドシートのカスタムメニュー]
└ 「不具合・要望を報告 (画像OK)」→ showFeedbackDialog()
└ HtmlService モーダル(FeedbackDialog.html)
├ 種別 / タイトル / 内容
└ 画像: Ctrl+V 貼り付け / ドラッグ&ドロップ / ファイル選択
↓ base64
google.script.run → Feedback_submitFromDialog(payload)
├ Drive の保存用フォルダへ画像を保存
├ 記録用シートへ1行 append → 同じ行を読み戻して照合
└ 通知(チャットツール等)へ画像を multipart で実添付
3. フロントエンド(要点だけ)
<div id="drop-zone">クリックでファイル選択 / <strong>Ctrl+V で貼り付け</strong> / ドラッグ&ドロップ</div>
<input type="file" id="file-input" accept="image/*" multiple hidden>
<script>
var MAX_FILES = 5, MAX_FILE_BYTES = 10*1024*1024, MAX_TOTAL_BYTES = 25*1024*1024;
var selected = [];
// ① スクショ運用の主経路。これを最優先で動かす
document.addEventListener('paste', function (e) {
var picked = extractPastedImages(e.clipboardData && e.clipboardData.items);
if (picked.length) { e.preventDefault(); addFiles(picked); }
});
function extractPastedImages(items) {
var out = [];
for (var i = 0; items && i < items.length; i++) {
if (items[i].kind === 'file' && String(items[i].type).indexOf('image/') === 0) {
var f = items[i].getAsFile();
if (f) out.push(f);
}
}
return out;
}
// ② ドラッグ&ドロップは preventDefault を両方に付けないとブラウザが画像を開いてしまう
var zone = document.getElementById('drop-zone');
zone.addEventListener('dragover', function (e) { e.preventDefault(); });
zone.addEventListener('drop', function (e) { e.preventDefault(); addFiles(Array.from(e.dataTransfer.files || [])); });
// ③ base64 化は必ず readAsDataURL。大きい画像で apply/spread を使うと RangeError で落ちる
function fileToPayload(file) {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function (ev) {
var s = ev.target.result || '', i = s.indexOf(',');
resolve({ name: file.name || 'screenshot.png', mimeType: file.type || 'image/png',
base64: i >= 0 ? s.slice(i + 1) : '' });
};
reader.onerror = function () { reject(reader.error || new Error('画像の読み込みに失敗')); };
reader.readAsDataURL(file); // NG: btoa(String.fromCharCode.apply(null, new Uint8Array(buf)))
});
}
</script>
送信ハンドラで必ず守ること:
- ハンドラ冒頭でフォーム要素と入力値をローカル変数へ確保する。
非同期コールバックの中で
event.currentTargetを辿ると null になり、 「保存は成功しているのに画面はエラー表示」という最悪の見え方になる。 - 送信中はボタンを
disabledにする(二重送信=重複行の原因)。 - 成功時は入力とサムネイルをクリアし、記録先へのリンクを出す。 失敗時は入力を消さない(書き直しを強要しない)。
4. サーバー側(GAS)
function Feedback_submitFromDialog(payload) {
payload = payload || {};
var title = String(payload.title || '').trim().slice(0, 200);
var body = String(payload.body || '').trim().slice(0, 4000);
if (!title && !body) return { ok: false, error: 'タイトルか内容を入力してください' };
var check = validateImages(payload.images || []); // 枚数/単体/合計の上限
if (!check.ok) return check;
var ss = SpreadsheetApp.getActive();
var urls = [], ids = [];
(payload.images || []).forEach(function (img, i) {
var blob = Utilities.newBlob(Utilities.base64Decode(img.base64),
img.mimeType || 'image/png',
'fb_' + stamp() + '_' + (i + 1) + '_' + safeName(img.name));
var file = getOrCreateFolder(ss, '<スクショ保存フォルダ名>').createFile(blob);
urls.push(file.getUrl()); ids.push(file.getId());
});
var sheet = getOrCreateLogSheet(ss); // 列: 日時/種別/タイトル/内容/状態/対応メモ/画像/送信元
sheet.appendRow([nowString(), kindOf(payload), title, body, 'new', '',
urls.join('\n'), 'ダイアログ(' + ss.getActiveSheet().getName() + ')']);
var row = sheet.getLastRow(); // read-back verify
if (String(sheet.getRange(row, 3).getValue()) !== title) throw new Error('記録の読み戻し確認に失敗');
return { ok: true, row: row, images: urls, logSheetUrl: sheetUrl(ss, sheet) };
}
細部で効くところ:
- 日時列は文字列
yyyy-MM-dd HH:mmで固定する。Dateオブジェクトを混ぜると列の型が揺れ、 既存行との並び・フィルタが壊れる。 - 既存の記録シートに列を足すときはマイグレーションを書く
(
if (!sheet.getRange('G1').getValue()) sheet.getRange('G1').setValue('画像'))。 新規作成パスだけ列を増やすと、既存環境では永遠に反映されない。 - 保存フォルダは「アプリのスプレッドシートの親フォルダ直下」に取り、取れなければルートへフォールバック。
- 通知は画像を実添付する。リンクだけだと権限や既定アカウントの都合で結局見られない。
チャットツールの webhook なら
payload_json+files[0]の multipart で送り、 添付付きが 2xx 以外なら本文だけで再送する(通知は best-effort、記録は成功扱い)。 - 秘匿値(webhook URL 等)はコードに書かず
PropertiesServiceから読む。 未設定でも例外にせず{ok:true, skipped:'no webhook'}を返す(記録が主、通知は従)。
5. シートに欄を二重に持たない
シート上にも入力欄があり、メニューにもダイアログがある状態は現場を必ず迷わせる。 画像を送れるダイアログが上位互換なので、シート側の入力欄は撤去し、機能一覧の行は 「メニューから実行」の案内行(チェックボックス無し)に置き換える。
同じ理由で、メニューと機能一覧に同じ実行項目を並べない。 メニューに残すのは「一覧では実現できないもの」だけ(画面を開く、ファイル選択ダイアログ、修復)。
6. 検証(ここまでやって初めて「できた」と言える)
- 上限・multipart 組み立て・列マイグレーションは実装から抜いた純関数を Node で単体テストする (UI を立ち上げずに回帰を止められる)。コピーした別実装をテストしても意味がない。
- サーバー関数は実データで1回走らせ、記録行を読み戻し、テスト行と保存ファイルを後片付けする。
- 画像が読めない経路(シート貼り付け)を残す場合は、読めなかった枚数を数えて利用者に案内し、 その画像を消さない。「送信後に貼り付け画像を掃除する」実装は、送れていない画像を黙って捨てる= 報告者の資料喪失になる。
- HTML ダイアログ自体は無人で開けない。ダイアログを開く1回だけは人の操作が残ると割り切り、 それ以外(保存・記録・通知・後片付け)を自動検証で固める。
7. 上限の目安
| 項目 | 値 | 理由 |
|---|---|---|
| 1枚 | 10MB | スクショなら十分。超過はフロントで止める |
| 合計 | 25MB | 一般的なチャット添付上限に合わせる |
| 枚数 | 5枚 | それ以上は分けて報告してもらう |
| タイトル | 200字 | 記録シートの可読性 |
| 本文 | 4000字 | 同上 |
よくある質問
+「スプレッドシート業務アプリに画面キャプチャ付き不具合報告フォームを付ける(GAS)」とは何ですか?
GASのOverGridImageにgetBlob()が無くシート貼付画像は読めない。HTMLダイアログでCtrl+Vを受ける正しい設計・記録列マイグレーション・read-back検証・画像を捨てない後片付けまでの実装指示書
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約12万トークンかかりますが、この巻物を使えば約1.5万トークンで済みます。差し引き約10.5万トークン(API料金換算で約160円)・88%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
GAS完全自動化テンプレ — Driveコマンドキュー方式
Google Apps Script の「毎回エディタで▶実行」を根絶。Drive 経由のコマンドキューで、初回1クリック以降は AI がすべての GAS 関数をリモート実行できるようになるテンプレート指示書。
人間の手入力台帳を壊さずに自動更新する — GAS Web App upsert 設計
各PC/各拠点の点検結果を、人間が手運用しているスプレッドシート台帳へ自動反映する。手入力列とコメントを絶対に壊さない突合設計、タブ/列の解決、並行POST対策、配布シークレットの落とし穴まで。
数式まみれの業務スプレッドシートを、Webアプリから壊さずに編集させる型
ArrayFormula と per-row 数式が混在する台帳を、セル単位 allowlist・dry-run 既定・適用前バックアップ・触っていないセルの数式不変検査で安全に書き換える設計手順。列ごとの数式復元と、テストが緑のまま壊れる典型例つき。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア