@dayflow/google-sync

@dayflow/google-sync๋Š” DayFlow๋ฅผ Google Calendar REST API v3์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. @dayflow/caldav์™€๋Š” ๋ณ„๊ฐœ์˜ ํŒจํ‚ค์ง€๋กœ, CalDAV๊ฐ€ ์•„๋‹ˆ๋ผ Google Calendar API์™€ ์ง์ ‘ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค.

์„ค์น˜

npm install @dayflow/google-sync
pnpm add @dayflow/google-sync
yarn add @dayflow/google-sync
bun add @dayflow/google-sync

๋น ๋ฅธ ์‹œ์ž‘

import { useRef, useEffect, useState } from 'react';
import {
  DayFlowCalendar,
  useCalendarApp,
  createMonthView,
} from '@dayflow/react';
import {
  attachGoogleSyncToDayFlow,
  createGoogleSync,
  createGoogleSyncAdapter,
  type GoogleDayFlowController,
  type GoogleSyncStatus,
} from '@dayflow/google-sync';

function MyCalendar() {
  const calendar = useCalendarApp({
    views: [createMonthView()],
    calendars: [],
    events: [],
  });

  const controllerRef = useRef<GoogleDayFlowController | null>(null);
  const [syncStatus, setSyncStatus] = useState<GoogleSyncStatus>({
    state: 'idle',
  });

  useEffect(() => {
    if (controllerRef.current) return;

    const adapter = createGoogleSyncAdapter({
      baseUrl: '/api/google-calendar',
    });

    const sync = createGoogleSync(adapter);
    const controller = attachGoogleSyncToDayFlow(calendar.app, sync, {
      writable: true,
      onStatusChange: setSyncStatus,
      onWriteError: (error, ctx) =>
        console.error(`[google-sync] ${ctx.action} failed:`, error.message),
      onSyncComplete: delta => {
        console.log(
          `Sync done: +${delta.events.added} ~${delta.events.updated} -${delta.events.deleted}`
        );
      },
    });

    controllerRef.current = controller;
    controller.start();

    return () => {
      controller.stop();
      controllerRef.current = null;
    };
  }, [calendar.app]);

  return <DayFlowCalendar calendar={calendar} />;
}
<template>
  <DayFlowCalendar :calendar="calendar" />
</template>

<script setup>
  import { ref, onMounted, onBeforeUnmount } from 'vue';
  import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';
  import { createMonthView } from '@dayflow/core';
  import {
    attachGoogleSyncToDayFlow,
    createGoogleSync,
    createGoogleSyncAdapter,
  } from '@dayflow/google-sync';

  const calendar = useCalendarApp({
    views: [createMonthView()],
    calendars: [],
    events: [],
  });

  const syncStatus = ref({ state: 'idle' });
  let controller;

  onMounted(() => {
    const adapter = createGoogleSyncAdapter({
      baseUrl: '/api/google-calendar',
    });

    const sync = createGoogleSync(adapter);
    controller = attachGoogleSyncToDayFlow(calendar.app, sync, {
      writable: true,
      onStatusChange: status => {
        syncStatus.value = status;
      },
      onWriteError: (error, ctx) =>
        console.error(`[google-sync] ${ctx.action} failed:`, error.message),
      onSyncComplete: delta => {
        console.log(
          `Sync done: +${delta.events.added} ~${delta.events.updated} -${delta.events.deleted}`
        );
      },
    });

    controller.start();
  });

  onBeforeUnmount(() => {
    controller?.stop();
  });
</script>
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CalendarApp, createMonthView } from '@dayflow/core';
import { DayFlowCalendarModule } from '@dayflow/angular';
import {
  attachGoogleSyncToDayFlow,
  createGoogleSync,
  createGoogleSyncAdapter,
  type GoogleDayFlowController,
  type GoogleSyncStatus,
} from '@dayflow/google-sync';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [DayFlowCalendarModule],
  template: `<dayflow-calendar [calendar]="calendar"></dayflow-calendar>`,
})
export class AppComponent implements OnInit, OnDestroy {
  calendar = new CalendarApp({
    views: [createMonthView()],
    calendars: [],
    events: [],
  });

  syncStatus: GoogleSyncStatus = { state: 'idle' };
  private controller?: GoogleDayFlowController;

