[GAS] Googleエコシステム内でシフトボードからGoogleカレンダーに自動登録する

# はじめに
シフト管理アプリ「シフトボード」の予定をGoogleカレンダーに自動で同期したいと思ったことはありませんか？
この記事では、 **Google Apps Script（GAS）** を使って、シフトボードからエクスポートしたテキストファイルをGoogleドライブに保存し、それをトリガーで定期的にチェックしてGoogleカレンダーに登録する方法を解説します。
前提として、シフトボードはAPIを公開していないため、データを直接保存するには、iOSカレンダーを一枚噛ませてGoogleカレンダーに同期させる必要があります。この方法でも登録は可能ですが、同期のタイミングが合わず不安定になることがあり、かつ特定の予定のみ限定して登録することもできないため、今回はより確実に登録でき、柔軟に設定できる方法を採用しました。

# GAS（Google Apps Script）とは
Google Apps Script（GAS）は、Googleが提供するクラウドベースのスクリプト言語で、Googleのサービス（ドライブ、カレンダー、スプレッドシートなど）を簡単に連携・自動化できます。GASは**JavaScript**をベースにしています。
#### GASを使用するメリット
- **全てGoogleサービス内で完結:** 外部サーバーや複雑な環境構築が不要。
- **無料で使える:** Googleアカウントがあれば無料枠で十分な機能を利用可能。詳細は[公式ドキュメント](https://developers.google.com/apps-script/guides/services/quotas?hl=ja)を参照。
- **簡単なトリガー設定:** 時間ベースの自動実行がGUIで設定できる。
- **高い統合性:** ドライブやカレンダーとの連携がスムーズ。


# プロジェクトのフロー
このプロジェクトの全体像は以下の通りです：

1. **登録用ディレクトリの作成**
まず、Googleドライブ上にシフト専用のフォルダを作成します。このフォルダが、シフトボードから転送されたデータを一時的に保管する場所となります。
2. **シフトボードからGoogleドライブにテキスト形式で保存**
シフトボードアプリの「シフトの共有」機能（アプリ上部の共有ボタン）を使用して、シフトデータをテキスト形式でGoogleドライブに保存します。この方法により、シフトデータをクラウド上に保存し、後で処理できるようにします。
    2.1 **シフトボードのテキスト形式（例）**
    シフトの共有で保存したファイル内のテキスト形式は以下のようになっています。
    ```txt
    03/03（月）09:00 - 18:00
    - part-time1
    03/05（水）09:00 - 14:00
    - part-time2
    ```
3. **GASのトリガー設定による発火とテキスト抽出**
Google Apps Script（GAS）のトリガー機能（後述）を活用して、特定の時間ごとにGoogleドライブ上の指定フォルダをチェックします。新しいテキストファイルが追加された場合、GASが自動的に発火し、ファイル内容を抽出します。
4. **テキスト解析とGoogleカレンダー用の形式に変換**
抽出したテキストから、シフトの日時とタイトルを解析します。この情報を元に、Googleカレンダーで読み込める形式に変換します。たとえば、シフトの開始日時と終了日時、シフト名などを抽出し、カレンダーイベントとして扱えるようにします。
5. **Googleカレンダーに送信**
最終的に、変換したシフトデータをGoogleカレンダーに登録します。これにより、シフトボードの情報が手動で入力することなく、Googleカレンダーに自動的に反映されるようになります。


# プロジェクトの作成
### GASプロジェクトの作成
まず、GASプロジェクトを作成します。[公式ドキュメント](https://developers.google.com/apps-script/guides/projects?hl=ja)を参考に、プロジェクトを作成してください。
自動生成された**コード.gs**を編集していきます。メソッドごとに解説します。

### 環境変数の取得
```jsx
function getEnv (key) {
  const properties = PropertiesService.getScriptProperties();
  return properties.getProperty(key);
}
```
GASに.envファイルはありません。そのため、**スクリプト プロパティ**として環境変数を設定します（[公式ドキュメント](https://developers.google.com/apps-script/guides/properties?hl=ja)）。スクリプト プロパティはKey-Valueのペア形式になっています。
`getScriptProperties()`でアクセスし、`getProperty(key)`でValueを返します。

### 年の取得
```jsx
function getYear(month) {
  const today = new Date();
  const currentYear = today.getFullYear();
  const currentMonth = today.getMonth() + 1;
  if (month < currentMonth) {
    const nextYear = currentYear + 1;
    return nextYear;
  }
  return currentYear;
}
```
元データには年数が書かれていないため、現在の日付から取得する必要があります。`today.getMonth() + 1`としているのは`getMonth`の出力が0~11(1月~12月)であるからです。

### Googleカレンダー形式に変換
```jsx
function parseShifts(text) {
  const shiftPattern = /(\d{2}\/\d{2})（.）(\d{2}:\d{2}) - (\d{2}:\d{2})\n- (.+)/g;
  const events = [];
  const currentYear = '2025';
  let match;
  while ((match = shiftPattern.exec(text)) !== null) {
      const [, date, startTime, endTime, calendarTitle] = match;
      const [month, day] = date.split('/');
      events.push({
        title: calendarTitle,
        start: { dateTime: `${currentYear}-${month}-${day}T${startTime}:00+09:00` },
        end: { dateTime: `${currentYear}-${month}-${day}T${endTime}:00+09:00` },
      });
  }
  return events;
}
```
これはGoogleカレンダーに登録する際、Googleカレンダーに合った形式へと変換するメソッドです（形式は[公式ドキュメント](https://support.google.com/calendar/answer/37118?hl=ja&co=GENIE.Platform=Desktop&visit_id=638765265644035476-1095389607&rd=1)参照）。日付の判定基準は`shiftPattern`の正規表現で表しています。前述したように、シフトボードのテキスト形式を`shiftPattern`から抽出し、`events`に辞書形式で格納していきます。


### Googleカレンダーに追加
```jsx
function processFile(file, calendarId) {
  // ファイル内の情報を取得
  const text = file.getBlob().getDataAsString();
  // Googleカレンダー形式に変換
  const events = parseShifts(text);
  const calendar = CalendarApp.getCalendarById(calendarId);
  events.forEach(event => calendar.createEvent(event.title, new Date(event.start.dateTime), new Date(event.end.dateTime)));
  // 処理済みにセット
  file.setDescription('processed');
}
```
これは受け取ったファイルをテキスト形式に変換、さらにGoogleカレンダーに登録できる形へと変換するメソッドです。`CalendarApp`でスクリプトがユーザのGoogleカレンダーを読み取り、更新できるようにします（[公式ドキュメント](https://developers.google.com/apps-script/reference/calendar/calendar-app?hl=ja)）。
`crateEvent`で予定の「タイトル」、「開始日（時間）」、「終了日（時間）」をGoogleカレンダーに登録します。その際、ファイルに`processed`というDescriptionを追加することで、再度同じファイルが読み込まれないようにしています。

### エントリーポイント
```jsx
function syncShiftsToCalendar() {
  const folderId = getEnv('FOLDER_ID');
  const calendarId = getEnv('CALENDAR_ID');
  const folder = DriveApp.getFolderById(folderId);
  const files = folder.getFilesByType(MimeType.PLAIN_TEXT);

  while (files.hasNext()) {
    const file = files.next();
    if (!file.getDescription()?.includes('processed')) { // 処理済みチェック
      processFile(file, calendarId);
    }
  }
}
```
こちらがエントリーポイントとなります。まず、**スクリプト プロパティ**を読み込み、`DriveApp`でGoogleドライブの指定したフォルダにアクセスします（[公式ドキュメント](https://developers.google.com/apps-script/reference/drive/drive-app?hl=ja)）。`processed`状態を判定し、ファイルがすでに実行されているかを判定します。次に、`getFilesByType`により、フォルダー内にある指定したMIMEタイプのファイルを取得します。
（ここで、`processed`状態を判定し、Googleカレンダーに登録するかを判断しています）

# 注意点
### 1. Googleドライブの権限周り
実行の際、GoogleDriveに関してアクセス権限が制御されることがあります。その際は[こちらの記事](https://note.com/light_mimosa502/n/na397f2eee97b)を参考に許可を行ってください。

### 2. トリガー設定
定期的にメソッドを実行させるためにトリガー設定を忘れないようにしてください。トリガー設定の詳細は[こちらの記事](https://qiita.com/STSHISHO/items/9efe8e7308ba49ca3e74)を参照しました。

# 改善点
- 終了時刻（日付更新）
    - バイト等の上がり時間が24時を超える場合は、シフトボードは24時を超える表記になるため、それを考慮する必要がある
- `processed`ではなく削除でもよいのでは？
    - 登録完了の際、ファイルを削除しても問題ない
- シフトの重複チェック

# まとめ
このプロジェクトでは、GASを活用してシフトボードのテキストデータをGoogleドライブ経由でGoogleカレンダーに同期する仕組みを構築しました。特別なサーバーやフロントエンド開発が不要で、Googleサービスだけで完結する点が魅力です。トリガー設定により、定期的に自動更新されるため、手動操作を最小限に抑えられます。


# 参考
https://qiita.com/taikis/items/7bef00127300423f0315