70名超のラボメンバーを抱える研究室で、毎日の研究ノートを全員分管理するシステムを Google Apps Script (GAS) だけで構築した。本稿ではそのアーキテクチャと、実装上の技術的工夫を解説する。
システム概要
学生(ブラウザ) ──→ GAS① Web App ──→ Google Drive (Docs生成)
│ │
↓ ↓
Spreadsheet 活動ログ記録
│
GAS② Dashboard
│
Slack通知 (9時 / 18時 / 23時)
学生(Docs) ─→ テンプレートスクリプト ─→ GAS① doPost ─→ Slack通知
│
GAS③ Archive
使用技術は意図的にシンプルに保っている。
- Google Docs(研究ノート本体)
- Google Drive(
年/月/日構造のファイル管理) - Google Spreadsheet(ユーザー管理 + 活動ログ DB)
- Google Apps Script × 3本(App / Dashboard / Archive)
- Slack API(チャンネル通知 + DM リマインダー)
特殊なフレームワークやサービスは一切使っておらず、Workspace ライセンスさえあれば追加コストゼロで動く。
GAS の構成(3本立て)
GAS① — LabNote-App.js(学生向け入力 + 受信 API)
学生がアクセスする唯一のエントリーポイント。doGet は Google セッションからメールアドレスを取得し、未登録なら登録フォームを、登録済みなら当日の Docs を自動生成してリダイレクトする。
function doGet(e) {
const email = Session.getActiveUser().getEmail();
const student = getStudentByEmail(email);
if (!student) return buildRegistrationForm(email, email.split('@')[0]);
const doc = getOrCreateTodayDoc(student);
return HtmlService.createHtmlOutput(`
<script>window.location.href='${doc.getUrl()}';</script>
`).setTitle('研究ノート');
}
Docs のフォルダ構造は {SharedDrive}/{account_name}/research-record/{year}/{month}/{day} で、ファイル名は YYYY-MM-DD 形式の日付になる。当日分がすでに存在すれば既存ファイルを返すだけなので、何度 URL を踏んでも冪等に動く。
提出受付は doPost エンドポイントが担当する。Docs 側のテンプレートスクリプト(後述)から JSON で {email, docId, docUrl, charCount, today} を受け取り、Spreadsheet に記録してメンターチャンネルへ Slack 通知を飛ばす。
GAS② — LabNote-Dashboard.js(教員向けダッシュボード)
全メンバーの状況をタイル表示する。カラーコードは 3 段階。
| 色 | 状態 |
|---|---|
| 🟢 緑 | 当日提出済み |
| 🟡 黄 | 未提出だが 7 日以内にノートあり |
| 🔴 赤 | 7 日以上更新なし |
連続提出日数(streak)も計算して各タイルに表示する。Spreadsheet の全 activity 行を読み込んだうえで、submitted ステータスの日付を降順に並べ、連続性を検査するアルゴリズムで算出している。
Slack 通知はトリガーで自動実行される 3 つの関数が担う。
notifyAt9() // 朝: 全員に今日のノートを開くよう DM
notifyAt18() // 夕: 提出状況をメンターチャンネルに報告
remindAt23() // 夜: 未提出者に最終リマインダー DM
23 時時点で全員提出済みの場合は「🎉 全員提出完了!」をメンターチャンネルに通知するだけで終わる。
GAS③ — LabNote-Archive.js(学生向けアーカイブ)
学生本人が過去ノートをカレンダー形式で閲覧できる SPA。左ペインにカレンダーと日付リスト、右ペインにセクション別の折りたたみ表示。ノートを持つ日付はカレンダーセルがハイライトされる。
コンテンツ取得時は、activity シートで student.id と docId の一致を確認してから Docs を開くオーナーシップ検証が入っており、他人のノートは参照できない。
技術的工夫点
1. 排他制御とデッドロック回避
複数学生が同時登録・同時アクセスした場合のレースコンディションを LockService で防いでいる。ポイントは ui.alert などユーザー応答待ちが発生する処理を必ず finally ブロックの外に置くこと。ロック保持中にダイアログを出すとデッドロックが起きる。
const lock = LockService.getScriptLock();
lock.waitLock(15000);
try {
SpreadsheetApp.flush(); // キャッシュを破棄して最新状態を強制同期
// ── クリティカルセクション ──
} finally {
lock.releaseLock();
}
// ← ui.alert はここで呼ぶ
flush() をロック直後に呼ぶのは、GAS がスプレッドシートの読み書きをキャッシュするため。これを怠ると古いデータを見て二重登録を見逃す可能性がある。
2. getDisplayValues() による型安全
GAS の getValues() は日時セルを Date オブジェクトとして返すため、=== による文字列比較が無言で失敗する。getDisplayValues() に統一することでセルの表示文字列をそのまま取得し、型不整合によるサイレントバグを防いでいる。
// NG: Date オブジェクトが混じると === 比較が壊れる
const rows = sheet.getRange(2, 1, lastRow - 1, 9).getValues();
// OK: 表示文字列として統一取得
const rows = sheet.getRange(2, 1, lastRow - 1, 9).getDisplayValues();
3. 正規表現による堅牢な ID 採番
ユーザー ID(s001, s002, ...)の採番では Number() による単純キャストを避け、正規表現で数値部分のみを抽出している。
const numMatch = lastIdStr.match(/\d+/);
const nextNum = numMatch ? parseInt(numMatch[0], 10) + 1 : lastRow;
newId = 's' + String(nextNum).padStart(3, '0');
Number('s001') は NaN を返すため、そのままインクリメントするとスプレッドシートの数式エラー(#NUM!)に波及する。正規表現パースでこれを防いでいる。
4. クライアント・サーバー分離
Docs テンプレートに埋め込むスクリプト(クライアント)は提出 POST を GAS① に投げるだけで、スプレッドシート ID や Slack トークンといった機密情報を一切保持しない。実際のデータ操作はサーバー側(GAS①)に集約されており、学生側スクリプトが漏洩してもセキュリティ上の影響がない。
// クライアント側(テンプレート埋め込み)
const payload = { email, docId, docUrl, charCount, today };
UrlFetchApp.fetch(API_URL, { method: 'post', payload: JSON.stringify(payload) });
テンプレートには「ラボノート → 📤 本日のノートを提出」のカスタムメニューが追加され、文字数 200 字未満・必須セクション(実施内容 / 結果 / 考察 / 明日の予定)未記入の場合はクライアント側でブロックする。
5. N+1 問題の回避(バッチ巡回)
6 時間ごとに実行される scanAllDocs は全学生のノートを走査して文字数を更新する。ループ内でスプレッドシートを読み書きするとAPIコールが O(n) になり、70 名超では GAS の実行時間制限(6 分)に抵触する。
// 事前に全データをメモリへ読み込む
const rows = actSheet.getRange(2, 1, lastRow - 1, 9).getDisplayValues();
students.forEach(s => {
const rowIndex = rows.findIndex(r => ...);
// 更新は setValues で一括書き込み
actSheet.getRange(rowIndex + 2, 7, 1, 2).setValues([[chars, scannedAt]]);
});
6. URLドメイン埋め込みによるセッション固定
大学アカウント(@arakawa-lab.com)と個人 Gmail が同一ブラウザ上で混在する環境では、リンクのドメインパス(/a/arakawa-lab.com/)を明示することで正しいアカウントのセッションを強制している。
const link = `https://docs.google.com/a/arakawa-lab.com/document/d/${docId}/edit`;
7. IAM 設計(最小権限の原則)
| 主体 | 共有ドライブ権限 |
|---|---|
| 全学生(デフォルト) | 閲覧者 |
| 各学生の自ドライブフォルダ | 編集者(プログラムが動的付与) |
| GAS① 実行ユーザー | コンテンツ管理者(教員アカウント) |
GAS① のデプロイを「実行するユーザー:自分(教員)」にすることで、学生はドライブ書き込み権限なしでもフォルダ作成やログ記録が行われる。学生から見えるのは自分のフォルダだけであり、他学生のノートへのアクセスは物理的に遮断されている。
まとめ
今回、研究ノートシステムを既存インフラ(Google Workspace + Slack)の上に構築した。Docs 単体でも読めるし、Google Drive の全文検索もそのまま使える。特殊なビューアやデータベースに縛られない柔軟性が長期運用には重要だと感じている。
今後は、学生が発展版として日中の作業を自動記録するような機能を自ら開発してくれることを期待している。GAS は試行錯誤のコストが低く、研究室内 hackathon のテーマとしてもちょうどよいレベル感だ。