/**
* 모바일 친화적 캘린더 컴포넌트
* 터치 및 스와이프 지원, 날짜 범위 선택 기능
*/
class MobileCalendar {
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
this.options = {
locale: 'ko-KR',
startDate: null,
endDate: null,
maxRange: 90, // 최대 90일 범위
onDateSelect: null,
onRangeSelect: null,
...options
};
this.currentDate = new Date();
this.selectedStartDate = null;
this.selectedEndDate = null;
this.isSelecting = false;
this.touchStartX = 0;
this.touchStartY = 0;
this.init();
}
init() {
this.render();
this.bindEvents();
}
render() {
const calendarHTML = `
📅 날짜를 터치하여 시작일을 선택하고, 다시 터치하여 종료일을 선택하세요
`;
this.container.innerHTML = calendarHTML;
this.updateCalendar();
}
updateCalendar() {
const year = this.currentDate.getFullYear();
const month = this.currentDate.getMonth();
// 월/년 표시 업데이트
document.getElementById('monthYear').textContent =
`${year}년 ${month + 1}월`;
// 캘린더 그리드 생성
this.generateCalendarGrid(year, month);
}
generateCalendarGrid(year, month) {
const grid = document.getElementById('calendarGrid');
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - firstDay.getDay());
let html = '';
const today = new Date();
// 6주 표시 (42일)
for (let i = 0; i < 42; i++) {
const date = new Date(startDate);
date.setDate(startDate.getDate() + i);
const isCurrentMonth = date.getMonth() === month;
const isToday = this.isSameDate(date, today);
const isSelected = this.isDateInRange(date);
const isStart = this.selectedStartDate && this.isSameDate(date, this.selectedStartDate);
const isEnd = this.selectedEndDate && this.isSameDate(date, this.selectedEndDate);
let classes = ['calendar-day'];
if (!isCurrentMonth) classes.push('other-month');
if (isToday) classes.push('today');
if (isSelected) classes.push('selected');
if (isStart) classes.push('range-start');
if (isEnd) classes.push('range-end');
html += `
${date.getDate()}
`;
}
grid.innerHTML = html;
}
bindEvents() {
// 빠른 선택 버튼들
this.container.querySelectorAll('.quick-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const range = e.target.dataset.range;
this.selectQuickRange(range);
});
});
// 월 네비게이션
document.getElementById('prevMonth').addEventListener('click', () => {
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
this.updateCalendar();
});
document.getElementById('nextMonth').addEventListener('click', () => {
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
this.updateCalendar();
});
// 날짜 선택
this.container.addEventListener('click', (e) => {
if (e.target.classList.contains('calendar-day')) {
this.handleDateClick(e.target);
}
});
// 터치 이벤트 (스와이프 지원)
this.container.addEventListener('touchstart', (e) => {
this.touchStartX = e.touches[0].clientX;
this.touchStartY = e.touches[0].clientY;
});
this.container.addEventListener('touchend', (e) => {
if (!this.touchStartX || !this.touchStartY) return;
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
const diffX = this.touchStartX - touchEndX;
const diffY = this.touchStartY - touchEndY;
// 수평 스와이프가 수직 스와이프보다 클 때만 처리
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
if (diffX > 0) {
// 왼쪽으로 스와이프 - 다음 달
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
} else {
// 오른쪽으로 스와이프 - 이전 달
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
}
this.updateCalendar();
}
this.touchStartX = 0;
this.touchStartY = 0;
});
// 범위 지우기
document.getElementById('clearRange').addEventListener('click', () => {
this.clearSelection();
});
}
handleDateClick(dayElement) {
const dateStr = dayElement.dataset.date;
const date = new Date(dateStr + 'T00:00:00');
if (!this.selectedStartDate || (this.selectedStartDate && this.selectedEndDate)) {
// 새로운 선택 시작
this.selectedStartDate = date;
this.selectedEndDate = null;
this.isSelecting = true;
} else if (this.selectedStartDate && !this.selectedEndDate) {
// 종료일 선택
if (date < this.selectedStartDate) {
// 시작일보다 이전 날짜를 선택하면 시작일로 설정
this.selectedEndDate = this.selectedStartDate;
this.selectedStartDate = date;
} else {
this.selectedEndDate = date;
}
this.isSelecting = false;
// 범위가 너무 크면 제한
const daysDiff = Math.abs(this.selectedEndDate - this.selectedStartDate) / (1000 * 60 * 60 * 24);
if (daysDiff > this.options.maxRange) {
alert(`최대 ${this.options.maxRange}일까지만 선택할 수 있습니다.`);
this.clearSelection();
return;
}
}
this.updateCalendar();
this.updateSelectedRange();
// 콜백 호출
if (this.selectedStartDate && this.selectedEndDate && this.options.onRangeSelect) {
this.options.onRangeSelect(this.selectedStartDate, this.selectedEndDate);
}
}
selectQuickRange(range) {
const today = new Date();
let startDate, endDate;
switch (range) {
case 'today':
startDate = endDate = new Date(today);
break;
case 'week':
startDate = new Date(today);
startDate.setDate(today.getDate() - today.getDay());
endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
break;
case 'month':
startDate = new Date(today.getFullYear(), today.getMonth(), 1);
endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0);
break;
case 'last7':
endDate = new Date(today);
startDate = new Date(today);
startDate.setDate(today.getDate() - 6);
break;
case 'last30':
endDate = new Date(today);
startDate = new Date(today);
startDate.setDate(today.getDate() - 29);
break;
case 'all':
this.clearSelection();
if (this.options.onRangeSelect) {
this.options.onRangeSelect(null, null);
}
return;
}
this.selectedStartDate = startDate;
this.selectedEndDate = endDate;
this.updateCalendar();
this.updateSelectedRange();
if (this.options.onRangeSelect) {
this.options.onRangeSelect(startDate, endDate);
}
}
updateSelectedRange() {
const rangeElement = document.getElementById('selectedRange');
const rangeText = document.getElementById('rangeText');
if (this.selectedStartDate && this.selectedEndDate) {
const startStr = this.formatDate(this.selectedStartDate);
const endStr = this.formatDate(this.selectedEndDate);
const daysDiff = Math.ceil((this.selectedEndDate - this.selectedStartDate) / (1000 * 60 * 60 * 24)) + 1;
rangeText.textContent = `${startStr} ~ ${endStr} (${daysDiff}일)`;
rangeElement.style.display = 'block';
} else if (this.selectedStartDate) {
rangeText.textContent = `시작일: ${this.formatDate(this.selectedStartDate)} (종료일을 선택하세요)`;
rangeElement.style.display = 'block';
} else {
rangeElement.style.display = 'none';
}
}
clearSelection() {
this.selectedStartDate = null;
this.selectedEndDate = null;
this.isSelecting = false;
this.updateCalendar();
this.updateSelectedRange();
}
isDateInRange(date) {
if (!this.selectedStartDate) return false;
if (!this.selectedEndDate) return this.isSameDate(date, this.selectedStartDate);
return date >= this.selectedStartDate && date <= this.selectedEndDate;
}
isSameDate(date1, date2) {
return date1.toDateString() === date2.toDateString();
}
formatDate(date) {
return date.toLocaleDateString('ko-KR', {
month: 'short',
day: 'numeric'
});
}
// 외부에서 호출할 수 있는 메서드들
getSelectedRange() {
return {
startDate: this.selectedStartDate,
endDate: this.selectedEndDate
};
}
setSelectedRange(startDate, endDate) {
this.selectedStartDate = startDate;
this.selectedEndDate = endDate;
this.updateCalendar();
this.updateSelectedRange();
}
}
// 전역으로 노출
window.MobileCalendar = MobileCalendar;