1. システム概要
プロジェクトの目的と要件
本プロジェクトは、Googleスプレッドシートで管理されているコンテンツ制作の進捗状況を監視し、特定の条件(期限間近、ステータス変更、コメント投稿など)に基づいてChatworkに自動通知を行うシステムです。 主な要件は以下の通りです。
- 定期実行(毎日9時、12時)によるリマインド通知
- 編集イベント(onEdit)によるリアルタイム通知
- 複雑な条件判定(「投稿日の2日前かつ未完了」など)
- メンテナンス性の高いコードベース
技術選定の理由
Google Apps Script (GAS)
Google Workspace(スプレッドシート)との親和性が最も高く、サーバーレスで手軽に定期実行やイベントトリガーを実装できるため採用しました。
TypeScript
GAS標準のJavaScriptではなくTypeScriptを採用することで、型安全性によるバグの抑制、IDE(VS Code)での補完機能による開発効率向上を実現しています。特にスプレッドシートの列定義やAPIレスポンスの型定義において威力を発揮します。
esbuild
TypeScriptで記述された複数のモジュールを、GASが解釈可能な1つのJavaScriptファイルにバンドルするために使用しています。高速なビルドと、依存関係の解決を担います。
Biome
コードのフォーマットとリンティングに使用。高速かつ設定不要でモダンなJavaScript/TypeScriptのベストプラクティスを強制できます。
2. アーキテクチャ設計
ファイル構成と責務分離
コードベースは機能ごとに適切に分割し、責務を明確にしています。
packages/[プロジェクト名]/src/
├── index.ts # エントリーポイント(GASから呼び出されるグローバル関数)
├── constant.ts # 定数定義(列番号、メッセージテンプレートなど)
├── environment.ts # 環境変数(スクリプトプロパティ)へのアクセス
├── chatwork.ts # Chatwork API連携(commonパッケージのラッパー)
├── spreadsheet.ts # スプレッドシートからのデータ取得・整形
├── message.ts # 通知メッセージの生成ロジック
├── commentNotification.ts # M列コメント通知のビジネスロジック
├── progressNotification.ts # 週次進捗通知のビジネスロジック
├── postDateNotification.ts # 投稿日関連(2日前、当日)のビジネスロジック
├── statusNotification.ts # ステータスFIX通知のビジネスロジック
├── trigger.ts # GASトリガーの作成・管理
└── test.ts # GAS上で実行するテスト関数群
関心の分離
- データアクセス層:
spreadsheet.tsがスプレッドシートの生データを扱い、型付きのオブジェクト(ContentRow)に変換して返します。 - ビジネスロジック層:
*Notification.tsが通知を行うべきかの判定ロジックを持ちます。 - プレゼンテーション層:
message.tsが通知文言の生成を担当します。 - インフラ層:
chatwork.tsが外部APIとの通信を担います。
再利用性 (Monorepo)
本プロジェクトはMonorepo構成を採用しており、packages/common に汎用的な処理(Chatwork APIの基本処理、日付操作など)を切り出しています。これにより、他のGASツールを作成する際にも共通コードを再利用可能です。
3. 実装のポイント
3.1 トリガーとイベント処理
GASのエントリーポイントとなる関数は、グローバルスコープに露出させる必要があります。index.ts で各機能をエクスポートし、GASのトリガーから呼び出せるようにしています。
コード例:トリガー作成関数 (src/trigger.ts)
/**
* 毎日9時のトリガーを作成する
* 実行関数: dailyAt9
*/
export const createDailyAt9Trigger = (): void => {
ScriptApp.newTrigger("dailyAt9")
.timeBased()
.everyDays(1)
.atHour(9)
.create();
};
/**
* onEditトリガーを作成する
* 実行関数: onEditTrigger
*/
export const createOnEditTrigger = (): void => {
ScriptApp.newTrigger("onEditTrigger")
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create();
};
3.2 スプレッドシート操作
スプレッドシートの操作は実行速度のボトルネックになりやすいため、getValues() で一括取得してからメモリ上で処理を行うのが定石です。また、列番号を定数管理することで、シートレイアウト変更時の修正漏れを防ぎます。
コード例:データ抽出と型変換 (src/spreadsheet.ts)
// 行データをオブジェクトに変換
const parseContentRow = (row: unknown[], index: number): ContentRow | null => {
const { HEADER_ROW_COUNT } = getEnvironment();
// 列定義定数を使ってデータにアクセス
const postDate = row[SHEET.POST_DATE_COL - 1];
const content = row[SHEET.CONTENT_COL - 1];
const status = row[SHEET.STATUS_COL - 1];
return {
rowIndex: index + HEADER_ROW_COUNT + 1,
postDate: postDate instanceof Date ? postDate : null,
content: String(content).trim() || "内容未設定",
status: status ? String(status).trim() : "",
// ...
};
};
3.3 Chatwork API連携
APIトークンなどの機密情報はコードにハードコードせず、GASの「スクリプトプロパティ」で管理し、environment.ts 経由でアクセスします。
コード例:Chatwork APIラッパー (src/chatwork.ts)
/**
* Chatworkにメッセージを投稿する
*/
export const postChatworkMessage = (roomId: string, body: string): boolean => {
const { CHATWORK_API_TOKEN } = getEnvironment();
if (!CHATWORK_API_TOKEN) {
throw new Error("CHATWORK_API_TOKEN が設定されていません。");
}
// commonパッケージの関数を利用
return postChatworkMessageCommon({
apiToken: CHATWORK_API_TOKEN,
roomId: roomId,
}, body);
};
3.4 日付処理
「投稿予定日の2日前」などの判定では、タイムゾーン(JST)を考慮する必要があります。GASのデフォルトタイムゾーンとDateオブジェクトの扱いに注意が必要です。本プロジェクトでは、日付の正規化(時刻部分の切り捨て)を行った上で比較を行っています。
4. 開発環境とビルド
TypeScriptからGASへの変換
GASは標準ではES Modulesのインポート/エクスポート構文を理解しません(※V8ランタイムでもファイル間のimportは制限があります)。そのため、esbuild を使用してすべてのコードを単一のファイルにバンドルしています。
esbuild設定 (esbuild.config.js)
主な設定内容は以下の通りです。
bundle: true: 依存関係をすべて含めるformat: 'iife': 即時実行関数式として出力(グローバル汚染を防ぐため、ただしGASのエントリーポイントは明示的にglobalに代入)target: 'es2020': GASのV8ランタイムに合わせて設定
claspによる自動デプロイ
Google公式のCLIツール clasp を使用し、ローカルでビルドしたコードをコマンド一つでGASプロジェクトへプッシュします。
5. テスト戦略
GASはローカル環境での完全な実行シミュレーションが難しいため、以下の戦略をとっています。
- ロジックの単体テスト: 日付計算や文字列処理などはローカルのJest等でテスト可能(今回は簡易的に実装)。
- GAS上での結合テスト:
test.tsにテスト用関数を実装し、GASエディタ上から手動実行して動作確認を行う。
コード例:テスト関数の実装 (src/test.ts)
/**
* 毎日9時実行のテスト
* 実際に通知が飛ぶので注意
*/
export const testDailyAt9 = (): void => {
console.log("=== dailyAt9 テスト開始 ===");
try {
dailyAt9();
console.log("=== dailyAt9 テスト終了: 成功 ===");
} catch (e) {
console.error(`=== dailyAt9 テスト終了: 失敗 ===\n${e}`);
}
};
6. 運用とメンテナンス
スクリプトプロパティによる設定管理
通知先のChatworkルームIDやメンバーID、対象シート名などはすべてスクリプトプロパティで管理しています。これにより、担当者の変更やシートの変更があった場合でも、コードを修正することなく設定変更のみで対応可能です。
まとめ
本システムは、TypeScriptによる型安全性とモジュール化された設計により、拡張性と保守性の高いGASアプリケーションとなっています。単なる「スクリプト」の枠を超え、小さな「アプリケーション」として設計することで、長期的な運用に耐えうるシステムを実現しました。