  ngOnInit() {
    const adapter = createGoogleSyncAdapter({
      baseUrl: '/api/google-calendar',
    });

    const sync = createGoogleSync(adapter);
    this.controller = attachGoogleSyncToDayFlow(this.calendar, sync, {
      writable: true,
      onStatusChange: status => {
        this.syncStatus = status;
      },
      onWriteError: (error, ctx) =>
        console.error(`[google-sync] ${ctx.action} failed:`, error.message),
      onSyncComplete: delta => {
        console.log(
          `Sync done: +${delta.events.added} ~${delta.events.updated} -${delta.events.deleted}`
        );
      },
    });

    this.controller.start();
  }

  ngOnDestroy() {
    this.controller?.stop();
  }
}
<script>
  import { onMount, onDestroy } from 'svelte';
  import { DayFlowCalendar, useCalendarApp } from '@dayflow/svelte';
  import { createMonthView } from '@dayflow/core';
  import {
    attachGoogleSyncToDayFlow,
    createGoogleSync,
    createGoogleSyncAdapter,
  } from '@dayflow/google-sync';

  const calendar = useCalendarApp({
    views: [createMonthView()],
    calendars: [],
    events: [],
  });

  let syncStatus = { state: 'idle' };
  let controller;

  onMount(() => {
    const adapter = createGoogleSyncAdapter({
      baseUrl: '/api/google-calendar',
    });

    const sync = createGoogleSync(adapter);
    controller = attachGoogleSyncToDayFlow(calendar.app, sync, {
      writable: true,
      onStatusChange: status => {
        syncStatus = status;
      },
      onWriteError: (error, ctx) =>
        console.error(`[google-sync] ${ctx.action} failed:`, error.message),
      onSyncComplete: delta => {
        console.log(
          `Sync done: +${delta.events.added} ~${delta.events.updated} -${delta.events.deleted}`
        );
      },
    });

    controller.start();
  });

  onDestroy(() => {
    controller?.stop();
  });
</script>

<DayFlowCalendar {calendar} />

ํ† ํฐ ์ฃผ์ž…

getToken ์‚ฌ์šฉํ•˜๊ธฐ (ํด๋ผ์ด์–ธํŠธ ํ† ํฐ์— ๊ถŒ์žฅ)

Supabase ์„ธ์…˜์ด๋‚˜ ์ธ์ฆ ์ œ๊ณต์ž์—์„œ์ฒ˜๋Ÿผ ๋Ÿฐํƒ€์ž„์— ํ† ํฐ์„ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค๋ฉด getToken์„ ์‚ฌ์šฉํ•˜์„ธ์š”. ๋ชจ๋“  ์š”์ฒญ ์ „์— ํ˜ธ์ถœ๋˜๋ฏ€๋กœ ํ† ํฐ ๊ฐฑ์‹ ์ด ํˆฌ๋ช…ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋˜๊ณ , ํ† ํฐ์ด ๋ฐ”๋€Œ์–ด๋„ ์–ด๋Œ‘ํ„ฐ๋ฅผ ๋‹ค์‹œ ๋งŒ๋“ค ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

const adapter = createGoogleSyncAdapter({
  getToken: async () => {
    const { data } = await supabase.auth.getSession();
    return data.session?.provider_token ?? '';
  },
});

๋ฐฑ์—”๋“œ ํ”„๋ก์‹œ ์‚ฌ์šฉํ•˜๊ธฐ (์šด์˜ ํ™˜๊ฒฝ์— ๊ถŒ์žฅ)

Google Calendar API๋Š” CORS๋ฅผ ์ง€์›ํ•˜์ง€๋งŒ, ๋ธŒ๋ผ์šฐ์ €์—์„œ OAuth ํ† ํฐ์„ ๋ณด๋‚ด๋ฉด ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๋“ค์—ฌ๋‹ค๋ณผ ์ˆ˜ ์žˆ๋Š” ๋ˆ„๊ตฌ์—๊ฒŒ๋‚˜ ํ† ํฐ์ด ๋…ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ํ† ํฐ์„ ์„œ๋ฒ„์— ๋‘์„ธ์š”:

const adapter = createGoogleSyncAdapter({
  baseUrl: '/api/google-calendar',
  // No getToken needed โ€” your proxy injects Authorization
});
// proxy.mjs (Node.js example)
import { createServer } from 'node:http';

