드래그 앤 드롭
Week/Day 그리드에서 이벤트 이동과 리사이즈를 지원합니다.
동작 방식
- 이동: 이벤트 본체를 드래그 → 새 시각으로 이동 (hour-snapping)
- 리사이즈: 이벤트 하단 8px 핸들을 드래그 → 종료 시각 변경 (최소 1시간)
- 드래그 중 ghost overlay로 새 위치 미리보기
- 원본 이벤트는 40% 투명도로 표시
키보드 대안 (REV-B2)
포인터 드래그는 마우스/터치 전용이라 WCAG 2.2 operable 요구를 단독으로 충족하지 못합니다. 이벤트(.timed-event)는 기본적으로 포커스 가능하며, 포커스된 상태에서 아래 단축키로 동일한 이동/리사이즈를 수행할 수 있습니다 — emit되는 schedule-move/schedule-resize는 포인터 드래그와 완전히 동일한 payload 형태입니다.
| 단축키 | 동작 |
|---|---|
↑ / ↓ | 1시간 단위로 이동 (포인터 드래그와 동일한 시간 단위 스냅) |
Shift + ↑ / ↓ | 종료 시각을 1시간 단위로 조정 (최소 1시간 지속) |
이동/리사이즈가 그리드 경계(시작~종료 시각, 최소 지속시간)에 걸리면 emit이 발생하지 않습니다 — 포인터 드래그가 경계에서 값이 바뀌지 않을 때 emit하지 않는 것과 동일한 규칙입니다.
이벤트 연결
vue
<script setup lang="ts">
import { useScheduleCalendarHost } from '@vuepkg/calendar'
import type { Schedule } from '@vuepkg/calendar'
import { ref } from 'vue'
const schedules = ref<Schedule[]>([...])
const { view, date, calendarListeners } = useScheduleCalendarHost({
onScheduleMove({ schedule, newStart, newEnd }) {
// schedule: 이동된 원본 일정
// newStart, newEnd: 이동 후 시각 (hour-snapped)
schedules.value = schedules.value.map((s) =>
s.id === schedule.id ? { ...s, start: newStart, end: newEnd } : s,
)
},
onScheduleResize({ schedule, newEnd }) {
// schedule: 리사이즈된 원본 일정
// newEnd: 변경된 종료 시각 (hour-snapped)
schedules.value = schedules.value.map((s) =>
s.id === schedule.id ? { ...s, end: newEnd } : s,
)
},
})
</script>Payload 타입
ts
interface CalendarScheduleMovePayload {
schedule: Schedule // 원본 일정 객체
date: Date // 이동 후 날짜 (자정 기준)
newStart: Date // 새 시작 시각
newEnd: Date // 새 종료 시각
}
interface CalendarScheduleResizePayload {
schedule: Schedule // 원본 일정 객체
date: Date // 날짜 (자정 기준)
newEnd: Date // 새 종료 시각
}서버 저장 패턴
ts
const { calendarListeners } = useScheduleCalendarHost({
async onScheduleMove({ schedule, newStart, newEnd }) {
const prev = schedules.value.find((s) => s.id === schedule.id)!
// 낙관적 업데이트
schedules.value = schedules.value.map((s) =>
s.id === schedule.id ? { ...s, start: newStart, end: newEnd } : s,
)
try {
await api.patch(`/schedules/${schedule.id}`, { start: newStart, end: newEnd })
} catch {
// 롤백
schedules.value = schedules.value.map((s) => (s.id === schedule.id ? prev : s))
toast.error('일정 이동에 실패했습니다.')
}
},
})주의 사항
- DnD는 Week/Day 뷰의 timed 그리드에서만 동작합니다 (All-day 바·Month 뷰는 해당 없음)
- 위치 변경 없이 클릭만 한 경우에는
schedule-moveemit이 발생하지 않습니다 (schedule-click만 발생) - 드래그 직후 브라우저가 발생시키는 click 이벤트는 내부적으로 억제됩니다