Files
dnssash/adm/consultant_manage/components/reservation_popup.php
T
2026-06-11 18:47:38 +09:00

868 lines
45 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 상담 예약 팝업 UI 및 데이터 처리
*/
// AJAX 요청 처리
if (isset($_POST['action'])) {
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
header('Content-Type: application/json');
$action = $_POST['action'] ?? '';
$response = ['success' => false, 'message' => '알 수 없는 요청입니다.'];
// 월별 예약 가능일 조회
if ($action === 'get_month_availability') {
$year = (int) ($_POST['year'] ?? 0);
$month = (int) ($_POST['month'] ?? 0);
if ($year && $month) {
$start_date = date('Y-m-d', mktime(0, 0, 0, $month, 1, $year));
$end_date = date('Y-m-t', strtotime($start_date));
$max_advance_days = consultant_get_config('max_advance_days', 30);
$max_date = date('Y-m-d', strtotime("+" . $max_advance_days . " days"));
$sql = "SELECT
specific_date,
SUM(CASE WHEN is_available = 1 AND max_persons > (SELECT COUNT(*) FROM consultant_reservations r WHERE r.reservation_date = s.specific_date AND r.reservation_time = s.start_time AND r.status != 'cancelled') THEN 1 ELSE 0 END) as available_slots,
MAX(CASE WHEN temp_1 = 'holiday' THEN 1 ELSE 0 END) as is_holiday
FROM consultant_schedule s
WHERE specific_date BETWEEN '{$start_date}' AND '{$end_date}'
GROUP BY specific_date";
$result = sql_query($sql);
$availability = [];
while ($row = sql_fetch_array($result)) {
$is_bookable = true;
$reason = '';
if ($row['is_holiday']) {
$is_bookable = false;
$reason = 'holiday';
} elseif ($row['specific_date'] < date('Y-m-d')) {
$is_bookable = false;
$reason = 'past_date';
} elseif ($row['specific_date'] > $max_date) {
$is_bookable = false;
$reason = 'too_far';
} elseif ($row['available_slots'] == 0) {
$is_bookable = false;
$reason = 'full';
}
$availability[date('j', strtotime($row['specific_date']))] = [
'available' => $is_bookable,
'reason' => $reason
];
}
$response = ['success' => true, 'data' => $availability];
}
}
// 특정일의 예약 가능 시간 조회
if ($action === 'get_time_slots') {
$date = preg_replace('/[^0-9\-]/', '', $_POST['date'] ?? '');
if ($date) {
$min_advance_hours = consultant_get_config('min_advance_hours', 24);
$min_datetime = date('Y-m-d H:i:s', strtotime("+" . $min_advance_hours . " hours"));
$sql = "SELECT
s.start_time,
s.max_persons,
(SELECT COUNT(*) FROM consultant_reservations r WHERE r.reservation_date = s.specific_date AND r.reservation_time = s.start_time AND r.status != 'cancelled') as reserved_count
FROM consultant_schedule s
WHERE s.specific_date = '{$date}' AND s.is_available = 1
ORDER BY s.start_time";
$result = sql_query($sql);
$slots = [];
while ($row = sql_fetch_array($result)) {
$slot_datetime = $date . ' ' . $row['start_time'];
$is_too_soon = ($slot_datetime < $min_datetime);
$available_count = $row['max_persons'] - $row['reserved_count'];
$is_full = ($available_count <= 0);
$slots[] = [
'time' => substr($row['start_time'], 0, 5),
'available' => !$is_too_soon && !$is_full,
'reason' => $is_too_soon ? 'too_soon' : ($is_full ? 'full' : ''),
'reserved_count' => (int)$row['reserved_count'],
'max_persons' => (int)$row['max_persons']
];
}
$response = ['success' => true, 'data' => $slots];
}
}
echo json_encode($response);
exit;
}
// 💡 [수정] 아래는 팝업의 HTML/CSS/JS 부분입니다.
if (!defined('_GNUBOARD_')) exit;
include_once(G5_ADMIN_PATH . '/consultant_manage/_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
$consultant_installed = is_consultant_installed();
if($consultant_installed) { // 💡 [추가] 시스템이 설치된 경우에만 팝업을 렌더링합니다.
// 현재 날짜 정보
$current_year = date('Y');
$current_month_num = date('n');
// 💡 [개선] 설정 값을 DB에서 직접 가져와 일관성을 유지합니다.
$consultation_fee = consultant_get_config('consultation_fee', 50000);
$account_info = consultant_get_config('account_info', '');
$max_advance_days = consultant_get_config('max_advance_days', 30);
// 💡 [수정] 스킨 URL 및 AJAX 엔드포인트 설정
$ajax_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_popup.php';
$form_action_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_submit.php';
// 💡 [추가] 리소스 목록 조회
$resources = [];
$res_result = sql_query("SELECT id, name FROM consultant_resources WHERE is_active = 1 ORDER BY group_id, name");
while ($row = sql_fetch_array($res_result)) {
$resources[] = $row;
}
?>
<!-- 예약 팝업 오버레이 -->
<div id="reservation-popup" class="reservation-modal-overlay">
<div class="reservation-modal-content">
<!-- 팝업 헤더 -->
<div class="reservation-modal-header">
<h2>상담 예약 신청</h2>
<button type="button" class="reservation-modal-close" aria-label="팝업 닫기">
<span>&times;</span>
</button>
</div>
<!-- 팝업 본문 -->
<div class="reservation-modal-body">
<!-- 💡 [개선] 로딩 스피너 추가 -->
<div class="loading-overlay" style="display: none;">
<div class="loading-spinner"></div>
</div>
<!-- 단계 표시 -->
<div class="reservation-steps">
<div class="step active" data-step="1">
<span class="step-number">1</span>
<span class="step-text">날짜 선택</span>
</div>
<div class="step" data-step="2">
<span class="step-number">2</span>
<span class="step-text">시간 선택</span>
</div>
<div class="step" data-step="3">
<span class="step-number">3</span>
<span class="step-text">정보 입력</span>
</div>
</div>
<!-- 예약 폼 -->
<form id="reservation-form" method="post"
action="<?php echo $form_action_url; ?>">
<!-- 1단계: 달력 -->
<div class="reservation-step-content" data-step="1">
<div class="step-description">
<h4>📅 상담 날짜를 선택해주세요</h4>
<p>최대 <?php echo $max_advance_days; ?>일 후까지 예약 가능합니다.</p>
</div>
<div class="calendar-container">
<div class="calendar-header">
<button type="button" class="calendar-nav prev-month" aria-label="이전 달">
<span></span>
</button>
<h3 class="calendar-title">
<span id="calendar-year"><?php echo $current_year; ?></span>년
<span id="calendar-month"><?php echo $current_month_num; ?></span>월
</h3>
<button type="button" class="calendar-nav next-month" aria-label="다음 달">
<span></span>
</button>
</div>
<div class="calendar-grid">
<div class="calendar-weekdays">
<div class="weekday">일</div>
<div class="weekday">월</div>
<div class="weekday">화</div>
<div class="weekday">수</div>
<div class="weekday">목</div>
<div class="weekday">금</div>
<div class="weekday">토</div>
</div>
<div class="calendar-days" id="calendar-days">
<!-- 달력 날짜들이 JavaScript로 동적 생성됩니다 -->
</div>
</div>
<div class="calendar-legend">
<div class="legend-item"><span class="legend-color available"></span><span>예약 가능</span></div>
<!-- 💡 [추가] 휴일/마감 상태를 구분하는 범례 추가 -->
<div class="legend-item"><span class="legend-color holiday"></span><span>휴일</span></div>
<div class="legend-item"><span class="legend-color full"></span><span>예약 마감</span></div>
<div class="legend-item"><span class="legend-color unavailable"></span><span>지난날짜</span></div>
<div class="legend-item"><span class="legend-color selected"></span><span>선택</span></div>
</div>
</div>
</div>
<!-- 2단계: 시간 선택 -->
<div class="reservation-step-content" data-step="2" style="display: none;">
<div class="step-description">
<h4>🕐 상담 시간을 선택해주세요</h4>
<div class="selected-date-info"><strong>선택된 날짜: <span id="selected-date-display"></span></strong></div>
</div>
<div class="time-slots-container">
<div class="time-slots-grid" id="time-slots-grid">
<!-- 시간대들이 JavaScript로 동적 생성됩니다 -->
</div>
<div class="time-legend">
<div class="legend-item"><span class="legend-color time-available"></span><span>예약 가능</span></div>
<div class="legend-item"><span class="legend-color time-full"></span><span>예약 마감</span></div>
<div class="legend-item"><span class="legend-color time-too-soon"></span><span>예약 임박</span></div>
<div class="legend-item"><span class="legend-color time-selected"></span><span>선택</span></div>
</div>
</div>
</div>
<!-- 3단계: 고객 정보 입력 -->
<div class="reservation-step-content" data-step="3" style="display: none;">
<div class="step-description">
<h4>📝 고객 정보를 입력해주세요</h4>
</div>
<div class="reservation-summary">
<h5>📋 예약 정보 확인</h5>
<div class="summary-grid">
<div class="summary-item">
<span class="label">📅 예약 날짜:</span>
<span id="summary-date">-</span>
</div>
<div class="summary-item">
<span class="label">🕐 예약 시간:</span>
<span id="summary-time">-</span>
</div>
<div class="summary-item">
<span class="label">💰 상담 비용:</span>
<span><?php echo number_format($consultation_fee); ?>원</span>
</div>
</div>
</div>
<div class="customer-info-form">
<div class="form-row">
<div class="form-group">
<label for="customer-name">👤 이름 <span class="required">*</span></label>
<input type="text" id="customer-name" name="customer_name" required placeholder="홍길동">
</div>
<div class="form-group">
<label for="customer-phone">📱 연락처 <span class="required">*</span></label>
<input type="tel" id="customer-phone" name="customer_phone" required placeholder="010-1234-5678">
</div>
</div>
<div class="form-group">
<label for="customer-email">📧 이메일 <span class="required">*</span></label>
<input type="email" id="customer-email" name="customer_email" required placeholder="example@email.com">
</div>
<div class="form-group">
<label for="consultation-type">🏠 상담 유형</label>
<select id="consultation-type" name="consultation_type">
<option value="onsite">현장 상담</option>
<option value="online">온라인 상담</option>
<option value="phone">전화 상담</option>
</select>
</div>
<!-- 💡 [추가] 담당자/공간 선택 드롭다운 -->
<div class="form-group">
<label for="consultation-resource">👨‍⚕️ 담당자/공간 선택</label>
<select id="consultation-resource" name="resource_id">
<option value="">선택 안 함 (빠른 배정)</option>
<?php foreach ($resources as $res): ?>
<option value="<?php echo $res['id']; ?>"><?php echo htmlspecialchars($res['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="customer-request">📝 요청사항</label>
<textarea id="customer-request" name="customer_request" rows="4" placeholder="상담 관련 요청사항이나 문의사항을 입력해주세요."></textarea>
</div>
</div>
<!-- 숨겨진 필드들 -->
<input type="hidden" id="selected-date" name="reservation_date">
<input type="hidden" id="selected-time" name="reservation_time">
<input type="hidden" name="payment_amount" value="<?php echo $consultation_fee; ?>">
<input type="hidden" name="status" value="payment_pending">
</div>
<!-- 네비게이션 버튼 -->
<div class="reservation-nav-buttons">
<button type="button" class="btn-prev" style="display: none;">← 이전</button>
<button type="button" class="btn-next">다음 →</button>
<button type="submit" class="btn-submit" style="display: none;">예약 신청</button>
</div>
</form>
</div>
</div>
</div>
<!-- 예약 팝업 스타일 -->
<style>
/* 💡 [추가] 로딩 오버레이 스타일 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* 팝업 오버레이 */
.reservation-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
overflow-y: auto;
}
.reservation-modal-overlay.active {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.reservation-modal-content {
background: #fff;
border-radius: 10px;
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
position: relative;
}
.reservation-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 25px;
border-bottom: 1px solid #eee;
background: #f8f9fa;
border-radius: 10px 10px 0 0;
}
.reservation-modal-header h2 { margin: 0; font-size: 20px; font-weight: 600; color: #333; }
.reservation-modal-close {
background: none; border: none; font-size: 24px; cursor: pointer; color: #666;
padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
border-radius: 50%; transition: all 0.2s ease;
}
.reservation-modal-close:hover { background: #e9ecef; color: #333; }
.reservation-modal-body { padding: 25px; }
/* 단계 표시 */
.reservation-steps { display: flex; justify-content: center; margin-bottom: 30px; position: relative; }
.reservation-steps::before {
content: ''; position: absolute; top: 15px; left: 25%; right: 25%;
height: 2px; background: #e9ecef; z-index: 1;
}
.step { display: flex; flex-direction: column; align-items: center; position: relative; z-index: 2; background: #fff; padding: 0 15px; }
.step-number {
width: 30px; height: 30px; border-radius: 50%; background: #e9ecef; color: #6c757d;
display: flex; align-items: center; justify-content: center; font-weight: 600; margin-bottom: 8px; transition: all 0.3s ease;
}
.step.active .step-number { background: #007bff; color: #fff; }
.step.completed .step-number { background: #28a745; color: #fff; }
.step-text { font-size: 12px; color: #6c757d; font-weight: 500; }
.step.active .step-text { color: #007bff; font-weight: 600; }
/* 달력 */
.calendar-container { max-width: 100%; }
.calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.calendar-nav {
background: none; border: 1px solid #ddd; width: 35px; height: 35px; border-radius: 50%;
cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease;
}
.calendar-nav:hover { background: #f8f9fa; border-color: #007bff; }
.calendar-title { margin: 0; font-size: 18px; font-weight: 600; color: #333; }
.calendar-grid { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }
.calendar-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); background: #f8f9fa; }
.weekday { padding: 12px 8px; text-align: center; font-weight: 600; color: #495057; font-size: 14px; border-right: 1px solid #e9ecef; }
.weekday:last-child { border-right: none; }
.calendar-days { display: grid; grid-template-columns: repeat(7, 1fr); }
.calendar-day {
aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
border-right: 1px solid #e9ecef; border-bottom: 1px solid #e9ecef; cursor: pointer;
transition: all 0.2s ease; font-size: 14px; position: relative;
}
.calendar-day:nth-child(7n) { border-right: none; }
.calendar-day.other-month { color: #ccc; background: #f8f9fa; cursor: default; }
.calendar-day.available { background: #fff; color: #333; }
.calendar-day.available:hover { background: #e3f2fd; color: #1976d2; }
.calendar-day.unavailable { background: #f5f5f5; color: #999; cursor: not-allowed; }
.calendar-day.full { background: #fbe9e7; color: #c62828; cursor: not-allowed; } /* 예약 마감 */
.calendar-day.holiday { background: #e8eaf6; color: #3f51b5; cursor: not-allowed; } /* 휴일 */
.calendar-day.selected { background: #007bff; color: #fff; font-weight: 600; }
.calendar-day.today { font-weight: 600; color: #dc3545; }
.calendar-loading, .loading, .error, .no-slots { grid-column: 1 / -1; text-align: center; padding: 40px 20px; color: #666; font-style: italic; }
.error { color: #dc3545; }
/* 범례 */
.calendar-legend, .time-legend { display: flex; justify-content: center; gap: 20px; margin-top: 15px; flex-wrap: wrap; }
.legend-item { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #666; }
.legend-color { width: 12px; height: 12px; border-radius: 2px; border: 1px solid #ddd; }
.legend-color.available { background: #fff; }
.legend-color.unavailable { background: #f5f5f5; }
.legend-color.full { background: #fbe9e7; }
.legend-color.holiday { background: #e8eaf6; }
.legend-color.selected { background: #007bff; }
.legend-color.time-available { background: #e8f5e8; border-color: #28a745; }
.legend-color.time-full { background: #f8d7da; border-color: #dc3545; }
.legend-color.time-too-soon { background: #f1f3f5; border-color: #dee2e6; }
.legend-color.time-selected { background: #007bff; border-color: #007bff; }
/* 시간 선택 */
.selected-date-info { margin-bottom: 10px; }
.step-description { margin-bottom: 20px; }
.step-description h4 { margin: 0 0 10px 0; font-size: 18px; }
.step-description p { margin: 0; font-size: 14px; color: #666; }
.selected-date-info h4 { margin: 0; color: #333; font-size: 16px; }
.time-slots-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: 20px; }
.time-slot {
padding: 12px 8px; border: 2px solid #e9ecef; border-radius: 6px; text-align: center; cursor: pointer;
transition: all 0.2s ease; font-size: 14px; font-weight: 500; display: flex; flex-direction: column; gap: 4px;
}
.time-text { font-weight: 600; }
.slot-info { font-size: 11px; opacity: 0.8; }
/* 💡 [수정] 예약 가능 슬롯 스타일 변경 */
.time-slot.available { background: #e7f3ff; border-color: #007bff; color: #004085; }
.time-slot.available:hover { background: #d4edda; transform: translateY(-1px); }
.time-slot.full { background: #f8d7da; border-color: #dc3545; color: #721c24; cursor: not-allowed; }
.time-slot.too-soon { background: #f1f3f5; color: #868e96; cursor: not-allowed; border-color: #dee2e6; }
.time-slot.selected { background: #007bff; border-color: #007bff; color: #fff; }
/* 고객 정보 폼 */
.reservation-summary { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
.reservation-summary h5 { margin: 0 0 10px 0; font-size: 16px; }
.summary-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }
.summary-item { display: flex; justify-content: space-between; font-size: 14px; }
.summary-item .label { font-weight: 500; color: #495057; }
.summary-item span { color: #333; }
.customer-info-form h4 { margin: 0 0 15px 0; color: #333; font-size: 16px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 8px; font-weight: 500; color: #495057; font-size: 14px; }
.required { color: #dc3545; font-weight: bold; }
.form-group input, .form-group textarea, .form-group select {
width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px;
font-size: 14px; transition: border-color 0.2s ease; box-sizing: border-box;
}
.form-group textarea { resize: vertical; min-height: 80px; }
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
outline: none; border-color: #007bff; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
/* 네비게이션 버튼 */
.reservation-nav-buttons {
display: flex; justify-content: space-between; gap: 10px; margin-top: 25px;
padding-top: 20px; border-top: 1px solid #e9ecef;
}
.reservation-nav-buttons button {
padding: 12px 24px; border: none; border-radius: 6px; font-size: 14px;
font-weight: 600; cursor: pointer; transition: all 0.2s ease; min-width: 100px;
}
.btn-prev { background: #6c757d; color: #fff; }
.btn-prev:hover { background: #5a6268; }
.btn-next, .btn-submit { background: #007bff; color: #fff; }
.btn-next:hover, .btn-submit:hover { background: #0056b3; }
/* 반응형 디자인 - 모바일 최적화 */
@media (max-width: 768px) {
.reservation-modal-content { margin: 10px; max-height: 95vh; border-radius: 8px; }
.reservation-modal-header, .reservation-modal-body { padding: 15px; }
.reservation-modal-header h2 { font-size: 18px; }
.reservation-steps { margin-bottom: 20px; }
.step { padding: 0 10px; }
.step-number { width: 28px; height: 28px; font-size: 13px; }
.step-text { font-size: 11px; }
.calendar-day { font-size: 13px; }
.time-slots-grid { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); }
.form-row { grid-template-columns: 1fr; gap: 0; }
.form-group input, .form-group textarea, .form-group select { font-size: 16px; /* iOS 줌 방지 */ }
}
@media (max-width: 480px) {
.reservation-modal-overlay { padding: 0; align-items: flex-end; }
.reservation-modal-content { margin: 0; width: 100%; max-height: 90vh; border-radius: 10px 10px 0 0; }
.reservation-nav-buttons { flex-direction: column-reverse; gap: 10px; }
.reservation-nav-buttons button { width: 100%; }
}
</style>
<!-- 💡 [개선] JavaScript 로직 전면 수정 -->
<script>
const ReservationPopup = {
elements: {},
state: {
currentStep: 1,
selectedDate: null,
selectedTime: null,
currentYear: new Date().getFullYear(),
currentMonth: new Date().getMonth() + 1,
},
config: {
ajaxUrl: '<?php echo $ajax_url; ?>',
maxAdvanceDays: <?php echo (int)$max_advance_days; ?>,
},
init() {
this.elements.popup = document.getElementById('reservation-popup');
if (!this.elements.popup) return;
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
this.elements.form = this.elements.popup.querySelector('#reservation-form');
this.elements.calendar = {
year: this.elements.popup.querySelector('#calendar-year'),
month: this.elements.popup.querySelector('#calendar-month'),
days: this.elements.popup.querySelector('#calendar-days'),
prevBtn: this.elements.popup.querySelector('.prev-month'),
nextBtn: this.elements.popup.querySelector('.next-month'),
};
this.elements.timeSlotsGrid = this.elements.popup.querySelector('#time-slots-grid');
this.elements.nav = {
prevBtn: this.elements.popup.querySelector('.btn-prev'),
nextBtn: this.elements.popup.querySelector('.btn-next'),
submitBtn: this.elements.popup.querySelector('.btn-submit'),
};
this.addEventListeners();
},
addEventListeners() {
// 💡 [수정] 닫기 버튼을 더 안정적으로 찾아 이벤트를 추가하고, null 체크를 추가합니다.
const closeBtn = this.elements.popup.querySelector('.reservation-modal-close');
if (closeBtn) closeBtn.addEventListener('click', () => this.close());
this.elements.popup.addEventListener('click', e => {
if (e.target === this.elements.popup) this.close();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && this.elements.popup.classList.contains('active')) this.close();
});
// 💡 [수정] 모든 이벤트 리스너에 null 체크를 추가하여 스크립트 오류를 방지합니다.
if (this.elements.calendar.prevBtn) this.elements.calendar.prevBtn.addEventListener('click', () => this.changeMonth(-1));
if (this.elements.calendar.nextBtn) this.elements.calendar.nextBtn.addEventListener('click', () => this.changeMonth(1));
if (this.elements.nav.prevBtn) this.elements.nav.prevBtn.addEventListener('click', () => this.goToStep(this.state.currentStep - 1));
if (this.elements.nav.nextBtn) this.elements.nav.nextBtn.addEventListener('click', () => this.goToNextStep());
if (this.elements.form) {
this.elements.form.addEventListener('submit', e => {
e.preventDefault();
this.submitForm();
});
}
},
open() {
this.state.currentYear = new Date().getFullYear();
this.state.currentMonth = new Date().getMonth() + 1;
this.goToStep(1);
this.renderCalendar();
this.elements.popup.classList.add('active');
document.body.style.overflow = 'hidden';
},
close() {
this.elements.popup.classList.remove('active');
document.body.style.overflow = '';
this.elements.form.reset();
},
changeMonth(delta) {
this.state.currentMonth += delta;
if (this.state.currentMonth < 1) {
this.state.currentMonth = 12;
this.state.currentYear--;
} else if (this.state.currentMonth > 12) {
this.state.currentMonth = 1;
this.state.currentYear++;
}
this.renderCalendar();
},
async renderCalendar() {
this.elements.calendar.year.textContent = this.state.currentYear;
this.elements.calendar.month.textContent = this.state.currentMonth;
this.elements.calendar.days.innerHTML = '<div class="loading">달력 정보를 불러오는 중...</div>';
const availability = await this.fetchMonthAvailability();
if (!availability) {
this.elements.calendar.days.innerHTML = '<div class="error">달력 정보를 불러올 수 없습니다.</div>';
return;
}
this.elements.calendar.days.innerHTML = '';
const firstDay = new Date(this.state.currentYear, this.state.currentMonth - 1, 1);
const daysInMonth = new Date(this.state.currentYear, this.state.currentMonth, 0).getDate();
const startDayOfWeek = firstDay.getDay();
for (let i = 0; i < startDayOfWeek; i++) {
this.elements.calendar.days.appendChild(this.createDayElement(0, true));
}
for (let day = 1; day <= daysInMonth; day++) {
this.elements.calendar.days.appendChild(this.createDayElement(day, false, availability[day]));
}
},
createDayElement(day, isOtherMonth, availability = null) {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';
if (isOtherMonth) {
dayElement.classList.add('other-month');
return dayElement;
}
dayElement.textContent = day;
const dateStr = `${this.state.currentYear}-${String(this.state.currentMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const today = new Date();
today.setHours(0, 0, 0, 0);
const currentDate = new Date(dateStr);
currentDate.setHours(0, 0, 0, 0);
if (currentDate.getTime() === today.getTime()) {
dayElement.classList.add('today');
} else if (currentDate.getTime() < today.getTime()){
dayElement.classList.add('unavailable');
} else if (availability && availability.available) {
dayElement.classList.add('available');
dayElement.addEventListener('click', () => this.selectDate(dateStr, dayElement));
} else {
// 💡 [개선] 예약 불가 사유에 따라 다른 스타일을 적용합니다.
if (availability && availability.reason === 'holiday') {
dayElement.classList.add('holiday');
} else if (availability && availability.reason === 'full') {
dayElement.classList.add('full');
} else {
dayElement.classList.add('unavailable');
}
}
return dayElement;
},
selectDate(dateStr, element) {
const prevSelected = this.elements.calendar.days.querySelector('.selected');
if (prevSelected) prevSelected.classList.remove('selected');
element.classList.add('selected');
this.state.selectedDate = dateStr;
},
async fetchMonthAvailability() {
this.showLoading();
try {
const formData = new FormData();
formData.append('action', 'get_month_availability');
formData.append('year', this.state.currentYear);
formData.append('month', this.state.currentMonth);
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
const result = await response.json();
return result.success ? result.data : null;
} catch (error) {
console.error('Error fetching month availability:', error);
return null;
} finally {
this.hideLoading();
}
},
async fetchTimeSlots() {
this.showLoading();
this.elements.timeSlotsGrid.innerHTML = '';
try {
const formData = new FormData();
formData.append('action', 'get_time_slots');
formData.append('date', this.state.selectedDate);
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
this.renderTimeSlots(result.data);
} else {
this.elements.timeSlotsGrid.innerHTML = `<div class="error">${result.message || '시간 정보를 불러올 수 없습니다.'}</div>`;
}
} catch (error) {
console.error('Error fetching time slots:', error);
this.elements.timeSlotsGrid.innerHTML = '<div class="error">오류가 발생했습니다. 다시 시도해주세요.</div>';
} finally {
this.hideLoading();
}
},
renderTimeSlots(slots) {
// 💡 [수정] 슬롯이 하나도 없는 경우와, 예약 가능한 슬롯이 없는 경우를 구분하여 메시지를 표시합니다.
if (slots.length === 0) {
this.elements.timeSlotsGrid.innerHTML = '<div class="no-slots">해당 날짜에 운영되는 상담 시간이 없습니다.</div>';
return;
}
let hasAvailableSlots = false;
this.elements.timeSlotsGrid.innerHTML = ''; // 그리드 초기화
slots.forEach(slot => {
const slotElement = document.createElement('div');
slotElement.className = 'time-slot';
let slotInfoText = '';
const reservedCount = slot.reserved_count;
if (slot.available) {
hasAvailableSlots = true;
slotElement.classList.add('available');
slotElement.addEventListener('click', () => this.selectTime(slot.time, slotElement));
slotInfoText = `예약 ${reservedCount} / ${slot.max_persons}`;
} else {
if (slot.reason === 'full') {
slotElement.classList.add('full');
slotInfoText = '마감';
} else if (slot.reason === 'too_soon') {
slotElement.classList.add('too-soon');
slotInfoText = '예약 임박';
} else {
slotElement.classList.add('full'); // Fallback
slotInfoText = '마감';
}
}
slotElement.innerHTML = `<span class="time-text">${slot.time}</span> <span class="slot-info">${slotInfoText}</span>`;
this.elements.timeSlotsGrid.appendChild(slotElement);
});
// 예약 가능한 슬롯이 하나도 없는 경우 안내 메시지 추가
if (!hasAvailableSlots) {
const noSlotsMessage = document.createElement('div');
noSlotsMessage.className = 'no-slots';
noSlotsMessage.textContent = '현재 예약 가능한 시간이 없습니다. 다른 날짜를 선택해주세요.';
this.elements.timeSlotsGrid.prepend(noSlotsMessage);
}
},
selectTime(time, element) {
const prevSelected = this.elements.timeSlotsGrid.querySelector('.selected');
if (prevSelected) prevSelected.classList.remove('selected');
element.classList.add('selected');
this.state.selectedTime = time;
},
goToStep(step) {
this.state.currentStep = step;
this.elements.popup.querySelectorAll('.step').forEach((el, i) => {
el.classList.toggle('active', i + 1 === step);
el.classList.toggle('completed', i + 1 < step);
});
this.elements.popup.querySelectorAll('.reservation-step-content').forEach(el => {
el.style.display = parseInt(el.dataset.step) === step ? 'block' : 'none';
});
this.elements.nav.prevBtn.style.display = step > 1 ? 'inline-block' : 'none';
this.elements.nav.nextBtn.style.display = step < 3 ? 'inline-block' : 'none';
this.elements.nav.submitBtn.style.display = step === 3 ? 'inline-block' : 'none';
},
goToNextStep() {
if (this.state.currentStep === 1 && !this.state.selectedDate) {
alert('날짜를 선택해주세요.');
return;
}
if (this.state.currentStep === 2 && !this.state.selectedTime) {
alert('시간을 선택해주세요.');
return;
}
if (this.state.currentStep < 3) {
this.goToStep(this.state.currentStep + 1);
if (this.state.currentStep === 2) this.fetchTimeSlots();
if (this.state.currentStep === 3) this.updateSummary();
}
},
updateSummary() {
const date = new Date(this.state.selectedDate);
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
this.elements.popup.querySelector('#summary-date').textContent = date.toLocaleDateString('ko-KR', options);
this.elements.popup.querySelector('#summary-time').textContent = this.state.selectedTime;
this.elements.popup.querySelector('#selected-date-display').textContent = date.toLocaleDateString('ko-KR', options);
this.elements.form.querySelector('#selected-date').value = this.state.selectedDate;
this.elements.form.querySelector('#selected-time').value = this.state.selectedTime;
},
async submitForm() {
if (!this.elements.form.checkValidity()) {
alert('필수 입력 항목을 모두 채워주세요.');
this.elements.form.reportValidity();
return;
}
this.showLoading();
this.elements.nav.submitBtn.disabled = true;
this.elements.nav.submitBtn.textContent = '처리 중...';
try {
const formData = new FormData(this.elements.form);
const response = await fetch('<?php echo $form_action_url; ?>', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
alert('예약 신청이 완료되었습니다. 입금 확인 후 예약이 확정됩니다.');
this.close();
// 필요시 페이지 새로고침 또는 다른 동작 수행
// location.reload();
} else {
alert(result.message || '예약 처리 중 오류가 발생했습니다.');
}
} catch (error) {
console.error('Form submission error:', error);
alert('네트워크 오류가 발생했습니다. 다시 시도해주세요.');
} finally {
this.hideLoading();
this.elements.nav.submitBtn.disabled = false;
this.elements.nav.submitBtn.textContent = '예약 신청';
}
},
showLoading() { if (this.elements.loading) this.elements.loading.style.display = 'flex'; },
hideLoading() { if (this.elements.loading) this.elements.loading.style.display = 'none'; },
};
document.addEventListener('DOMContentLoaded', () => ReservationPopup.init());
// 외부에서 팝업을 열기 위한 전역 함수
function openReservationPopup() {
ReservationPopup.open();
}
</script>
<?php } // end if($consultant_installed) ?>