const {
  GOOGLE_ACCESS_TOKEN,
  GOOGLE_CLIENT_ID,
  GOOGLE_CLIENT_SECRET,
  GOOGLE_REFRESH_TOKEN,
  PORT = '3002',
} = process.env;

const GOOGLE_API_BASE = 'https://www.googleapis.com/calendar/v3';
const ALLOWED_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);

let cachedToken = GOOGLE_ACCESS_TOKEN
  ? { token: GOOGLE_ACCESS_TOKEN, expiresAt: Infinity }
  : null;

async function getAccessToken() {
  if (cachedToken && cachedToken.expiresAt > Date.now() + 30_000) {
    return cachedToken.token;
  }
  const res = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: GOOGLE_CLIENT_ID,
      client_secret: GOOGLE_CLIENT_SECRET,
      refresh_token: GOOGLE_REFRESH_TOKEN,
      grant_type: 'refresh_token',
    }),
  });
  const data = await res.json();
  cachedToken = {
    token: data.access_token,
    expiresAt: Date.now() + data.expires_in * 1000,
  };
  return cachedToken.token;
}

createServer(async (req, res) => {
  const upstreamPath = req.url.replace(/^\/api\/google-calendar/, '');
  const upstreamUrl = `${GOOGLE_API_BASE}${upstreamPath}`;

  if (!ALLOWED_METHODS.has(req.method)) {
    res.writeHead(405);
    res.end();
    return;
  }

  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const body =
    req.method === 'GET' || req.method === 'DELETE'
      ? undefined
      : Buffer.concat(chunks).toString();

  const token = await getAccessToken();
  const upstream = await fetch(upstreamUrl, {
    method: req.method,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
      ...(req.headers['if-match']
        ? { 'If-Match': req.headers['if-match'] }
        : {}),
    },
    body,
  });

  const responseBody = upstream.status === 204 ? '' : await upstream.text();
  res.writeHead(upstream.status, { 'Content-Type': 'application/json' });
  res.end(responseBody);
}).listen(Number(PORT));

ํ”„๋ก์‹œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค:

# Quick test with an access token
GOOGLE_ACCESS_TOKEN=ya29.xxx node proxy.mjs

# Long-lived with refresh token
GOOGLE_CLIENT_ID=xxx \
GOOGLE_CLIENT_SECRET=xxx \
GOOGLE_REFRESH_TOKEN=xxx \
node proxy.mjs

ํ…Œ์ŠคํŠธ์šฉ ์•ก์„ธ์Šค ํ† ํฐ์€ Google OAuth 2.0 Playground์—์„œ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. https://www.googleapis.com/auth/calendar ์Šค์ฝ”ํ”„๋ฅผ ์„ ํƒํ•˜์„ธ์š”.

๋™๊ธฐํ™” ํ† ํฐ ์˜์†ํ™”

๊ธฐ๋ณธ์ ์œผ๋กœ Google Calendar ๋™๊ธฐํ™” ํ† ํฐ์€ ๋ฉ”๋ชจ๋ฆฌ์—๋งŒ ์ €์žฅ๋˜์–ด ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ ๊ณ ์น˜๋ฉด ์‚ฌ๋ผ์ง€๊ณ , ๋‹ค์Œ ์„ธ์…˜์—์„œ ์ „์ฒด ๋™๊ธฐํ™”๋ฅผ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. GoogleSyncStorage ๊ตฌํ˜„์„ ์ œ๊ณตํ•ด ํ† ํฐ์„ ๋ณด์กดํ•˜์„ธ์š”:

import { createGoogleSync, type GoogleSyncStorage } from '@dayflow/google-sync';

const storage: GoogleSyncStorage = {
  getSyncToken: async calendarId =>
    localStorage.getItem(`google-token:${calendarId}`),
  setSyncToken: async (calendarId, token) =>
    token
      ? localStorage.setItem(`google-token:${calendarId}`, token)
      : localStorage.removeItem(`google-token:${calendarId}`),
};

const sync = createGoogleSync(adapter, { storage });

