見出し画像

Googleカレンダーの予定&タスクを Notion に自動同期する方法

仕事では Googleカレンダーや Googleタスクを使っているけれど、
「本当は Notion にまとめて管理したい…」という方は多いはずです。

ただ、

  • 予定は Googleカレンダー

  • タスクは Googleタスク

  • 管理は Notion

とアプリが分かれていると、確認が面倒になりがち。
さらに「カレンダーに先の予定が大量にあって、移し替えるのが大変…」と感じて、移行をためらう人も少なくありません。

そこで今回は、
Googleカレンダーの予定も、Googleタスクに入力したタスクも
自動で Notion に取り込む方法

をわかりやすく解説します。

Notionに切り替えたいけれど、移行の手間が気になっていた方にぴったりの内容です。


この仕組みでできること

  • Googleカレンダーに登録した予定 → Notionのデータベースに自動反映

  • Google Tasks のタスク → 同じく Notion に自動追加

  • 取り込み対象は 今日〜3ヶ月先まで

  • Notion側には

    • タイトル

    • 日時(時間付き)

    • 種別(予定 / タスク)

    • メモ

    • EventID(自動付与)
      が登録される

  • 同じ予定を何度実行しても、重複登録されない

「予定」「タスク」を Notion 一箇所で見れるようになるので、
日程管理が一気にラクになります。


 全体の流れ

やることは大きく分けて「5ステップ」だけです。

  1. Notion側にデータベースを作る

  2. Notion Integration(APIキー)を取得する

  3. GASの準備(プロパティ設定とTasks API 有効化)

  4. Google Apps Script(GAS)を貼り付ける

  5. 自動実行(トリガー設定)で運用開始

一つずつ丁寧に説明していきます。


1. Notion側の準備:データベースを作成

まず、GoogleカレンダーとGoogleタスクから取得した内容を保存する
Notionのデータベースを作成します。

■ 手順

  1. Notionで新規ページを作成

  2. 「データベース(表)」を選択

  3. 以下のプロパティ(列)を追加してください

項目名:名前、日時、種別、メモ、EventID
種類:タイトル、日付、選択、テキスト、テキスト
※  日時は時間付きにしてください。
※「種別」の選択肢に 予定・タスク を必ず入れておいてください。

データベース名はお好みで問題ありません

2. Notion インテグレーションの準備

Notion へ外部から書き込むために「インテグレーション」を作ります。

  1. Notion → メニューバーの左下「設定」をクリック

  2. コネクト」 → 「インテグレーションを作成または管理する」 → 「新しいインテグレーション」をクリック

  3. インテグレーション名は自由。関連ワークスペースは今回の使用するワークスペースを選択 → 保存

  4. 作成すると「インテグレーションが作成されました」が表示される

  5. インテグレーション設定をクリック

  6. 内部インテグレーションシークレットが表示される
    → これは後でGASに設定するので大切に保管

  7. Googleカレンダー連携タスクのDBを開いて右上の「」→「接続

  8. 作成したインテグレーションを選んで接続します。

これで GAS から Notion を操作できるようになります。


3. GAS の準備:プロパティ設定 & Tasks API を有効化

次は Google Apps Script(GAS)の準備です。

3-1. 新規作成

Googleドライブ → 「新規」→「その他」→「Apps Script」
プロジェクト名は「Notion連携_カレンダー同期」などでOK。
https://script.google.com/から開いてもOKです

3-2. スクリプトプロパティに値を登録する

GASの左側メニューバーから「プロジェクトの設定」→「スクリプトプロパティ」へ。

以下を登録します:

プロパティ    :値(例)
NOTION_TOKEN  :Notion のシークレットキー
NOTION_DB_ID :タスクDBのID(URLから取得)

3-3. Google Tasks API を有効化

Googleタスクを Notion に反映するために必要です。

  • GASの左側の「サービスの+」をクリックし、Google Tasks API を「追加」する

これでタスクも自動で取り込めるようになります。


4. GASのコードを貼り付け(重複防止付き)

次のコードをコード.gs にそのまま貼り付けてください。

/**
 * Notion設定をスクリプトプロパティから取得
 * 必要なプロパティ:
 *  - NOTION_TOKEN      : Notion Integration のシークレット
 *  - NOTION_DB_ID      : Notion データベースID
 *  - NOTION_VERSION    : (任意) Notion APIバージョン。未設定なら 2022-06-28
 */
