캘린더 회의 변경사항 동기화 중

사용자는 Google Calendar 일정을 자유롭게 업데이트하거나 삭제할 수 있습니다. 사용자가 회의를 만든 후 이벤트를 업데이트하는 경우 부가기능에서 회의 데이터를 업데이트하여 변경사항에 응답해야 할 수 있습니다. 서드 파티 회의 시스템이 이벤트 데이터를 추적하는 데 종속되는 경우 이벤트 변경 시 회의를 업데이트하지 않으면 회의를 사용할 수 없게 되어 사용자 환경이 저하될 수 있습니다.

Google Calendar 일정 변경사항에 따라 회의 데이터를 업데이트하는 프로세스를 동기화라고 합니다. 지정된 캘린더에서 일정이 변경될 때마다 실행되는 Apps Script 설치 가능한 트리거를 만들어 일정 변경사항을 동기화할 수 있습니다. 안타깝게도 트리거는 변경된 이벤트를 보고하지 않으며 내가 만든 회의가 있는 이벤트로만 제한할 수 없습니다. 대신 마지막 동기화 이후 캘린더에 적용된 모든 변경사항 목록을 요청하고, 일정 목록을 필터링한 후, 그에 따라 업데이트해야 합니다.

일반적인 동기화 절차는 다음과 같습니다.

  1. 사용자가 회의를 처음 만들면 동기화 프로세스가 초기화됩니다.
  2. 사용자가 캘린더 일정 중 하나를 만들거나 업데이트하거나 삭제할 때마다 트리거가 부가기능 프로젝트에서 트리거 함수를 실행합니다.
  3. 트리거 함수는 마지막 동기화 이후의 이벤트 변경사항 집합을 검사하고 연결된 서드 파티 회의를 업데이트해야 하는지 확인합니다.
  4. 서드 파티 API 요청을 통해 회의에 필요한 업데이트가 이루어집니다.
  5. 다음 트리거 실행 시 달력의 최근 변경사항만 검사하면 되도록 새 동기화 토큰이 저장됩니다.

동기화 초기화

부가기능이 서드 파티 시스템에 회의를 성공적으로 만든 후에는 이 캘린더의 이벤트 변경사항에 응답하는 설치 가능한 트리거를 만들어야 합니다(트리거가 아직 없는 경우).

트리거를 만든 후에는 초기 동기화 토큰을 만들어 초기화를 완료해야 합니다. 트리거 함수를 직접 실행하면 됩니다.

Calendar 트리거 만들기

동기화하려면 부가기능에서 회의가 첨부된 Calendar 일정이 변경될 때 이를 감지해야 합니다. EventUpdated 설치 가능한 트리거를 만들어서 이 작업을 실행할 수 있습니다. 부가기능에는 캘린더마다 트리거가 하나만 필요하며 프로그래매틱 방식으로 만들 수 있습니다.

트리거를 만드는 좋은 시점은 사용자가 첫 번째 회의를 만들 때입니다. 이때 사용자가 부가기능을 사용하기 시작하기 때문입니다. 회의를 만들고 오류가 없는지 확인한 후에는 부가기능에서 이 사용자에 대한 트리거가 있는지 확인하고 없는 경우 트리거를 만들어야 합니다.

동기화 트리거 함수 구현

트리거 함수는 Apps Script에서 트리거가 실행되는 조건을 감지하면 실행됩니다. EventUpdated 캘린더 트리거는 사용자가 지정된 캘린더에서 일정을 생성, 수정 또는 삭제할 때 실행됩니다.

부가기능에서 사용하는 트리거 함수를 구현해야 합니다. 이 트리거 함수는 다음을 실행해야 합니다.

  1. syncToken를 사용하여 Calendar 고급 서비스 Calendar.Events.list()를 호출하여 마지막 동기화 이후 변경된 일정 목록을 가져옵니다. 동기화 토큰을 사용하면 부가기능에서 검사해야 하는 이벤트 수가 줄어듭니다.

    트리거 함수가 유효한 동기화 토큰 없이 실행되면 전체 동기화로 대체됩니다. 전체 동기화는 유효한 새 동기화 토큰을 생성하기 위해 지정된 시간 내에 모든 이벤트를 검색하려고 시도합니다.

  2. 수정된 각 이벤트를 검사하여 연결된 서드 파티 회의가 있는지 확인합니다.
  3. 이벤트에 회의가 있는 경우 회의를 검토하여 변경된 사항을 확인합니다. 변경사항에 따라 연결된 회의를 수정해야 할 수도 있습니다. 예를 들어 일정이 삭제되면 부가기능에서 회의도 삭제해야 합니다.
  4. 회의에 필요한 변경사항은 서드 파티 시스템에 API를 호출하여 적용됩니다.
  5. 필요한 모든 변경사항을 적용한 후 Calendar.Events.list() 메서드에서 반환된 nextSyncToken를 저장합니다. 이 동기화 토큰은 Calendar.Events.list() 호출에서 반환된 결과의 마지막 페이지에 있습니다.

Google Calendar 일정 업데이트

동기화할 때 Google Calendar 일정을 업데이트해야 하는 경우도 있습니다. 이 방법을 선택하는 경우 적절한 Google Calendar 고급 서비스 요청으로 일정을 업데이트합니다. If-Match 헤더와 함께 조건부 업데이트를 사용해야 합니다. 이렇게 하면 변경사항이 다른 클라이언트에서 사용자가 실행한 동시 변경사항을 의도치 않게 덮어쓰지 않습니다.

