@dayflow/outlook-sync

@dayflow/outlook-sync๋Š” DayFlow๋ฅผ Microsoft Graph ์บ˜๋ฆฐ๋” API์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. @dayflow/caldav์™€๋Š” ๋ณ„๊ฐœ์˜ ํŒจํ‚ค์ง€๋กœ, Microsoft Graph API์™€ ์ง์ ‘ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค.

์„ค์น˜

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

๋น ๋ฅธ ์‹œ์ž‘

import { useRef, useEffect, useState } from 'react';
import {
  DayFlowCalendar,
  useCalendarApp,
  createMonthView,
} from '@dayflow/react';
import {
  attachOutlookSyncToDayFlow,
  createOutlookSync,
  createOutlookSyncAdapter,
  type OutlookDayFlowController,
  type OutlookSyncStatus,
} from '@dayflow/outlook-sync';

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

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

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

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

    const sync = createOutlookSync(adapter);
    const controller = attachOutlookSyncToDayFlow(calendar.app, sync, {
      writable: true,
      onStatusChange: setSyncStatus,
      onWriteError: (error, ctx) =>
        console.error(`[outlook-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 {
    attachOutlookSyncToDayFlow,
    createOutlookSync,
    createOutlookSyncAdapter,
  } from '@dayflow/outlook-sync';

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

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

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

    const sync = createOutlookSync(adapter);
    controller = attachOutlookSyncToDayFlow(calendar.app, sync, {
      writable: true,
      onStatusChange: status => {
        syncStatus.value = status;
      },
      onWriteError: (error, ctx) =>
        console.error(`[outlook-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 {
  attachOutlookSyncToDayFlow,
  createOutlookSync,
  createOutlookSyncAdapter,
  type OutlookDayFlowController,
  type OutlookSyncStatus,
} from '@dayflow/outlook-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: OutlookSyncStatus = { state: 'idle' };
  private controller?: OutlookDayFlowController;

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

    const sync = createOutlookSync(adapter);
    this.controller = attachOutlookSyncToDayFlow(this.calendar, sync, {
      writable: true,
      onStatusChange: status => {
        this.syncStatus = status;
      },
      onWriteError: (error, ctx) =>
        console.error(`[outlook-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 {
    attachOutlookSyncToDayFlow,
    createOutlookSync,
    createOutlookSyncAdapter,
  } from '@dayflow/outlook-sync';

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

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

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

    const sync = createOutlookSync(adapter);
    controller = attachOutlookSyncToDayFlow(calendar.app, sync, {
      writable: true,
      onStatusChange: status => {
        syncStatus = status;
      },
      onWriteError: (error, ctx) =>
        console.error(`[outlook-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 ์‚ฌ์šฉํ•˜๊ธฐ (ํด๋ผ์ด์–ธํŠธ ํ† ํฐ์— ๊ถŒ์žฅ)

getToken ํŒฉํ† ๋ฆฌ๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ์–ด๋Œ‘ํ„ฐ๊ฐ€ ๋งค ์š”์ฒญ ์ „์— ์ƒˆ ํ† ํฐ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. MSAL์ฒ˜๋Ÿผ ํ† ํฐ ๊ฐฑ์‹ ์„ ๋Œ€์‹  ๊ด€๋ฆฌํ•ด ์ฃผ๋Š” ์ธ์ฆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ธ ๋•Œ ํŠนํžˆ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค:

import { PublicClientApplication } from '@azure/msal-browser';

const msalInstance = new PublicClientApplication(msalConfig);

const adapter = createOutlookSyncAdapter({
  getToken: async () => {
    const result = await msalInstance.acquireTokenSilent({
      scopes: ['Calendars.ReadWrite'],
    });
    return result.accessToken;
  },
});

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

OAuth ํ† ํฐ์€ ์„œ๋ฒ„์— ๋‘๊ณ , ๋ชจ๋“  Graph API ์š”์ฒญ์„ ํ”„๋ก์‹œ๋กœ ์šฐํšŒ์‹œํ‚ค์„ธ์š”.

const adapter = createOutlookSyncAdapter({
  baseUrl: '/api/outlook-calendar',
  // No getToken needed โ€” the proxy injects Authorization
});
// proxy.mjs (Node.js example using MSAL Node)
import { createServer } from 'node:http';
import { ConfidentialClientApplication } from '@azure/msal-node';

const msalClient = new ConfidentialClientApplication({
  auth: {
    clientId: process.env.AZURE_CLIENT_ID,
    authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`,
    clientSecret: process.env.AZURE_CLIENT_SECRET,
  },
});

const GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
const ALLOWED_METHODS = new Set(['GET', 'POST', 'PATCH', 'DELETE']);

async function getToken() {
  const result = await msalClient.acquireTokenByClientCredential({
    scopes: ['https://graph.microsoft.com/.default'],
  });
  return result?.accessToken ?? '';
}

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

  if (!ALLOWED_METHODS.has(req.method ?? 'GET')) {
    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 getToken();
  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(3003);

๋ธํƒ€ ํ† ํฐ ์˜์†ํ™”

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

import {
  createOutlookSync,
  type OutlookSyncStorage,
} from '@dayflow/outlook-sync';

const storage: OutlookSyncStorage = {
  getDeltaToken: async calendarId =>
    localStorage.getItem(`outlook-delta:${calendarId}`),
  setDeltaToken: async (calendarId, token) =>
    token
      ? localStorage.setItem(`outlook-delta:${calendarId}`, token)
      : localStorage.removeItem(`outlook-delta:${calendarId}`),
};

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

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

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

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

const controller = attachOutlookSyncToDayFlow(calendar.app, sync, {
  getInitialSnapshot: async () => {
    const { calendars, events } = await loadFromLocalDB();
    return { calendars, events };
  },
  onSyncComplete: delta => {
    saveChanges(delta);
  },
  onWriteComplete: (operation, event) => {
    persistEvent(operation, event);
  },
});

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

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

import { applyRemoteSnapshot, getOutlookMeta } from '@dayflow/outlook-sync';

const delta = await applyRemoteSnapshot(
  calendar.app,
  { calendars, events },
  {
    isOwnedEvent: event => Boolean(getOutlookMeta(event)),
    isOwnedCalendar: calendar => calendar.source === 'Outlook',
    snapshotMode: 'authoritative',
    resolveConflict: (remote, local) =>
      mergeLocalEditsOntoRemote(remote, local),
  }
);

snapshotMode: 'authoritative'๋Š” ์ „์ฒด ์ œ๊ณต์ž ์Šค๋ƒ…์ˆ์—๋งŒ ์‚ฌ์šฉํ•˜์„ธ์š”. ๋ฒ”์œ„๊ฐ€ ์ œํ•œ๋˜์—ˆ๊ฑฐ๋‚˜ ํ•„ํ„ฐ๋งยทํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ๋œ ์Šค๋ƒ…์ˆ์€ ๊ธฐ๋ณธ ๋ถ€๋ถ„ ๋ชจ๋“œ๋ฅผ ์œ ์ง€ํ•ด์•ผ ๋ˆ„๋ฝ๋œ ๋กœ์ปฌ ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋ณด์กด๋ฉ๋‹ˆ๋‹ค.

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

attachOutlookSyncToDayFlow ์˜ต์…˜

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

createOutlookSyncAdapter ์˜ต์…˜

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

OutlookSyncStatus

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

OutlookSyncDelta

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

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

// Load calendars, sync initial events, and subscribe to 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: 'AAMk...' });

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

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

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

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

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

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

์ด๋ฒคํŠธ๋Š” Microsoft Graph์˜ calendarView/delta ์—”๋“œํฌ์ธํŠธ์— startDateTime๊ณผ endDateTime ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ถ™์—ฌ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. ์ด ์—”๋“œํฌ์ธํŠธ๋Š” ํ•ด๋‹น ๊ธฐ๊ฐ„ ์•ˆ์˜ ๋ฐ˜๋ณต ์ด๋ฒคํŠธ๋ฅผ ํŽผ์ณ ์ค๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋™ํ•˜๋ฉด ์ƒˆ ๋ฒ”์œ„์˜ ์ด๋ฒคํŠธ๊ฐ€ ์ž๋™์œผ๋กœ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค.

๋ธํƒ€ ํ† ํฐ์„ ์ด์šฉํ•œ ์ฆ๋ถ„ ๋™๊ธฐํ™”

์ตœ์ดˆ ๋กœ๋“œ ์ดํ›„ Graph API๋Š” @odata.deltaLink๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„ ๋™๊ธฐํ™”์—์„œ๋Š” ์ด ๋งํฌ๋ฅผ ๋”ฐ๋ผ๊ฐ€ ์ „์ฒด ๋ฒ”์œ„๊ฐ€ ์•„๋‹ˆ๋ผ ๋ณ€๊ฒฝ๋œ ์ด๋ฒคํŠธ๋งŒ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. OutlookSyncStorage๋ฅผ ์ œ๊ณตํ•˜๋ฉด ๋ธํƒ€ ํ† ํฐ์ด ํŽ˜์ด์ง€ ์ƒˆ๋กœ ๊ณ ์นจ ํ›„์—๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

๋ธํƒ€ ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜๋ฉด(Graph๊ฐ€ 410 Gone์„ ๋ฐ˜ํ™˜) ํŒจํ‚ค์ง€๊ฐ€ ์ž๋™์œผ๋กœ ์ „์ฒด ๋ฒ”์œ„ ์กฐํšŒ๋กœ ๋˜๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.

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

writable: true์ด๋ฉด ๋กœ์ปฌ ์ด๋ฒคํŠธ ๋ณ€๊ฒฝ์ด Outlook ์บ˜๋ฆฐ๋”์— ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค:

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

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

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

์บ˜๋ฆฐ๋” ์ƒ‰์ƒ

Outlook์€ 16์ง„ ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ด๋ฆ„์ด ์ง€์ •๋œ ์ƒ‰์ƒ(์˜ˆ: lightBlue, darkGreen)์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํŒจํ‚ค์ง€๋Š” ์ด๋ฅผ ๊ทผ์‚ฌํ•œ 16์ง„ ๊ฐ’์œผ๋กœ ๋งคํ•‘ํ•œ ๋’ค getCalendarColorsForHex์— ํ†ต๊ณผ์‹œ์ผœ DayFlow ํ…Œ๋งˆ์™€ ์ผ๊ด€๋˜๊ฒŒ ๋งž์ถฅ๋‹ˆ๋‹ค.

Microsoft Graph API ์Šค์ฝ”ํ”„

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

์Šค์ฝ”ํ”„์ ‘๊ทผ ๊ถŒํ•œ
Calendars.ReadWrite์ฝ๊ธฐยท์“ฐ๊ธฐ ์ „์ฒด ๊ถŒํ•œ
Calendars.Read์ฝ๊ธฐ ์ „์šฉ ๊ถŒํ•œ(writable: false์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ)

์•ฑ ์ „์šฉ(์„œ๋ฒ„ ๊ฐ„) ํ๋ฆ„์—์„œ๋Š” ์„œ๋น„์Šค ์ฃผ์ฒด์™€ ํ•จ๊ป˜ .default ์Šค์ฝ”ํ”„๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:

https://graph.microsoft.com/.default

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