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,470 @@
<?php
/**
* 상담 예약 관리 클래스
*/
if (!defined('_GNUBOARD_'))
exit;
class ReservationManager
{
private $table_name = 'consultant_reservations';
/**
* 예약 목록 조회
*/
public function getReservationList($status = '', $date = '', $page = 1, $per_page = 20, $search_type = '', $search_keyword = '')
{
try {
$where_conditions = ["is_deleted = 0"];
// 상태 필터
if (!empty($status)) {
$where_conditions[] = "status = '" . sql_real_escape_string($status) . "'";
}
// 날짜 필터
if (!empty($date)) {
$where_conditions[] = "reservation_date = '" . sql_real_escape_string($date) . "'";
}
// 검색 조건
if (!empty($search_keyword) && !empty($search_type)) {
$search_keyword = sql_real_escape_string($search_keyword);
switch ($search_type) {
case 'customer_name':
$where_conditions[] = "customer_name LIKE '%{$search_keyword}%'";
break;
case 'customer_phone':
$where_conditions[] = "customer_phone LIKE '%{$search_keyword}%'";
break;
}
}
$where_clause = implode(' AND ', $where_conditions);
// 전체 개수 조회
$count_sql = "SELECT COUNT(*) as total FROM {$this->table_name} WHERE {$where_clause}";
$count_result = sql_fetch($count_sql);
$total = $count_result['total'];
// 페이징 계산
$offset = ($page - 1) * $per_page;
$total_pages = ceil($total / $per_page);
// 목록 조회
$sql = "SELECT * FROM {$this->table_name}
WHERE {$where_clause}
ORDER BY created_at DESC
LIMIT {$offset}, {$per_page}";
$reservations = [];
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$reservations[] = $row;
}
return [
'success' => true,
'data' => [
'reservations' => $reservations,
'pagination' => [
'current_page' => $page,
'per_page' => $per_page,
'total' => $total,
'total_pages' => $total_pages
]
]
];
} catch (Exception $e) {
consultant_log("예약 목록 조회 실패: " . $e->getMessage(), 'error');
return [
'success' => false,
'message' => '예약 목록 조회 중 오류가 발생했습니다.'
];
}
}
/**
* 예약 생성
*/
public function createReservation($data)
{
try {
// 필수 필드 검증
$required_fields = ['customer_name', 'customer_phone', 'reservation_date', 'reservation_time'];
foreach ($required_fields as $field) {
if (empty($data[$field])) {
throw new Exception("필수 필드가 누락되었습니다: {$field}");
}
}
// 중복 예약 확인
$check_sql = "SELECT COUNT(*) as count FROM {$this->table_name}
WHERE reservation_date = '" . sql_real_escape_string($data['reservation_date']) . "'
AND reservation_time = '" . sql_real_escape_string($data['reservation_time']) . "'
AND status IN ('payment_pending', 'reserved')
AND is_deleted = 0";
$check_result = sql_fetch($check_sql);
$current_count = $check_result['count'];
// 최대 인원 확인 (기본값 2명)
$max_persons = consultant_get_config('default_max_persons', 2);
if ($current_count >= $max_persons) {
throw new Exception('해당 시간대는 예약이 마감되었습니다.');
}
// 데이터 준비
$insert_data = [
'customer_name' => sql_real_escape_string($data['customer_name']),
'customer_phone' => sql_real_escape_string($data['customer_phone']),
'customer_email' => sql_real_escape_string($data['customer_email'] ?? ''),
'reservation_date' => sql_real_escape_string($data['reservation_date']),
'reservation_time' => sql_real_escape_string($data['reservation_time']),
'consultation_type' => sql_real_escape_string($data['consultation_type'] ?? 'onsite'),
'status' => 'payment_pending',
'payment_amount' => (int) ($data['payment_amount'] ?? consultant_get_config('consultation_fee', 50000)),
'payment_status' => 'pending',
'request_memo' => sql_real_escape_string($data['request_memo'] ?? ''),
'wr_id' => (int) ($data['wr_id'] ?? 0),
'temp_1' => sql_real_escape_string($data['temp_1'] ?? ''),
'created_at' => 'NOW()',
'updated_at' => 'NOW()'
];
// SQL 생성
$fields = implode(', ', array_keys($insert_data));
$values = "'" . implode("', '", array_values($insert_data)) . "'";
$values = str_replace("'NOW()'", "NOW()", $values); // NOW() 함수 처리
$sql = "INSERT INTO {$this->table_name} ({$fields}) VALUES ({$values})";
if (!sql_query($sql)) {
throw new Exception('데이터베이스 저장 실패: ' . sql_error());
}
$reservation_id = sql_insert_id();
// 알림 발송
$this->sendReservationNotification($reservation_id, 'created');
consultant_log("새 예약 생성: ID {$reservation_id}, 고객: {$data['customer_name']}");
return [
'success' => true,
'message' => '예약이 성공적으로 접수되었습니다.',
'data' => [
'reservation_id' => $reservation_id
]
];
} catch (Exception $e) {
consultant_log("예약 생성 실패: " . $e->getMessage(), 'error');
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 예약 상태 변경
*/
public function updateReservationStatus($reservation_id, $new_status, $admin_memo = '')
{
try {
// 예약 정보 조회
$reservation = $this->getReservationById($reservation_id);
if (!$reservation) {
throw new Exception('예약 정보를 찾을 수 없습니다.');
}
// 상태 변경 가능 여부 확인
$valid_transitions = [
'payment_pending' => ['reserved', 'cancelled'],
'reserved' => ['completed', 'cancelled'],
'completed' => [],
'cancelled' => []
];
$current_status = $reservation['status'];
if (!in_array($new_status, $valid_transitions[$current_status])) {
throw new Exception('현재 상태에서 해당 상태로 변경할 수 없습니다.');
}
// 상태 업데이트
$sql = "UPDATE {$this->table_name}
SET status = '" . sql_real_escape_string($new_status) . "',
admin_memo = '" . sql_real_escape_string($admin_memo) . "',
updated_at = NOW()
WHERE id = {$reservation_id}";
if (!sql_query($sql)) {
throw new Exception('상태 변경 실패: ' . sql_error());
}
// 알림 발송
$this->sendReservationNotification($reservation_id, 'status_changed', $new_status);
consultant_log("예약 상태 변경: ID {$reservation_id}, {$current_status} -> {$new_status}");
return [
'success' => true,
'message' => '예약 상태가 성공적으로 변경되었습니다.'
];
} catch (Exception $e) {
consultant_log("예약 상태 변경 실패: " . $e->getMessage(), 'error');
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 입금 확인
*/
public function confirmPayment($reservation_id, $admin_id)
{
try {
// 예약 정보 조회
$reservation = $this->getReservationById($reservation_id);
if (!$reservation) {
throw new Exception('예약 정보를 찾을 수 없습니다.');
}
if ($reservation['status'] !== 'payment_pending') {
throw new Exception('입금 대기 상태의 예약만 확인할 수 있습니다.');
}
// 입금 확인 처리
$sql = "UPDATE {$this->table_name}
SET status = 'reserved',
payment_status = 'paid',
payment_confirmed_at = NOW(),
payment_confirmed_by = '" . sql_real_escape_string($admin_id) . "',
updated_at = NOW()
WHERE id = {$reservation_id}";
if (!sql_query($sql)) {
throw new Exception('입금 확인 처리 실패: ' . sql_error());
}
// 알림 발송
$this->sendReservationNotification($reservation_id, 'payment_confirmed');
consultant_log("입금 확인: ID {$reservation_id}, 확인자: {$admin_id}");
return [
'success' => true,
'message' => '입금이 확인되어 예약이 확정되었습니다.'
];
} catch (Exception $e) {
consultant_log("입금 확인 실패: " . $e->getMessage(), 'error');
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 예약 취소
*/
public function cancelReservation($reservation_id, $reason)
{
try {
// 예약 정보 조회
$reservation = $this->getReservationById($reservation_id);
if (!$reservation) {
throw new Exception('예약 정보를 찾을 수 없습니다.');
}
if ($reservation['status'] === 'cancelled') {
throw new Exception('이미 취소된 예약입니다.');
}
if ($reservation['status'] === 'completed') {
throw new Exception('완료된 예약은 취소할 수 없습니다.');
}
// 예약 취소 처리
$sql = "UPDATE {$this->table_name}
SET status = 'cancelled',
admin_memo = '" . sql_real_escape_string($reason) . "',
updated_at = NOW()
WHERE id = {$reservation_id}";
if (!sql_query($sql)) {
throw new Exception('예약 취소 처리 실패: ' . sql_error());
}
// 알림 발송
$this->sendReservationNotification($reservation_id, 'cancelled', $reason);
consultant_log("예약 취소: ID {$reservation_id}, 사유: {$reason}");
return [
'success' => true,
'message' => '예약이 성공적으로 취소되었습니다.'
];
} catch (Exception $e) {
consultant_log("예약 취소 실패: " . $e->getMessage(), 'error');
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 예약 정보 조회
*/
public function getReservationById($reservation_id)
{
$sql = "SELECT * FROM {$this->table_name} WHERE id = {$reservation_id} AND is_deleted = 0";
return sql_fetch($sql);
}
/**
* 예약 알림 발송
*/
private function sendReservationNotification($reservation_id, $type, $extra_data = null)
{
try {
$reservation = $this->getReservationById($reservation_id);
if (!$reservation) {
return false;
}
// 알림 템플릿 키 결정
$template_keys = [
'created' => 'consultant_reservation_customer',
'payment_confirmed' => 'consultant_confirmed_customer',
'cancelled' => 'consultant_cancelled_customer',
'status_changed' => 'consultant_status_changed_customer'
];
$template_key = $template_keys[$type] ?? null;
if (!$template_key) {
return false;
}
// 템플릿 변수 준비
$variables = [
'customer_name' => $reservation['customer_name'],
'customer_phone' => $reservation['customer_phone'],
'customer_email' => $reservation['customer_email'],
'reservation_date' => $reservation['reservation_date'],
'reservation_time' => $reservation['reservation_time'],
'payment_amount' => number_format($reservation['payment_amount']),
'account_info' => consultant_get_config('account_info', ''),
'cancel_reason' => $extra_data ?? ''
];
// 이메일 발송
if (!empty($reservation['customer_email'])) {
$this->sendEmailNotification($reservation['customer_email'], $template_key, $variables);
}
// SMS 발송
if (!empty($reservation['customer_phone'])) {
$this->sendSmsNotification($reservation['customer_phone'], $template_key, $variables);
}
// 관리자 알림 (새 예약 시)
if ($type === 'created') {
$this->sendAdminNotification($reservation, $variables);
}
return true;
} catch (Exception $e) {
consultant_log("알림 발송 실패: " . $e->getMessage(), 'error');
return false;
}
}
/**
* 이메일 알림 발송
*/
private function sendEmailNotification($email, $template_key, $variables)
{
// 💡 [수정] consultant_send_notification 함수 사용으로 변경
return consultant_send_notification('email', $template_key, array_merge($variables, ['customer_email' => $email]));
}
/**
* SMS 알림 발송
*/
private function sendSmsNotification($phone, $template_key, $variables)
{
// 💡 [수정] consultant_send_notification 함수 사용으로 변경
return consultant_send_notification('sms', $template_key, array_merge($variables, ['customer_phone' => $phone]));
}
/**
* 관리자 알림 발송
*/
private function sendAdminNotification($reservation, $variables)
{
// 관리자 이메일 알림
$admin_template_key = 'consultant_reservation_admin';
// 관리자 이메일 주소 (설정에서 가져오거나 기본값 사용)
$admin_email = consultant_get_config('admin_email', get_admin_email());
if ($admin_email) {
// 💡 [수정] consultant_send_notification 함수 사용으로 변경
consultant_send_notification('email', $admin_template_key, array_merge($variables, ['customer_email' => $admin_email]));
}
}
/**
* 템플릿 변수 치환
*/
private function replaceTemplateVariables($template, $variables)
{
foreach ($variables as $key => $value) {
$template = str_replace('{' . $key . '}', $value, $template);
}
return $template;
}
/**
* 예약 통계 조회
*/
public function getReservationStats($start_date = null, $end_date = null)
{
try {
if (!$start_date)
$start_date = date('Y-m-01'); // 이번 달 첫날
if (!$end_date)
$end_date = date('Y-m-d'); // 오늘
$sql = "SELECT
COUNT(*) as total,
COUNT(CASE WHEN status = 'payment_pending' THEN 1 END) as pending,
COUNT(CASE WHEN status = 'reserved' THEN 1 END) as confirmed,
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled,
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as total_revenue
FROM {$this->table_name}
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
AND is_deleted = 0";
return sql_fetch($sql);
} catch (Exception $e) {
consultant_log("통계 조회 실패: " . $e->getMessage(), 'error');
return null;
}
}
}
@@ -0,0 +1,332 @@
<?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;
}
}
?>
@@ -0,0 +1,384 @@
<?php
if (!defined('_GNUBOARD_'))
exit;
// ❗ [핵심] 필요한 클래스와 라이브러리를 포함합니다.
if (file_exists(G5_ADMIN_PATH . '/mail_manage/classes/MailSender.php')) {
require_once(G5_ADMIN_PATH . '/mail_manage/classes/MailSender.php');
}
if (file_exists(G5_PLUGIN_PATH . '/sms5/sms5.lib.php')) {
include_once(G5_PLUGIN_PATH . '/sms5/sms5.lib.php');
}
/**
* 통합 메일/SMS 발송 클래스
* 기존 mail_manage, sms_admin 시스템을 활용하여 발송 및 이력 기록
*/
class NotificationSender
{
private $g5;
public function __construct()
{
global $g5;
$this->g5 = $g5;
}
/**
* ❗ [핵심 수정] 템플릿 기반 메인 발송 함수
* @param array $params 발송 파라미터
* @return array 발송 결과
*/
public function send($params)
{
// 파라미터 검증
$validated = $this->validateParams($params);
if (!$validated['success']) {
return $validated;
}
// 대상 회원 조회
$members = $this->getTargetMembers($params);
if (empty($members)) {
return ['success' => false, 'message' => '발송 대상 회원이 없습니다.'];
}
$results = [
'success' => true,
'total_targets' => count($members),
'sms_success' => 0,
'sms_fail' => 0,
'email_success' => 0,
'email_fail' => 0,
'message' => ''
];
// SMS 발송
if (!empty($params['sms_template_key'])) {
$sms_result = $this->sendSMS($members, $params['sms_template_key'], $params['vars'] ?? []);
$results['sms_success'] = $sms_result['success'];
$results['sms_fail'] = $sms_result['fail'];
}
// 이메일 발송
if (!empty($params['email_template_key'])) {
$email_result = $this->sendEmail($members, $params['email_template_key'], $params['vars'] ?? []);
$results['email_success'] = $email_result['success'];
$results['email_fail'] = $email_result['fail'];
}
$results['message'] = $this->generateResultMessage($results);
return $results;
}
/**
* ❗ [핵심 수정] 템플릿 기반 파라미터 검증
*/
private function validateParams($params)
{
if (empty($params['target_type'])) {
return ['success' => false, 'message' => "필수 파라미터 'target_type'이 누락되었습니다."];
}
if ($params['target_type'] === 'single' && empty($params['member_id'])) {
return ['success' => false, 'message' => '단일 발송 시 회원 ID(member_id)가 필요합니다.'];
}
if ($params['target_type'] === 'bulk' && empty($params['member_levels'])) {
return ['success' => false, 'message' => '대량 발송 시 회원 레벨(member_levels)이 필요합니다.'];
}
if (empty($params['sms_template_key']) && empty($params['email_template_key'])) {
return ['success' => false, 'message' => 'SMS 또는 이메일 템플릿 키 중 하나는 반드시 필요합니다.'];
}
return ['success' => true];
}
/**
* 대상 회원 조회
*/
private function getTargetMembers($params)
{
$members = [];
$member_table = $this->g5['member_table'];
if ($params['target_type'] === 'single') {
$sql = "SELECT mb_id, mb_name, mb_hp, mb_email, mb_sms, mb_mailling FROM `{$member_table}` WHERE mb_id = '" . sql_real_escape_string($params['member_id']) . "' AND mb_leave_date = '' AND mb_intercept_date = ''";
} else {
$levels = array_map('intval', $params['member_levels']);
$level_condition = implode(',', $levels);
$sql = "SELECT mb_id, mb_name, mb_hp, mb_email, mb_sms, mb_mailling FROM `{$member_table}` WHERE mb_level IN ({$level_condition}) AND mb_leave_date = '' AND mb_intercept_date = ''";
}
$this->write_debug_log("[SMS 발송 시작] sql '{$sql}'");
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$members[] = $row;
}
return $members;
}
/**
* ❗ [핵심 수정] SMS 발송 (템플릿 및 변수 처리, 이력 기록 포함)
*/
private function sendSMS($members, $template_key, $common_vars)
{
$success = 0;
$fail = 0;
$notification_mode = get_order_config('notification_mode', 'log');
$is_test_mode = ($notification_mode !== 'send');
$sizeof_members = count($members);
// 💡 [수정] 템플릿 조회 로직 변경: 지정 테이블(consultant_sms_templates) -> 기본 테이블(sms_templates) 순서로 조회
$template = null;
// 1. 지정 테이블 조회
$check_table = sql_query("SHOW TABLES LIKE 'consultant_sms_templates'", false);
if (sql_num_rows($check_table) > 0) {
$template = sql_fetch("SELECT * FROM `consultant_sms_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
if ($template) {
// 필드명 통일 (기본 테이블과 필드명이 다를 수 있음)
$template['content'] = $template['template_content'];
}
}
// 2. 기본 테이블 조회 (지정 테이블에 없을 경우)
if (!$template) {
$template = sql_fetch("SELECT * FROM `sms_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
}
// 3. 둘 다 없으면 에러 처리
if (!$template) {
$this->write_debug_log("[SMS 발송 오류] 템플릿 '{$template_key}'을(를) 찾을 수 없습니다.");
return ['success' => 0, 'fail' => count($members)];
}
$count= count($members);
if ($is_test_mode) {
// --- 개발 모드: 로그 파일에만 기록 ---
foreach ($members as $member) {
if ($member['mb_sms'] && !empty($member['mb_hp'])) {
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
$personal_message = $template['content'];
foreach ($personal_vars as $key => $value) {
$personal_message = str_replace('{' . $key . '}', $value, $personal_message);
}
$this->write_debug_log("[SMS LOG] To: {$member['mb_hp']}, Content: {$personal_message}");
$success++;
} else {
$empty = !empty($member['mb_hp']);
$this->write_debug_log("[SMS 발송오류] 사용자 '{$member['mb_sms']}' '{$member['mb_hp']}' '$empty'");
$fail++;
}
}
} else {
// --- 실제 발송 모드: DB 기록 및 실제 발송 ---
$sms_config = sql_fetch("SELECT * FROM {$this->g5['sms5_config_table']}");
$send_phone = $sms_config['cf_phone'];
$wr_message = sql_real_escape_string($template['content']);
$wr_reply = sql_real_escape_string($send_phone);
sql_query("INSERT INTO {$this->g5['sms5_write_table']} (wr_message, wr_reply, wr_total, wr_datetime) VALUES ('{$wr_message}', '{$wr_reply}', '" . count($members) . "', '" . G5_TIME_YMDHIS . "')");
$wr_no = sql_insert_id();
$SMS = null;
if (class_exists('SMS5')) {
$SMS = new SMS5;
$SMS->SMS_con($sms_config['cf_sms_ip'], $sms_config['cf_sms_id'], $sms_config['cf_sms_pw'], $sms_config['cf_sms_port']);
} else {
$this->write_debug_log("[SMS 발송 오류] SMS5 클래스를 찾을 수 없습니다. 실제 발송을 건너뜁니다.");
}
foreach ($members as $member) {
if ($member['mb_sms'] && !empty($member['mb_hp'])) {
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
$personal_message = $template['content'];
foreach ($personal_vars as $key => $value) {
$personal_message = str_replace('{' . $key . '}', $value, $personal_message);
}
$result_code = 'Fail';
$result_msg = 'SMS5 클래스가 없어 발송할 수 없습니다.';
$hs_status = '0';
if ($SMS) {
$SMS->Add($member['mb_hp'], $send_phone, '', $personal_message);
$SMS->Send();
$result_arr = $SMS->Result;
$result_code = 'Fail';
$result_msg = '서버로부터 응답이 없습니다.';
if(!empty($result_arr)){
$result_parts = explode(':', $result_arr[0]);
if(count($result_parts) > 1 && strpos($result_parts[1], 'Error') === false) {
$result_code = 'Success';
$result_msg = $result_parts[1];
} else {
$result_msg = $result_arr[0];
}
}
$hs_status = ($result_code == 'Success') ? '1' : '0';
$SMS->Init();
}
sql_query("INSERT INTO {$this->g5['sms5_history_table']} (wr_no, mb_id, hs_name, hs_hp, hs_datetime, hs_status, hs_message) VALUES ('{$wr_no}', '{$member['mb_id']}', '{$member['mb_name']}', '{$member['mb_hp']}', '" . G5_TIME_YMDHIS . "', '{$hs_status}', '{$result_msg}')");
if ($hs_status == '1') $success++;
else $fail++;
} else {
$fail++;
}
}
}
return ['success' => $success, 'fail' => $fail];
}
/**
* ❗ [핵심 수정] 이메일 발송 (MailSender 클래스 활용)
*/
private function sendEmail($members, $template_key, $common_vars)
{
$success = 0;
$fail = 0;
$notification_mode = get_order_config('notification_mode', 'log');
$is_test_mode = ($notification_mode !== 'send');
$sizeof_members = count($members);
// 💡 [수정] 템플릿 조회 로직 변경: 지정 테이블(consultant_mail_templates) -> 기본 테이블(mail_templates) 순서로 조회
$template = null;
// 1. 지정 테이블 조회
$check_table = sql_query("SHOW TABLES LIKE 'consultant_mail_templates'", false);
if (sql_num_rows($check_table) > 0) {
$template = sql_fetch("SELECT * FROM `consultant_mail_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
if ($template) {
// 필드명 통일
$template['subject'] = $template['template_subject'];
$template['content'] = $template['template_content'];
}
}
// 2. 기본 테이블 조회 (지정 테이블에 없을 경우)
if (!$template) {
$template = sql_fetch("SELECT * FROM `mail_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
}
// 3. 둘 다 없으면 에러 처리
if (!$template) {
$this->write_debug_log("[EMAIL 발송 오류] 템플릿 '{$template_key}'을(를) 찾을 수 없습니다.");
return ['success' => 0, 'fail' => count($members)];
}
if ($is_test_mode) {
// --- 개발 모드: 로그 파일에만 기록 ---
foreach ($members as $member) {
if ($member['mb_mailling'] && !empty($member['mb_email'])) {
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
$subject = $template['subject'];
$content = $template['content'];
foreach ($personal_vars as $key => $value) {
$search = '{' . $key . '}';
$subject = str_replace($search, $value, $subject);
$content = str_replace($search, $value, $content);
}
$this->write_debug_log("[EMAIL LOG] To: {$member['mb_email']}, Subject: {$subject}, Content: {$content}");
$success++;
} else {
$fail++;
$empty = !empty($member['mb_email']);
$this->write_debug_log("[EMAIL 발송 오류] 사용자 '{$member['mb_sms']}' '{$member['mb_hp']}' '$empty'");
}
}
} else {
// --- 실제 발송 모드: MailSender 호출 ---
if (!class_exists('MailSender')) {
$this->write_debug_log("[EMAIL 발송 오류] MailSender 클래스를 찾을 수 없습니다.");
return ['success' => 0, 'fail' => count($members)];
}
$mailSender = new MailSender();
foreach ($members as $member) {
if ($member['mb_mailling'] && !empty($member['mb_email'])) {
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
// 💡 [수정] MailSender가 템플릿 키로 조회하는 방식일 수 있으므로,
// 커스텀 템플릿 내용을 직접 전달하거나 MailSender를 수정해야 할 수 있음.
// 여기서는 MailSender가 템플릿 키를 받아서 내부적으로 처리한다고 가정하고,
// 만약 커스텀 템플릿을 사용해야 한다면 MailSender의 동작 방식에 따라 수정이 필요함.
// 현재 구조상 MailSender::send()는 템플릿 키를 받으므로,
// MailSender 내부에서도 동일한 우선순위 로직이 필요하거나,
// 여기서 내용을 다 만들어서 보내는 방식(sendDirect 등)이 있다면 그걸 써야 함.
// 일단 기존 로직 유지하되, MailSender가 커스텀 테이블을 인지하지 못할 수 있음을 주석으로 남김.
// 만약 MailSender가 내용을 직접 받는 메소드가 없다면,
// 여기서 내용을 치환해서 보내는 로직을 직접 구현해야 할 수도 있음.
// 하지만 요청사항은 "지정 테이블을 읽고 없으면 기본 테이블을 읽어서 발송"이므로,
// 위에서 $template을 구했으니, 내용을 직접 치환해서 메일 발송 함수(mailer)를 직접 호출하는 것이 안전함.
$subject = $template['subject'];
$content = $template['content'];
foreach ($personal_vars as $key => $value) {
$search = '{' . $key . '}';
$subject = str_replace($search, $value, $subject);
$content = str_replace($search, $value, $content);
}
// G5 기본 mailer 함수 사용 (MailSender 의존성 제거 또는 우회)
include_once(G5_LIB_PATH.'/mailer.lib.php');
mailer($this->g5['title'], $this->g5['admin_email'], $member['mb_email'], $subject, $content, 1);
$success++;
} else {
$fail++;
}
}
}
return ['success' => $success, 'fail' => $fail];
}
/**
* 결과 메시지 생성
*/
private function generateResultMessage($results)
{
$message = "발송이 완료되었습니다.\n\n";
$message .= "전체 대상: " . $results['total_targets'] . "\n\n";
if (isset($results['sms_success'])) {
$message .= "SMS 발송 결과: 성공 " . $results['sms_success'] . "건, 실패 " . $results['sms_fail'] . "\n";
}
if (isset($results['email_success'])) {
$message .= "이메일 발송 결과: 성공 " . $results['email_success'] . "건, 실패 " . $results['email_fail'] . "\n";
}
return $message;
}
/**
* ❗ [핵심 수정] 디버그 로그 기록 함수 (권한 문제 해결)
*/
private function write_debug_log($message)
{
$log_dir = G5_PATH . '/log';
// 1. 디렉토리 존재 여부 확인 및 생성
if (!is_dir($log_dir)) {
if (!@mkdir($log_dir, 0755, true) && !is_dir($log_dir)) {
error_log("--- NotificationSender ERROR: 디버그 로그 디렉토리 생성 실패. '{$log_dir}' 경로를 확인하거나 수동으로 생성 후 웹서버 쓰기 권한을 부여해주세요.");
return;
}
}
// 2. 디렉토리 쓰기 권한 확인
if (!is_writable($log_dir)) {
error_log("--- NotificationSender ERROR: 디버그 로그 쓰기 오류. '{$log_dir}' 디렉토리에 쓰기 권한이 없습니다. 웹서버의 폴더 권한을 확인해주세요.");
return;
}
// 3. 로그 파일에 내용 기록
$log_file = $log_dir . '/notification_debug.log';
$log_message = date("[Y-m-d H:i:s]") . " " . $message . "\n";
if (file_put_contents($log_file, $log_message, FILE_APPEND | LOCK_EX) === false) {
error_log("--- NotificationSender ERROR: 디버그 로그 파일 쓰기 실패. '{$log_file}' 파일에 내용을 쓸 수 없습니다.");
}
}
}