@vuepkg/calendar
v1.0.0
Published
Vue 3 schedule calendar — Month / Week / Day / List views, zero extra dependencies
Maintainers
Readme
@vuepkg/calendar
Vue 3 schedule calendar — Month / Week / Day / List views, zero extra dependencies (vue peer only).
Tailwind/shadcn 친화적 — scoped slot으로 부분 커스터마이징하거나, 스타일드 컴포넌트 없이 로직만 쓰는 @vuepkg/calendar/headless로 완전히 직접 그릴 수 있습니다.
📖 문서 사이트 · 🚀 시작하기 · 📦 npm · ⚡️ Playground
English Quick Start
npm install @vuepkg/calendar// main.ts
import '@vuepkg/calendar/style.css'<script setup lang="ts">
import { ref } from 'vue'
import { ScheduleCalendar, useScheduleCalendarHost, type Schedule } from '@vuepkg/calendar'
const schedules = ref<Schedule[]>([])
const { view, date, listFilterDate, viewScope, scheduleTypes, calendarListeners } =
useScheduleCalendarHost({
onQueryChange: async ({ range }) => {
schedules.value = await fetchSchedules(range.start, range.end)
},
})
</script>
<template>
<div style="height: 100vh">
<ScheduleCalendar
:schedules="schedules"
v-model:view="view"
v-model:date="date"
v-model:list-filter-date="listFilterDate"
v-model:view-scope="viewScope"
v-model:schedule-types="scheduleTypes"
v-on="calendarListeners"
/>
</div>
</template>Key props: schedules, holidays, view, date, monthWeekCount, scheduleTypeOptions, fetchPublicHolidays, hideToolbar
Key emits: query-change, schedule-click, time-slot-select, schedule-move, schedule-resize, navigate
Browser support: Latest Chrome / Firefox / Safari / Edge (Evergreen) — IE not supported
For full API docs → vuepkg.github.io/calendar
주요 기능
- 4가지 뷰 — Month / Week / Day / List
- 월간 동적 레이아웃 — 6주 행이 부모 높이에 맞춰 균등 분할
- 월간
+N팝오버 — 숨긴 일정 목록 - 멀티데이 spanning 바 — 2일 이상 종일 일정 바 표시
- 월간 뷰 변형 —
monthWeekCount: 2 | 3 | 6prop으로 2주/3주 컴팩트 뷰 전환 - Week/Day 시간 슬롯 선택 — 빈 셀 드래그로 시간 범위 선택
- 드래그&드롭 — Week/Day 뷰 일정 이동(schedule-move) · 리사이즈(schedule-resize)
- 반복 일정 — 매일/매주(요일 지정)/매월/매년, 횟수·종료일 조건
- 일정 CRUD 모달 —
ScheduleFormModal컴포넌트 (반복 일정 UI 내장) - 공휴일·기념일 — 사내 기념일 prop + 한국 공공 API opt-in
- 커스텀 일정 타입 —
scheduleTypeOptions로 도메인 타입·색상 등록 - CSS 변수 테마 —
--vp-*변수로 색상·크기·간격 전면 커스터마이징, 다크 모드 지원 - TypeScript — 완전한 타입 선언 포함
적합한 사용처
- Admin Dashboard — 내부 운영 도구의 일정 관리 패널
- Booking / Reservation — 예약 시스템의 시간대 배정 UI
- Company Groupware — 팀 일정 공유 캘린더
- Task Management — 마감일 기반 작업 트래킹
다른 라이브러리와 비교
| 기능 | @vuepkg/calendar | FullCalendar | vue-cal |
| ---- | :---: | :---: | :---: |
| Vue 3 Composition API 네이티브 | ✅ | ⚠️ 프레임워크 어댑터(@fullcalendar/vue3) | ⚠️ props/emit 중심 |
| TypeScript | ✅ | ✅ | ✅ |
| Zero dependency | ✅ (vue peer만) | ⚠️ core + 플러그인 다중 조합 | ✅ |
| Headless (컴포넌트 없이 로직만) | ✅ /headless | ⚠️ | ❌ |
| Slot 커스터마이징 | ✅ toolbar/day-cell/event/month-overflow-item | ⚠️ | ✅ |
| Drag & Drop 이동·리사이즈 | ✅ | ✅ | ✅ |
| 반복 일정 | ✅ 무료 | ✅ RRule 플러그인, 무료 | ❌ |
| Timeline / Resource 뷰 | ✅ 무료 (v1: 읽기 전용, /timeline) | ✅ Premium 유료 | ❌ |
| 번들 사이즈 | 21.78KB (brotli, size-limit CI 게이트) | core+플러그인 조합 — bundlephobia에서 직접 비교 | bundlephobia에서 직접 비교 |
FullCalendar Premium 여부는 2026-07-02 공식 pricing·premium 문서 기준. 각 라이브러리의 최신 조건은 공식 문서를 확인하세요. 자체 번들 사이즈는 eager 로드되는 공유 UI 청크까지 포함해 측정한 수치입니다(과거 측정에서 누락돼 있었음 — roadmap.md 참고).
설치
npm install @vuepkg/calendar// main.ts — CSS 전역 임포트 (필수)
import '@vuepkg/calendar/style.css'빠른 시작
<script setup lang="ts">
import { ref } from 'vue'
import {
ScheduleCalendar,
useScheduleCalendarHost,
type Schedule,
} from '@vuepkg/calendar'
const schedules = ref<Schedule[]>([
{
id: '1',
title: '팀 회의',
type: 'team_schedule',
participantId: 'user-1',
participantName: '홍길동',
start: new Date(2026, 5, 15, 10, 0),
end: new Date(2026, 5, 15, 11, 0),
},
])
const { view, date, listFilterDate, viewScope, scheduleTypes, calendarListeners } =
useScheduleCalendarHost()
</script>
<template>
<!-- 부모에 height 지정 필수 — 캘린더가 100% 채움 -->
<div style="height: 100vh; display: flex; flex-direction: column;">
<ScheduleCalendar
:schedules="schedules"
v-model:view="view"
v-model:date="date"
v-model:list-filter-date="listFilterDate"
v-model:view-scope="viewScope"
v-model:schedule-types="scheduleTypes"
v-on="calendarListeners"
/>
</div>
</template>초보자 팁:
useScheduleCalendarHost와v-on="calendarListeners"가 낯설다면 시작하기 가이드에서 단계별 설명을 확인하세요.
API에서 일정 가져오기
뷰·날짜가 바뀔 때마다 query-change가 발생합니다. onQueryChange로 API 호출을 연결합니다:
const { view, date, listFilterDate, viewScope, scheduleTypes, calendarListeners } =
useScheduleCalendarHost({
onQueryChange: async (payload) => {
schedules.value = await fetchSchedules(payload.range.start, payload.range.end)
},
})일정 생성·수정 연결
ScheduleFormModal은 일정 생성/수정/삭제 폼을 제공하는 선택적 컴포넌트입니다. ScheduleCalendar와 마찬가지로 controlled — 모달의 열림 상태와 schedules 갱신은 항상 부모가 소유합니다.
<script setup lang="ts">
import { ref } from 'vue'
import {
ScheduleCalendar,
ScheduleFormModal,
useScheduleCalendarHost,
upsertSchedule,
removeSchedule,
type Schedule,
type Participant,
} from '@vuepkg/calendar'
const schedules = ref<Schedule[]>([/* ... */])
const participants: Participant[] = [{ id: 'user-1', name: '홍길동' }]
const modalOpen = ref(false)
const modalMode = ref<'create' | 'edit'>('create')
const activeSchedule = ref<Schedule | null>(null)
const initialStart = ref<Date>()
const initialEnd = ref<Date>()
const { view, date, listFilterDate, viewScope, scheduleTypes, calendarListeners } =
useScheduleCalendarHost({
onTimeSlotSelect(payload) {
modalMode.value = 'create'
activeSchedule.value = null
initialStart.value = payload.start
initialEnd.value = payload.end
modalOpen.value = true
},
onScheduleClick(payload) {
const masterId = payload.schedule.recurrenceId ?? payload.schedule.id
modalMode.value = 'edit'
activeSchedule.value = schedules.value.find((s) => s.id === masterId) ?? payload.schedule
modalOpen.value = true
},
})
function handleSubmit(schedule: Schedule) {
schedules.value = upsertSchedule(schedules.value, schedule)
modalOpen.value = false
}
function handleDelete(scheduleId: string) {
schedules.value = removeSchedule(schedules.value, scheduleId)
modalOpen.value = false
}
</script>
<template>
<ScheduleCalendar
:schedules="schedules"
v-model:view="view"
v-model:date="date"
v-model:list-filter-date="listFilterDate"
v-model:view-scope="viewScope"
v-model:schedule-types="scheduleTypes"
v-on="calendarListeners"
/>
<ScheduleFormModal
:open="modalOpen"
:mode="modalMode"
:schedule="activeSchedule"
:initial-start="initialStart"
:initial-end="initialEnd"
:participants="participants"
:existing-schedules="schedules"
@close="modalOpen = false"
@submit="handleSubmit"
@delete="handleDelete"
/>
</template>ScheduleFormModal이 원하는 폼 UI와 다르다면 데이터 헬퍼만 가져다 직접 다이얼로그를 구성할 수 있습니다:
import { upsertSchedule, removeSchedule, buildScheduleFromDraft } from '@vuepkg/calendar'
schedules.value = upsertSchedule(
schedules.value,
buildScheduleFromDraft(draft, participants, schedules.value),
)
schedules.value = removeSchedule(schedules.value, targetId)ScheduleFormModal Props / Emits
| Prop | 타입 | 설명 |
| ---- | ---- | ---- |
| open | boolean | 모달 표시 여부 |
| mode | 'create' \| 'edit' | 생성/수정 모드 |
| schedule | Schedule \| null | edit 모드 수정 대상 |
| initialStart / initialEnd | Date | create 모드 초기 시간 — time-slot-select payload 전달 |
| participants | Participant[] | 참가자 선택지 + 이름 조회 |
| existingSchedules | Schedule[] | create 모드 ID 충돌 방지용 (기본 []) |
| scheduleTypeOptions | ScheduleTypeOption[] | 유형 선택지 (기본 SCHEDULE_TYPE_OPTIONS) |
| labels | Partial<ScheduleFormLabels> | 폼 표시 텍스트 국제화 오버라이드 — 미지정 필드는 한국어 기본값 유지 |
| 이벤트 | 페이로드 | 설명 |
| ------ | -------- | ---- |
| close | — | 취소·Esc·배경 클릭 |
| submit | Schedule | buildScheduleFromDraft로 완성된 일정 — upsertSchedule에 바로 전달 |
| delete | string (scheduleId) | edit 모드 삭제 버튼 — removeSchedule에 바로 전달 |
국제화(i18n)
ScheduleFormModal의 기본 텍스트는 한국어입니다. labels prop에 원하는 필드만 넘기면 해당 항목만 덮어씁니다(나머지는 한국어 기본값 유지). 전체 필드 목록은 ScheduleFormLabels 타입, 기본값은 DEFAULT_SCHEDULE_FORM_LABELS 상수를 참고하세요.
<ScheduleFormModal
:labels="{
createTitle: 'Create Event',
editTitle: 'Edit Event',
titleField: 'Title',
typeField: 'Type',
participantField: 'Participant',
startField: 'Start',
endField: 'End',
cancelButton: 'Cancel',
saveButton: 'Save',
createButton: 'Create',
deleteButton: 'Delete',
recurrenceWeekdayLabels: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
}"
...
/>라벨 오버라이드만으로 부족하다면(필드 재배치, 다른 위젯 등) buildScheduleFromDraft + 직접 구현한 폼을 권장합니다 — 위 "데이터 헬퍼만 가져다 직접 다이얼로그를 구성" 예제를 참고하세요.
반복 일정
Schedule.recurrence에 RecurrenceRule을 지정하면 캘린더가 표시 중인 기간 내에서 자동으로 개별 회차를 생성합니다. 부모는 마스터 일정 한 건만 관리하면 됩니다.
import type { Schedule } from '@vuepkg/calendar'
const schedule: Schedule = {
id: 's-standup',
title: '주간 스탠드업',
type: 'team_schedule',
participantId: 'user-1',
participantName: '홍길동',
start: new Date(2026, 3, 6, 9, 0),
end: new Date(2026, 3, 6, 9, 30),
recurrence: {
freq: 'weekly', // 'daily' | 'weekly' | 'monthly' | 'yearly'
interval: 1,
byWeekday: [1, 3, 5], // 0=일 ~ 6=토
count: 20, // count 또는 until 중 하나
},
}- 각 회차는
id: "${마스터id}::YYYY-MM-DD",recurrenceId,isRecurrenceInstance: true를 가진 파생 객체입니다. ScheduleFormModal은 반복 없음/매일/매주/매월/매년 선택 UI를 내장합니다.- 반복 일정 칩에는 ⟳ 아이콘이 표시됩니다.
알려진 제약: 자체 RRULE 서브셋으로 iCal 전체 사양과 호환되지 않습니다(BYSETPOS 미지원, 월말 기준일 롤오버 등) — 상세: 반복 일정 가이드 § 알려진 제약
Props
| Prop | 타입 | 기본값 | 설명 |
| ---- | ---- | ------ | ---- |
| schedules | Schedule[] | [] | 표시할 일정 목록 |
| holidays | Holiday[] | [] | 기념일·공휴일 목록 |
| view | CalendarView | 'month' | 현재 뷰 ('month'|'week'|'day'|'list') |
| date | Date | 오늘 | 현재 기준 날짜 |
| monthWeekCount | 2 \| 3 \| 6 | 6 | 월간 뷰 표시 주 수 (2주·3주 컴팩트 모드) |
| viewScope | ViewScope | 'company' | 일정 범위 필터 ('my'|'company') |
| scheduleTypes | string[] \| null | null | 활성 일정 타입 필터 (null=전체) |
| listFilterDate | Date \| null | null | List 뷰 날짜 필터 |
| scheduleTypeOptions | ScheduleTypeOption[] | 기본 3종 | 커스텀 일정 타입 정의 |
| fetchPublicHolidays | boolean | false | 한국 공공 API 공휴일 opt-in |
| hideToolbar | boolean | false | 툴바 숨김 (뷰 고정 임베딩용) |
Emits
| 이벤트 | 페이로드 | 설명 |
| ------ | -------- | ---- |
| view-change | CalendarViewChangePayload | 뷰 탭 전환 |
| date-select | CalendarDateSelectPayload | 날짜 클릭 |
| navigate | CalendarNavigatePayload | ‹ › · Today 네비 |
| schedule-click | CalendarScheduleClickPayload | 일정 클릭 |
| time-slot-select | CalendarTimeSlotSelectPayload | Week/Day 빈 셀 드래그 (시간 범위 선택) |
| schedule-move | CalendarScheduleMovePayload | Week/Day 일정 드래그 이동 |
| schedule-resize | CalendarScheduleResizePayload | Week/Day 일정 하단 리사이즈 |
| overflow-click | CalendarOverflowClickPayload | 월간 +N 클릭 |
| list-filter-clear | — | List 날짜 필터 해제 |
| query-change | ScheduleQueryChangePayload | 범위·필터 변경 (API 조회용) |
useScheduleCalendarHost를 사용하면 위 이벤트가 자동으로 연결됩니다.v-on="calendarListeners"만 추가하면 됩니다.
Headless 서브패스
ScheduleCalendar/ScheduleFormModal Vue 컴포넌트 없이 composable·타입·일정 CRUD 유틸만 필요하다면 @vuepkg/calendar/headless를 사용하세요. 스타일드 컴포넌트를 전혀 import하지 않아 커스텀 UI 위에 캘린더 로직만 재사용하고 싶을 때 유용합니다.
import {
useCalendar,
useTimeSlotSelection,
useScheduleDrag,
expandRecurringSchedules,
buildScheduleFromDraft,
} from '@vuepkg/calendar/headless'
usePublicHolidays(한국 공공데이터포털 연동)는 별도 서브패스@vuepkg/calendar/locale-kr로 분리되어 있습니다 — 아래 참고.
한국 공휴일 서브패스 (locale-kr)
ScheduleCalendar의 fetchPublicHolidays/publicHolidayServiceKey는 한국 공공데이터포털 API 연동입니다. 이 기능은 코어 번들(index.js/headless.js)에서 완전히 분리되어 있어 — 한국 공휴일을 쓰지 않는 소비자는 이 코드의 바이트를 전혀 지불하지 않고, fetchPublicHolidays가 true가 되는 시점에만 런타임에 로드됩니다.
import { usePublicHolidays } from '@vuepkg/calendar/locale-kr'다른 나라 공휴일은 holidays prop을 직접 채우면 됩니다(국가 무관) — 자세한 레시피는 공휴일 연동 가이드를 참고하세요.
메인 엔트리(@vuepkg/calendar)에서도 동일한 이름으로 그대로 가져올 수 있습니다 — /headless는 별도 export가 추가된 것이 아니라 같은 로직을 컴포넌트 없이 가져오는 경로입니다.
Timeline / Resource 뷰 서브패스
회의실·장비·담당자별로 하루 일정을 가로로 비교하는 뷰입니다. ScheduleCalendar의 5번째 탭이 아니라 @vuepkg/calendar/timeline의 완전히 분리된 독립 컴포넌트입니다 — import하지 않으면 코어 번들에 0바이트 영향입니다. v1은 단일일·시간 단위·읽기 전용(드래그는 v1.1 예정)입니다.
<script setup lang="ts">
import { CalendarTimeline } from '@vuepkg/calendar/timeline'
import type { TimelineResource } from '@vuepkg/calendar/timeline'
const resources: TimelineResource[] = [{ id: 'room-a', label: 'Meeting Room A' }]
</script>
<template>
<div style="height: 400px">
<CalendarTimeline
:date="date"
:resources="resources"
:schedules="schedules"
:get-type-style="getTypeStyle"
/>
</div>
</template>일정을 리소스 행에 연결하려면 Schedule.resourceId를 resources의 id와 매칭시키세요 — 매칭되지 않는 일정(또는 resourceId 미지정)은 표시되지 않습니다. 자세한 내용은 Timeline 가이드를 참고하세요.
커스텀 일정 타입
import { SCHEDULE_TYPE_OPTIONS, type ScheduleTypeOption } from '@vuepkg/calendar'
const typeOptions: ScheduleTypeOption[] = [
...SCHEDULE_TYPE_OPTIONS,
{ type: 'project', label: '프로젝트', color: '#fff', backgroundColor: '#6366f1' },
{ type: 'leave', label: '휴가', color: '#fff', backgroundColor: '#f59e0b' },
]<ScheduleCalendar :schedule-type-options="typeOptions" ... />테마 커스터마이징
모든 색상·크기·간격은 CSS 변수(--vp-*)로 정의되어 있어 한 줄만 덮어쓰면 전체 테마가 변경됩니다.
:root {
--vp-color-primary: #7c3aed;
--vp-today-badge-bg: #7c3aed;
--vp-month-cell-selected-bg: #f5f3ff;
}다크 모드 — 시스템 설정(prefers-color-scheme: dark) 자동 반영 또는 .vp-dark 클래스로 수동 전환:
document.documentElement.classList.toggle('vp-dark')토큰 전체 목록은 테마 커스터마이징 가이드를 참고하세요.
접근성 (a11y)
- 뷰 탭 —
role="group"+aria-pressed, 화살표 키 네비게이션(roving tabindex) - 월간 셀 —
role="grid", 날짜aria-label - 팝오버 — focus trap,
Esc닫기, 외부 클릭 닫기, 닫힘 시 트리거로 focus 복원 - 데이터 테이블(List 뷰) — 행
Enter/Space활성화,aria-label - 키보드 포커스 — 모든 인터랙티브 요소에
:focus-visible아웃라인 - 자동 검증 —
@axe-core/playwright로 Month/Week/Day/List/Timeline 뷰 + 일정 폼 모달을 라이트·다크 모드 각각 CI에서 매 push마다 스캔(e2e/accessibility.spec.ts)
TypeScript
import type {
Schedule,
Holiday,
CalendarView,
ViewScope,
ScheduleTypeOption,
RecurrenceRule,
ScheduleQueryChangePayload,
CalendarScheduleClickPayload,
CalendarTimeSlotSelectPayload,
CalendarScheduleMovePayload,
CalendarScheduleResizePayload,
} from '@vuepkg/calendar'기술 스택
- Vue 3 + TypeScript
- 커스텀 HTML/CSS (외부 UI 라이브러리 의존 없음)
- Vitest · Playwright E2E
Contributing
github.com/vuepkg/calendar · 상세 절차·RFC 프로세스는 CONTRIBUTING.md 참고
요구 사항: Node 24+ · pnpm 9+
pnpm install # 의존성 설치 + Husky pre-push 훅 등록
pnpm --filter @vuepkg/calendar dev # 개발 서버
pnpm verify:push # push 전 검증 (lint + typecheck + vitest)
pnpm turbo run build:lib # 전체 빌드
pnpm test:e2e:ci # 기능 E2E (CI와 동일)
pnpm test:e2e:visual # 시각 회귀 (UI/CSS 변경 시)License
MIT © vuepkg
