@dayflow/caldav
@dayflow/caldav๋ ์ด๋ํฐ ์ค์ฌ์ผ๋ก ์ค๊ณ๋ ํค๋๋ฆฌ์ค CalDAV ๋๊ธฐํ ์์ง์
๋๋ค. iCloud ์บ๋ฆฐ๋, Nextcloud, Radicale, Fastmail์ ๋น๋กฏํด RFC 4791์ ๋ฐ๋ฅด๋ ๋ชจ๋ CalDAV ์๋ฒ์ ํจ๊ป ๋์ํฉ๋๋ค.
์ค์น
๋น ๋ฅธ ์์
import { useRef, useEffect } from 'react';
import {
DayFlowCalendar,
useCalendarApp,
createMonthView,
} from '@dayflow/react';
import {
attachCalDAVToDayFlow,
createCalDAVAdapter,
createCalDAVSync,
type CalDAVDayFlowController,
} from '@dayflow/caldav';
function MyCalendar() {
const calendar = useCalendarApp({
views: [createMonthView()],
calendars: [],
events: [],
});
const controllerRef = useRef<CalDAVDayFlowController | null>(null);
useEffect(() => {
if (controllerRef.current) return;
const adapter = createCalDAVAdapter({
calendarHomeUrl: 'https://caldav.example.com/calendars/alice/',
fetch: (url, init) =>
fetch('/api/caldav-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, init }),
}),
});
const sync = createCalDAVSync({ adapter });
const controller = attachCalDAVToDayFlow(calendar.app, sync, {
writable: true,
maxConcurrentCalendars: 4,
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 { onMounted, onBeforeUnmount } from 'vue';
import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';
import { createMonthView } from '@dayflow/core';
import {
attachCalDAVToDayFlow,
createCalDAVAdapter,
createCalDAVSync,
} from '@dayflow/caldav';
const calendar = useCalendarApp({
views: [createMonthView()],
calendars: [],
events: [],
});
let controller;
onMounted(() => {
const adapter = createCalDAVAdapter({
calendarHomeUrl: 'https://caldav.example.com/calendars/alice/',
fetch: (url, init) =>
fetch('/api/caldav-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, init }),
}),
});
const sync = createCalDAVSync({ adapter });
controller = attachCalDAVToDayFlow(calendar.app, sync, {
writable: true,
maxConcurrentCalendars: 4,
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 {
attachCalDAVToDayFlow,
createCalDAVAdapter,
createCalDAVSync,
type CalDAVDayFlowController,
} from '@dayflow/caldav';
@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: [],
});
private controller?: CalDAVDayFlowController;
ngOnInit() {
const adapter = createCalDAVAdapter({
calendarHomeUrl: 'https://caldav.example.com/calendars/alice/',
fetch: (url, init) =>
fetch('/api/caldav-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, init }),
}),
});
const sync = createCalDAVSync({ adapter });
this.controller = attachCalDAVToDayFlow(this.calendar, sync, {
writable: true,
maxConcurrentCalendars: 4,
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 {
attachCalDAVToDayFlow,
createCalDAVAdapter,
createCalDAVSync,
} from '@dayflow/caldav';
const calendar = useCalendarApp({
views: [createMonthView()],
calendars: [],
events: [],
});
let controller;
onMount(() => {
const adapter = createCalDAVAdapter({
calendarHomeUrl: 'https://caldav.example.com/calendars/alice/',
fetch: (url, init) =>
fetch('/api/caldav-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, init }),
}),
});
const sync = createCalDAVSync({ adapter });
controller = attachCalDAVToDayFlow(calendar.app, sync, {
writable: true,
maxConcurrentCalendars: 4,
onSyncComplete: delta => {
console.log(
`Sync done: +${delta.events.added} ~${delta.events.updated} -${delta.events.deleted}`
);
},
});
controller.start();
});
onDestroy(() => {
controller?.stop();
});
</script>
<DayFlowCalendar {calendar} />Calendar Home ํ์
iCloud์ฒ๋ผ ๋์ ํ์์ด ํ์ํ ์๋ฒ์์๋ createCalDAVAdapterFromServer๋ฅผ ์ฌ์ฉํ์ธ์. 2๋จ๊ณ PROPFIND๋ฅผ ์ํํ๊ณ ์ค๋น๋ ์ด๋ํฐ๋ฅผ ํ ๋ฒ์ ๋ฐํํฉ๋๋ค.
import {
createCalDAVAdapterFromServer,
ICLOUD_CALDAV_SERVER,
} from '@dayflow/caldav';
const adapter = await createCalDAVAdapterFromServer(ICLOUD_CALDAV_SERVER, {
fetch: proxiedFetch,
});์บ์ฑ์ด๋ ๋ก๊น
๋๋ฌธ์ home URL์ด ๋ฐ๋ก ํ์ํ๋ค๋ฉด ํ์ ์์ค ํจ์์ธ discoverCalendarHome์ ์ฌ์ฉํ์ธ์:
import {
discoverCalendarHome,
createCalDAVAdapter,
ICLOUD_CALDAV_SERVER,
} from '@dayflow/caldav';
const calendarHomeUrl = await discoverCalendarHome(
ICLOUD_CALDAV_SERVER,
authenticatedFetch
);
const adapter = createCalDAVAdapter({
calendarHomeUrl,
fetch: authenticatedFetch,
});discoverCalendarHome์ 2๋จ๊ณ PROPFIND๋ฅผ ์ํํฉ๋๋ค:
- ์๋ฒ ๋ฃจํธ์์
current-user-principal์ ๊ฐ์ ธ์ต๋๋ค - principal URL์์
calendar-home-set์ ๊ฐ์ ธ์ต๋๋ค
fetch ์ธ์๋ ์ธ์ฆ ์ ๋ณด๋ฅผ ํจ๊ป ์ค์ด ๋ณด๋ด์ผ ํฉ๋๋ค. ํ์ ์์ฒญ๋ ๋ค๋ฅธ CalDAV ์์ฒญ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ์ธ์ฆ์ด ํ์ํฉ๋๋ค.
์ ๊ณต์ ํ๋ฆฌ์
๋ด์ฅ ํ๋ฆฌ์
์ ์ฌ์ฉํ๋ฉด ์๋ ค์ง ์ ๊ณต์์ ๋ง๋ calendarHomeUrl์ ์์ฝ๊ฒ ๊ตฌ์ฑํ ์ ์์ต๋๋ค:
import {
ICLOUD_CALDAV_SERVER,
createCalDAVAdapterFromServer,
nextcloudConfig,
radicaleConfig,
fastmailConfig,
createCalDAVAdapter,
} from '@dayflow/caldav';
// iCloud โ must always discover dynamically
const icloudAdapter = await createCalDAVAdapterFromServer(
ICLOUD_CALDAV_SERVER,
{ fetch: proxiedFetch }
);
// Nextcloud
const nextcloudAdapter = createCalDAVAdapter({
...nextcloudConfig('https://nextcloud.example.com', 'alice'),
fetch: proxiedFetch,
});
// Radicale
const radicaleAdapter = createCalDAVAdapter({
...radicaleConfig('https://radicale.example.com', 'alice'),
fetch: proxiedFetch,
});
// Fastmail
const fastmailAdapter = createCalDAVAdapter({
...fastmailConfig('https://caldav.fastmail.com/dav', 'alice@fastmail.com'),
fetch: proxiedFetch,
});iCloud ์บ๋ฆฐ๋
iCloud CalDAV์๋ Apple ID ๋น๋ฐ๋ฒํธ๊ฐ ์๋๋ผ ์ฑ ์ ์ฉ ๋น๋ฐ๋ฒํธ๊ฐ ํ์ํฉ๋๋ค. appleid.apple.com โ ๋ก๊ทธ์ธ ๋ฐ ๋ณด์ โ ์ฑ ์ ์ฉ ์ํธ์์ ๋ฐ๊ธํ ์ ์์ต๋๋ค.
iCloud๋ ๋ธ๋ผ์ฐ์ CORS๋ฅผ ์ง์ํ์ง ์์ผ๋ฏ๋ก, ํ์์ ํฌํจํ ๋ชจ๋ ์์ฒญ์ ๋ฐฑ์๋ ํ๋ก์๊ฐ ํ์ํฉ๋๋ค.
์ด๋ํฐ๊ฐ ์๋์ผ๋ก ์ฒ๋ฆฌํด ์ฃผ๋ iCloud ๊ณ ์ ๋์:
- 8์๋ฆฌ RGBA 16์ง ์์(
#RRGGBBAA) โ ์ํ ์ฑ๋์ด ์ ๊ฑฐ๋ฉ๋๋ค - ์กฐ๊ฑด๋ถ ์ฐ๊ธฐ(
If-MatchETag)๊ฐ ์ ์ ๋์ํฉ๋๋ค - ์ข
์ผ ์ด๋ฒคํธ๋ ํ์ค
VALUE=DATEํ์์ ์ฌ์ฉํฉ๋๋ค - ์๊ฐ๋๊ฐ ์ง์ ๋ ์ด๋ฒคํธ๋ ํ์ค
TZID๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฉํฉ๋๋ค
Nextcloud
๊ณ์ ๋น๋ฐ๋ฒํธ๊ฐ ์๋๋ผ ๊ฐ์ธ ์ค์ โ ๋ณด์์์ ๋ฐ๊ธํ ์ฑ ๋น๋ฐ๋ฒํธ๋ฅผ ์ฌ์ฉํ์ธ์.
Nextcloud๋ current-user-privilege-set์ ์ฌ๋ฐ๋ฅด๊ฒ ๋ฐํํ๋ฏ๋ก ์ฝ๊ธฐยท์ฐ๊ธฐ ๊ถํ์ด ์๋์ผ๋ก ๊ฐ์ง๋ฉ๋๋ค. CORS๋ ์ง์ํ์ง ์์ผ๋ ํ๋ก์๋ฅผ ์ฌ์ฉํ์ธ์.
Radicale
Radicale์ ๋ณดํต current-user-privilege-set์ ๋ฐํํ์ง ์๊ธฐ ๋๋ฌธ์, ์ด๋ํฐ๋ ํ์๋ ์บ๋ฆฐ๋๋ฅผ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฝ๊ธฐ ์ ์ฉ์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค. ์ฌ์ฉ์์๊ฒ ์ฐ๊ธฐ ๊ถํ์ด ์๋ค๋ ๊ฒ์ ์๋ค๋ฉด ํ์ ํ CalendarType์ readOnly: false๋ฅผ ์ง์ ํ์ธ์.
Fastmail
Fastmail์ ํ์ค CalDAV๋ฅผ ์ง์ํฉ๋๋ค. fastmailConfig์ Fastmail ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์ฌ์ฉ์ ์ด๋ฆ์ผ๋ก ์ ๋ฌํ์ธ์.
๋ก์ปฌ ์บ์๋ก ์ด๊ธฐํํ๊ธฐ
๋๊ธฐํ๋ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ๋ก์ปฌ ์ ์ฅ์(์: Supabase, IndexedDB)์ ๋ณด๊ดํ๋ค๋ฉด, ์ฒซ ์๊ฒฉ ๋๊ธฐํ ์ ์ DayFlow๋ฅผ ๋ฏธ๋ฆฌ ์ฑ์ ์บ๋ฆฐ๋๋ฅผ ์ฆ์ ํ์ํ ์ ์์ต๋๋ค:
const controller = attachCalDAVToDayFlow(calendar.app, sync, {
getInitialSnapshot: async () => {
const { calendars, events } = await loadFromLocalDB();
return { calendars, events };
},
onSyncComplete: delta => {
// Save what changed to your local store
console.log(
`+${delta.events.added} ~${delta.events.updated} -${delta.events.deleted}`
);
},
onWriteComplete: (operation, event) => {
// Update local store after a successful write-back
persistEvent(operation, event);
},
});getInitialSnapshot์ CalDAV ์์ฒญ์ด ์ผ์ด๋๊ธฐ ์ , start() ๋์ค ํ ๋ฒ๋ง ํธ์ถ๋ฉ๋๋ค. ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋๊ธฐํ๊ฐ ๋๋ ๋์ DayFlow๋ ์บ์๋ ๋ฐ์ดํฐ๋ก ๋ ๋๋ง๋ฉ๋๋ค. getInitialSnapshot์์ ๋ฐ์ํ ์ค๋ฅ๋ onError๋ก ์ ๋ฌ๋๋ฉฐ ์๊ฒฉ ๋๊ธฐํ๋ฅผ ์ค๋จ์ํค์ง ์์ต๋๋ค.
์๊ฒฉ ์ค๋ ์ ์ง์ ์ ์ฉํ๊ธฐ
๋ธ๋ผ์ฐ์ ๊ฐ ์๋๋ผ ๋ฐฑ์๋ ์์
์์ ๋๊ธฐํํ๋ ๋ฑ ๋๊ธฐํ ํ๋ฆ์ ์ง์ ๊ด๋ฆฌํ๋ ์ ํ๋ฆฌ์ผ์ด์
์ด๋ผ๋ฉด, applyRemoteSnapshot์ผ๋ก ์๊ฒฉ ์ด๋ฒคํธ ๋ฌถ์์ ์ ๊ณต์์๊ฒ ๋ค์ ์ ์กํ์ง ์๊ณ DayFlow์ ์ ์ฉํ ์ ์์ต๋๋ค:
import { applyRemoteSnapshot, getCalDAVMeta } from '@dayflow/caldav';
const delta = await applyRemoteSnapshot(
calendar.app,
{ calendars, events },
{
// Identify events owned by this provider so stale ones are cleaned up
isOwnedEvent: event => Boolean(getCalDAVMeta(event)),
isOwnedCalendar: calendar => calendar.source === 'iCloud',
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'๋ ์ค๋
์์ด ์ ๊ณต์๊ฐ ์์ ํ ๋ชจ๋ ์บ๋ฆฐ๋์ ์ด๋ฒคํธ๋ฅผ ์จ์ ํ ๋ด๊ณ ์์ ๋๋ง ์ ๋ฌํ์ธ์. ๋ฒ์๊ฐ ์ ํ๋์๊ฑฐ๋ ํํฐ๋งยทํ์ด์ง ์ฒ๋ฆฌ๋ ์๋ต์๋ ๊ธฐ๋ณธ ๋ถ๋ถ ๋ชจ๋๋ฅผ ์ ์งํ๊ฑฐ๋, deleteMissingEvents: false์ deleteMissingCalendars: false๋ฅผ ๋ช
์์ ์ผ๋ก ์ง์ ํ์ธ์.
๋ฐฑ์๋ ํ๋ก์
CalDAV ์๋ฒ๋ ๋ธ๋ผ์ฐ์ CORS๋ฅผ ์ง์ํ์ง ์์ผ๋ฏ๋ก ๋ธ๋ผ์ฐ์ ๊ฐ ์ง์ ํธ์ถํ ์ ์์ต๋๋ค. ๋ค์ ์ญํ ์ ํ๋ ๋ฐฑ์๋ ํ๋ก์๊ฐ ํ์ํฉ๋๋ค:
- DayFlow๊ฐ ๋ณด๋ธ ์์ฒญ(JSON์ผ๋ก ๊ฐ์ผ CalDAV ์์ฒญ)์ ์์ ํฉ๋๋ค
- ์๊ฒฉ ์ฆ๋ช (Basic ์ธ์ฆ, ํ ํฐ ๋ฑ)์ ์ฃผ์ ํฉ๋๋ค
- CalDAV ์๋ฒ๋ก ์ ๋ฌํ๊ณ ์๋ต์ ๋๋ ค์ค๋๋ค
// proxy.mjs (minimal Node.js example)
import { createServer } from 'node:http';
const {
CALDAV_BASE_URL, // e.g. https://caldav.icloud.com
CALDAV_USERNAME,
CALDAV_PASSWORD,
PORT = '3001',
} = process.env;
const ALLOWED_METHODS = new Set(['PROPFIND', 'REPORT', 'PUT', 'DELETE']);
const CORS_HEADERS = {
'Access-Control-Allow-Origin': 'http://localhost:5173',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Expose-Headers': 'ETag, DAV',
};
createServer(async (req, res) => {
if (req.method === 'OPTIONS') {
res.writeHead(204, CORS_HEADERS);
res.end();
return;
}
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const { url, init } = JSON.parse(Buffer.concat(chunks).toString());
if (!url.startsWith(CALDAV_BASE_URL)) {
res.writeHead(403);
res.end();
return;
}
if (!ALLOWED_METHODS.has(init?.method ?? 'PROPFIND')) {
res.writeHead(405);
res.end();
return;
}
const upstream = await fetch(url, {
...init,
headers: {
...init?.headers,
Authorization:
'Basic ' +
Buffer.from(`${CALDAV_USERNAME}:${CALDAV_PASSWORD}`).toString('base64'),
},
});
const body = await upstream.text();
res.writeHead(upstream.status, {
'Content-Type': upstream.headers.get('Content-Type') ?? 'text/xml',
ETag: upstream.headers.get('ETag') ?? '',
...CORS_HEADERS,
});
res.end(body);
}).listen(Number(PORT));์๊ฒฉ ์ฆ๋ช ์ ํ๊ฒฝ ๋ณ์์ ๋ด์ ํ๋ก์๋ฅผ ์คํํ์ธ์:
CALDAV_BASE_URL=https://caldav.icloud.com \
CALDAV_USERNAME=alice@icloud.com \
CALDAV_PASSWORD=xxxx-xxxx-xxxx-xxxx \
node proxy.mjs์ต์ ๋ ํผ๋ฐ์ค
attachCalDAVToDayFlow ์ต์
| ์ต์ | ํ์ | ๊ธฐ๋ณธ๊ฐ | ์ค๋ช |
|---|---|---|---|
writable | boolean | true | ๋ก์ปฌ ๋ณ๊ฒฝ ์ฌํญ์ CalDAV ์๋ฒ์ ๋ค์ ์ฐ๋๋ก ํ์ฉํฉ๋๋ค. |
refreshOnVisibleRangeChange | boolean | true | ์ฌ์ฉ์๊ฐ ์๋ก์ด ๋ ์ง ๋ฒ์๋ก ์ด๋ํ๋ฉด ๋ค์ ๋๊ธฐํํฉ๋๋ค. |
maxConcurrentCalendars | number | 4 | ๋์์ ๋๊ธฐํํ ์๊ฒฉ ์บ๋ฆฐ๋์ ์ต๋ ๊ฐ์์ ๋๋ค. |
eventMode.recurring | 'read-only' | 'read-only' | ๋ฐ๋ณต ์ด๋ฒคํธ๋ ์ฝ๊ธฐ ์ ์ฉ์
๋๋ค. 'read-only'๋ง ์ง์ํฉ๋๋ค. |
onError | (error, context) => void | โ | ๋๊ธฐํ๋ ์ฐ๊ธฐ๊ฐ ์คํจํ ๋ ํธ์ถ๋ฉ๋๋ค. |
getInitialSnapshot | () => Promise<{ events, calendars }> | โ | ์ฒซ ์๊ฒฉ ๋๊ธฐํ ์ ์ ๋ก์ปฌ ์บ์๋ก DayFlow๋ฅผ ์ฑ์๋๋ค. ์ค๋ฅ๋ onError๋ก ์ ๋ฌ๋ฉ๋๋ค. |
onSyncComplete | (delta: CalDAVSyncDelta) => void | โ | ๋๊ธฐํ๊ฐ ์ฑ๊ณตํ ๋๋ง๋ค ๋ณ๊ฒฝ ๊ฑด์์ ํจ๊ป ํธ์ถ๋ฉ๋๋ค. |
onWriteComplete | (operation, event) => void | โ | ๋ก์ปฌ ๋ณ๊ฒฝ์ด CalDAV ์๋ฒ์ ์ฑ๊ณต์ ์ผ๋ก ๋ฐ์๋ ๋ค ํธ์ถ๋ฉ๋๋ค. |
createEventId | (input) => string | ๋ค์์คํ์ด์ค ์ ์ฉ | ์๊ฒฉ CalDAV ์ด๋ฒคํธ์ DayFlow id๋ฅผ ๋ง๋ญ๋๋ค. ์ถฉ๋์ ํผํ๊ธฐ ์ํด ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณต์ ๋ฒ์์ id๋ฅผ ์ฌ์ฉํฉ๋๋ค. |
CalDAVErrorContext์๋ operation('list-calendars' | 'initial-sync' | 'range-sync' | 'create' | 'update' | 'delete'), calendarId, eventId๊ฐ ๋ค์ด ์์ต๋๋ค.
CalDAVSyncDelta:
type CalDAVSyncDelta = {
calendars: { added: number; updated: number; deleted: number };
events: { added: number; updated: number; deleted: number };
};createCalDAVAdapter ์ต์
| ์ต์ | ํ์ | ํ์ ์ฌ๋ถ | ์ค๋ช |
|---|---|---|---|
calendarHomeUrl | string | โ | CalDAV calendar home ์ปฌ๋ ์ ์ URL์ ๋๋ค. |
fetch | function | โ | ์ธ์ฆ์ด ์ ์ฉ๋ fetch ํจ์์ ๋๋ค. ๋ฐฑ์๋ ํ๋ก์๋ฅผ ๊ฑฐ์น๋๋ก ๊ตฌ์ฑํ์ธ์. |
createCalDAVAdapterFromServer ์ต์
| ๋งค๊ฐ๋ณ์ | ํ์ | ์ค๋ช |
|---|---|---|
serverUrl | string | CalDAV ์๋ฒ์ ๋ฃจํธ URL์
๋๋ค(์: ICLOUD_CALDAV_SERVER). |
options | object | calendarHomeUrl์ ์ ์ธํ๋ฉด createCalDAVAdapter์ ์ต์
๊ณผ ๊ฐ์ต๋๋ค. |
๋ด๋ถ์ ์ผ๋ก ํ์์ ์ํํ๊ณ ๋ฐ๋ก ์ฌ์ฉํ ์ ์๋ CalDAVAdapter๋ฅผ ๋ฐํํฉ๋๋ค.
์ปจํธ๋กค๋ฌ API
// Discover remote calendars, load initial events, and subscribe to DayFlow changes
await controller.start();
// Unsubscribe all listeners (does not clear DayFlow state)
controller.stop();
// Re-sync all calendars using the last known visible range
await controller.refresh();
// Re-sync a specific calendar
await controller.refresh({ calendarId: 'my-calendar-id' });
// Re-sync with an explicit date range
await controller.refresh({
range: { start: new Date('2025-01-01'), end: new Date('2025-02-01') },
});
// Get current sync status
const status = controller.getStatus();
// { state: 'idle' | 'syncing' | 'error', lastSyncedAt?: Date, error?: unknown }์ ์ฅ์ ์ธํฐํ์ด์ค
createCalDAVSync๋ ๋๊ธฐํ ํ ํฐ, ์บ๋ฆฐ๋ ctag, ETag๋ฅผ ํ์ด์ง ์๋ก ๊ณ ์นจ ํ์๋ ์ ์งํ๊ธฐ ์ํ ์ ํ์ storage ๊ฐ์ฒด๋ฅผ ๋ฐ์ต๋๋ค. ์ ๊ณตํ์ง ์์ผ๋ฉด ๋๊ธฐํ ์ํ๊ฐ ๋ฉ๋ชจ๋ฆฌ์๋ง ๋จ์ต๋๋ค. ํ
์คํธ๋ ๋ฐ๋ชจ์๋ ๊ทธ ๊ธฐ๋ณธ๊ฐ์ด ํธ๋ฆฌํ์ง๋ง, ์ด์ ํ๊ฒฝ์์๋ ์ฆ๋ถ ๋๊ธฐํ์ ์กฐ๊ฑด๋ถ ์ฐ๊ธฐ๊ฐ ์๋ก ๊ณ ์นจ ํ์๋ ์ด์ด์ง๋๋ก ์ง์ ์ ์ฅ์๋ฅผ ์ ๊ณตํด์ผ ํฉ๋๋ค.
import { createCalDAVSync, type CalDAVStorage } from '@dayflow/caldav';
const storage: CalDAVStorage = {
getSyncToken: async calendarId =>
localStorage.getItem(`sync:${calendarId}`) ?? null,
setSyncToken: async (calendarId, token) => {
if (token) localStorage.setItem(`sync:${calendarId}`, token);
else localStorage.removeItem(`sync:${calendarId}`);
},
getCtag: async calendarId =>
localStorage.getItem(`ctag:${calendarId}`) ?? null,
setCtag: async (calendarId, ctag) =>
localStorage.setItem(`ctag:${calendarId}`, ctag),
getEtag: async href => localStorage.getItem(`etag:${href}`) ?? null,
setEtag: async (href, etag) => localStorage.setItem(`etag:${href}`, etag),
deleteEtag: async href => localStorage.removeItem(`etag:${href}`),
getEventState: async id =>
JSON.parse(localStorage.getItem(`event:${id}`) ?? 'null'),
setEventState: async (id, state) =>
localStorage.setItem(`event:${id}`, JSON.stringify(state)),
deleteEventState: async id => localStorage.removeItem(`event:${id}`),
clearCalendar: async calendarId => {
for (const key of Object.keys(localStorage)) {
if (
key.startsWith(`sync:${calendarId}`) ||
key.startsWith(`ctag:${calendarId}`)
) {
localStorage.removeItem(key);
}
}
},
};
const sync = createCalDAVSync({
adapter,
storage,
// Optional: split broad visible-range REPORTs into smaller windows.
rangeChunkDays: 31,
});์บ๋ฆฐ๋์ ctag ๊ฐ์ ์ฌ์ฉํ ์ ์์ผ๋ฉด, ๋ง์ง๋ง์ผ๋ก ๋๋ ๋๊ธฐํ ์ดํ ์ปฌ๋ ์
์ด ๋ฐ๋์ง ์์ ๊ฒฝ์ฐ ์ปฌ๋ ์
์ ์ฒด ๋๊ธฐํ๊ฐ ๋คํธ์ํฌ ์์
์ ๊ฑด๋๋ธ ์ ์์ต๋๋ค. ํ์ ๋ฒ์ ๋๊ธฐํ๋ ์์ฒญํ ๋ฒ์๋ฅผ ํญ์ ์กฐํํ๋ฏ๋ก, ์ ๋ฒ์๋ก ์ด๋ํ๋ฉด ์์ง ๋ณด์ง ๋ชปํ ์ด๋ฒคํธ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.
์ด๋ฒคํธ ์๋ณ์
DayFlow ๋ฐ์ธ๋ฉ์ ๊ธฐ๋ณธ์ ์ผ๋ก ์๊ฒฉ CalDAV ์ด๋ฒคํธ์ ์ ๊ณต์ ๋ฒ์์ id๋ฅผ ์ฌ์ฉํฉ๋๋ค:
import { createNamespacedCalDAVEventId } from '@dayflow/caldav';
const controller = attachCalDAVToDayFlow(calendar.app, sync, {
createEventId: createNamespacedCalDAVEventId,
});๋งคํผ๋ฅผ ์ง์ ํธ์ถํ๋ฉด, ํฉํ ๋ฆฌ๋ฅผ ์ ๋ฌํ์ง ์๋ ํ UID๋ฅผ id๋ก ์ฐ๋ ๊ธฐ์กด ๋ฐฉ์์ด ์ ์ง๋ฉ๋๋ค:
const event = mapCalDAVEventToDayFlow(data, {
createEventId: createNamespacedCalDAVEventId,
});๋๋ถ์ ๋ก์ปฌ ์ด๋ฒคํธ๋ ๋ค๋ฅธ ์ ๊ณต์์ id๊ฐ ์ถฉ๋ํ์ง ์์ผ๋ฉด์๋, ๋๊ธฐํ ์ค์๋ CalDAV ๋ฉํ๋ฐ์ดํฐ๋ก ๊ธฐ์กด ์ด๋ฒคํธ๋ฅผ ์ ํํ ๋์กฐํ ์ ์์ต๋๋ค.