사이드바 플러그인

사이드바 플러그인은 DayFlow 캘린더에 캘린더 관리용 사이드바를 추가합니다. 날짜 이동을 위한 미니 캘린더, 표시 여부를 켜고 끌 수 있는 캘린더 목록, 그리고 캘린더 생성·이름 변경·색상 변경·병합·삭제·가져오기/내보내기 기능을 제공합니다.

설치

플러그인 패키지를 설치하세요:

npm install @dayflow/plugin-sidebar
pnpm add @dayflow/plugin-sidebar
yarn add @dayflow/plugin-sidebar
bun add @dayflow/plugin-sidebar

사용법

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} />

설정

속성타입기본값설명
widthnumber | string'240px'사이드바의 너비입니다(예: 280 또는 '20rem')
miniWidthstring'50px'사이드바가 접혔을 때의 너비입니다
initialCollapsedbooleanfalse사이드바를 접힌 상태로 시작할지 여부입니다
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) => TNodeundefined사이드바 UI 전체를 대체합니다
renderSidebarHeader(args: SidebarHeaderSlotArgs) => TNodeundefined사이드바 헤더를 직접 렌더링합니다
renderCalendarContextMenu(calendar, onClose) => TNodeundefined캘린더 항목의 컨텍스트 메뉴를 직접 렌더링합니다
renderCreateCalendarDialog(props) => TNodeundefined캘린더 생성 대화상자를 직접 렌더링합니다

프로그래밍 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 state

SidebarService

메서드반환값설명
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는 CalendarTypesubscription 객체가 있는지로 구독 캘린더를 식별합니다.

구독의 기본 동작:

  • 읽기 전용: 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()로 갱신하세요.

createSidebarPluginrender 함수를 전달하거나(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>

이 페이지의 내용