storage๋ฅผ ์—ฐ๊ฒฐํ•˜๋ฉด ๊ฐ ์„ธ์…˜์ด ์ „์ฒด ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ฐ€์ ธ์˜ค๋Š” ๋Œ€์‹  ์ฆ๋ถ„ ๋™๊ธฐํ™”๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

๋กœ์ปฌ ์บ์‹œ๋กœ ์ดˆ๊ธฐํ™”ํ•˜๊ธฐ

๋™๊ธฐํ™”๋œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‚˜ ๋กœ์ปฌ ์ €์žฅ์†Œ์— ๋ณด๊ด€ํ•œ๋‹ค๋ฉด, ์ฒซ ์›๊ฒฉ ๋™๊ธฐํ™” ์ „์— DayFlow๋ฅผ ๋ฏธ๋ฆฌ ์ฑ„์›Œ ์บ˜๋ฆฐ๋”๋ฅผ ์ฆ‰์‹œ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

const controller = attachGoogleSyncToDayFlow(calendar.app, sync, {
  getInitialSnapshot: async () => {
    const { calendars, events } = await loadFromLocalDB();
    return { calendars, events };
  },
  onSyncComplete: delta => {
    // Save what changed to your local store
    saveChanges(delta);
  },
  onWriteComplete: (operation, event) => {
    // Update local store after a successful write-back
    persistEvent(operation, event);
  },
});

getInitialSnapshot์€ Google Calendar API ์š”์ฒญ์ด ์ผ์–ด๋‚˜๊ธฐ ์ „, start() ๋„์ค‘ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋™๊ธฐํ™”๊ฐ€ ๋„๋Š” ๋™์•ˆ DayFlow๋Š” ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋กœ ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค.

์›๊ฒฉ ์Šค๋ƒ…์ˆ ์ง์ ‘ ์ ์šฉํ•˜๊ธฐ

๋™๊ธฐํ™” ํ๋ฆ„์„ ์ง์ ‘ ๊ด€๋ฆฌํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด๋ผ๋ฉด, applyRemoteSnapshot์œผ๋กœ ์›๊ฒฉ ์ด๋ฒคํŠธ ๋ฌถ์Œ์„ ์ œ๊ณต์ž์—๊ฒŒ ๋‹ค์‹œ ์ „์†กํ•˜์ง€ ์•Š๊ณ  DayFlow์— ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

import { applyRemoteSnapshot, getGoogleMeta } from '@dayflow/google-sync';

const delta = await applyRemoteSnapshot(
  calendar.app,
  { calendars, events },
  {
    isOwnedEvent: event => Boolean(getGoogleMeta(event)),
    isOwnedCalendar: calendar => calendar.source === 'Google',
    snapshotMode: 'authoritative',

    // Optionally preserve local in-progress edits during optimistic sync
    resolveConflict: (remote, local) =>
      mergeLocalEditsOntoRemote(remote, local),
  }
);

console.log(delta.events.added, delta.events.updated, delta.events.deleted);

applyRemoteSnapshot์€ ๋“ค์–ด์˜จ ์Šค๋ƒ…์ˆ๊ณผ ํ˜„์žฌ ์•ฑ ์ƒํƒœ์˜ ์ฐจ์ด๋ฅผ ๊ณ„์‚ฐํ•œ ๋’ค, ๋™๊ธฐํ™” ๋ฃจํ”„๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด source: 'remote'๋กœ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜ํ™˜๊ฐ’์€ ์ž‘์—…๋ณ„ ๊ฑด์ˆ˜๋ฅผ ๋‹ด์€ RemoteSnapshotDelta์ž…๋‹ˆ๋‹ค. snapshotMode: 'authoritative'๋Š” ์ „์ฒด ์ œ๊ณต์ž ์Šค๋ƒ…์ˆ์—๋งŒ ์‚ฌ์šฉํ•˜๊ณ , ๋ฒ”์œ„๊ฐ€ ์ œํ•œ๋˜์—ˆ๊ฑฐ๋‚˜ ํŽ˜์ด์ง€ ๋‹จ์œ„์ธ ์Šค๋ƒ…์ˆ์€ ๊ธฐ๋ณธ ๋ถ€๋ถ„ ๋ชจ๋“œ๋ฅผ ์œ ์ง€ํ•˜์„ธ์š”.