function getNotionConfig() {
  const props   = PropertiesService.getScriptProperties();
  const token   = props.getProperty('NOTION_TOKEN');
  const dbId    = props.getProperty('NOTION_DB_ID');
  const version = props.getProperty('NOTION_VERSION') || '2022-06-28';

  if (!token || !dbId) {
    throw new Error('NOTION_TOKEN または NOTION_DB_ID がスクリプトプロパティに設定されていません。');
  }

  return { token, dbId, version };
}

/**
 * EventID で Notion データベースを検索し、
 * 既に存在する場合はそのページIDを返す。
 * 存在しなければ null を返す。
 *
 * ※ Notion側の「EventID」プロパティは Rich text 型想定。
 */
function findNotionPageIdByEventId(eventKey, notion) {
  const url = 'https://api.notion.com/v1/databases/' + notion.dbId + '/query';

  const payload = {
    filter: {
      property: 'EventID',
      rich_text: {
        equals: eventKey
      }
    },
    page_size: 1
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'Authorization': 'Bearer ' + notion.token,
      'Notion-Version': notion.version
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const res = UrlFetchApp.fetch(url, options);
  const code = res.getResponseCode();
  if (code !== 200) {
    Logger.log('Notion query error: ' + code);
    Logger.log(res.getContentText());
    return null;
  }

  const data = JSON.parse(res.getContentText());
  if (data.results && data.results.length > 0) {
    return data.results[0].id; // 既存ページID
  }

  return null;
}

/**
 * メイン関数
 * Googleカレンダーの予定 & Googleタスク を Notion に同期
 * トリガーを設定する場合は、この関数 syncAllToNotion を指定するのがおすすめ
 */
function syncAllToNotion() {
  syncCalendarEventsToNotion(); // カレンダー予定の同期
  syncGoogleTasksToNotion();    // Googleタスクの同期
}

/**
 * Googleカレンダーの予定(イベント)を Notion へ同期
 * 対象期間: 本日 ~ 3か月後
 * 種別: 「予定」
 * EventID で重複チェックして、既存ならスキップ
 */
function syncCalendarEventsToNotion() {
  const notion = getNotionConfig();

  const cal = CalendarApp.getDefaultCalendar(); // デフォルトカレンダー
  const now = new Date();

  // 本日から3か月後まで
  const threeMonthsLater = new Date(now);
  threeMonthsLater.setMonth(threeMonthsLater.getMonth() + 3);

  const events = cal.getEvents(now, threeMonthsLater);

  events.forEach(event => {
    const title       = event.getTitle();        // タイトル
    const start       = event.getStartTime();    // 開始日時(Date)
    const description = event.getDescription();  // 説明(メモ)
    const eventId     = event.getId();           // イベントID

    const typeName = '予定';                     // 種別は「予定」で固定
    const startIso = start.toISOString();        // Notion用のISO形式

    // ▼ Notion側の EventID用キー(event- でプレフィックス)
    const eventKey = 'event-' + eventId;

    // ▼ 既に同じ EventID のページがあるかチェック
    const existingPageId = findNotionPageIdByEventId(eventKey, notion);
    if (existingPageId) {
      Logger.log('[Event] skip duplicate: ' + title + ' (' + eventKey + ')');
      return; // この予定はスキップ
    }

    // Notionへ送るペイロード(新規作成)
    const payload = {
      parent: { database_id: notion.dbId },
      properties: {
        // タイトル → Notion「名前」(Title型)
        '名前': {
          title: [
            { text: { content: title || '(無題の予定)' } }
          ]
        },

        // 日時 → Notion「日時」(Date型)
        '日時': {
          date: {
            start: startIso
          }
        },

        // 種別 → Notion「種別」(Select型)
        '種別': {
          select: {
            name: typeName
          }
        },

        // EventID → Notion「EventID」(Text/Rich text型)
        'EventID': {
          rich_text: [
            { text: { content: eventKey } }
          ]
        },

        // メモ → Notion「メモ」(Rich text型)
        'メモ': {
          rich_text: description
            ? [{ text: { content: description } }]
            : []
        }
      }
    };

    const options = {
      method: 'post',
      contentType: 'application/json',
      headers: {
        'Authorization': 'Bearer ' + notion.token,
        'Notion-Version': notion.version
      },
      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    };

    const res = UrlFetchApp.fetch('https://api.notion.com/v1/pages', options);
    Logger.log('[Event CREATED] ' + title + ' : ' + res.getResponseCode());
    Logger.log(res.getContentText());
  });
}

/**
 * Googleタスク を Notion へ同期
 * 対象: メインタスクリスト(@default)
 * 対象期間: 本日 ~ 3か月後 の due を持つタスク
 * 種別: 「タスク」
 * EventID で重複チェックして、既存ならスキップ
 *
 * ※事前に「高度なGoogleサービス」で Tasks API を ON にし、
 *   GCPコンソールでも Google Tasks API を有効化しておくこと。
 */
function syncGoogleTasksToNotion() {
  const notion = getNotionConfig();

  const now = new Date();
  const threeMonthsLater = new Date(now);
  threeMonthsLater.setMonth(threeMonthsLater.getMonth() + 3);

  const dueMin = now.toISOString();
  const dueMax = threeMonthsLater.toISOString();

  const tasklistId = '@default'; // メインのタスクリスト

  let pageToken;
  do {
    const res = Tasks.Tasks.list(tasklistId, {
      showCompleted: false, // 完了済みタスクを除外(必要なら true に変更)
      dueMin: dueMin,
      dueMax: dueMax,
      pageToken: pageToken
    });

    const items = res.items || [];
    items.forEach(task => {
      const title  = task.title || '(無題のタスク)';
      const due    = task.due || null;   // 期限(無い場合もある)
      const notes  = task.notes || '';   // メモ
      const taskId = task.id;            // タスクID

      const typeName = 'タスク';        // 種別は「タスク」で固定

      // ▼ Notion側の EventID用キー(task- でプレフィックス)
      const eventKey = 'task-' + taskId;

      // ▼ 既存チェック
      const existingPageId = findNotionPageIdByEventId(eventKey, notion);
      if (existingPageId) {
        Logger.log('[Task] skip duplicate: ' + title + ' (' + eventKey + ')');
        return; // このタスクはスキップ
      }

      // Notionへ送るプロパティを定義
      const properties = {
        '名前': {
          title: [{ text: { content: title } }]
        },
        '種別': {
          select: { name: typeName }
        },
        'EventID': {
          rich_text: [
            { text: { content: eventKey } }
          ]
        },
        'メモ': {
          rich_text: notes
            ? [{ text: { content: notes } }]
            : []
        }
      };

      // due がある場合だけ「日時」プロパティを追加
      if (due) {
        properties['日時'] = {
          date: {
            start: due // RFC3339形式なのでそのまま渡してOK
          }
        };
      }

      const payload = {
        parent: { database_id: notion.dbId },
        properties: properties
      };

      const options = {
        method: 'post',
        contentType: 'application/json',
        headers: {
          'Authorization': 'Bearer ' + notion.token,
          'Notion-Version': notion.version
        },
        payload: JSON.stringify(payload),
        muteHttpExceptions: true
      };

      const notionRes = UrlFetchApp.fetch('https://api.notion.com/v1/pages', options);
      Logger.log('[Task CREATED] ' + title + ' : ' + notionRes.getResponseCode());
      Logger.log(notionRes.getContentText());
    });

    pageToken = res.nextPageToken;
  } while (pageToken);
}

5. 動作確認 → 自動化

5-1. まず手動で1回だけ実行します

  1. GAS 上部の関数選択で → syncAllToNotion を選択

  2. ▶ 実行

  3. 初回は権限承認が必要なので、許可を進める

  4. Notion側にデータが入っているか確認

5-2. 自動実行の設定(トリガー)

  1. GAS左側メニューの「トリガー」をクリック

  2. 画面右下の「+トリガーを追加」をクリック

  3. 以下のように設定

  • 実行する関数:syncAllToNotion

  • イベントのソースを選択:時間主導型

時間ベースのトリガーや時間の間隔はお好みで設定してください。
(例:時間ベースのタイマーで、1時間おきなど)

これで、GoogleカレンダーとGoogleタスクの変更が
自動的にNotionへ反映されます。


便利ポイントまとめ

  • Notionに「予定」「タスク」「メモ」が自動で反映される

  • カレンダーを見なくても、Notionを見ればすべて把握できる

  • EventID による重複チェックで、同じ予定が何度も登録されない


さいごに

今回の自動連携は、少し設定するだけで「予定」と「タスク」が自然に Notion に流れてくる、とても便利な仕組みです。

とくに、予定はカレンダー、タスクは別アプリ、管理はNotion…と
アプリが分かれている方や、これからNotionを使ってみようかなと思う方にはぜひ試してみてほしい方法です。

今後は、Notion側の更新をGoogle側にも反映したい、通知機能の自動化など
面白い使い方も紹介していく予定なので、楽しみにしていてください。

最後に、Notionのインテグレーションの準備やGASの準備で分からないところがありましたら、以前投稿した記事を参照していただけると理解が深まるかと思います。(記事はこちら



いいなと思ったら応援しよう!