332 lines
13 KiB
PHP
332 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* 상담 예약 시스템 - 월별 스케줄 자동 생성 클래스
|
|
*
|
|
* 요일별 설정을 기반으로 월별 상세 스케줄을 자동 생성하고 관리합니다.
|
|
*/
|
|
|
|
class ScheduleGenerator
|
|
{
|
|
private $db;
|
|
|
|
public function __construct()
|
|
{
|
|
global $connect_db;
|
|
$this->db = $connect_db;
|
|
}
|
|
|
|
/**
|
|
* 특정 월의 전체 스케줄 생성
|
|
*
|
|
* @param int $year 년도
|
|
* @param int $month 월
|
|
* @return bool 성공 여부
|
|
*/
|
|
public function generateMonth($year, $month)
|
|
{
|
|
try {
|
|
// 기본 설정 조회
|
|
$basicSettings = $this->getBasicSettings();
|
|
|
|
// 요일별 설정 조회
|
|
$weeklySettings = $this->getWeeklySettings();
|
|
|
|
// 해당 월의 모든 날짜 생성
|
|
$dates = self::getMonthDates($year, $month);
|
|
|
|
// 기존 스케줄 중 예약이 없는 것들 삭제 (재생성을 위해)
|
|
$this->clearAutoGeneratedSchedules($year, $month);
|
|
|
|
// 각 날짜별로 스케줄 생성
|
|
foreach ($dates as $date) {
|
|
$dayOfWeek = date('N', strtotime($date)); // 1=월요일, 7=일요일
|
|
$this->generateDay($date, $dayOfWeek, $weeklySettings, $basicSettings);
|
|
}
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
consultant_log("스케줄 생성 실패: " . $e->getMessage(), 'error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 날짜의 스케줄 생성
|
|
*/
|
|
private function generateDay($date, $dayOfWeek, $weeklySettings, $basicSettings)
|
|
{
|
|
$dayNames = ['', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
|
$dayName = $dayNames[$dayOfWeek];
|
|
|
|
// 해당 요일이 운영일인지 확인
|
|
$isEnabled = $weeklySettings[$dayName . '_enabled'] ?? '0';
|
|
|
|
if ($isEnabled == '0') {
|
|
// 휴무일 처리
|
|
$this->insertHolidaySchedule($date);
|
|
return;
|
|
}
|
|
|
|
// 운영시간 정보
|
|
$startTime = $weeklySettings[$dayName . '_start'] ?? '09:00';
|
|
$endTime = $weeklySettings[$dayName . '_end'] ?? '18:00';
|
|
$lunchStart = $weeklySettings[$dayName . '_lunch_start'] ?? '12:00';
|
|
$lunchEnd = $weeklySettings[$dayName . '_lunch_end'] ?? '13:00';
|
|
|
|
// 시간 슬롯 생성
|
|
$this->createTimeSlots($date, $startTime, $endTime, $basicSettings['consultation_duration'], $basicSettings['max_persons_per_slot']);
|
|
|
|
// 점심시간 블록 처리
|
|
// 💡 [수정] 시작 시간과 종료 시간이 다를 때만 점심시간으로 처리합니다.
|
|
if ($lunchStart && $lunchEnd && strtotime($lunchStart) < strtotime($lunchEnd)) {
|
|
$this->blockLunchTime($date, $lunchStart, $lunchEnd);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 시간 슬롯 생성
|
|
*/
|
|
private function createTimeSlots($date, $startTime, $endTime, $slotDuration, $maxPersons)
|
|
{
|
|
$currentTime = strtotime($startTime);
|
|
$endTimeStamp = strtotime($endTime);
|
|
$slotMinutes = (int) $slotDuration;
|
|
|
|
while ($currentTime < $endTimeStamp) {
|
|
$slotStart = date('H:i', $currentTime);
|
|
$slotEnd = date('H:i', $currentTime + ($slotMinutes * 60));
|
|
|
|
// 종료시간을 넘지 않는 경우만 생성
|
|
if (strtotime($slotEnd) <= $endTimeStamp) {
|
|
// 재생성 시 중복 생성을 방지하기 위해 기존 슬롯이 있는지 확인
|
|
$check_sql = "SELECT id FROM consultant_schedule
|
|
WHERE specific_date = '" . sql_real_escape_string($date) . "'
|
|
AND start_time = '" . sql_real_escape_string($slotStart) . "'";
|
|
|
|
$existing_slot = sql_fetch($check_sql);
|
|
|
|
// 기존에 수동으로 추가되었거나 예약으로 보호된 슬롯이 없으면 새로 생성
|
|
if (!$existing_slot) {
|
|
$sql = "INSERT INTO consultant_schedule
|
|
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, created_at)
|
|
VALUES (
|
|
'" . sql_real_escape_string($date) . "',
|
|
'" . sql_real_escape_string($slotStart) . "',
|
|
'" . sql_real_escape_string($slotEnd) . "',
|
|
" . (int) $slotMinutes . ",
|
|
" . (int) $maxPersons . ",
|
|
1,
|
|
'auto_generated',
|
|
NOW()
|
|
)";
|
|
|
|
if (!sql_query($sql)) {
|
|
// sql_query가 false를 반환하면 오류를 던짐
|
|
throw new Exception("시간 슬롯 생성 실패: " . sql_error());
|
|
}
|
|
}
|
|
}
|
|
$currentTime += ($slotMinutes * 60);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 점심시간 블록 처리
|
|
*/
|
|
private function blockLunchTime($date, $lunchStart, $lunchEnd)
|
|
{
|
|
$sql = "UPDATE consultant_schedule
|
|
SET is_available = 0, temp_1 = 'lunch_time', temp_2 = '점심시간', updated_at = NOW()
|
|
WHERE specific_date = '" . sql_real_escape_string($date) . "'
|
|
-- 💡 [수정] start_time이 휴게시간 범위에 포함되는 모든 슬롯을 대상으로 하도록 변경합니다.
|
|
-- 이렇게 하면 상담 시간 단위(30분, 60분 등)에 상관없이 정확하게 동작합니다.
|
|
AND start_time >= '" . sql_real_escape_string($lunchStart) . "' AND start_time < '" . sql_real_escape_string($lunchEnd) . "'";
|
|
|
|
sql_query($sql);
|
|
}
|
|
|
|
/**
|
|
* 휴무일 스케줄 삽입
|
|
*/
|
|
private function insertHolidaySchedule($date)
|
|
{
|
|
// 💡 [수정] 휴무일 데이터 중복 생성을 방지하기 위해, 해당 날짜에 이미 스케줄이 있는지 확인합니다.
|
|
$check_sql = "SELECT id FROM consultant_schedule WHERE specific_date = '" . sql_real_escape_string($date) . "'";
|
|
$existing_schedule = sql_fetch($check_sql);
|
|
|
|
// 해당 날짜에 아무 스케줄도 없을 때만 휴무일 데이터를 삽입합니다.
|
|
if (!$existing_schedule) {
|
|
$sql = "INSERT INTO consultant_schedule
|
|
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, temp_2, created_at)
|
|
VALUES (
|
|
'" . sql_real_escape_string($date) . "',
|
|
'00:00',
|
|
'23:59',
|
|
0,
|
|
0,
|
|
0,
|
|
'holiday',
|
|
'휴무일',
|
|
NOW()
|
|
)";
|
|
|
|
sql_query($sql);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기존 예약 보호 로직 - 예약이 있는 시간대는 삭제하지 않음
|
|
*/
|
|
private function protectExistingReservations($year, $month)
|
|
{
|
|
$startDate = sprintf('%04d-%02d-01', $year, $month);
|
|
$endDate = date('Y-m-t', strtotime($startDate));
|
|
|
|
// 해당 월에 예약이 있는 스케줄 ID 조회
|
|
$sql = "SELECT DISTINCT s.id
|
|
FROM consultant_schedule s
|
|
INNER JOIN consultant_reservations r ON (
|
|
s.specific_date = r.reservation_date
|
|
AND s.start_time = r.reservation_time
|
|
)
|
|
WHERE s.specific_date BETWEEN '{$startDate}' AND '{$endDate}'
|
|
AND r.status NOT IN ('cancelled')
|
|
AND r.is_deleted = 0";
|
|
|
|
$result = sql_query($sql);
|
|
$protectedIds = [];
|
|
|
|
while ($row = sql_fetch_array($result)) {
|
|
$protectedIds[] = $row['id'];
|
|
}
|
|
|
|
return $protectedIds;
|
|
}
|
|
|
|
/**
|
|
* 설정 변경으로 인한 기존 예약과의 충돌 감지 (향상된 로직)
|
|
* - 휴무일로 변경된 경우
|
|
* - 운영 시간 밖으로 밀려난 경우
|
|
* - 점심시간과 겹치는 경우
|
|
*/
|
|
public function findConflictsWithNewSettings($year, $month)
|
|
{
|
|
$conflicts = [];
|
|
$startDate = sprintf('%04d-%02d-01', $year, $month);
|
|
$endDate = date('Y-m-t', strtotime($startDate));
|
|
|
|
// 해당 월의 모든 예약 조회
|
|
$sql = "SELECT r.reservation_date, r.reservation_time, r.customer_name, r.customer_phone
|
|
FROM consultant_reservations r
|
|
WHERE r.reservation_date BETWEEN '{$startDate}' AND '{$endDate}'
|
|
AND r.status NOT IN ('cancelled')
|
|
AND r.is_deleted = 0";
|
|
|
|
$weeklySettings = $this->getWeeklySettings();
|
|
|
|
$result = sql_query($sql);
|
|
while ($row = sql_fetch_array($result)) {
|
|
$dayOfWeek = date('N', strtotime($row['reservation_date']));
|
|
$dayNames = ['', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
|
$dayName = $dayNames[$dayOfWeek];
|
|
|
|
$isEnabled = $weeklySettings[$dayName . '_enabled'] ?? '0';
|
|
$startTime = $weeklySettings[$dayName . '_start'] ?? '09:00';
|
|
$endTime = $weeklySettings[$dayName . '_end'] ?? '18:00';
|
|
$lunchStart = $weeklySettings[$dayName . '_lunch_start'] ?? '12:00';
|
|
$lunchEnd = $weeklySettings[$dayName . '_lunch_end'] ?? '13:00';
|
|
|
|
$reservationTime = $row['reservation_time'];
|
|
$conflictReason = '';
|
|
|
|
if ($isEnabled == '0') {
|
|
$conflictReason = '휴무일로 변경됨';
|
|
} elseif (strtotime($reservationTime) < strtotime($startTime) || strtotime($reservationTime) >= strtotime($endTime)) {
|
|
$conflictReason = '운영 시간 벗어남';
|
|
} elseif (strtotime($reservationTime) >= strtotime($lunchStart) && strtotime($reservationTime) < strtotime($lunchEnd)) {
|
|
$conflictReason = '점심시간과 겹침';
|
|
}
|
|
|
|
if ($conflictReason) {
|
|
$conflicts[] = [
|
|
'date' => $row['reservation_date'],
|
|
'time' => $reservationTime,
|
|
'customer' => $row['customer_name'],
|
|
'phone' => $row['customer_phone'],
|
|
'reason' => $conflictReason
|
|
];
|
|
}
|
|
}
|
|
return $conflicts;
|
|
}
|
|
|
|
/**
|
|
* 자동 생성된 스케줄 삭제 (예약이 없는 것만)
|
|
*/
|
|
private function clearAutoGeneratedSchedules($year, $month)
|
|
{
|
|
$startDate = sprintf('%04d-%02d-01', (int)$year, (int)$month);
|
|
$endDate = date('Y-m-t', strtotime($startDate));
|
|
|
|
// 예약이 있는 '시간 슬롯'의 ID를 보호
|
|
$protectedIds = $this->protectExistingReservations($year, $month);
|
|
|
|
// 💡 [수정] 예약이 있는 날짜라도 'holiday' 타입의 스케줄은 삭제될 수 있도록 temp_1 조건만 사용합니다.
|
|
$whereClause = "specific_date BETWEEN '{$startDate}' AND '{$endDate}'
|
|
AND temp_1 IN ('auto_generated', 'lunch_time', 'holiday', 'manual_block')";
|
|
|
|
if (!empty($protectedIds)) {
|
|
$whereClause .= " AND id NOT IN (" . implode(',', $protectedIds) . ")";
|
|
}
|
|
|
|
$sql = "DELETE FROM consultant_schedule WHERE " . $whereClause;
|
|
sql_query($sql);
|
|
}
|
|
|
|
/**
|
|
* 해당 월의 모든 날짜 배열 생성
|
|
*/
|
|
private static function getMonthDates($year, $month)
|
|
{
|
|
$dates = [];
|
|
$daysInMonth = date('t', mktime(0, 0, 0, $month, 1, $year));
|
|
|
|
for ($day = 1; $day <= $daysInMonth; $day++) {
|
|
$dates[] = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
|
}
|
|
|
|
return $dates;
|
|
}
|
|
|
|
/**
|
|
* 기본 설정 조회
|
|
*/
|
|
private function getBasicSettings()
|
|
{
|
|
return [
|
|
'consultation_duration' => (int) consultant_get_config('consultation_duration', 60),
|
|
'max_persons_per_slot' => (int) consultant_get_config('max_persons_per_slot', 2)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 요일별 설정 조회
|
|
*/
|
|
private function getWeeklySettings()
|
|
{
|
|
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
|
$settings = [];
|
|
|
|
foreach ($days as $day) {
|
|
$settings[$day . '_enabled'] = consultant_get_config($day . '_enabled', $day == 'saturday' || $day == 'sunday' ? '0' : '1');
|
|
$settings[$day . '_start'] = consultant_get_config($day . '_start', '09:00');
|
|
$settings[$day . '_end'] = consultant_get_config($day . '_end', '18:00');
|
|
$settings[$day . '_lunch_start'] = consultant_get_config($day . '_lunch_start', '12:00');
|
|
$settings[$day . '_lunch_end'] = consultant_get_config($day . '_lunch_end', '13:00');
|
|
}
|
|
|
|
return $settings;
|
|
}
|
|
}
|
|
?>
|