予定共有！Googleカレンダー × Apps Scriptで“見せすぎない”スケジュール連携

# はじめに
※2025/4/6追記 エラーがあったのでコード修正しました

こんにちは。Qiitaに最後書いたのが前職時代だったので、知ってくださっている方はお久しぶりです。
現在はいわゆる複業をしていろいろなところに所属させてもらっていますが、こういろんなところに所属してると

**スケジュール管理、悩みませんか?**

最初のうちはなんとかコピペを駆使してやってみたものの、数が多くなるとそもそもきつい。
なんか純正機能のみでうまいことできないかなあとチャレンジしたものの、これといった解決策もなくモヤモヤとし続けていましたが、今回全てのカレンダーがGWSに切り替わったことで重い腰を上げてやってみました。そう、僕のバディ、ChatGPTさんと一緒に。

# 前提と課題
## 所属状況
A社：ごく少人数
B社：社員数それなり。スケジュール共有の文化が強い

## 使っているツール
両社ともGWS

## 課題
両方のカレンダーを使わざるを得ないけど、カレンダーのメインは人が多いB社側。なので、A社の予定をB社のカレンダーに載せないと、ダブルブッキングの可能性があるただし、A社の予定のすべてをB社に見せるのはNGも多い。

ま、ここでいうA社とは僕自身の会社で、少なくともやっていることはみんなに共有しているので、別に見せちゃっても構わないんですけどねw まれにあっ!というものもあるのでとりあえず。

# やりたかったこと
A社に予定を入れたら自動的にB社のカレンダーに反映したい。でも、タイトルなどは隠したい。

# やったこと（概要）
- A社でプライマリカレンダー(APカレンダー)の他にセカンダリカレンダー(ASカレンダー)を作成
- APカレンダーに予定が入ると、ASカレンダーにコピー
    - コピーの際にタイトルを「🔒他社予定」に変更、ゲスト・場所・説明などの詳細も削除
- ASカレンダーにコピーされた予定にB社のカレンダー(Bカレンダー)をゲスト招待
- Bカレンダーに予定の招待がきたら自動的に「出席」にする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104684/01632f40-1c6d-4c13-af21-69c612bdbe88.png)

こんな感じ。

# 実装
## その1
まず、A社側でセカンダリカレンダーを作成してカレンダーIDを取得してください
![名称未設定 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104684/4b7b8eee-8648-49c9-93bc-f0737c91d8d8.png)

