マキモノ
Google Workspace無料✅ 公式検証済みv1.0.0 / 更新

スプレッドシート業務アプリに画面キャプチャ付き不具合報告フォームを付ける(GAS)

GASのOverGridImageにgetBlob()が無くシート貼付画像は読めない。HTMLダイアログでCtrl+Vを受ける正しい設計・記録列マイグレーション・read-back検証・画像を捨てない後片付けまでの実装指示書

出品者: kim@orgiast.jp3 DL📖 読込 約3,389トークン (約5円)💰 コスパ 31
トークン節約メーター88%節約
ゼロからAIに作らせた場合12万トークン
このMDを読ませた場合1.5万トークン

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 のターミナルに貼るだけです。

⬇ .md をダウンロード
claude "https://makimono-md.vercel.app/api/v1/files/gas-2/raw を読み込んで、この指示書どおりに実装して"
claude-codecursorcodex-cliライセンス: 商用利用可 (再販不可)

中身

Google スプレッドシート業務アプリに「画面キャプチャ付き 不具合・要望フォーム」を付ける(GAS)

対象: Apps Script(GAS) で作った社内ツールが Google スプレッドシート上で動いていて、 利用者から「不具合と一緒にスクショを送りたい」と言われたとき。

結論を先に: シートに画像を貼らせる設計は原理的に動かない。HTML のモーダルダイアログを開き、 その中で Ctrl+V を受け取る。ここを知らないまま「シートに貼って▶」を実装すると、実機で画像が0枚になる。


1. 最初に潰すべき落とし穴(実測)

Apps Script の Sheet.getImages() は取得できるが、返る OverGridImagegetBlob() が存在しない

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> / ドラッグ&amp;ドロップ</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. 検証(ここまでやって初めて「できた」と言える)

  1. 上限・multipart 組み立て・列マイグレーションは実装から抜いた純関数を Node で単体テストする (UI を立ち上げずに回帰を止められる)。コピーした別実装をテストしても意味がない。
  2. サーバー関数は実データで1回走らせ、記録行を読み戻し、テスト行と保存ファイルを後片付けする。
  3. 画像が読めない経路(シート貼り付け)を残す場合は、読めなかった枚数を数えて利用者に案内し、 その画像を消さない。「送信後に貼り付け画像を掃除する」実装は、送れていない画像を黙って捨てる= 報告者の資料喪失になる。
  4. 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で頼むだけで、仕事が完成」。

AI代行堂を見る →

関連する巻物

この巻物、誰かのトークンも救えます

𝕏 で節約レシートをシェア