์˜ต์…˜ ๋ ˆํผ๋Ÿฐ์Šค

attachGoogleSyncToDayFlow ์˜ต์…˜

์˜ต์…˜ํƒ€์ž…๊ธฐ๋ณธ๊ฐ’์„ค๋ช…
writablebooleantrue๋กœ์ปฌ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ Google Calendar์— ๋‹ค์‹œ ์“ฐ๋„๋ก ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
onStatusChange(status: GoogleSyncStatus) => voidโ€”๋™๊ธฐํ™” ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
onWriteError(error: Error, ctx) => voidconsole.error์ƒ์„ฑยท์ˆ˜์ •ยท์‚ญ์ œ๋ฅผ ๋‹ค์‹œ ์“ฐ๋Š” ๋ฐ ์‹คํŒจํ–ˆ์„ ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ctx์—๋Š” action๊ณผ eventId๊ฐ€ ๋“ค์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
getInitialSnapshot() => Promise<{ events, calendars }>โ€”์ฒซ ์›๊ฒฉ ๋™๊ธฐํ™” ์ „์— ๋กœ์ปฌ ์บ์‹œ๋กœ DayFlow๋ฅผ ์ฑ„์›๋‹ˆ๋‹ค.
onSyncComplete(delta: GoogleSyncDelta) => voidโ€”๋™๊ธฐํ™”๊ฐ€ ์„ฑ๊ณตํ•  ๋•Œ๋งˆ๋‹ค ๋ณ€๊ฒฝ ๊ฑด์ˆ˜์™€ ํ•จ๊ป˜ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
onWriteComplete(operation, event) => voidโ€”๋กœ์ปฌ ๋ณ€๊ฒฝ์ด Google Calendar์— ์„ฑ๊ณต์ ์œผ๋กœ ๋ฐ˜์˜๋œ ๋’ค ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

createGoogleSyncAdapter ์˜ต์…˜

์˜ต์…˜ํƒ€์ž…๊ธฐ๋ณธ๊ฐ’์„ค๋ช…
baseUrlstringhttps://www.googleapis.com/calendar/v3๋ฐฑ์—”๋“œ ํ”„๋ก์‹œ๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋„๋ก ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
fetchfunctionglobalThis.fetch์‚ฌ์šฉ์ž ์ •์˜ fetch ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค(์˜ˆ: ํ…Œ์ŠคํŠธ์šฉ).
getToken() => string | Promise<string>โ€”๋ชจ๋“  ์š”์ฒญ ์ „์— ํ˜ธ์ถœ๋˜๋ฉฐ, Authorization: Bearer <token>์œผ๋กœ ์ฃผ์ž…ํ•  ์•ก์„ธ์Šค ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. fetch๋ฅผ ์ง์ ‘ ๊ฐ์‹ธ๋Š” ๋Œ€์‹  ์ด ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์„ธ์š”.

GoogleSyncStatus

type GoogleSyncStatus = {
  state: 'idle' | 'syncing' | 'error';
  lastSyncedAt?: string; // ISO timestamp
  error?: {
    message: string;
    calendarId?: string;
  };
};

GoogleSyncDelta

type GoogleSyncDelta = {
  calendars: { added: number; updated: number; deleted: number };
  events: { added: number; updated: number; deleted: number };
};

์ปจํŠธ๋กค๋Ÿฌ API

// Load calendars, sync initial events, and subscribe to visible-range and event changes
await controller.start();

// Unsubscribe all listeners
controller.stop();

// Re-sync all calendars for the current visible range
await controller.refresh();

// Re-sync a specific calendar
await controller.refresh({ calendarId: 'primary' });

// Re-sync with an explicit range
await controller.refresh({
  calendarId: 'primary',
  range: { start: new Date('2025-01-01'), end: new Date('2025-02-01') },
});

// Current sync state
const status = controller.getStatus();

๋™๊ธฐํ™” ๋™์ž‘ ๋ฐฉ์‹

์บ˜๋ฆฐ๋” ํƒ์ƒ‰

controller.start()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ํŒจํ‚ค์ง€๊ฐ€ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ ์บ˜๋ฆฐ๋” ๋ชฉ๋ก(GET /calendarList)์„ ๊ฐ€์ ธ์™€ ๊ฐ ์บ˜๋ฆฐ๋”๋ฅผ DayFlow์˜ CalendarType์œผ๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. accessRole์ด 'reader'๋‚˜ 'freeBusyReader'์ธ ์บ˜๋ฆฐ๋”๋Š” readOnly: true๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