## その2
次にA社のApps Scriptに以下のコードをコピペ
```js:code_a.gs
const PRIMARY_ID = CalendarApp.getDefaultCalendar().getId(); // プライマリカレンダーのID
const SECONDARY_ID = "ここにセカンダリID@group.calendar.google.com"; // セカンダリのカレンダーID
const EMAIL = "a@a.com"; // 特定のゲストアドレス

function syncCalendar() {
  const now = new Date();
  const to = new Date();
  to.setMonth(to.getMonth() + 12); // 今から12ヶ月後まで対象
  const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);

  Logger.log(`プライマリカレンダーID: ${PRIMARY_ID}`);
  Logger.log(`セカンダリカレンダーID: ${SECONDARY_ID}`);

  const primaryCal = CalendarApp.getCalendarById(PRIMARY_ID);
  const secondaryCal = CalendarApp.getCalendarById(SECONDARY_ID);

  const primaryEvents = primaryCal.getEvents(yesterday, to);
  const secondaryEvents = secondaryCal.getEvents(yesterday, to);

  Logger.log(`プライマリから取得したイベント数: ${primaryEvents.length}`);
  primaryEvents.forEach((event, index) => {
    Logger.log(`[${index + 1}] タイトル: '${event.getTitle()}', 開始: ${event.getStartTime()}, ID: ${event.getId()}`);
  });

  primaryEvents.forEach(event => {
    const myEmail = Session.getActiveUser().getEmail();

    //「主催者または参加者（YES）」でないイベントはスキップ
    const isOrganizer = event.getCreators().includes(myEmail);
    const guestList = event.getGuestList();
    const myGuestInfo = guestList.find(g => g.getEmail() === myEmail);
    const isAttendee = myGuestInfo !== undefined;
    const isAccepted = myGuestInfo ? myGuestInfo.getGuestStatus() === CalendarApp.GuestStatus.YES : false;

    if (!isOrganizer && (!isAttendee || !isAccepted)) {
      Logger.log(`[SKIP] '${event.getTitle()}' は主催でも参加確定でもないためスキップ`);
      return;
    }

    const matching = secondaryEvents.find(se => se.getTag('linkedId') === event.getId());

    if (matching) {
      //変更があれば更新
      const isUpdated =
        matching.getStartTime().getTime() !== event.getStartTime().getTime() ||
        matching.getEndTime().getTime() !== event.getEndTime().getTime();

      if (isUpdated) {
        matching.setTime(event.getStartTime(), event.getEndTime());
        matching.setTitle("🔒他社予定");
        Logger.log(`[UPDATE] '${event.getTitle()}' をセカンダリで更新しました。`);
      }
    } else {
      // 📋 新規コピー処理（特定のゲストを追加）
      const newEvent = secondaryCal.createEvent(
        "他社予定",
        event.getStartTime(),
        event.getEndTime(),
        {
          guests: EMAIL // 特定のゲストを追加
        }
      );
      newEvent.setTag('linkedId', event.getId());
      Logger.log(`[COPY] '${event.getTitle()}' をセカンダリにコピーしました。開始: ${event.getStartTime()}`);
    }
  });

  //削除処理
  secondaryEvents.forEach(se => {
    const linkedId = se.getTag('linkedId');
    if (linkedId && !primaryEvents.find(pe => pe.getId() === linkedId)) {
      Logger.log(`[DELETE] '${se.getTitle()}' をセカンダリから削除しました。開始: ${se.getStartTime()}`);
      se.deleteEvent();
    }
  });

  Logger.log(`同期完了: ${now.toLocaleString()}`);
}
```

## その3
A社のトリガーをこんな感じで設定
なお、カレンダーのオーナーのメールアドレスはA社プライマリカレンダーのアドレスになりました。
![名称未設定 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104684/1f716335-a548-427e-a871-9599a9096c56.png)


## その4
次にB社のApps Scriptに以下のコードをコピペ
```js:code_b.gs
function acceptEventsByTitle() {
  const calendar = CalendarApp.getDefaultCalendar();
  const now = new Date();
  const future = new Date();
  future.setMonth(future.getMonth() + 12); // 今から12ヶ月後まで対象
  const keyword = "他社予定"; //タイトルに含まれるキーワード ←ここを変更

  // 指定期間の予定を取得
  const events = calendar.getEvents(now, future);

  for (const event of events) {
    const title = event.getTitle();

    // タイトルにキーワードが含まれているか確認
    if (title.includes(keyword)) {
      const myStatus = event.getMyStatus();
      
      if (myStatus !== CalendarApp.GuestStatus.YES) {
        // 出席に設定
        event.setMyStatus(CalendarApp.GuestStatus.YES);
        Logger.log(`出席に設定：${title}（${event.getStartTime()}）`);
      }
    }
  }
}
```

## その5
B社のトリガーをこんな感じで設定
なお、カレンダーのオーナーのメールアドレスはB社プライマリカレンダーのアドレスになりました。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104684/865df30e-a7ca-4dd7-821b-2b6ee5cd690f.png)

以上。

# 効果とまとめ
- B社にA社の予定を共有でき、かつ自動処理されるので、ダブルブッキング防止。余計な作業なし。
- 内容は秘匿できるので、情報漏えいの心配もなし


複業の働き方が広がる中で、スケジュール連携の悩みは意外と多いと思います。
GoogleカレンダーとApps Scriptを使えば、意外と手軽に“ちょうどいい見せ方”ができるので、同じような悩みを抱える方の参考になれば幸いです！