white printer paperr

공간 예약할 때, 이미 등록된 일시는 알림으로 표시

연구자의 집 홈페이지 대관을 수정.  기존에는 관리자가 공간 예약을 직접 확인하고 있었는데, 예약이 많아 질 수도 있어서 수정.  전에는 겹치는 때가 거의 없었음.

 

jQuery를 기본으로 datetimepicker를 사용하고 있음.

먼저, 예약된 시간 데이터를 AJAX를 통해 미리 불러오고, 대관 시작일 (rent_day_date-place-start)과 대관 끝나는 시간 (rent_day_date-place-end)을 데이터베이스에서 가져와 배열 형태로 전달.

var reservedSlots = [];

 

function.php코드는

/**
 * 대관 시작일시 중복되지 않게 신청 자체가 안 되게 합시다
 */
add_action('wp_ajax_get_reserved_dates', 'get_reserved_dates');
add_action('wp_ajax_nopriv_get_reserved_dates', 'get_reserved_dates');

function get_reserved_dates() {
    global $wpdb;

    // 오늘 날짜의 00시부터 포함되도록 설정
    $today = date('Y-m-d 00:00:00');

    $results = $wpdb->get_results($wpdb->prepare("
        SELECT pm.meta_value AS start_time, 
               (SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = pm.post_id AND meta_key = 'rent_day_date-place-end') AS end_time
        FROM {$wpdb->postmeta} pm
        INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
        WHERE pm.meta_key = 'rent_day_date-place-start'
        AND pm.meta_value >= %s
        AND p.post_status = 'publish'
    ", $today));

    $reserved_dates = [];
    foreach ($results as $row) {
        $startDateTime = new DateTime($row->start_time);
        $endTime = $row->end_time;

        // date-place-end의 시간을 start의 날짜에 붙여줌
        $endDateTime = new DateTime($startDateTime->format('Y-m-d') . ' ' . $endTime);

        $reserved_dates[] = [
            'start' => $startDateTime->format('Y-m-d H:i:s'),
            'end' => $endDateTime->format('Y-m-d H:i:s')
        ];
    }

    wp_send_json_success($reserved_dates);
    wp_die();
}

 

 

style은 나중에 반영하고.


/* 시간 선택 필요 표시 */
    .xdsoft_time_variant.time-selection-needed {
        border: 2px solid #ff5722;
        border-radius: 4px;
    }
    
    /* 사용자 안내 메시지 스타일 */
    .time-selection-message {
        color: #ff5722;
        background-color: #fff8f6;
        padding: 5px 10px;
        border-radius: 4px;
        font-size: 12px;
        text-align: center;
        margin-top: 5px;
        display: none;
    }
    
    /* 날짜 선택 완료 시 표시 */
    .xdsoft_datetimepicker.date-selected .xdsoft_calendar {
        border-bottom: 2px solid #4CAF50;
    }
    
    /* 중복된 필드 강조 표시 */
    input.overlap-error {
        border: 2px solid #ff5722 !important;
        background-color: #fff8f6 !important;
    }
    
    /* 중복 안내 메시지 */
    .overlap-message {
        color: #ff5722;
        font-size: 12px;
        margin-top: 5px;
        display: none;
    }

이제 페이지 로드 시 AJAX를 통해 예약된 시간을 가져와 DateTimePicker에 반영.

콘솔에서 Network → XHR → admin-ajax.php → Response에서 JSON 데이터가 제대로 오는지 확인

// 메시지 요소를 추가합니다
jQuery('#date-place-start').after('<div class="time-selection-message">시간을 선택해주세요 ↓</div>');
jQuery('#date-place-start').after('<div class="overlap-message start-overlap">선택한 시작 시간이 이미 예약된 시간과 겹칩니다</div>');
jQuery('#date-place-end').after('<div class="overlap-message end-overlap">선택한 종료 시간이 이미 예약된 시간과 겹칩니다</div>');

jQuery.datetimepicker.setLocale('ko');

// 날짜 제한 설정 (오늘부터 3일 이후 ~ 90일 후)
var myDateStart = new Date(new Date().getTime() + (3 * 24 * 60 * 60 * 1000));
var myDateEnd = new Date(new Date().getTime() + (90 * 24 * 60 * 60 * 1000));

// 예약된 시간 목록
var reservedSlots = [];

// AJAX 응답 처리 (초 단위 + 날짜 포맷 조정)
jQuery.ajax({
    url: "<?php echo admin_url('admin-ajax.php'); ?>",
    type: "POST",
    data: { action: 'get_reserved_dates' },
    success: function(response) {
        if (response.success) {
            reservedSlots = response.data.map(function(slot) {
                // 시작 시간은 날짜 포함, 종료 시간은 시간만 추출
                var startDate = new Date(slot.start);
                var endDate = new Date(slot.end);
                
                var formattedStart = 
                    startDate.getFullYear() + '-' +
                    String(startDate.getMonth() + 1).padStart(2, '0') + '-' +
                    String(startDate.getDate()).padStart(2, '0') + ' ' +
                    String(startDate.getHours()).padStart(2, '0') + ':' +
                    String(startDate.getMinutes()).padStart(2, '0');
                
                var formattedEnd = 
                    String(endDate.getHours()).padStart(2, '0') + ':' +
                    String(endDate.getMinutes()).padStart(2, '0');
                
                return {
                    start: startDate,
                    end: endDate,
                    formattedStart: formattedStart, // "2025-06-06 18:00"
                    formattedEnd: formattedEnd      // "21:00"
                };
            });
        }
    }
});

// 중복 시간 체크 함수 (초 제거)
function checkReservationOverlap(selectedStart, selectedEnd) {
    return reservedSlots.find(function(slot) {
        // 날짜 비교 로직은 동일
        return (
            (selectedStart >= slot.start && selectedStart < slot.end) ||
            (selectedEnd > slot.start && selectedEnd <= slot.end) ||
            (selectedStart <= slot.start && selectedEnd >= slot.end)
        );
    });
}

// 상세 중복 체크 (어떤 부분이 중복되는지 확인)
function checkDetailedOverlap(selectedStart, selectedEnd) {
    // 같은 날짜인지 먼저 확인 (월과 일이 맞아야 함)
    function isSameDate(date1, date2) {
        return date1.getFullYear() === date2.getFullYear() && 
               date1.getMonth() === date2.getMonth() && 
               date1.getDate() === date2.getDate();
    }
    
    // 각 예약된 슬롯에 대해 확인
    for (var i = 0; i < reservedSlots.length; i++) {
        var slot = reservedSlots[i];
        
        // 같은 날짜가 아니면 건너뜀
        if (!isSameDate(selectedStart, slot.start)) {
            continue;
        }
        
        // 중복 유형 판별
        var startOverlap = (selectedStart >= slot.start && selectedStart < slot.end);
        var endOverlap = (selectedEnd > slot.start && selectedEnd <= slot.end);
        var containsOverlap = (selectedStart <= slot.start && selectedEnd >= slot.end);
        
        // 중복이 발견되면
        if (startOverlap || endOverlap || containsOverlap) {
            var overlapType;
            
            if (startOverlap && endOverlap) {
                // 시작과 종료 시간 모두 중복
                overlapType = 'both';
            } else if (startOverlap) {
                // 시작 시간만 중복
                overlapType = 'start';
            } else if (endOverlap) {
                // 종료 시간만 중복
                overlapType = 'end';
            } else if (containsOverlap) {
                // 선택 범위가 기존 예약을 완전히 포함
                overlapType = 'both';
            }
            
            // 알림 표시 및 적절한 필드로 포커스 이동
            showOverlapAlert(slot, overlapType);
            return true;
        }
    }
    
    // 중복 없음
    return false;
}

// 중복 체크 시 알림 메시지 생성 및 커서 이동
function showOverlapAlert(overlap, overlapType) {
    // 모든 중복 표시 초기화
    jQuery('#date-place-start, #date-place-end').removeClass('overlap-error');
    jQuery('.overlap-message').hide();
    
    // 포맷: "시작날짜 시작시간 ~ 종료시간"
    alert(`⚠️ 이미 예약된 시간대:\n${overlap.formattedStart} ~ ${overlap.formattedEnd}`);
    
    // 중복 타입에 따라 적절한 필드로 포커스 이동 및 시각적 표시
    if (overlapType === 'start') {
        // 시작 시간이 중복되면 시작 필드로 포커스 및 강조
        jQuery('#date-place-start').addClass('overlap-error');
        jQuery('.start-overlap').show();
        setTimeout(function() {
            jQuery('#date-place-start').datetimepicker('show');
        }, 100);
    } else if (overlapType === 'end') {
        // 종료 시간이 중복되면 종료 필드로 포커스 및 강조
        jQuery('#date-place-end').addClass('overlap-error');
        jQuery('.end-overlap').show();
        setTimeout(function() {
            jQuery('#date-place-end').datetimepicker('show');
        }, 100);
    } else if (overlapType === 'both') {
        // 둘 다 중복이면 시작 필드로 포커스 및 둘 다 강조
        jQuery('#date-place-start, #date-place-end').addClass('overlap-error');
        jQuery('.start-overlap, .end-overlap').show();
        setTimeout(function() {
            jQuery('#date-place-start').datetimepicker('show');
        }, 100);
    }
}

// 날짜만 선택된 상태 추적 변수
var dateOnlySelected = false;

// 첫 번째 필드 설정 (날짜 + 시간)
jQuery('#date-place-start').datetimepicker({
    minDate: myDateStart,
    maxDate: myDateEnd,
    step: 60,
    format: 'Y-m-d H:i',
    allowTimes: ['11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00'],
    timepicker: true,
    // 날짜와 시간을 분리해서 선택하도록 설정
    onGenerate: function(ct, $input) {
        // datetimepicker가 생성될 때마다 이벤트 설정
        if (dateOnlySelected) {
            // 시간 선택 영역 강조
            jQuery('.xdsoft_time_variant').addClass('time-selection-needed');
            jQuery('.time-selection-message').show();
            jQuery('.xdsoft_datetimepicker').addClass('date-selected');
        }
    },
    onSelectDate: function(ct, $input) {
        // 날짜만 선택된 상태에서는 시간 선택 화면으로 자동 전환
        var dateStr = ct.getFullYear() + '-' + 
                      String(ct.getMonth() + 1).padStart(2, '0') + '-' + 
                      String(ct.getDate()).padStart(2, '0');
                      
        // 두 번째 필드에 사용할 날짜 저장
        jQuery('#date-place-end').data('selected-date', dateStr);
        
        // 날짜만 선택 상태 설정
        dateOnlySelected = true;
        
        // 시간 선택 영역 강조
        setTimeout(function() {
            jQuery('.xdsoft_time_variant').addClass('time-selection-needed');
            jQuery('.time-selection-message').show();
            jQuery('.xdsoft_datetimepicker').addClass('date-selected');
        }, 50);
        
        // 날짜만 선택 - 시간은 선택하게 함
        // 현재 input에 날짜만 설정 (시간 선택으로 넘어감)
        $input.val(dateStr + ' ');
    },
    onSelectTime: function(ct, $input) {
        if (!ct) return;
        
        // 시간 선택 표시 제거
        dateOnlySelected = false;
        jQuery('.xdsoft_time_variant').removeClass('time-selection-needed');
        jQuery('.time-selection-message').hide();
        jQuery('.xdsoft_datetimepicker').removeClass('date-selected');
        
        var dateStr = ct.getFullYear() + '-' + 
                      String(ct.getMonth() + 1).padStart(2, '0') + '-' + 
                      String(ct.getDate()).padStart(2, '0');
        
        // 현재 종료 시간 값 확인
        var currentEndTime = jQuery('#date-place-end').val();
        var startTime = ct.getHours() + ':' + String(ct.getMinutes()).padStart(2, '0');
        
        // 두번째 필드 값이 없는 경우에만 자동 설정
        if (!currentEndTime) {
            // 종료 시간 자동 설정 (시작시간 + 1시간)
            var startHour = ct.getHours();
            var endHour = startHour + 1;
            
            // 종료 시간이 22시를 넘으면 22시로 설정
            if (endHour > 22) {
                endHour = 22;
            }
            
            var endTime = String(endHour).padStart(2, '0') + ':00';
            jQuery('#date-place-end').val(endTime);
        } else {
            // 두번째 필드에 값이 있는 경우, 시간 간격 유지 (옵션)
            var endTimeParts = currentEndTime.split(':');
            var startHour = ct.getHours();
            var endHour = parseInt(endTimeParts[0]);
            
            // 종료 시간과 시작 시간의 차이가 최소 1시간 이상인지 확인
            if (endHour <= startHour) {
                endHour = startHour + 1;
                if (endHour > 22) endHour = 22;
                jQuery('#date-place-end').val(String(endHour).padStart(2, '0') + ':00');
            }
        }
        
        setTimeout(function() {
            validateTimeRange();
        }, 100);
    },
    onChangeDateTime: function(currentDateTime) {
        if (!currentDateTime) return;
        
        var dateStr = currentDateTime.getFullYear() + '-' + 
                      String(currentDateTime.getMonth() + 1).padStart(2, '0') + '-' + 
                      String(currentDateTime.getDate()).padStart(2, '0');
        jQuery('#date-place-end').data('selected-date', dateStr);
    },
    onClose: function(selectedDate, $input) {
        // 닫을 때 시간이 선택되지 않았으면 다시 열기
        var inputVal = $input.val();
        if (dateOnlySelected && (!inputVal.includes(':') || inputVal.endsWith(' '))) {
            setTimeout(function() {
                $input.datetimepicker('show');
                // 시간 선택 영역 강조
                jQuery('.xdsoft_time_variant').addClass('time-selection-needed');
                jQuery('.time-selection-message').show();
                jQuery('.xdsoft_datetimepicker').addClass('date-selected');
            }, 100);
        } else {
            // 시간 선택 완료 시 강조 스타일 제거
            dateOnlySelected = false;
            jQuery('.xdsoft_time_variant').removeClass('time-selection-needed');
            jQuery('.time-selection-message').hide();
            jQuery('.xdsoft_datetimepicker').removeClass('date-selected');
        }
    }
});

// 두 번째 필드 설정 (시간만)
jQuery('#date-place-end').datetimepicker({
    datepicker: false,
    format: 'H:i',
    allowTimes: ['12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00', '22:00'],
    step: 60,
    onSelectTime: function(ct, $input) {
        setTimeout(function() {
            validateTimeRange();
        }, 100);
    }
});

// 공통 검증 함수 (두 필드 값 조합 후 중복 체크)
function validateTimeRange() {
    var startInput = jQuery('#date-place-start').val();
    var endInput = jQuery('#date-place-end').val();
    var dateStr = jQuery('#date-place-end').data('selected-date');
    
    // 데이터가 완전하지 않으면 검증 건너뛰기
    if (!startInput || !endInput || !dateStr || startInput.indexOf(':') === -1) return;

    var startDate = new Date(startInput);
    var endDate = new Date(dateStr + ' ' + endInput);

    // 종료 시간이 시작 시간보다 이후인지 확인
    if (endDate <= startDate) {
        alert('종료 시간은 시작 시간보다 이후여야 합니다.');
        // 기본값으로 종료 시간 설정 (시작 시간 + 1시간)
        var startHour = startDate.getHours();
        var endHour = startHour + 1;
        if (endHour > 22) endHour = 22;
        var newEndTime = String(endHour).padStart(2, '0') + ':00';
        jQuery('#date-place-end').val(newEndTime);
        // 종료 시간 필드로 포커스 이동
        setTimeout(function() {
            jQuery('#date-place-end').datetimepicker('show');
        }, 100);
        return;
    }
    
    // 상세 중복 체크 (어떤 부분이 중복되는지 확인)
    checkDetailedOverlap(startDate, endDate);
}

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다