454 lines
22 KiB
PHP
454 lines
22 KiB
PHP
<?php
|
|
/**
|
|
* 스케줄 생성 및 빠른 관리 페이지
|
|
*/
|
|
|
|
$sub_menu = '850300';
|
|
include_once('./_common.php');
|
|
|
|
// 권한 확인
|
|
auth_check_menu($auth, $sub_menu, 'w');
|
|
|
|
// ScheduleGenerator 클래스 로드
|
|
require_once('classes/ScheduleGenerator.class.php');
|
|
|
|
// 설치 확인
|
|
if (!is_consultant_installed()) {
|
|
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
|
|
}
|
|
|
|
$g5['title'] = '스케줄 빠른 관리';
|
|
|
|
// AJAX 요청 처리
|
|
$action = $_REQUEST['action'] ?? '';
|
|
|
|
if ($action) {
|
|
header('Content-Type: application/json');
|
|
$response = ['success' => false, 'message' => '알 수 없는 요청입니다.'];
|
|
|
|
// 월별 스케줄 데이터 조회 (달력용)
|
|
if ($action == 'get_monthly_schedule') {
|
|
$year = (int) ($_GET['year'] ?? 0);
|
|
$month = (int) ($_GET['month'] ?? 0);
|
|
|
|
if ($year && $month) {
|
|
$sql = "SELECT id, specific_date, start_time, is_available, temp_1, temp_2
|
|
FROM consultant_schedule
|
|
WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}
|
|
ORDER BY specific_date, start_time";
|
|
$result = sql_query($sql);
|
|
$schedule_data = [];
|
|
while ($row = sql_fetch_array($result)) {
|
|
$schedule_data[] = $row;
|
|
}
|
|
$response = ['success' => true, 'data' => $schedule_data];
|
|
} else {
|
|
$response['message'] = '년도와 월 정보가 올바르지 않습니다.';
|
|
}
|
|
}
|
|
|
|
// 스케줄 슬롯 상태 변경 (블락/해제)
|
|
if ($action == 'toggle_slot_status') {
|
|
$id = (int) ($_POST['id'] ?? 0);
|
|
if ($id) {
|
|
$slot = sql_fetch("SELECT is_available, temp_1, temp_2 FROM consultant_schedule WHERE id = {$id}");
|
|
if ($slot) {
|
|
$is_lunch_override = $_POST['is_lunch_override'] ?? '0';
|
|
$new_status = $slot['is_available'] ? 0 : 1; // 기본 토글 동작
|
|
$new_temp1 = $slot['temp_1'];
|
|
$new_temp2 = $slot['temp_2'];
|
|
$log_msg_action = '';
|
|
|
|
// 💡 [로직 개선] 휴게시간을 상담시간으로 변경하는 경우
|
|
if ($is_lunch_override === '1' && $slot['temp_1'] === 'lunch_time' && $new_status == 1) {
|
|
$new_temp1 = 'manual_override'; // 휴게시간을 수동으로 변경했음을 명시
|
|
$new_temp2 = '관리자 긴급 설정';
|
|
$log_msg_action = "휴게시간을 상담 슬롯으로 변경";
|
|
}
|
|
// 💡 [로직 개선] 긴급 설정된 상담시간을 다시 휴게시간으로 되돌리는 경우
|
|
else if ($slot['temp_1'] === 'manual_override' && $new_status == 0) {
|
|
$new_temp1 = 'lunch_time';
|
|
$new_temp2 = '점심시간';
|
|
$log_msg_action = "긴급 슬롯을 다시 휴게시간으로 복원";
|
|
}
|
|
// 일반 슬롯을 블락/해제하는 경우
|
|
else {
|
|
$new_temp1 = $new_status ? 'auto_generated' : 'manual_block';
|
|
$new_temp2 = $new_status ? '' : '관리자 설정';
|
|
$log_msg_action = $new_status ? "슬롯 활성화" : "슬롯 비활성화";
|
|
}
|
|
|
|
$sql = "UPDATE consultant_schedule
|
|
SET is_available = '{$new_status}',
|
|
temp_1 = '{$new_temp1}',
|
|
temp_2 = '{$new_temp2}',
|
|
updated_at = NOW()
|
|
WHERE id = {$id}";
|
|
|
|
if (sql_query($sql)) {
|
|
consultant_log("스케줄 수동 변경 (ID:{$id}): {$log_msg_action} (관리자: " . ($member['mb_id'] ?? 'unknown') . ")");
|
|
$response = ['success' => true, 'new_status' => $new_status];
|
|
} else {
|
|
$response['message'] = '데이터베이스 업데이트에 실패했습니다.';
|
|
}
|
|
} else {
|
|
$response['message'] = '해당 스케줄을 찾을 수 없습니다.';
|
|
}
|
|
} else {
|
|
$response['message'] = 'ID가 제공되지 않았습니다.';
|
|
}
|
|
}
|
|
|
|
// 월별 스케줄 생성
|
|
if ($action == 'generate_schedule') {
|
|
$year = (int) ($_POST['year'] ?? 0);
|
|
$month = (int) ($_POST['month'] ?? 0);
|
|
if ($year && $month) {
|
|
try {
|
|
$generator = new ScheduleGenerator();
|
|
if ($generator->generateMonth($year, $month)) {
|
|
consultant_log("스케줄 생성/재생성 완료: {$year}년 {$month}월");
|
|
$response = ['success' => true, 'message' => "{$year}년 {$month}월 스케줄이 성공적으로 생성되었습니다."];
|
|
} else {
|
|
$response['message'] = "{$year}년 {$month}월 스케줄 생성에 실패했습니다.";
|
|
}
|
|
} catch (Exception $e) {
|
|
$response['message'] = '스케줄 생성 중 오류 발생: ' . $e->getMessage();
|
|
}
|
|
} else {
|
|
$response['message'] = '년도와 월 정보가 올바르지 않습니다.';
|
|
}
|
|
}
|
|
|
|
echo json_encode($response);
|
|
exit;
|
|
}
|
|
|
|
// --- 페이지 로드 시 실행 ---
|
|
|
|
// 월별 스케줄 상태 조회 함수
|
|
function get_schedule_generation_status($year, $month) {
|
|
$year = (int)$year;
|
|
$month = (int)$month;
|
|
$sql = "SELECT
|
|
COUNT(*) as total_slots,
|
|
SUM(CASE WHEN temp_1 = 'auto_generated' AND is_available = 1 THEN 1 ELSE 0 END) as available_slots,
|
|
SUM(CASE WHEN temp_1 = 'lunch_time' THEN 1 ELSE 0 END) as lunch_slots,
|
|
SUM(CASE WHEN temp_1 = 'holiday' THEN 1 ELSE 0 END) as holiday_slots,
|
|
SUM(CASE WHEN is_available = 0 AND temp_1 NOT IN ('lunch_time', 'holiday') THEN 1 ELSE 0 END) as blocked_slots
|
|
FROM consultant_schedule
|
|
WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}";
|
|
return sql_fetch($sql);
|
|
}
|
|
|
|
// 다음 3개월 상태 조회
|
|
$next_months = [];
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$target_date = mktime(0, 0, 0, date('n') + $i, 1, date('Y'));
|
|
$year = date('Y', $target_date);
|
|
$month = date('m', $target_date);
|
|
|
|
$next_months[] = [
|
|
'year' => $year,
|
|
'month' => $month,
|
|
'name' => date('Y년 n월', $target_date),
|
|
'status' => get_schedule_generation_status($year, $month)
|
|
];
|
|
}
|
|
|
|
include_once(G5_ADMIN_PATH . '/admin.head.php');
|
|
?>
|
|
|
|
<style>
|
|
.schedule-container { max-width: 1000px; margin: 0 auto; padding: 20px; }
|
|
.schedule-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
|
|
.status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
.status-card { background: white; border: 1px solid #ddd; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); }
|
|
.status-card h3 { margin: 0 0 15px 0; color: #333; display: flex; align-items: center; gap: 8px; }
|
|
.status-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f0f0; font-size: 14px; }
|
|
.status-item:last-child { border-bottom: none; }
|
|
.status-label { color: #666; }
|
|
.status-value { font-weight: bold; color: #333; }
|
|
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; text-decoration: none; display: inline-block; transition: all 0.2s; font-size: 14px; }
|
|
.btn-primary { background: #007bff; color: white; }
|
|
.btn-success { background: #28a745; color: white; }
|
|
.btn-warning { background: #ffc107; color: #212529; }
|
|
.btn-secondary { background: #6c757d; color: white; }
|
|
.btn-info { background: #17a2b8; color: white; }
|
|
.alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; }
|
|
.card-actions { display: flex; gap: 10px; margin-top: 20px; }
|
|
.card-actions .btn { flex-grow: 1; }
|
|
|
|
/* Modal Styles */
|
|
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); }
|
|
.modal-content { background-color: #fefefe; margin: 5% auto; padding: 20px; border: 1px solid #888; width: 90%; max-width: 1200px; border-radius: 8px; }
|
|
.modal-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 10px; border-bottom: 1px solid #ddd; }
|
|
.modal-header h2 { margin: 0; font-size: 20px; }
|
|
.close-button { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
|
|
.close-button:hover, .close-button:focus { color: black; }
|
|
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; margin-top: 20px; }
|
|
.calendar-day { border: 1px solid #eee; min-height: 150px; }
|
|
.calendar-day-header { background: #f9f9f9; padding: 5px; font-weight: bold; text-align: center; font-size: 14px; }
|
|
.calendar-day-body { padding: 5px; max-height: 300px; overflow-y: auto; }
|
|
.day-name-header { background: #f1f1f1; text-align: center; padding: 8px; font-weight: bold; }
|
|
.time-slot { padding: 4px 6px; margin: 3px 0; border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; justify-content: space-between; align-items: center; border: 1px solid transparent; }
|
|
.time-slot.available { background-color: #e7f3ff; border-color: #b3d7ff; color: #004085; }
|
|
.time-slot.available:hover { background-color: #cce5ff; }
|
|
.time-slot.blocked { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; text-decoration: line-through; }
|
|
.time-slot.blocked:hover { background-color: #f5c6cb; }
|
|
.time-slot.lunch { background-color: #fff3cd; border-color: #ffeeba; color: #856404; cursor: pointer; }
|
|
.time-slot.lunch:hover { background-color: #ffeeba; }
|
|
.time-slot.holiday { background-color: #e2e3e5; color: #383d41; text-align: center; justify-content: center; cursor: not-allowed; }
|
|
.other-month { background-color: #fafafa; }
|
|
.spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 20px auto; }
|
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
</style>
|
|
|
|
<div class="schedule-container">
|
|
<div class="schedule-header">
|
|
<h2><?php echo $g5['title']; ?></h2>
|
|
<div>
|
|
<a href="settings.php" class="btn btn-secondary">⚙️ 설정 관리</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-info">
|
|
<strong>스케줄 관리 안내:</strong> 월별 스케줄 생성 상태를 확인하고, '달력 보기'를 통해 각 시간 슬롯을 빠르게 예약 마감 처리할 수 있습니다.
|
|
</div>
|
|
|
|
<!-- 월별 상태 표시 -->
|
|
<div class="status-grid">
|
|
<?php foreach ($next_months as $month_info):
|
|
$is_generated = ($month_info['status']['total_slots'] ?? 0) > 0;
|
|
?>
|
|
<div class="status-card">
|
|
<h3>
|
|
📅 <?php echo $month_info['name']; ?>
|
|
<?php if ($is_generated): ?>
|
|
<span style="color: #28a745; font-weight: bold;">✓ 생성됨</span>
|
|
<?php else: ?>
|
|
<span style="color: #dc3545; font-weight: bold;">✗ 미생성</span>
|
|
<?php endif; ?>
|
|
</h3>
|
|
|
|
<div class="status-item">
|
|
<span class="status-label">상담 가능 슬롯</span>
|
|
<span class="status-value"><?php echo number_format($month_info['status']['available_slots'] ?? 0); ?>개</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="status-label">예약 마감 슬롯</span>
|
|
<span class="status-value"><?php echo number_format($month_info['status']['blocked_slots'] ?? 0); ?>개</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="status-label">전체 슬롯 (휴무/점심 제외)</span>
|
|
<span class="status-value"><?php echo number_format(($month_info['status']['available_slots'] ?? 0) + ($month_info['status']['blocked_slots'] ?? 0)); ?>개</span>
|
|
</div>
|
|
|
|
<div class="card-actions">
|
|
<button onclick="generateSchedule(<?php echo $month_info['year']; ?>, <?php echo $month_info['month']; ?>,this)" class="btn btn-success">
|
|
<?php echo $is_generated ? '🔄 스케줄 재생성' : '✨ 스케줄 생성'; ?>
|
|
</button>
|
|
<?php if ($is_generated): ?>
|
|
<button onclick="openCalendarModal(<?php echo $month_info['year']; ?>, <?php echo $month_info['month']; ?>)" class="btn btn-info">
|
|
달력 보기
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- The Modal -->
|
|
<div id="calendarModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 id="modalTitle"></h2>
|
|
<span class="close-button">×</span>
|
|
</div>
|
|
<div id="modalBody">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// --- Modal Control ---
|
|
const modal = document.getElementById("calendarModal");
|
|
const closeButton = document.querySelector(".close-button");
|
|
if(closeButton) closeButton.onclick = () => modal.style.display = "none";
|
|
window.onclick = (event) => {
|
|
if (event.target == modal) {
|
|
modal.style.display = "none";
|
|
}
|
|
};
|
|
|
|
async function openCalendarModal(year, month) {
|
|
document.getElementById("modalTitle").innerText = `${year}년 ${month}월 스케줄`;
|
|
const modalBody = document.getElementById("modalBody");
|
|
modalBody.innerHTML = '<div class="spinner"></div>';
|
|
modal.style.display = "block";
|
|
|
|
try {
|
|
const response = await fetch(`?action=get_monthly_schedule&year=${year}&month=${month}`);
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
renderCalendar(year, month, result.data);
|
|
} else {
|
|
modalBody.innerHTML = `<p style="color: red;">스케줄을 불러오는 데 실패했습니다: ${result.message}</p>`;
|
|
}
|
|
} catch (error) {
|
|
modalBody.innerHTML = `<p style="color: red;">오류가 발생했습니다: ${error}</p>`;
|
|
}
|
|
}
|
|
|
|
function renderCalendar(year, month, scheduleData) {
|
|
const modalBody = document.getElementById("modalBody");
|
|
const firstDay = new Date(year, month - 1, 1).getDay(); // 0=일, 1=월, ...
|
|
const daysInMonth = new Date(year, month, 0).getDate();
|
|
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
|
|
|
let html = '<div class="calendar-grid">';
|
|
dayNames.forEach(name => html += `<div class="day-name-header">${name}</div>`);
|
|
|
|
// Group data by date
|
|
const scheduleByDate = scheduleData.reduce((acc, slot) => {
|
|
const day = new Date(slot.specific_date).getDate();
|
|
if (!acc[day]) acc[day] = [];
|
|
acc[day].push(slot);
|
|
return acc;
|
|
}, {});
|
|
|
|
// Pad start of month
|
|
for (let i = 0; i < firstDay; i++) {
|
|
html += '<div class="calendar-day other-month"></div>';
|
|
}
|
|
|
|
// Render days
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
|
html += `<div class="calendar-day">
|
|
<div class="calendar-day-header">${day}</div>
|
|
<div class="calendar-day-body">`;
|
|
|
|
if (scheduleByDate[day]) {
|
|
scheduleByDate[day].forEach(slot => {
|
|
let slotClass = '';
|
|
let slotText = `<span>${slot.start_time.substring(0, 5)}</span>`;
|
|
if (slot.temp_1 === 'holiday') {
|
|
slotClass = 'holiday';
|
|
slotText = `<strong>${slot.temp_2 || '휴무일'}</strong>`;
|
|
} else if (slot.temp_1 === 'lunch_time' || slot.temp_1 === 'manual_override') {
|
|
slotText = `<span>${slot.start_time.substring(0, 5)} (휴게)</span>`;
|
|
slotClass = (slot.is_available == '1') ? 'available' : 'lunch';
|
|
} else if (slot.is_available == '1') {
|
|
slotClass = 'available';
|
|
} else {
|
|
slotClass = 'blocked';
|
|
}
|
|
html += `<div class="time-slot ${slotClass}" data-id="${slot.id}" onclick="toggleSlot(this, '${slot.temp_1}')">
|
|
${slotText}
|
|
</div>`;
|
|
});
|
|
}
|
|
html += `</div></div>`;
|
|
}
|
|
html += '</div>';
|
|
modalBody.innerHTML = html;
|
|
}
|
|
|
|
async function toggleSlot(element, type) {
|
|
if (type === 'holiday') return;
|
|
|
|
let isLunchOverride = false;
|
|
if (type === 'lunch_time' || type === 'manual_override') {
|
|
const slotIsAvailable = element.classList.contains('available');
|
|
if (!slotIsAvailable) {
|
|
if (!confirm('휴게시간입니다. 정말 이 시간에 상담을 등록하시겠습니까?')) {
|
|
return;
|
|
}
|
|
isLunchOverride = true;
|
|
}
|
|
}
|
|
|
|
const id = element.dataset.id;
|
|
const originalClasses = element.className;
|
|
element.innerHTML = '<span>처리중...</span>';
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('action', 'toggle_slot_status');
|
|
formData.append('id', id);
|
|
if (isLunchOverride) formData.append('is_lunch_override', '1');
|
|
|
|
const response = await fetch('', { method: 'POST', body: formData });
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// Re-render the slot based on the new state
|
|
const timeText = element.innerText.split(' ')[0];
|
|
if (type === 'lunch_time' || type === 'manual_override') {
|
|
if (element.classList.contains('lunch')) {
|
|
element.className = 'time-slot available';
|
|
} else {
|
|
element.className = 'time-slot lunch';
|
|
}
|
|
element.innerHTML = `<span>${timeText} (휴게)</span>`;
|
|
} else {
|
|
element.className = result.new_status == 1 ? 'time-slot available' : 'time-slot blocked';
|
|
element.innerHTML = `<span>${timeText}</span>`;
|
|
}
|
|
} else {
|
|
alert('상태 변경 실패: ' + result.message);
|
|
element.className = originalClasses;
|
|
element.innerHTML = `<span>${element.innerText.split(' ')[0]}</span>`;
|
|
}
|
|
} catch (error) {
|
|
alert('오류 발생: ' + error);
|
|
element.className = originalClasses;
|
|
element.innerHTML = `<span>${element.innerText.split(' ')[0]}</span>`;
|
|
}
|
|
}
|
|
|
|
// 💡 [추가] 스케줄 생성/재생성 함수
|
|
async function generateSchedule(year, month,btn) {
|
|
// const btn = document.querySelector('.status-card button[onclick="generateSchedule("+year+","+month")"]');
|
|
const actionText = btn.textContent.trim().includes('재생성') ? '재생성' : '생성';
|
|
|
|
if (!confirm(`${year}년 ${month}월 스케줄을 ${actionText}하시겠습니까?\n\n기존에 수동으로 변경한 슬롯이 있다면 초기화될 수 있습니다.`)) {
|
|
return;
|
|
}
|
|
|
|
const originalText = btn.innerHTML;
|
|
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> 생성 중...';
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('action', 'generate_schedule');
|
|
formData.append('year', year);
|
|
formData.append('month', month);
|
|
|
|
// 현재 페이지 URL('')로 AJAX 요청을 보냅니다.
|
|
const response = await fetch('', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
const result = await response.json();
|
|
|
|
alert(result.message);
|
|
if (result.success) {
|
|
location.reload();
|
|
}
|
|
} catch (error) {
|
|
alert('스케줄 생성 중 오류가 발생했습니다: ' + error);
|
|
} finally {
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<?php
|
|
include_once(G5_ADMIN_PATH . '/admin.tail.php');
|
|
?>
|