本文へ移動
知恵の一覧へ
運営/DX/AIX道具・素材

エラーがでにくいSheet to SlackGAS

スプレッドシート更新のSlack通知が二重に飛ぶのを止める、ロック付きGASコード一式

この知恵で考えられること

function onChange(e) { if (!e || (e.changeType !== 'EDIT' && e.changeType !== 'INSERTROW')) { return; // 不要な呼び出しを防ぐため }

const SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/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'; // WebhookのURL
const DEBOUNCE_INTERVAL = 2000; // デバウンス間隔(ミリ秒)
const LOCK_TIMEOUT = 30000; // ロックのタイムアウト(ミリ秒)。最大6分(360000ms)

function onChange(e) {
  if (!e || (e.changeType !== 'EDIT' && e.changeType !== 'INSERT_ROW')) {
    return; // 不要な呼び出しを防ぐため
  }

  // --- ロック処理の開始 ---
  const lock = LockService.getScriptLock();
  try {
    // 他のインスタンスが処理中の場合、待機する(最大LOCK_TIMEOUTミリ秒)
    // 待機してもロックが取得できない場合は、nullが返る
    const hasLock = lock.tryLock(LOCK_TIMEOUT); 
    if (!hasLock) {
      Logger.log('別の処理が進行中のため、スキップします。');
      return; // ロックが取得できなかった場合はスキップ
    }

    // --- デバウンス処理 ---
    const properties = PropertiesService.getScriptProperties();
    const lastExecutionTimeStr = properties.getProperty('lastExecutionTime');
    const lastExecutionTime = lastExecutionTimeStr ? parseInt(lastExecutionTimeStr, 10) : 0;
    Logger.log('PropertiesServiceから取得したlastExecutionTime: ' + lastExecutionTime);

    const currentTime = new Date().getTime();
    Logger.log('現在の時間: ' + currentTime);
    Logger.log('経過時間: ' + (currentTime - lastExecutionTime));

    if (currentTime - lastExecutionTime < DEBOUNCE_INTERVAL) {
      Logger.log('デバウンスにより処理がスキップされました。');
      return; // デバウンス間隔内の場合はスキップ
    }

    const sheet = e.source.getActiveSheet();
    const lastRow = sheet.getLastRow();
    const flagColumn = 17; // Q列は17列目

    // --- 「処理済み」フラグの最終チェックと設定 ---
    const flag = sheet.getRange(lastRow, flagColumn).getValue();
    if (flag === '処理済み') {
      Logger.log('この行はすでに処理済みです。(二重チェック)');
      return; // 処理済みの場合は終了
    }
    
    // ここでフラグを設定し、次のイベントトリガーがすぐに起動しないようにする(重要)
    // この setValue が新たな onChange を発火させる可能性があるため、注意
    sheet.getRange(lastRow, flagColumn).setValue('処理済み'); 
    SpreadsheetApp.flush(); // スプレッドシートへの変更を即座に反映させる

    const data = sheet.getRange(lastRow, 1, 1, sheet.getLastColumn()).getValues()[0];
    const message = createSlackMessage(data);
    try {
      sendToSlack(message);
      Logger.log('Slackへの送信成功。lastExecutionTimeを更新します。');
      // Slack送信成功後のみ最終実行時間を更新
      properties.setProperty('lastExecutionTime', currentTime.toString());
    } catch (error) {
      Logger.log('Slackへの送信エラー: ' + error);
      // エラーが発生した場合はフラグを元に戻すなど、エラーハンドリングを検討
      sheet.getRange(lastRow, flagColumn).setValue(''); // 例: フラグをクリア
    }

  } finally {
    // --- ロックの解放 ---
    if (lock) { // ロックが取得できた場合のみ解放
      lock.releaseLock();
      Logger.log('ロックを解放しました。');
    }
  }
}

// ... createSlackMessage, sendToSlack は変更なし
function createSlackMessage(data) {
  const schedule = data[1]; // スケジュール
  const cancelReason = data[4]; // キャンセル理由
  const participants = data[6]; // 参加メンバー
  const companyName = data[8]; // 会社名
  const name = data[9]; // 名前
  const comment = data[10]; // コメント
  const lineInstagramName = data[13]; // LINE/Instagramのお名前

  return {
    text: `スケジュール: ${schedule}\n` +
          `キャンセル理由: ${cancelReason}\n` +
          `参加メンバー: ${participants}\n` +
          `会社名: ${companyName}\n` +
          `名前: ${name}\n` +
          `コメント: ${comment}\n` +
          `LINE/Instagramのお名前: ${lineInstagramName}`,
  };
}

function sendToSlack(message) {
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(message),
  };

  const response = UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options);
  Logger.log('Response: ' + response.getContentText());
}