first commit 2

This commit is contained in:
hmw1001
2026-06-11 18:47:38 +09:00
parent c768729ce6
commit 6f534e33a6
11095 changed files with 1595758 additions and 0 deletions
@@ -0,0 +1,14 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 이 파일은 상담 예약 관련 팝업들을 한번에 쉽게 포함하기 위해 사용됩니다.
// 현재 파일의 경로를 기준으로 팝업 파일들의 경로를 정의합니다.
$expert_visit_components_path = dirname(__FILE__);
// 상담 예약 신청 팝업 포함
include_once($expert_visit_components_path . '/expert_visit_popup.php');
// 예약 확인/취소 팝업 포함
//include_once($expert_visit_components_path . '/my_reservations_popup.php');
?>
@@ -0,0 +1,207 @@
/* 💡 [추가] 로딩 오버레이 스타일 */
.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); } }
/* 팝업 오버레이 */
.expert-visit-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;
}
.expert-visit-modal-overlay.active {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.expert-visit-modal-content {
background: #fff;
border-radius: 10px;
width: 100%;
max-width: 550px; /* 너비 약간 축소 */
max-height: 85vh; /* 높이 제한 축소 */
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
position: relative;
}
.expert-visit-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px; /* 패딩 축소 */
border-bottom: 1px solid #eee;
background: #f8f9fa;
border-radius: 10px 10px 0 0;
}
.expert-visit-modal-header h2 { margin: 0; font-size: 18px; font-weight: 600; color: #333; }
.expert-visit-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;
}
.expert-visit-modal-close:hover { background: #e9ecef; color: #333; }
.expert-visit-modal-body { padding: 20px; } /* 패딩 축소 */
/* 단계 표시 */
.expert-visit-steps { display: flex; justify-content: center; margin-bottom: 20px; position: relative; } /* 마진 축소 */
.expert-visit-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: 28px; height: 28px; border-radius: 50%; background: #e9ecef; color: #6c757d; /* 크기 축소 */
display: flex; align-items: center; justify-content: center; font-weight: 600; margin-bottom: 6px; transition: all 0.3s ease; font-size: 13px;
}
.step.active .step-number { background: #007bff; color: #fff; }
.step.completed .step-number { background: #28a745; color: #fff; }
.step-text { font-size: 11px; 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: 15px; } /* 마진 축소 */
.calendar-nav {
background: none; border: 1px solid #ddd; width: 30px; height: 30px; 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: 16px; 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: 8px 4px; text-align: center; font-weight: 600; color: #495057; font-size: 13px; 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: 13px; position: relative; padding: 2px; /* 폰트 및 패딩 축소 */
}
.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: 30px 20px; color: #666; font-style: italic; }
.error { color: #dc3545; }
/* 범례 */
.calendar-legend, .time-legend { display: flex; justify-content: center; gap: 15px; margin-top: 10px; flex-wrap: wrap; }
.legend-item { display: flex; align-items: center; gap: 4px; font-size: 11px; color: #666; }
.legend-color { width: 10px; height: 10px; 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: 15px; }
.step-description h4 { margin: 0 0 8px 0; font-size: 16px; }
.step-description p { margin: 0; font-size: 13px; color: #666; }
.selected-date-info h4 { margin: 0; color: #333; font-size: 15px; }
.time-slots-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 8px; margin-bottom: 15px; }
.time-slot {
padding: 10px 6px; border: 2px solid #e9ecef; border-radius: 6px; text-align: center; cursor: pointer;
transition: all 0.2s ease; font-size: 13px; font-weight: 500; display: flex; flex-direction: column; gap: 3px;
}
.time-text { font-weight: 600; }
.slot-info { font-size: 10px; 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; }
/* 고객 정보 폼 */
.expert-visit-summary { background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 15px; }
.expert-visit-summary h5 { margin: 0 0 8px 0; font-size: 15px; }
.summary-grid { display: grid; grid-template-columns: 1fr; gap: 6px; }
.summary-item { display: flex; justify-content: space-between; font-size: 13px; }
.summary-item .label { font-weight: 500; color: #495057; }
.summary-item span { color: #333; }
.customer-info-form h4 { margin: 0 0 12px 0; color: #333; font-size: 15px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.form-group { margin-bottom: 12px; }
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; color: #495057; font-size: 13px; }
.required { color: #dc3545; font-weight: bold; }
.form-group input, .form-group textarea, .form-group select {
width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 6px;
font-size: 13px; transition: border-color 0.2s ease; box-sizing: border-box;
}
.form-group textarea { resize: vertical; min-height: 60px; }
.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);
}
/* 네비게이션 버튼 */
.expert-visit-nav-buttons {
display: flex; justify-content: space-between; gap: 10px; margin-top: 15px;
padding-top: 15px; border-top: 1px solid #e9ecef;
}
.expert-visit-nav-buttons button {
padding: 10px 20px; border: none; border-radius: 6px; font-size: 13px;
font-weight: 600; cursor: pointer; transition: all 0.2s ease; min-width: 90px;
}
.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) {
.expert-visit-modal-content { margin: 10px; max-height: 95vh; border-radius: 8px; }
.expert-visit-modal-header, .expert-visit-modal-body { padding: 15px; }
.expert-visit-modal-header h2 { font-size: 16px; }
.expert-visit-steps { margin-bottom: 15px; }
.step { padding: 0 8px; }
.step-number { width: 24px; height: 24px; font-size: 12px; }
.step-text { font-size: 10px; }
.calendar-day { font-size: 12px; }
.time-slots-grid { grid-template-columns: repeat(auto-fit, minmax(80px, 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) {
.expert-visit-modal-overlay { padding: 0; align-items: flex-end; }
.expert-visit-modal-content { margin: 0; width: 100%; max-height: 90vh; border-radius: 10px 10px 0 0; }
.expert-visit-nav-buttons { flex-direction: column-reverse; gap: 10px; }
.expert-visit-nav-buttons button { width: 100%; }
}
@@ -0,0 +1,726 @@
<?php
/**
* 전문가 방문 예약 팝업 UI 및 데이터 처리
*/
// AJAX 요청 처리
if (isset($_POST['action'])) {
include_once('../_common_con.php');
include_once('../lib/notification_helper.php');
header('Content-Type: application/json');
$action = $_POST['action'] ?? '';
$response = ['success' => false, 'message' => '알 수 없는 요청입니다.'];
// 월별 예약 가능일 조회
if ($action === 'get_expert_visit_month_availability') {
$year = (int) ($_POST['year'] ?? 0);
$month = (int) ($_POST['month'] ?? 0);
$expert_id = $_POST['expert_id'] ?? ''; // 전문가 ID 추가
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 = get_order_config('expert_visit_max_advance_days', 30);
$max_date = date('Y-m-d', strtotime("+" . $max_advance_days . " days"));
// 날짜별로 루프를 돌며 가용성을 체크합니다.
// 쿼리 하나로 처리하기 복잡하므로, 기간 내의 모든 스케줄을 가져와서 PHP에서 병합합니다.
// 1. 기간 내의 모든 '지정일' 스케줄 가져오기
$specific_schedules = [];
$sql_specific = "SELECT * FROM expert_visit_schedules
WHERE specific_date BETWEEN '{$start_date}' AND '{$end_date}'
AND (expert_id = '{$expert_id}' OR expert_id IS NULL)
ORDER BY expert_id DESC"; // 전문가 설정 우선
$res_specific = sql_query($sql_specific);
while ($row = sql_fetch_array($res_specific)) {
$date = $row['specific_date'];
if (!isset($specific_schedules[$date])) { // 이미 전문가 설정이 있으면 공통 설정은 무시
$specific_schedules[$date] = $row;
}
}
// 2. '요일별' 스케줄 가져오기
$weekly_schedules = [];
$sql_weekly = "SELECT * FROM expert_visit_schedules
WHERE day_of_week IS NOT NULL
AND (expert_id = '{$expert_id}' OR expert_id IS NULL)
ORDER BY expert_id DESC"; // 전문가 설정 우선
$res_weekly = sql_query($sql_weekly);
while ($row = sql_fetch_array($res_weekly)) {
$dow = $row['day_of_week'];
if (!isset($weekly_schedules[$dow])) {
$weekly_schedules[$dow] = $row;
}
}
$availability = [];
$current = strtotime($start_date);
$end = strtotime($end_date);
while ($current <= $end) {
$date_str = date('Y-m-d', $current);
$day_num = date('j', $current);
$dow = date('N', $current); // 1(월) ~ 7(일)
// 우선순위: 지정일 > 요일별
$schedule = $specific_schedules[$date_str] ?? ($weekly_schedules[$dow] ?? null);
$is_bookable = false;
$reason = '';
if (!$schedule) {
$reason = 'no_schedule'; // 스케줄 없음
} elseif ($schedule['is_available'] == 0) {
$reason = 'holiday'; // 휴무
} elseif ($date_str < date('Y-m-d')) {
$reason = 'past_date'; // 지난 날짜
} elseif ($date_str > $max_date) {
$reason = 'too_far'; // 예약 가능 기간 초과
} else {
// 예약 꽉 찼는지 확인
// 해당 날짜, 해당 전문가(또는 전체)의 예약 건수 확인
// 시간대별로 체크해야 정확하지만, 여기서는 '하루 전체 마감' 여부를 대략적으로 판단하거나
// 일단 '가능'으로 표시하고 시간 선택에서 막을 수 있습니다.
// 정확도를 위해 해당 날짜의 총 슬롯 수와 예약 수를 비교합니다.
$start_time = strtotime($date_str . ' ' . $schedule['start_time']);
$end_time = strtotime($date_str . ' ' . $schedule['end_time']);
$slot_duration = $schedule['time_slot'] * 60;
$total_slots = 0;
for ($t = $start_time; $t < $end_time; $t += $slot_duration) {
$total_slots++;
}
$max_capacity = $total_slots * $schedule['max_persons'];
// 예약된 건수 조회
$sql_reserved = "SELECT COUNT(*) as cnt FROM expert_visit_reservations
WHERE visit_date = '{$date_str}' AND status != 'cancelled'";
if ($expert_id) {
$sql_reserved .= " AND expert_id = '{$expert_id}'";
}
$row_reserved = sql_fetch($sql_reserved);
if ($row_reserved['cnt'] >= $max_capacity) {
$reason = 'full';
} else {
$is_bookable = true;
}
}
$availability[$day_num] = [
'available' => $is_bookable,
'reason' => $reason
];
$current = strtotime('+1 day', $current);
}
$response = ['success' => true, 'data' => $availability];
}
}
// 특정일의 예약 가능 시간 조회
if ($action === 'get_expert_visit_time_slots') {
$date = preg_replace('/[^0-9\-]/', '', $_POST['date'] ?? '');
$expert_id = $_POST['expert_id'] ?? '';
if ($date) {
$min_advance_hours = get_order_config('expert_visit_min_advance_hours', 24);
$min_datetime = date('Y-m-d H:i:s', strtotime("+" . $min_advance_hours . " hours"));
$dow = date('N', strtotime($date));
// 1. 해당 날짜의 스케줄 조회 (우선순위 적용)
$schedule = sql_fetch("SELECT * FROM expert_visit_schedules
WHERE (specific_date = '{$date}' OR (specific_date IS NULL AND day_of_week = '{$dow}'))
AND (expert_id = '{$expert_id}' OR expert_id IS NULL)
ORDER BY specific_date DESC, expert_id DESC LIMIT 1");
$slots = [];
if ($schedule && $schedule['is_available']) {
$start_time = strtotime($date . ' ' . $schedule['start_time']);
$end_time = strtotime($date . ' ' . $schedule['end_time']);
$slot_duration = $schedule['time_slot'] * 60;
$max_persons = $schedule['max_persons'];
for ($t = $start_time; $t < $end_time; $t += $slot_duration) {
$current_slot_time = date('H:i', $t);
$slot_datetime = $date . ' ' . $current_slot_time;
// 예약 건수 조회
$sql_reserved = "SELECT COUNT(*) as cnt FROM expert_visit_reservations
WHERE visit_date = '{$date}'
AND visit_time = '{$current_slot_time}:00'
AND status != 'cancelled'";
if ($expert_id) {
$sql_reserved .= " AND expert_id = '{$expert_id}'";
}
$reserved_count = (int)sql_fetch($sql_reserved)['cnt'];
$is_too_soon = ($slot_datetime < $min_datetime);
$is_full = ($reserved_count >= $max_persons);
$slots[] = [
'time' => $current_slot_time,
'available' => !$is_too_soon && !$is_full,
'reason' => $is_too_soon ? 'too_soon' : ($is_full ? 'full' : ''),
'reserved_count' => $reserved_count,
'max_persons' => $max_persons
];
}
}
$response = ['success' => true, 'data' => $slots];
}
}
echo json_encode($response);
exit;
}
if (!defined('_GNUBOARD_')) exit;
include_once(G5_ADMIN_PATH . '/order_manage/_common_con.php');
include_once(G5_ADMIN_PATH . '/order_manage/lib/notification_helper.php');
$current_year = date('Y');
$current_month_num = date('n');
$visit_fee = get_order_config('expert_visit_fee', 50000);
$account_info = get_order_config('expert_visit_account_info', '');
$max_advance_days = get_order_config('expert_visit_max_advance_days', 30);
$ajax_url = G5_ADMIN_URL . '/order_manage/components/expert_visit_popup.php';
$form_action_url = G5_ADMIN_URL . '/order_manage/components/expert_visit_submit.php';
$css_url = G5_ADMIN_URL . '/order_manage/components/expert_visit_popup.css';
$wr_id = isset($_GET['wr_id']) ? (int)$_GET['wr_id'] : 0;
// 💡 [추가] 로그인 회원 정보 가져오기
$customer_name = '';
$customer_phone = '';
$customer_email = '';
if (isset($member) && $member['mb_id']) {
$customer_name = $member['mb_name'];
$customer_phone = $member['mb_hp'];
$customer_email = $member['mb_email'];
}
?>
<link rel="stylesheet" href="<?php echo $css_url; ?>">
<div id="expert-visit-popup-overlay" class="expert-visit-modal-overlay">
<div id="expert-visit-popup" class="expert-visit-modal-content">
<div class="expert-visit-modal-header">
<h2>전문가 방문 예약</h2>
<button type="button" class="expert-visit-modal-close" aria-label="팝업 닫기">
<span>&times;</span>
</button>
</div>
<div class="expert-visit-modal-body">
<div class="loading-overlay" style="display: none;">
<div class="loading-spinner"></div>
</div>
<div class="expert-visit-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="expert-visit-form" method="post" action="<?php echo $form_action_url; ?>">
<div class="expert-visit-step-content" data-step="1">
<div class="step-description">
<h4>📅 방문 날짜를 선택해주세요</h4>
<p>최대 <?php echo $max_advance_days; ?>일 후까지 예약 가능합니다.</p>
<!-- 전문가 선택을 1단계로 이동 -->
<div class="form-group" style="margin-top: 15px;">
<label for="expert-select-step1">👨‍⚕️ 전문가 선택</label>
<select id="expert-select-step1" name="expert_id_display" class="form-control">
<option value="">선택 안 함 (전체 일정 보기)</option>
<?php
/* 전문가 목록 조회 (실제 DB 연동 필요)
$experts = get_experts();
foreach ($experts as $expert):
?>
<option value="<?php echo $expert['id']; ?>"><?php echo htmlspecialchars($expert['name']); ?></option>
<?php endforeach;
*/
?>
</select>
</div>
</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"></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>
<div class="expert-visit-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"></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>
<div class="expert-visit-step-content" data-step="3" style="display: none;">
<div class="step-description">
<h4>📝 고객 정보를 입력해주세요</h4>
</div>
<div class="expert-visit-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($visit_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="홍길동" value="<?php echo htmlspecialchars($customer_name); ?>">
</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" value="<?php echo htmlspecialchars($customer_phone); ?>">
</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" value="<?php echo htmlspecialchars($customer_email); ?>">
</div>
<div class="form-group">
<label for="expert-visit-type">🏠 방문 유형</label>
<select id="expert-visit-type" name="visit_type">
<option value="onsite">현장 방문</option>
<option value="online">온라인 상담</option>
<option value="phone">전화 상담</option>
</select>
</div>
<!-- 3단계 전문가 선택은 hidden으로 처리하고 1단계 값과 동기화 -->
<input type="hidden" id="expert-visit-resource" name="expert_id">
<div class="form-group">
<label for="customer-request">📝 요청사항</label>
<textarea id="customer-request" name="request_memo" rows="4" placeholder="방문 관련 요청사항이나 문의사항을 입력해주세요."></textarea>
</div>
</div>
<input type="hidden" id="selected-date" name="visit_date">
<input type="hidden" id="selected-time" name="visit_time">
<input type="hidden" name="payment_amount" value="<?php echo $visit_fee; ?>">
<input type="hidden" name="status" value="payment_pending">
<input type="hidden" name="wr_id" value="<?php echo $wr_id; ?>">
</div>
<div class="expert-visit-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>
<script>
const ExpertVisitPopup = {
elements: {},
state: {
currentStep: 1,
selectedDate: null,
selectedTime: null,
currentYear: new Date().getFullYear(),
currentMonth: new Date().getMonth() + 1,
expertId: '', // 전문가 ID 상태 추가
},
config: {
ajaxUrl: '<?php echo $ajax_url; ?>',
maxAdvanceDays: <?php echo (int)$max_advance_days; ?>,
},
init() {
this.elements.overlay = document.getElementById('expert-visit-popup-overlay');
if (!this.elements.overlay) return;
this.elements.popup = this.elements.overlay.querySelector('.expert-visit-modal-content');
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
this.elements.form = this.elements.popup.querySelector('#expert-visit-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.expertSelect = this.elements.popup.querySelector('#expert-select-step1');
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() {
const closeBtn = this.elements.popup.querySelector('.expert-visit-modal-close');
if (closeBtn) closeBtn.addEventListener('click', () => this.close());
this.elements.overlay.addEventListener('click', e => {
if (e.target === this.elements.overlay) this.close();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && this.elements.overlay.classList.contains('active')) this.close();
});
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();
});
}
// 전문가 선택 변경 시 달력 갱신
if (this.elements.expertSelect) {
this.elements.expertSelect.addEventListener('change', (e) => {
this.state.expertId = e.target.value;
this.state.selectedDate = null; // 날짜 선택 초기화
this.renderCalendar();
});
}
},
open() {
this.state.currentYear = new Date().getFullYear();
this.state.currentMonth = new Date().getMonth() + 1;
this.goToStep(1);
this.renderCalendar();
this.elements.overlay.classList.add('active');
document.body.style.overflow = 'hidden';
},
close() {
this.elements.overlay.classList.remove('active');
document.body.style.overflow = '';
this.elements.form.reset();
this.state.selectedDate = null;
this.state.selectedTime = null;
this.state.expertId = '';
},
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('unavailable');
} else if (availability && availability.available) {
dayElement.classList.add('available');
// 이미 선택된 날짜라면 selected 클래스 추가
if (this.state.selectedDate === dateStr) {
dayElement.classList.add('selected');
}
dayElement.addEventListener('click', () => this.selectDate(dateStr, dayElement));
} else {
const reason = availability ? availability.reason : 'unavailable';
dayElement.classList.add(reason);
}
if (currentDate.getTime() === today.getTime()) {
dayElement.classList.add('today');
}
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_expert_visit_month_availability');
formData.append('year', this.state.currentYear);
formData.append('month', this.state.currentMonth);
formData.append('expert_id', this.state.expertId); // 전문가 ID 전송
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 = '<div class="loading">시간 정보를 불러오는 중...</div>';
try {
const formData = new FormData();
formData.append('action', 'get_expert_visit_time_slots');
formData.append('date', this.state.selectedDate);
formData.append('expert_id', this.state.expertId); // 전문가 ID 전송
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) {
this.elements.timeSlotsGrid.innerHTML = '';
if (slots.length === 0) {
this.elements.timeSlotsGrid.innerHTML = '<div class="no-slots">해당 날짜에 운영되는 시간이 없습니다.</div>';
return;
}
let hasAvailableSlots = false;
slots.forEach(slot => {
const slotElement = document.createElement('div');
slotElement.className = 'time-slot';
let slotInfoText = '';
if (slot.available) {
hasAvailableSlots = true;
slotElement.classList.add('available');
slotElement.addEventListener('click', () => this.selectTime(slot.time, slotElement));
slotInfoText = `예약 ${slot.reserved_count} / ${slot.max_persons}`;
} else {
slotElement.classList.add(slot.reason || 'full');
slotInfoText = slot.reason === 'too_soon' ? '예약 임박' : '마감';
}
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('.expert-visit-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;
// 전문가 ID 동기화
this.elements.form.querySelector('#expert-visit-resource').value = this.state.expertId;
},
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', () => ExpertVisitPopup.init());
function openExpertVisitPopup(wr_id = 0) {
if (wr_id) {
const form = ExpertVisitPopup.elements.form;
if(form) {
let wrIdField = form.querySelector('input[name="wr_id"]');
if (!wrIdField) {
wrIdField = document.createElement('input');
wrIdField.type = 'hidden';
wrIdField.name = 'wr_id';
form.appendChild(wrIdField);
}
wrIdField.value = wr_id;
}
}
ExpertVisitPopup.open();
}
</script>
@@ -0,0 +1,95 @@
<?php
// 경로를 order_manage에 맞게 수정
include_once('../_common_con.php');
// 💡 [추가] 알림 헬퍼 포함
include_once('../lib/notification_helper.php');
header('Content-Type: application/json');
try {
// 입력 데이터 정리 및 컬럼명 변경
$reservation_data = [
'wr_id' => (int)($_POST['wr_id'] ?? 0),
'customer_name' => trim($_POST['customer_name'] ?? ''),
'customer_phone' => trim($_POST['customer_phone'] ?? ''),
'customer_email' => trim($_POST['customer_email'] ?? ''),
'visit_date' => trim($_POST['visit_date'] ?? ''), // 폼의 name="visit_date"와 일치
'visit_time' => trim($_POST['visit_time'] ?? ''), // 폼의 name="visit_time"와 일치
'expert_id' => trim($_POST['expert_id'] ?? ''), // [추가] 전문가 ID
'temp_2' => trim($_POST['visit_type'] ?? ''), // [추가] 방문 유형 (temp_2에 저장)
'request_memo' => trim($_POST['request_memo'] ?? ''), // 폼의 name="request_memo"와 일치
'payment_amount' => (int)($_POST['payment_amount'] ?? 0),
'status' => 'payment_pending',
'created_by' => $member['mb_id'] ?? 'guest'
];
// 필수 항목 유효성 검사
if (empty($reservation_data['customer_name']) || empty($reservation_data['customer_phone']) || empty($reservation_data['visit_date']) || empty($reservation_data['visit_time'])) {
throw new Exception('필수 예약 정보(이름, 연락처, 날짜, 시간)가 누락되었습니다.');
}
// 예약 가능 여부 재확인 (서버 측 검증)
// 1. 해당 날짜/요일에 운영하는 스케줄이 있는지 확인
$dow = date('N', strtotime($reservation_data['visit_date']));
$expert_condition = $reservation_data['expert_id'] ? "(expert_id = '{$reservation_data['expert_id']}' OR expert_id IS NULL)" : "expert_id IS NULL";
$sql_schedule = "SELECT * FROM expert_visit_schedules
WHERE (specific_date = '{$reservation_data['visit_date']}' OR (specific_date IS NULL AND day_of_week = '{$dow}'))
AND {$expert_condition}
AND is_available = 1
ORDER BY specific_date DESC, expert_id DESC LIMIT 1";
$schedule = sql_fetch($sql_schedule);
if (!$schedule) {
throw new Exception('선택하신 날짜는 예약이 불가능합니다.');
}
// 2. 해당 시간대가 운영 시간 내인지 확인
$visit_time_ts = strtotime($reservation_data['visit_date'] . ' ' . $reservation_data['visit_time']);
$start_time_ts = strtotime($reservation_data['visit_date'] . ' ' . $schedule['start_time']);
$end_time_ts = strtotime($reservation_data['visit_date'] . ' ' . $schedule['end_time']);
if ($visit_time_ts < $start_time_ts || $visit_time_ts >= $end_time_ts) {
throw new Exception('선택하신 시간은 운영 시간이 아닙니다.');
}
// 3. 해당 시간대에 예약 정원이 찼는지 확인
$sql_reserved = "SELECT COUNT(*) as cnt FROM expert_visit_reservations
WHERE visit_date = '{$reservation_data['visit_date']}'
AND visit_time = '{$reservation_data['visit_time']}'
AND status != 'cancelled'";
if ($reservation_data['expert_id']) {
$sql_reserved .= " AND expert_id = '{$reservation_data['expert_id']}'";
}
$reserved = sql_fetch($sql_reserved);
if ($reserved['cnt'] >= $schedule['max_persons']) {
throw new Exception('선택하신 시간은 이미 예약이 마감되었습니다.');
}
// 예약 생성
$columns = implode(", ", array_keys($reservation_data));
$values = "'" . implode("', '", array_map('sql_real_escape_string', $reservation_data)) . "'";
$sql = "INSERT INTO expert_visit_reservations ($columns, created_at, updated_at)
VALUES ($values, NOW(), NOW())";
if (sql_query($sql)) {
$reservation_id = sql_insert_id();
// 신규 예약 알림 발송
if (function_exists('notify_for_expert_visit')) {
notify_for_expert_visit($reservation_id, '신규예약');
}
echo json_encode(['success' => true, 'message' => '전문가 방문 예약 신청이 완료되었습니다.']);
} else {
throw new Exception('데이터베이스 저장 중 오류가 발생했습니다.');
}
} catch (Exception $e) {
// 500 오류 방지를 위해 예외 발생 시에도 JSON 응답 반환
http_response_code(200);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
?>
@@ -0,0 +1,59 @@
<?php
include_once('../../_common.php');
// 💡 [추가] 알림 헬퍼 포함
include_once('../lib/notification_helper.php');
header('Content-Type: application/json');
if (!$is_member) {
die(json_encode(['success' => false, 'message' => '로그인이 필요합니다.']));
}
$action = $_POST['action'] ?? '';
$reservation_id = (int)($_POST['id'] ?? 0);
try {
if ($action === 'cancel') {
if (!$reservation_id) {
throw new Exception('예약 ID가 없습니다.');
}
// 본인의 예약인지 확인
$sql = "SELECT * FROM expert_visit_reservations WHERE id = '{$reservation_id}' AND (customer_id = '{$member['mb_id']}' OR customer_phone = '{$member['mb_hp']}')";
$reservation = sql_fetch($sql);
if (!$reservation) {
throw new Exception('취소할 수 있는 예약 정보가 없거나 권한이 없습니다.');
}
// 이미 취소되었거나 완료된 예약은 변경 불가
if ($reservation['status'] === 'cancelled' || $reservation['status'] === 'completed') {
throw new Exception('이미 취소되었거나 완료된 예약입니다.');
}
// 예약 취소 처리
$sql_update = "UPDATE expert_visit_reservations
SET status = 'cancelled',
updated_at = NOW(),
updated_by = '{$member['mb_id']}'
WHERE id = '{$reservation_id}'";
if (sql_query($sql_update)) {
// 💡 [수정] 예약 취소 알림 발송
if (function_exists('notify_for_expert_visit')) {
notify_for_expert_visit($reservation_id, '예약취소');
}
echo json_encode(['success' => true, 'message' => '예약이 정상적으로 취소되었습니다.']);
} else {
throw new Exception('예약 취소 중 오류가 발생했습니다.');
}
} else {
throw new Exception('잘못된 요청입니다.');
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
?>
@@ -0,0 +1,279 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
// AJAX 요청 처리
if (isset($_POST['action'])) {
header('Content-Type: application/json');
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
include_once('../lib/notification_helper.php');
if (!$is_member) {
echo json_encode(['success' => false, 'message' => '로그인이 필요합니다.']);
exit;
}
$action = $_POST['action'] ?? '';
$reservation_id = (int)($_POST['id'] ?? 0);
try {
if ($action === 'cancel') {
if (!$reservation_id) {
throw new Exception('예약 ID가 없습니다.');
}
// 본인의 예약인지 확인
$sql = "SELECT * FROM expert_visit_reservations WHERE id = '{$reservation_id}' AND (customer_id = '{$member['mb_id']}' OR customer_phone = '{$member['mb_hp']}')";
$reservation = sql_fetch($sql);
if (!$reservation) {
throw new Exception('취소할 수 있는 예약 정보가 없거나 권한이 없습니다.');
}
// 이미 취소되었거나 완료된 예약은 변경 불가
if ($reservation['status'] === 'cancelled' || $reservation['status'] === 'completed') {
throw new Exception('이미 취소되었거나 완료된 예약입니다.');
}
// 예약 취소 처리
$sql_update = "UPDATE expert_visit_reservations
SET status = 'cancelled',
updated_at = NOW(),
updated_by = '{$member['mb_id']}'
WHERE id = '{$reservation_id}'";
if (sql_query($sql_update)) {
// 예약 취소 알림 발송
if (function_exists('notify_for_expert_visit')) {
notify_for_expert_visit($reservation_id, '예약취소');
}
echo json_encode(['success' => true, 'message' => '예약이 정상적으로 취소되었습니다.']);
} else {
throw new Exception('예약 취소 중 오류가 발생했습니다.');
}
} elseif ($action === 'get_list') {
// 예약 목록 조회
$sql = "SELECT *
FROM expert_visit_reservations
WHERE (customer_id = '{$member['mb_id']}' OR customer_phone = '{$member['mb_hp']}')
AND is_deleted = 0
ORDER BY visit_date DESC, visit_time DESC";
$result = sql_query($sql);
$reservations = [];
while($row = sql_fetch_array($result)) {
$row['status_text'] = get_order_config('reservation_status_' . $row['status'], $row['status']);
$row['is_cancellable'] = !in_array($row['status'], ['completed', 'cancelled']);
$reservations[] = $row;
}
echo json_encode(['success' => true, 'data' => $reservations]);
} else {
throw new Exception('잘못된 요청입니다.');
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
exit;
}
if (!defined('_GNUBOARD_')) exit;
$ajax_url = G5_ADMIN_URL . '/order_manage/components/my_reservations_popup.php';
include_once(G5_ADMIN_URL . '/order_manage/_common_con.php');
include_once(G5_ADMIN_URL . '/order_manage/lib/notification_helper.php');
?>
<!-- 나의 예약 현황 팝업 -->
<div id="my-reservations-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="팝업 닫기">&times;</button>
</div>
<div class="reservation-modal-body">
<div class="loading-overlay" style="display: none;">
<div class="loading-spinner"></div>
</div>
<div id="reservation-results">
<!-- 예약 목록이 여기에 표시됩니다. -->
</div>
</div>
</div>
</div>
<style>
/* 팝업 공통 스타일 (expert_visit_popup.php와 통일) */
.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; }
/* 로딩 스피너 */
.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-results { margin-top: 0; }
.reservation-item { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 15px; background: #fff; }
.reservation-item-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 10px; }
.reservation-status { padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; }
.status-payment_pending { background: #fff3cd; color: #856404; }
.status-reserved { background: #d4edda; color: #155724; }
.status-completed { background: #cce5ff; color: #004085; }
.status-cancelled { background: #f8d7da; color: #721c24; }
.reservation-details-grid { margin: 15px 0; font-size: 15px; line-height: 1.6; }
.detail-item { display: flex; margin-bottom: 10px; }
.detail-item .label { font-weight: 600; color: #555; width: 90px; flex-shrink: 0; }
.detail-item .value { color: #333; }
.reservation-actions { margin-top: 20px; text-align: right; }
.btn-danger { background: #dc3545; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 600; }
.btn-danger:hover { background: #c82333; }
.cancel-notice { font-size: 13px; color: #666; margin-top: 15px; text-align: right; padding: 10px; background-color: #f8f9fa; border-radius: 4px; }
.no-results { text-align: center; color: #666; padding: 40px 0; }
</style>
<script>
const MyReservationsPopup = {
elements: {},
config: {
ajaxUrl: '<?php echo $ajax_url; ?>'
},
init() {
this.elements.popup = document.getElementById('my-reservations-popup');
if (!this.elements.popup) return;
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
this.elements.resultsContainer = this.elements.popup.querySelector('#reservation-results');
this.addEventListeners();
},
addEventListeners() {
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();
});
if (this.elements.resultsContainer) {
this.elements.resultsContainer.addEventListener('click', e => {
if (e.target.classList.contains('btn-cancel-reservation')) {
const reservationId = e.target.dataset.id;
if (confirm('정말 이 예약을 취소하시겠습니까?')) {
this.cancelReservation(reservationId);
}
}
});
}
},
open() {
if (!this.elements.popup) return;
this.elements.popup.classList.add('active');
document.body.style.overflow = 'hidden';
this.getReservations(); // 팝업 열릴 때 목록 조회
},
close() {
if (!this.elements.popup) return;
this.elements.popup.classList.remove('active');
document.body.style.overflow = '';
},
async getReservations() {
this.showLoading();
const formData = new FormData();
formData.append('action', 'get_list');
try {
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
this.renderResults(result.data);
} else {
this.elements.resultsContainer.innerHTML = `<p class="no-results">${result.message}</p>`;
}
} catch (error) {
console.error("조회 오류:", error);
this.elements.resultsContainer.innerHTML = '<p class="no-results">조회 중 오류가 발생했습니다.</p>';
} finally {
this.hideLoading();
}
},
renderResults(reservations) {
if (reservations.length === 0) {
this.elements.resultsContainer.innerHTML = '<p class="no-results">예약 내역이 없습니다.</p>';
return;
}
let html = '';
reservations.forEach(res => {
const date = new Date(res.visit_date + ' ' + res.visit_time);
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: '2-digit', minute: '2-digit' };
const formattedDate = date.toLocaleDateString('ko-KR', options);
html += `
<div class="reservation-item" id="reservation-${res.id}">
<div class="reservation-item-header">
<strong>예약 번호: #${res.id}</strong>
<span class="reservation-status status-${res.status}">${res.status_text}</span>
</div>
<div class="reservation-details-grid">
<div class="detail-item"><span class="label">방문 일시:</span><span class="value">${formattedDate}</span></div>
<div class="detail-item"><span class="label">방문 지역:</span><span class="value">${res.temp_3 || '-'}</span></div>
<div class="detail-item"><span class="label">방문 비용:</span><span class="value">${Number(res.payment_amount).toLocaleString()}원</span></div>
</div>
<div class="reservation-actions">`;
if (res.is_cancellable) {
html += `<button type="button" class="btn-danger btn-cancel-reservation" data-id="${res.id}">예약 취소</button>`;
} else if (res.status !== 'cancelled' && res.status !== 'completed') {
html += `<p class="cancel-notice">취소 불가 상태입니다.</p>`;
}
html += `</div></div>`;
});
this.elements.resultsContainer.innerHTML = html;
},
async cancelReservation(reservationId) {
this.showLoading();
const formData = new FormData();
formData.append('action', 'cancel');
formData.append('id', reservationId);
try {
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
const result = await response.json();
alert(result.message);
if (result.success) {
await this.getReservations(); // 목록 새로고침
}
} catch (error) {
console.error("취소 오류:", error);
alert('예약 취소 처리 중 오류가 발생했습니다.');
} finally {
this.hideLoading();
}
},
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', () => MyReservationsPopup.init());
function openMyExpertReservationsPopup() {
MyReservationsPopup.open();
}
</script>