다음 예는 캘린더 일정 및 관련 회의의 동기화를 설정하는 방법을 보여줍니다.

/**
 *  Initializes syncing of conference data by creating a sync trigger and
 *  sync token if either does not exist yet.
 *
 *  @param {String} calendarId The ID of the Google Calendar.
 */
function initializeSyncing(calendarId) {
  // Create a syncing trigger if it doesn't exist yet.
  createSyncTrigger(calendarId);

  // Perform an event sync to create the initial sync token.
  syncEvents({'calendarId': calendarId});
}

/**
 *  Creates a sync trigger if it does not exist yet.
 *
 *  @param {String} calendarId The ID of the Google Calendar.
 */
function createSyncTrigger(calendarId) {
  // Check to see if the trigger already exists; if does, return.
  var allTriggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < allTriggers.length; i++) {
    var trigger = allTriggers[i];
    if (trigger.getTriggerSourceId() == calendarId) {
      return;
    }
  }

  // Trigger does not exist, so create it. The trigger calls the
  // 'syncEvents()' trigger function when it fires.
  var trigger = ScriptApp.newTrigger('syncEvents')
      .forUserCalendar(calendarId)
      .onEventUpdated()
      .create();
}

/**
 *  Sync events for the given calendar; this is the syncing trigger
 *  function. If a sync token already exists, this retrieves all events
 *  that have been modified since the last sync, then checks each to see
 *  if an associated conference needs to be updated and makes any required
 *  changes. If the sync token does not exist or is invalid, this
 *  retrieves future events modified in the last 24 hours instead. In
 *  either case, a new sync token is created and stored.
 *
 *  @param {Object} e If called by a event updated trigger, this object
 *      contains the Google Calendar ID, authorization mode, and
 *      calling trigger ID. Only the calendar ID is actually used here,
 *      however.
 */
function syncEvents(e) {
  var calendarId = e.calendarId;
  var properties = PropertiesService.getUserProperties();
  var syncToken = properties.getProperty('syncToken');

  var options;
  if (syncToken) {
    // There's an existing sync token, so configure the following event
    // retrieval request to only get events that have been modified
    // since the last sync.
    options = {
      syncToken: syncToken
    };
  } else {
    // No sync token, so configure to do a 'full' sync instead. In this
    // example only recently updated events are retrieved in a full sync.
    // A larger time window can be examined during a full sync, but this
    // slows down the script execution. Consider the trade-offs while
    // designing your add-on.
    var now = new Date();
    var yesterday = new Date();
    yesterday.setDate(now.getDate() - 1);
    options = {
      timeMin: now.toISOString(),          // Events that start after now...
      updatedMin: yesterday.toISOString(), // ...and were modified recently
      maxResults: 50,   // Max. number of results per page of responses
      orderBy: 'updated'
    }
  }

  // Examine the list of updated events since last sync (or all events
  // modified after yesterday if the sync token is missing or invalid), and
  // update any associated conferences as required.
  var events;
  var pageToken;
  do {
    try {
      options.pageToken = pageToken;
      events = Calendar.Events.list(calendarId, options);
    } catch (err) {
      // Check to see if the sync token was invalidated by the server;
      // if so, perform a full sync instead.
      if (err.message ===
            "Sync token is no longer valid, a full sync is required.") {
        properties.deleteProperty('syncToken');
        syncEvents(e);
        return;
      } else {
        throw new Error(err.message);
      }
    }

    // Read through the list of returned events looking for conferences
    // to update.
    if (events.items && events.items.length > 0) {
      for (var i = 0; i < events.items.length; i++) {
         var calEvent = events.items[i];
         // Check to see if there is a record of this event has a
         // conference that needs updating.
         if (eventHasConference(calEvent)) {
           updateConference(calEvent, calEvent.conferenceData.conferenceId);
         }
      }
    }

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

  // Record the new sync token.
  if (events.nextSyncToken) {
    properties.setProperty('syncToken', events.nextSyncToken);
  }
}

/**
 *  Returns true if the specified event has an associated conference
 *  of the type managed by this add-on; retuns false otherwise.
 *
 *  @param {Object} calEvent The Google Calendar event object, as defined by
 *      the Calendar API.
 *  @return {boolean}
 */
function eventHasConference(calEvent) {
  var name = calEvent.conferenceData.conferenceSolution.name || null;

  // This version checks if the conference data solution name matches the
  // one of the solution names used by the add-on. Alternatively you could
  // check the solution's entry point URIs or other solution-specific
  // information.
  if (name) {
    if (name === "My Web Conference" ||
        name === "My Recorded Web Conference") {
      return true;
    }
  }
  return false;
}

/**
 *  Update a conference based on new Google Calendar event information.
 *  The exact implementation of this function is highly dependant on the
 *  details of the third-party conferencing system, so only a rough outline
 *  is shown here.
 *
 *  @param {Object} calEvent The Google Calendar event object, as defined by
 *      the Calendar API.
 *  @param {String} conferenceId The ID used to identify the conference on
 *      the third-party conferencing system.
 */
function updateConference(calEvent, conferenceId) {
  // Check edge case: the event was cancelled
  if (calEvent.status === 'cancelled' || eventHasConference(calEvent)) {
    // Use the third-party API to delete the conference too.


  } else {
    // Extract any necessary information from the event object, then
    // make the appropriate third-party API requests to update the
    // conference with that information.

  }
}