์ด๋ฒคํŠธ ๋กœ๋”ฉ

์ด๋ฒคํŠธ๋Š” ํ˜„์žฌ ํ‘œ์‹œ ์ค‘์ธ ๋‚ ์งœ ๋ฒ”์œ„์— ๋Œ€ํ•ด ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋™ํ•˜๋ฉด(๋‹ค์Œ ์ฃผ, ์ด์ „ ๋‹ฌ ๋“ฑ) ํ‘œ์‹œ ๋ฒ”์œ„ ๋ณ€๊ฒฝ ๋ฆฌ์Šค๋„ˆ๋ฅผ ํ†ตํ•ด ์ƒˆ ๋ฒ”์œ„์˜ ์ด๋ฒคํŠธ๊ฐ€ ์ž๋™์œผ๋กœ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค.

์ฆ๋ถ„ ๋™๊ธฐํ™”

์ตœ์ดˆ ๋กœ๋“œ ์ดํ›„์—๋Š” Google Calendar์˜ syncToken์„ ์‚ฌ์šฉํ•ด ์ฆ๋ถ„ ๋™๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„ ๋™๊ธฐํ™”์—์„œ๋Š” ๋ณ€๊ฒฝ๋œ ์ด๋ฒคํŠธ๋งŒ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. GoogleSyncStorage๋ฅผ ์ œ๊ณตํ•˜๋ฉด ๋™๊ธฐํ™” ํ† ํฐ์ด ํŽ˜์ด์ง€ ์ƒˆ๋กœ ๊ณ ์นจ ํ›„์—๋„ ์œ ์ง€๋˜์–ด ๋‹ค์Œ ์„ธ์…˜๋„ ์ฆ๋ถ„์œผ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

์›๊ฒฉ ๋ฐ˜์˜(์“ฐ๊ธฐ)

writable: true์ด๋ฉด ๋กœ์ปฌ ์ด๋ฒคํŠธ ๋ณ€๊ฒฝ(์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ)์ด Google Calendar์— ์ž๋™์œผ๋กœ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค:

  • ์ƒ์„ฑ: POST /calendars/{calendarId}/events
  • ์ˆ˜์ •: PUT /calendars/{calendarId}/events/{eventId} (If-Match: <etag> ํฌํ•จ)
  • ์‚ญ์ œ: DELETE /calendars/{calendarId}/events/{eventId}

์ˆ˜์ • ์š”์ฒญ์ด 412 Precondition Failed(๋‹ค๋ฅธ ๊ธฐ๊ธฐ์—์„œ ์ด๋ฒคํŠธ๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด ๋ฐœ์ƒํ•œ ETag ์ถฉ๋Œ)๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด, ํŒจํ‚ค์ง€๊ฐ€ ์ตœ์‹  ETag๋ฅผ ๋‹ค์‹œ ๊ฐ€์ ธ์™€ ํ•œ ๋ฒˆ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋ณต ์ด๋ฒคํŠธ๋Š” ์ ˆ๋Œ€ ์›๊ฒฉ์— ๋ฐ˜์˜๋˜์ง€ ์•Š์œผ๋ฉฐ ์ฝ๊ธฐ ์ „์šฉ์ž…๋‹ˆ๋‹ค.

Google Calendar API ์Šค์ฝ”ํ”„

OAuth ํ† ํฐ์—๋Š” ๋‹ค์Œ ์Šค์ฝ”ํ”„ ์ค‘ ์ตœ์†Œ ํ•˜๋‚˜๊ฐ€ ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

์Šค์ฝ”ํ”„์ ‘๊ทผ ๊ถŒํ•œ
https://www.googleapis.com/auth/calendar์ฝ๊ธฐยท์“ฐ๊ธฐ ์ „์ฒด ๊ถŒํ•œ
https://www.googleapis.com/auth/calendar.readonly์ฝ๊ธฐ ์ „์šฉ ๊ถŒํ•œ(writable: false์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ)

์ด ํŽ˜์ด์ง€์˜ ๋‚ด์šฉ