/** * 모바일 친화적 캘린더 컴포넌트 * 터치 및 스와이프 지원, 날짜 범위 선택 기능 */ 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;