사이드바 플러그인
사이드바 플러그인은 DayFlow 캘린더에 캘린더 관리용 사이드바를 추가합니다. 날짜 이동을 위한 미니 캘린더, 표시 여부를 켜고 끌 수 있는 캘린더 목록, 그리고 캘린더 생성·이름 변경·색상 변경·병합·삭제·가져오기/내보내기 기능을 제공합니다.
설치
플러그인 패키지를 설치하세요:
사용법
import { useCalendarApp, DayFlowCalendar } from '@dayflow/react';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';
function MyCalendar() {
const sidebarPlugin = createSidebarPlugin({
width: 280,
createCalendarMode: 'modal',
});
const calendar = useCalendarApp({
views: [
/* your views */
],
plugins: [sidebarPlugin],
});
return <DayFlowCalendar calendar={calendar} />;
}<template>
<DayFlowCalendar :calendar="calendar" />
</template>
<script setup>
import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';
const sidebarPlugin = createSidebarPlugin({
width: 280,
createCalendarMode: 'modal',
});
const calendar = useCalendarApp({
views: [
/* your views */
],
plugins: [sidebarPlugin],
});
</script>import { Component } from '@angular/core';
import { DayFlowCalendarModule } from '@dayflow/angular';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';
@Component({
selector: 'app-root',
standalone: true,
imports: [DayFlowCalendarModule],
template: `<dayflow-calendar [calendar]="calendar"></dayflow-calendar>`
})
export class AppComponent {
calendar = {
views: [
/* your views */
],
plugins: [
createSidebarPlugin({
width: 280,
createCalendarMode: 'modal',
})
]
};
}<script>
import { DayFlowCalendar, useCalendarApp } from '@dayflow/svelte';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';
const sidebarPlugin = createSidebarPlugin({
width: 280,
createCalendarMode: 'modal',
});
const calendar = useCalendarApp({
views: [
/* your views */
],
plugins: [sidebarPlugin],
});
</script>
<DayFlowCalendar {calendar} />설정
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
width | number | string | '240px' | 사이드바의 너비입니다(예: 280 또는 '20rem') |
miniWidth | string | '50px' | 사이드바가 접혔을 때의 너비입니다 |
initialCollapsed | boolean | false | 사이드바를 접힌 상태로 시작할지 여부입니다 |
createCalendarMode | 'inline' | 'modal' | 'inline' | 캘린더 생성 폼을 어떤 방식으로 표시할지 정합니다 |
colorPickerMode | 'blossom' | 'default' | 'default' | 사용할 색상 선택기 컴포넌트입니다 |
onSubscribeCalendar | (calendar, events) => Promise<void> | undefined | 새 구독이 만들어진 뒤 호출되는 콜백입니다 |
onLoadSubscription | (calendar) => Promise<void> | undefined | 기존 구독을 불러오는 사용자 정의 로더입니다 |
onReorder | (calendars: CalendarType[]) => void | Promise<void> | undefined | 캘린더 순서를 바꾼 뒤 호출되는 콜백입니다 |
componentsOrder | ('calendarList' | 'miniCalendar')[] | ['calendarList', 'miniCalendar'] | 사이드바 구성 요소의 표시 순서입니다 |
render | (props: CalendarSidebarRenderProps) => TNode | undefined | 사이드바 UI 전체를 대체합니다 |
renderSidebarHeader | (args: SidebarHeaderSlotArgs) => TNode | undefined | 사이드바 헤더를 직접 렌더링합니다 |
renderCalendarContextMenu | (calendar, onClose) => TNode | undefined | 캘린더 항목의 컨텍스트 메뉴를 직접 렌더링합니다 |
renderCreateCalendarDialog | (props) => TNode | undefined | 캘린더 생성 대화상자를 직접 렌더링합니다 |
프로그래밍 API
캘린더가 렌더링된 뒤 app.getPlugin으로 SidebarService에 접근하세요:
import { type SidebarService } from '@dayflow/plugin-sidebar';
const sidebar = app.getPlugin<SidebarService>('sidebar');
sidebar?.collapse(); // Collapse the sidebar
sidebar?.expand(); // Expand the sidebar
sidebar?.setCollapsed(true); // Set state explicitly
console.log(sidebar?.isCollapsed()); // Read current stateSidebarService
| 메서드 | 반환값 | 설명 |
|---|---|---|
collapse() | void | 사이드바를 접습니다 |
expand() | void | 사이드바를 펼칩니다 |
setCollapsed(collapsed: boolean) | void | 접힘 상태를 명시적으로 지정합니다 |
isCollapsed() | boolean | 현재 접힘 상태를 반환합니다 |
키보드 단축키에 연결하기
window.addEventListener('keydown', e => {
if (e.metaKey && e.key === '[') {
const sidebar = app.getPlugin<SidebarService>('sidebar');
sidebar?.setCollapsed(!sidebar.isCollapsed());
}
});기능
사이드바는 표시 여부 토글, 캘린더 색상 견본, 접기 컨트롤이 갖춰진 완성된 레이아웃을 제공합니다. 캘린더 생성과 컨텍스트 메뉴도 지원하므로 사용자가 캘린더를 추가하고, 색을 바꾸고, 삭제할 수 있습니다.
기본 제공 기능은 다음과 같습니다:
- 미니 캘린더 – 날짜를 빠르게 이동할 수 있는 간결한 월 뷰
- 캘린더 목록 – 색상으로 구분된 체크박스로 캘린더별 표시 여부를 전환
- 캘린더 만들기 – 원하는 이름과 색상으로 새 캘린더 추가
- 이름·색상 변경 – 캘린더를 마우스 오른쪽 버튼으로 클릭해 이름이나 색상 변경
- 캘린더 순서 변경 – 목록에서 캘린더를 끌어다 놓아 순서를 바꿉니다.
onReorder콜백으로 새 순서를 백엔드에 저장하세요. - 캘린더 병합 – 한 캘린더의 모든 이벤트를 다른 캘린더로 옮기기
- 캘린더 삭제 – 확인 단계를 거쳐 캘린더 삭제(또는 먼저 병합)
- 가져오기 / 내보내기 –
.ics파일을 가져오거나 캘린더를.ics로 내보내기 - 캘린더 구독 – URL로 원격
.ics피드를 구독합니다. 구독한 캘린더는 목록에 배지가 표시됩니다
라이브 데모
아래에서 기본 사이드바 UI와, 프레임워크 네이티브 컴포넌트로 직접 구현한 예시를 살펴보세요.
살펴볼 만한 것
- 표시 여부 토글: 캘린더를 숨기고 다시 표시하면서 뷰가 즉시 갱신되는지 확인해 보세요.
- 드래그와 동기화: 뷰 사이로 이벤트를 옮겨 보세요. 사이드바가 다가올 일정을 계속 동기화합니다.
- 접기: 주 뷰가 빽빽할 때 사이드바를 접어 공간을 확보해 보세요.
캘린더 구독
사이드바에는 원격 ICS 캘린더 피드를 추가할 수 있는 구독 기능이 내장되어 있습니다. 캘린더나 사이드바의 빈 영역을 마우스 오른쪽 버튼으로 클릭해 컨텍스트 메뉴를 열고 캘린더 구독을 선택한 뒤, 공개적으로 접근 가능한 .ics URL을 입력하세요.
DayFlow가 파일을 내려받아 이벤트를 파싱하고 새 캘린더를 자동으로 만듭니다. 구독한 캘린더는 목록에 작은 배지가 표시되어 로컬에서 만든 캘린더와 구분됩니다.
자동 로딩과 중복 제거
초기 설정에서 subscription.url이 있는 캘린더를 전달하면, 사이드바 플러그인이 마운트 시 최신 이벤트를 자동으로 가져옵니다.
로컬에 캐시된 이벤트와 새로 받은 구독 데이터를 합칠 때 화면에 중복이 생기지 않도록, DayFlow는 ID 기반 중복 제거를 수행합니다:
- 구독에서 받은 이벤트가 코어 스토어에 있는 이벤트와
id가 같으면, 구독 쪽 버전이 우선합니다. - 덕분에 페이지를 새로 고쳐도 이벤트가 "두 번" 보이지 않고 항상 최신 정보가 표시됩니다.
구독한 캘린더 저장하기
DayFlow는 CalendarType에 subscription 객체가 있는지로 구독 캘린더를 식별합니다.
구독의 기본 동작:
- 읽기 전용: UI에서의 변경(제목 편집, 캘린더 변경, 삭제)은
canMutateFromUI()를 통해 비활성화됩니다. - 드래그 불가: 드래그 앤 드롭 동작(이동, 크기 조절)이 비활성화됩니다.
- 메모 숨김: 이벤트에 설명이 없으면 UI를 깔끔하게 유지하기 위해 상세 패널에서 "메모" 항목을 숨깁니다.
사용자가 구독한 이벤트를 수정하거나 옮길 수 있게 하려면, 캘린더 객체에 readOnly: false를 지정해 이 보호 설정을 명시적으로 해제하세요:
const calendar = useCalendarApp({
calendars: [
{
id: 'team-ics',
name: 'Team Calendar',
subscription: {
url: 'https://example.com/calendar.ics',
status: 'ready',
},
// Override default protections:
readOnly: false, // Enables both UI mutations and drag-and-drop
colors: {
/* ... */
},
},
],
});캘린더 상태를 백엔드에 저장할 때는 subscription 필드를 함께 보존했다가 다음 로드 시 복원하세요.
참고: DayFlow는 구독한 피드를 자동으로 새로 고치지 않습니다. 이벤트를 최신 상태로 유지하려면 앱에서 주기적으로 데이터를 가져와
app.addEvent()/app.removeEvent()로 갱신하세요.
createSidebarPlugin에 render 함수를 전달하거나(React), 컴포넌트 슬롯·템플릿을 사용해(Vue, Angular, Svelte) 사이드바를 직접 만든 컴포넌트로 교체할 수 있습니다.
import { useCalendarApp, DayFlowCalendar } from '@dayflow/react';
import {
createSidebarPlugin,
type CalendarSidebarRenderProps,
} from '@dayflow/plugin-sidebar';
const CustomSidebar = ({
app,
calendars,
toggleCalendarVisibility,
isCollapsed,
setCollapsed,
}: CalendarSidebarRenderProps) => {
if (isCollapsed) {
return (
<div className='p-2'>
<button onClick={() => setCollapsed(false)}>→</button>
</div>
);
}
return (
<aside className='flex h-full flex-col gap-4 p-4 bg-slate-50 border-r'>
<header className='flex items-center justify-between'>
<h3 className='font-semibold'>My Workspace</h3>
<button onClick={() => setCollapsed(true)}>←</button>
</header>
<nav className='space-y-1'>
{calendars.map(calendar => (
<label
key={calendar.id}
className='flex items-center gap-2 cursor-pointer'
>
<input
type='checkbox'
checked={calendar.isVisible}
onChange={e =>
toggleCalendarVisibility(calendar.id, e.target.checked)
}
/>
<span
className='w-3 h-3 rounded-full'
style={{ backgroundColor: calendar.colors.lineColor }}
/>
{calendar.name}
</label>
))}
</nav>
<div className='mt-auto pt-4 border-t text-xs text-slate-500'>
Total Events: {app.getEvents().length}
</div>
</aside>
);
};
function App() {
const calendar = useCalendarApp({
views: [/* views */],
plugins: [
createSidebarPlugin({
render: props => <CustomSidebar {...props} />,
}),
],
});
return <DayFlowCalendar calendar={calendar} />;
}<template>
<DayFlowCalendar :calendar="calendar">
<!-- Override using scoped slot 'sidebar' -->
<template #sidebar="args">
<aside v-if="!args.isCollapsed" class="flex h-full flex-col gap-4 p-4 bg-slate-50 border-r">
<header class="flex items-center justify-between">
<h3 class="font-semibold">My Workspace</h3>
<button @click="args.setCollapsed(true)">←</button>
</header>
<nav class="space-y-1">
<label
v-for="cal in args.calendars"
:key="cal.id"
class="flex items-center gap-2 cursor-pointer"
>
<input
type="checkbox"
:checked="cal.isVisible"
@change="args.toggleCalendarVisibility(cal.id, !cal.isVisible)"
/>
<span
class="w-3 h-3 rounded-full"
:style="{ backgroundColor: cal.colors.lineColor }"
/>
{{ cal.name }}
</label>
</nav>
<div class="mt-auto pt-4 border-t text-xs text-slate-500">
Total Events: {{ args.app.getEvents().length }}
</div>
</aside>
<div v-else class="p-2">
<button @click="args.setCollapsed(false)">→</button>
</div>
</template>
</DayFlowCalendar>
</template>
<script setup>
import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';
const calendar = useCalendarApp({
views: [/* views */],
plugins: [createSidebarPlugin()]
});
</script><!-- app.component.html -->
<dayflow-calendar
[calendar]="calendar"
[sidebar]="customSidebar"
>
</dayflow-calendar>
<ng-template #customSidebar let-args>
<aside *ngIf="!args.isCollapsed; else collapsedView" class="flex h-full flex-col gap-4 p-4 bg-slate-50 border-r">
<header class="flex items-center justify-between">
<h3 class="font-semibold">My Workspace</h3>
<button (click)="args.setCollapsed(true)">←</button>
</header>
<nav class="space-y-1">
<label
*ngFor="let cal of args.calendars"
class="flex items-center gap-2 cursor-pointer"
>
<input
type="checkbox"
[checked]="cal.isVisible"
(change)="args.toggleCalendarVisibility(cal.id, !cal.isVisible)"
/>
<span
class="w-3 h-3 rounded-full"
[style.background-color]="cal.colors.lineColor"
></span>
{{ cal.name }}
</label>
</nav>
<div class="mt-auto pt-4 border-t text-xs text-slate-500">
Total Events: {{ args.app.getEvents().length }}
</div>
</aside>
<ng-template #collapsedView>
<div class="p-2">
<button (click)="args.setCollapsed(false)">→</button>
</div>
</ng-template>
</ng-template><!-- App.svelte -->
<script lang="ts">
import { DayFlowCalendar, useCalendarApp } from '@dayflow/svelte';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';
import CustomSidebar from './CustomSidebar.svelte';
const calendar = useCalendarApp({
views: [/* views */],
plugins: [createSidebarPlugin()]
});
</script>
<DayFlowCalendar {calendar} sidebar={CustomSidebar} />
<!-- CustomSidebar.svelte -->
<script lang="ts">
import type { CalendarSidebarRenderProps } from '@dayflow/core';
let {
app,
calendars,
isCollapsed,
setCollapsed,
toggleCalendarVisibility,
} = $props<CalendarSidebarRenderProps>();
</script>
{#if isCollapsed}
<div class="p-2">
<button onclick={() => setCollapsed(false)}>→</button>
</div>
{:else}
<aside class="flex h-full flex-col gap-4 p-4 bg-slate-50 border-r">
<header class="flex items-center justify-between">
<h3 class="font-semibold">My Workspace</h3>
<button onclick={() => setCollapsed(true)}>←</button>
</header>
<nav class="space-y-1">
{#each calendars as cal (cal.id)}
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={cal.isVisible}
onchange={(e) => toggleCalendarVisibility(cal.id, e.currentTarget.checked)}
/>
<span
class="w-3 h-3 rounded-full"
style:background-color={cal.colors.lineColor}
></span>
{cal.name}
</label>
{/each}
</nav>
<div class="mt-auto pt-4 border-t text-xs text-slate-500">
Total Events: {app.getEvents().length}
</div>
</aside>
{/if}사용자 정의 컨텍스트 메뉴
캘린더 항목의 기본 우클릭 메뉴를 교체할 수 있습니다:
createSidebarPlugin({
renderCalendarContextMenu: (calendar, onClose) => (
<div className='bg-white shadow-lg border rounded p-2'>
<button
onClick={() => {
console.log('Custom action');
onClose();
}}
>
Custom Action for {calendar.name}
</button>
</div>
),
});<template>
<DayFlowCalendar :calendar="calendar">
<template #calendarContextMenu="{ calendar, onClose }">
<div class="bg-white shadow-lg border rounded p-2">
<button @click="onClose()">
Custom Action for {{ calendar.name }}
</button>
</div>
</template>
</DayFlowCalendar>
</template><dayflow-calendar
[calendar]="calendar"
[calendarContextMenu]="contextMenu"
>
</dayflow-calendar>
<ng-template #contextMenu let-args>
<div class="bg-white shadow-lg border rounded p-2">
<button (click)="args.onClose()">
Custom Action for {{ args.calendar.name }}
</button>
</div>
</ng-template><DayFlowCalendar {calendar} calendarContextMenu={CustomContextMenu} />
<!-- CustomContextMenu.svelte -->
<script lang="ts">
import type { CalendarContextMenuSlotArgs } from '@dayflow/core';
let { calendar, onClose } = $props<CalendarContextMenuSlotArgs>();
</script>
<div class="bg-white shadow-lg border rounded p-2">
<button onclick={() => { console.log('Custom action'); onClose(); }}>
Custom Action for {calendar.name}
</button>
</div>사용자 정의 캘린더 생성 대화상자
기본 캘린더 생성 대화상자를 교체할 수 있습니다:
createSidebarPlugin({
renderCreateCalendarDialog: ({ onCreate, onClose }) => (
<MyCustomDialog onSave={onCreate} onCancel={onClose} />
),
});<template>
<DayFlowCalendar :calendar="calendar">
<template #createCalendarDialog="{ onCreate, onClose }">
<MyCustomDialog @save="onCreate" @cancel="onClose" />
</template>
</DayFlowCalendar>
</template><dayflow-calendar
[calendar]="calendar"
[createCalendarDialog]="customDialog"
>
</dayflow-calendar>
<ng-template #customDialog let-args>
<my-custom-dialog
(save)="args.onCreate($event)"
(cancel)="args.onClose()"
>
</my-custom-dialog>
</ng-template><DayFlowCalendar {calendar} createCalendarDialog={CustomDialog} />
<!-- CustomDialog.svelte -->
<script lang="ts">
import type { CreateCalendarDialogProps } from '@dayflow/core';
let { onCreate, onClose } = $props<CreateCalendarDialogProps>();
</script>
<div class="dialog">
<!-- custom dialog markup -->
<button onclick={() => onCreate({ id: 'new', name: 'New' })}>Create</button>
<button onclick={onClose}>Cancel</button>
</div>사용자 정의 사이드바 헤더
기본 사이드바 헤더("캘린더" 제목과 접기 버튼이 있는 영역)를 교체할 수 있습니다:
createSidebarPlugin({
renderSidebarHeader: ({ isCollapsed, onCollapseToggle }) => (
<div className='flex items-center justify-between p-4 border-b'>
{!isCollapsed && <span className='font-bold text-lg'>My Calendars</span>}
<button
onClick={onCollapseToggle}
className='p-1 hover:bg-slate-100 rounded'
>
{isCollapsed ? '→' : '←'}
</button>
</div>
),
});<template>
<DayFlowCalendar :calendar="calendar">
<template #sidebarHeader="{ isCollapsed, onCollapseToggle }">
<div class="flex items-center justify-between p-4 border-b">
<span v-if="!isCollapsed" class="font-bold text-lg">My Calendars</span>
<button
@click="onCollapseToggle"
class="p-1 hover:bg-slate-100 rounded"
>
{{ isCollapsed ? '→' : '←' }}
</button>
</div>
</template>
</DayFlowCalendar>
</template><dayflow-calendar
[calendar]="calendar"
[sidebarHeader]="headerTemplate"
>
</dayflow-calendar>
<ng-template #headerTemplate let-args>
<div class="flex items-center justify-between p-4 border-b">
<span *ngIf="!args.isCollapsed" class="font-bold text-lg">My Calendars</span>
<button
(click)="args.onCollapseToggle()"
class="p-1 hover:bg-slate-100 rounded"
>
{{ args.isCollapsed ? '→' : '←' }}
</button>
</div>
</ng-template><DayFlowCalendar {calendar} sidebarHeader={CustomHeader} />
<!-- CustomHeader.svelte -->
<script lang="ts">
import type { SidebarHeaderSlotArgs } from '@dayflow/core';
let { isCollapsed, onCollapseToggle } = $props<SidebarHeaderSlotArgs>();
</script>
<div class="custom-header">
{#if !isCollapsed}
<span>My Calendars</span>
{/if}
<button onclick={onCollapseToggle}>
{isCollapsed ? '→' : '←'}
</button>
</div>