GASでイベント参加者リストを自動で生成・管理する方法

こんにちは、GASおじです。
イベントの申込者をGoogleフォームで集めたあと、参加者名・メールアドレス・参加状況を毎回コピーしていませんか。少人数なら手作業でも回りますが、更新漏れや重複、キャンセル反映忘れが起きやすい作業です。
この記事では、フォーム回答を読み取り、別シートへ参加者リストを安全に再構築する方法を紹介します。既存シートをいきなり空にせず、一時シートで完成形を作ってから切り替えます。以前の出力はバックアップとして残すため、問題があれば戻せます。
費用面では、Googleアカウントの利用枠内なら追加の専用SaaS料金をかけずに構築できます。ただしApps Scriptには日次quota(利用上限)や1回あたりの実行時間制限があります。無制限ではありません。最新の上限はGoogle公式の割り当てページで確認してください。
事前準備とシート構成
Googleフォームの回答先スプレッドシートを開き、次の2点を確認します。
- 回答元シート名:
フォームの回答 1 - 必須見出し:
参加者名、メールアドレス、参加ステータス
見出しの順番は問いません。コード側で見出し名から列位置を探します。表記が1文字でも違う場合は処理を停止するため、フォームの質問名と一致させてください。
出力先は参加者リスト_自動生成です。このシートは自動生成専用にし、手入力したい列は4列目以降へ追加してください。再構築時にはメールアドレスをキーに、4列目以降の値も新しい出力へ引き継ぎます。切り替え前のシート自体も参加者リスト_backup_日時として残します。
メールアドレスは個人情報です。スプレッドシートの共有範囲を必要な担当者だけに絞り、不要になった回答は社内ルールに従って削除してください。共有リンクを「リンクを知っている全員」にしない運用を推奨します。
既存データを守るGASコード
次のコードをスプレッドシートの「拡張機能」→「Apps Script」に貼り付けます。最初から本番データで試さず、回答を数件入れた複製シートで確認してください。
function rebuildParticipantListSafely() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sourceName = 'フォームの回答 1';
const outputName = '参加者リスト_自動生成';
const requiredHeaders = ['参加者名', 'メールアドレス', '参加ステータス'];
const source = ss.getSheetByName(sourceName);
if (!source) {
throw new Error(`回答元シート「${sourceName}」が見つかりません。`);
}
const currentOutput = ss.getSheetByName(outputName);
if (currentOutput && source.getSheetId() === currentOutput.getSheetId()) {
throw new Error('回答元と出力先が同じです。処理を停止しました。');
}
const sourceValues = source.getDataRange().getValues();
if (sourceValues.length < 2) {
throw new Error('回答データがありません。見出しと回答内容を確認してください。');
}
const sourceHeaders = sourceValues[0].map(value => String(value).trim());
const indexes = Object.fromEntries(
requiredHeaders.map(header => [header, sourceHeaders.indexOf(header)])
);
const missingHeaders = requiredHeaders.filter(header => indexes[header] < 0);
if (missingHeaders.length > 0) {
throw new Error(`必須見出しが不足しています: ${missingHeaders.join(', ')}`);
}
const participantsByEmail = new Map();
sourceValues.slice(1).forEach((row, rowIndex) => {
const name = String(row[indexes['参加者名']]).trim();
const email = String(row[indexes['メールアドレス']]).trim().toLowerCase();
const status = String(row[indexes['参加ステータス']]).trim();
if (!name || !email || !status) {
throw new Error(`${rowIndex + 2}行目に空欄があります。元データを修正してください。`);
}
if (!email.includes('@')) {
throw new Error(`${rowIndex + 2}行目のメールアドレス形式を確認してください。`);
}
participantsByEmail.set(email, [name, email, status]);
});
let extraHeaders = [];
const extrasByEmail = new Map();
if (currentOutput && currentOutput.getLastRow() > 0) {
const oldValues = currentOutput.getDataRange().getValues();
const oldHeaders = oldValues[0].map(value => String(value).trim());
const coreHeaders = oldHeaders.slice(0, requiredHeaders.length);
if (coreHeaders.join('|') !== requiredHeaders.join('|')) {
throw new Error('既存出力の先頭3列が想定と異なります。処理を停止しました。');
}
extraHeaders = oldHeaders.slice(requiredHeaders.length);
oldValues.slice(1).forEach(row => {
const email = String(row[1]).trim().toLowerCase();
if (email) extrasByEmail.set(email, row.slice(requiredHeaders.length));
});
}
const outputHeaders = [...requiredHeaders, ...extraHeaders];
const outputRows = [...participantsByEmail.values()].map(core => {
const email = core[1];
const savedExtras = extrasByEmail.get(email) || Array(extraHeaders.length).fill('');
return [...core, ...savedExtras];
});
const tempName = uniqueSheetName_(ss, `参加者リスト_tmp_${Date.now()}`);
const tempSheet = ss.insertSheet(tempName);
tempSheet
.getRange(1, 1, outputRows.length + 1, outputHeaders.length)
.setValues([outputHeaders, ...outputRows]);
SpreadsheetApp.flush();
let backupName = '';
try {
if (currentOutput) {
backupName = uniqueSheetName_(ss, `参加者リスト_backup_${timestamp_()}`);
currentOutput.setName(backupName);
}
tempSheet.setName(outputName);
} catch (error) {
if (currentOutput && backupName && !ss.getSheetByName(outputName)) {
currentOutput.setName(outputName);
}
throw error;
}
}
function uniqueSheetName_(ss, baseName) {
let candidate = baseName;
let suffix = 1;
while (ss.getSheetByName(candidate)) {
candidate = `${baseName}_${suffix}`;
suffix += 1;
}
return candidate;
}
function timestamp_() {
return Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyyMMdd_HHmmss');
}
この方式では、入力検証と出力行の組み立てが終わるまで既存出力を変更しません。書き込みもappendRow()の繰り返しではなく、setValues()で一括実行します。途中でエラーになった場合はエラーメッセージを確認し、元データを修正してから再実行してください。
初回実行と自動更新の設定
Apps Script画面でrebuildParticipantListSafelyを選び、「実行」を押します。初回はスプレッドシートへのアクセス許可が表示されます。内容を確認し、対象ファイルを管理するGoogleアカウントで許可してください。
正常終了後、参加者リスト_自動生成を開き、件数・メールアドレスの重複・参加ステータス・手入力列が正しいか確認します。
自動更新する場合は、Apps Script左側の「トリガー」→「トリガーを追加」を開きます。
- 実行する関数:
rebuildParticipantListSafely - イベントのソース:
スプレッドシートから - イベントの種類:
フォーム送信時 - エラー通知:
今すぐ通知を受け取る
インストール型トリガーは、トリガーを作成したユーザーの権限で動きます。担当者の退職やアカウント停止に備え、管理用アカウントで作成し、所有者を台帳へ記録してください。詳細はGoogle公式のインストール型トリガー説明を確認してください。
コスト試算と戻し方
たとえば年4回のイベントで、毎回1時間の集計作業を時給1,500円として見積もる場合、削減候補は4回 × 1時間 × 1,500円 = 年6,000円です。これは仮定に基づく試算であり、実際の削減額は参加者数、確認作業、運用方法によって変わります。
不具合が起きた場合は、まずトリガーを停止します。次に参加者リスト_backup_日時を確認し、問題がなければ現在の参加者リスト_自動生成を別名へ変更してから、バックアップシートを参加者リスト_自動生成へ戻してください。回答元のフォームの回答 1は処理対象として読み取るだけなので、出力切り替えで回答データを上書きしません。
最初は手動実行で結果を確認し、バックアップから戻せることまで試してから自動化する。ここまでが安全なGAS運用です。