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